gitwebhooks 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitwebhooks/__init__.py +18 -0
- gitwebhooks/__main__.py +10 -0
- gitwebhooks/auth/__init__.py +20 -0
- gitwebhooks/auth/custom.py +40 -0
- gitwebhooks/auth/factory.py +73 -0
- gitwebhooks/auth/gitee.py +86 -0
- gitwebhooks/auth/github.py +52 -0
- gitwebhooks/auth/gitlab.py +29 -0
- gitwebhooks/auth/verifier.py +34 -0
- gitwebhooks/cli/__init__.py +107 -0
- gitwebhooks/cli/config.py +216 -0
- gitwebhooks/cli/init_wizard.py +701 -0
- gitwebhooks/cli/prompts.py +275 -0
- gitwebhooks/cli/service.py +374 -0
- gitwebhooks/config/__init__.py +17 -0
- gitwebhooks/config/loader.py +170 -0
- gitwebhooks/config/models.py +145 -0
- gitwebhooks/config/registry.py +104 -0
- gitwebhooks/config/server.py +85 -0
- gitwebhooks/handlers/__init__.py +22 -0
- gitwebhooks/handlers/base.py +107 -0
- gitwebhooks/handlers/custom.py +73 -0
- gitwebhooks/handlers/factory.py +83 -0
- gitwebhooks/handlers/gitee.py +113 -0
- gitwebhooks/handlers/github.py +43 -0
- gitwebhooks/handlers/gitlab.py +43 -0
- gitwebhooks/handlers/request.py +248 -0
- gitwebhooks/logging/__init__.py +10 -0
- gitwebhooks/logging/setup.py +57 -0
- gitwebhooks/main/__init__.py +117 -0
- gitwebhooks/main/__main__.py +12 -0
- gitwebhooks/models/__init__.py +14 -0
- gitwebhooks/models/provider.py +22 -0
- gitwebhooks/models/request.py +61 -0
- gitwebhooks/models/result.py +40 -0
- gitwebhooks/server.py +128 -0
- gitwebhooks/utils/__init__.py +14 -0
- gitwebhooks/utils/constants.py +55 -0
- gitwebhooks/utils/exceptions.py +34 -0
- gitwebhooks/utils/executor.py +34 -0
- gitwebhooks/utils/systemd.py +274 -0
- gitwebhooks-2.1.0.dist-info/METADATA +494 -0
- gitwebhooks-2.1.0.dist-info/RECORD +47 -0
- gitwebhooks-2.1.0.dist-info/WHEEL +5 -0
- gitwebhooks-2.1.0.dist-info/entry_points.txt +2 -0
- gitwebhooks-2.1.0.dist-info/licenses/LICENSE +21 -0
- gitwebhooks-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Custom webhook handler
|
|
2
|
+
|
|
3
|
+
Handles webhook requests from custom webhook sources.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from gitwebhooks.handlers.base import WebhookHandler
|
|
9
|
+
from gitwebhooks.models.provider import Provider
|
|
10
|
+
from gitwebhooks.models.request import WebhookRequest
|
|
11
|
+
from gitwebhooks.models.result import SignatureVerificationResult
|
|
12
|
+
from gitwebhooks.config.models import ProviderConfig
|
|
13
|
+
from gitwebhooks.auth.factory import VerifierFactory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CustomHandler(WebhookHandler):
|
|
17
|
+
"""Custom webhook handler"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._verifier = None # Created dynamically based on configuration
|
|
21
|
+
|
|
22
|
+
def get_provider(self) -> Provider:
|
|
23
|
+
"""Return Provider.CUSTOM"""
|
|
24
|
+
return Provider.CUSTOM
|
|
25
|
+
|
|
26
|
+
def verify_signature(self, request: WebhookRequest,
|
|
27
|
+
config: ProviderConfig) -> SignatureVerificationResult:
|
|
28
|
+
"""Verify custom token"""
|
|
29
|
+
# Create verifier (verification enabled depends on whether header_token is configured)
|
|
30
|
+
verify_enabled = bool(config.header_token)
|
|
31
|
+
if not self._verifier:
|
|
32
|
+
self._verifier = VerifierFactory.create_custom_verifier(verify_enabled)
|
|
33
|
+
|
|
34
|
+
signature = request.headers.get(config.header_token) if config.header_token else ''
|
|
35
|
+
return self._verifier.verify(request.payload, signature, config.secret)
|
|
36
|
+
|
|
37
|
+
def extract_repository(self, request: WebhookRequest,
|
|
38
|
+
config: ProviderConfig) -> Optional[str]:
|
|
39
|
+
"""Extract repository identifier using JSON path"""
|
|
40
|
+
if request.post_data is None or not config.identifier_path:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Split path by '.'
|
|
44
|
+
path_parts = config.identifier_path.split('.')
|
|
45
|
+
current = request.post_data
|
|
46
|
+
|
|
47
|
+
# Traverse level by level
|
|
48
|
+
for part in path_parts:
|
|
49
|
+
if isinstance(current, dict):
|
|
50
|
+
current = current.get(part)
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Return final value (must be string)
|
|
55
|
+
return current if isinstance(current, str) else None
|
|
56
|
+
|
|
57
|
+
def is_event_allowed(self, event: Optional[str],
|
|
58
|
+
config: ProviderConfig) -> bool:
|
|
59
|
+
"""Check if event is allowed
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
event: Event type (already parsed from headers by caller)
|
|
63
|
+
config: Provider configuration
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if event is allowed, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
# If no event header configured, allow all events
|
|
69
|
+
if not config.header_event:
|
|
70
|
+
return config.allows_event(event)
|
|
71
|
+
|
|
72
|
+
# Use the passed event parameter (already parsed from headers by caller)
|
|
73
|
+
return config.allows_event(event)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Handler factory class
|
|
2
|
+
|
|
3
|
+
Creates corresponding handler instances based on request headers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
from gitwebhooks.handlers.base import WebhookHandler
|
|
9
|
+
from gitwebhooks.handlers.github import GithubHandler
|
|
10
|
+
from gitwebhooks.handlers.gitee import GiteeHandler
|
|
11
|
+
from gitwebhooks.handlers.gitlab import GitlabHandler
|
|
12
|
+
from gitwebhooks.handlers.custom import CustomHandler
|
|
13
|
+
from gitwebhooks.models.provider import Provider
|
|
14
|
+
from gitwebhooks.config.models import ProviderConfig
|
|
15
|
+
from gitwebhooks.utils.exceptions import UnsupportedProviderError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HandlerFactory:
|
|
19
|
+
"""Handler factory class
|
|
20
|
+
|
|
21
|
+
Creates corresponding handler instances based on request headers
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_headers(headers: Dict[str, str],
|
|
26
|
+
configs: Dict[Provider, ProviderConfig]) -> WebhookHandler:
|
|
27
|
+
"""Create handler based on request headers
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
headers: HTTP request header dictionary
|
|
31
|
+
configs: Provider configuration dictionary
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Corresponding WebhookHandler instance
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
UnsupportedProviderError: Unrecognized platform
|
|
38
|
+
"""
|
|
39
|
+
# Check X-GitHub-Event
|
|
40
|
+
if 'X-GitHub-Event' in headers:
|
|
41
|
+
return GithubHandler()
|
|
42
|
+
|
|
43
|
+
# Check X-Gitee-Event
|
|
44
|
+
if 'X-Gitee-Event' in headers:
|
|
45
|
+
return GiteeHandler()
|
|
46
|
+
|
|
47
|
+
# Check X-Gitlab-Event
|
|
48
|
+
if 'X-Gitlab-Event' in headers:
|
|
49
|
+
return GitlabHandler()
|
|
50
|
+
|
|
51
|
+
# Check custom header
|
|
52
|
+
custom_config = configs.get(Provider.CUSTOM)
|
|
53
|
+
if custom_config and custom_config.header_name:
|
|
54
|
+
if headers.get(custom_config.header_name) == custom_config.header_value:
|
|
55
|
+
return CustomHandler()
|
|
56
|
+
|
|
57
|
+
raise UnsupportedProviderError('Unable to identify provider from headers')
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def from_handler_type(provider: Provider) -> WebhookHandler:
|
|
61
|
+
"""Create handler based on provider type
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
provider: Provider type
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Corresponding WebhookHandler instance
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
UnsupportedProviderError: Unsupported provider
|
|
71
|
+
"""
|
|
72
|
+
handlers = {
|
|
73
|
+
Provider.GITHUB: GithubHandler,
|
|
74
|
+
Provider.GITEE: GiteeHandler,
|
|
75
|
+
Provider.GITLAB: GitlabHandler,
|
|
76
|
+
Provider.CUSTOM: CustomHandler,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handler_class = handlers.get(provider)
|
|
80
|
+
if handler_class is None:
|
|
81
|
+
raise UnsupportedProviderError(f'No handler for provider: {provider}')
|
|
82
|
+
|
|
83
|
+
return handler_class()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Gitee webhook handler
|
|
2
|
+
|
|
3
|
+
Handles webhook requests from Gitee.
|
|
4
|
+
|
|
5
|
+
Gitee supports two authentication modes:
|
|
6
|
+
1. Signature mode: X-Gitee-Timestamp + X-Gitee-Token (HMAC-SHA256)
|
|
7
|
+
2. Password mode: X-Gitee-Token only (plaintext)
|
|
8
|
+
|
|
9
|
+
According to Gitee documentation:
|
|
10
|
+
https://help.gitee.com/webhook/how-to-verify-webhook-keys
|
|
11
|
+
|
|
12
|
+
Even when verify=False, if a password is provided (no timestamp),
|
|
13
|
+
it should still be validated for security.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from gitwebhooks.handlers.base import WebhookHandler
|
|
19
|
+
from gitwebhooks.models.provider import Provider
|
|
20
|
+
from gitwebhooks.models.request import WebhookRequest
|
|
21
|
+
from gitwebhooks.models.result import SignatureVerificationResult
|
|
22
|
+
from gitwebhooks.config.models import ProviderConfig
|
|
23
|
+
from gitwebhooks.auth.factory import VerifierFactory
|
|
24
|
+
from gitwebhooks.utils.exceptions import SignatureValidationError, UnsupportedEventError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GiteeHandler(WebhookHandler):
|
|
28
|
+
"""Gitee webhook handler"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self._verifier = VerifierFactory.create_gitee_verifier()
|
|
32
|
+
|
|
33
|
+
def get_provider(self) -> Provider:
|
|
34
|
+
"""Return Provider.GITEE"""
|
|
35
|
+
return Provider.GITEE
|
|
36
|
+
|
|
37
|
+
def verify_signature(self, request: WebhookRequest,
|
|
38
|
+
config: ProviderConfig) -> SignatureVerificationResult:
|
|
39
|
+
"""Verify Gitee signature or password
|
|
40
|
+
|
|
41
|
+
Gitee has two modes:
|
|
42
|
+
1. Signature mode: Has X-Gitee-Timestamp header
|
|
43
|
+
2. Password mode: Only X-Gitee-Token header
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request: Webhook request
|
|
47
|
+
config: Provider configuration
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
SignatureVerificationResult instance
|
|
51
|
+
"""
|
|
52
|
+
signature = request.headers.get('X-Gitee-Token')
|
|
53
|
+
timestamp = request.headers.get('X-Gitee-Timestamp')
|
|
54
|
+
|
|
55
|
+
# Always validate if signature/token is provided
|
|
56
|
+
if signature is None:
|
|
57
|
+
return SignatureVerificationResult.failure('Missing signature or password')
|
|
58
|
+
|
|
59
|
+
return self._verifier.verify(
|
|
60
|
+
request.payload,
|
|
61
|
+
signature,
|
|
62
|
+
config.secret,
|
|
63
|
+
timestamp=timestamp
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def handle_request(self, request: WebhookRequest,
|
|
67
|
+
config: ProviderConfig) -> Optional[str]:
|
|
68
|
+
"""Handle complete webhook request
|
|
69
|
+
|
|
70
|
+
Override base implementation to handle Gitee's special case:
|
|
71
|
+
Even when verify=False, password validation is performed if provided.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
request: Webhook request data
|
|
75
|
+
config: Provider configuration
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Repository identifier, None if processing failed
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
SignatureValidationError: Signature/password verification failed
|
|
82
|
+
UnsupportedEventError: Event type not allowed
|
|
83
|
+
"""
|
|
84
|
+
# Step 1: Check event
|
|
85
|
+
if not self.is_event_allowed(request.event, config):
|
|
86
|
+
raise UnsupportedEventError(f'Event not configured: {request.event}')
|
|
87
|
+
|
|
88
|
+
# Step 2: Verify signature or password
|
|
89
|
+
# For Gitee, we always validate if token is provided
|
|
90
|
+
signature = request.headers.get('X-Gitee-Token')
|
|
91
|
+
if signature is not None:
|
|
92
|
+
result = self.verify_signature(request, config)
|
|
93
|
+
if not result.is_valid:
|
|
94
|
+
raise SignatureValidationError(result.error_message or 'Verification failed')
|
|
95
|
+
elif config.verify:
|
|
96
|
+
# verify=True but no signature provided
|
|
97
|
+
raise SignatureValidationError('Missing signature')
|
|
98
|
+
|
|
99
|
+
# Step 3: Extract repository identifier
|
|
100
|
+
return self.extract_repository(request, config)
|
|
101
|
+
|
|
102
|
+
def extract_repository(self, request: WebhookRequest,
|
|
103
|
+
config: ProviderConfig) -> Optional[str]:
|
|
104
|
+
"""Extract Gitee repository full name from request"""
|
|
105
|
+
if request.post_data is None:
|
|
106
|
+
return None
|
|
107
|
+
repo = request.post_data.get('repository', {})
|
|
108
|
+
return repo.get('full_name') if isinstance(repo, dict) else None
|
|
109
|
+
|
|
110
|
+
def is_event_allowed(self, event: Optional[str],
|
|
111
|
+
config: ProviderConfig) -> bool:
|
|
112
|
+
"""Check if event is in config.handle_events list"""
|
|
113
|
+
return config.allows_event(event)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""GitHub webhook handler
|
|
2
|
+
|
|
3
|
+
Handles webhook requests from GitHub.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from gitwebhooks.handlers.base import WebhookHandler
|
|
9
|
+
from gitwebhooks.models.provider import Provider
|
|
10
|
+
from gitwebhooks.models.request import WebhookRequest
|
|
11
|
+
from gitwebhooks.models.result import SignatureVerificationResult
|
|
12
|
+
from gitwebhooks.config.models import ProviderConfig
|
|
13
|
+
from gitwebhooks.auth.factory import VerifierFactory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GithubHandler(WebhookHandler):
|
|
17
|
+
"""GitHub webhook handler"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._verifier = VerifierFactory.create_github_verifier()
|
|
21
|
+
|
|
22
|
+
def get_provider(self) -> Provider:
|
|
23
|
+
"""Return Provider.GITHUB"""
|
|
24
|
+
return Provider.GITHUB
|
|
25
|
+
|
|
26
|
+
def verify_signature(self, request: WebhookRequest,
|
|
27
|
+
config: ProviderConfig) -> SignatureVerificationResult:
|
|
28
|
+
"""Verify GitHub HMAC-SHA1 signature"""
|
|
29
|
+
signature = request.headers.get('X-Hub-Signature')
|
|
30
|
+
return self._verifier.verify(request.payload, signature, config.secret)
|
|
31
|
+
|
|
32
|
+
def extract_repository(self, request: WebhookRequest,
|
|
33
|
+
config: ProviderConfig) -> Optional[str]:
|
|
34
|
+
"""Extract GitHub repository full name from request"""
|
|
35
|
+
if request.post_data is None:
|
|
36
|
+
return None
|
|
37
|
+
repo = request.post_data.get('repository', {})
|
|
38
|
+
return repo.get('full_name') if isinstance(repo, dict) else None
|
|
39
|
+
|
|
40
|
+
def is_event_allowed(self, event: Optional[str],
|
|
41
|
+
config: ProviderConfig) -> bool:
|
|
42
|
+
"""Check if event is in config.handle_events list"""
|
|
43
|
+
return config.allows_event(event)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""GitLab webhook handler
|
|
2
|
+
|
|
3
|
+
Handles webhook requests from GitLab.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from gitwebhooks.handlers.base import WebhookHandler
|
|
9
|
+
from gitwebhooks.models.provider import Provider
|
|
10
|
+
from gitwebhooks.models.request import WebhookRequest
|
|
11
|
+
from gitwebhooks.models.result import SignatureVerificationResult
|
|
12
|
+
from gitwebhooks.config.models import ProviderConfig
|
|
13
|
+
from gitwebhooks.auth.factory import VerifierFactory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitlabHandler(WebhookHandler):
|
|
17
|
+
"""GitLab webhook handler"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._verifier = VerifierFactory.create_gitlab_verifier()
|
|
21
|
+
|
|
22
|
+
def get_provider(self) -> Provider:
|
|
23
|
+
"""Return Provider.GITLAB"""
|
|
24
|
+
return Provider.GITLAB
|
|
25
|
+
|
|
26
|
+
def verify_signature(self, request: WebhookRequest,
|
|
27
|
+
config: ProviderConfig) -> SignatureVerificationResult:
|
|
28
|
+
"""Verify GitLab token"""
|
|
29
|
+
signature = request.headers.get('X-Gitlab-Token')
|
|
30
|
+
return self._verifier.verify(request.payload, signature, config.secret)
|
|
31
|
+
|
|
32
|
+
def extract_repository(self, request: WebhookRequest,
|
|
33
|
+
config: ProviderConfig) -> Optional[str]:
|
|
34
|
+
"""Extract GitLab project path from request"""
|
|
35
|
+
if request.post_data is None:
|
|
36
|
+
return None
|
|
37
|
+
project = request.post_data.get('project', {})
|
|
38
|
+
return project.get('path_with_namespace') if isinstance(project, dict) else None
|
|
39
|
+
|
|
40
|
+
def is_event_allowed(self, event: Optional[str],
|
|
41
|
+
config: ProviderConfig) -> bool:
|
|
42
|
+
"""Check if event is in config.handle_events list"""
|
|
43
|
+
return config.allows_event(event)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""HTTP request handler
|
|
2
|
+
|
|
3
|
+
Handles webhook endpoint for HTTP POST requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from http.server import BaseHTTPRequestHandler
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
from urllib.parse import parse_qs
|
|
11
|
+
|
|
12
|
+
from gitwebhooks.models.provider import Provider
|
|
13
|
+
from gitwebhooks.models.request import WebhookRequest
|
|
14
|
+
from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
|
|
15
|
+
from gitwebhooks.handlers.factory import HandlerFactory
|
|
16
|
+
from gitwebhooks.utils.constants import *
|
|
17
|
+
from gitwebhooks.utils.exceptions import *
|
|
18
|
+
from gitwebhooks.utils.executor import execute_deployment
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WebhookRequestHandler(BaseHTTPRequestHandler):
|
|
22
|
+
"""Webhook request handler
|
|
23
|
+
|
|
24
|
+
Handles HTTP POST requests, verifies signatures, triggers deployment commands.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Class variables: configuration injection
|
|
28
|
+
_provider_configs: Dict[Provider, ProviderConfig] = {}
|
|
29
|
+
_repository_configs: Dict[str, RepositoryConfig] = {}
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def configure(cls, provider_configs: Dict[Provider, ProviderConfig],
|
|
33
|
+
repository_configs: Dict[str, RepositoryConfig]) -> None:
|
|
34
|
+
"""Configure handler class (dependency injection)
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
provider_configs: Provider configuration dictionary
|
|
38
|
+
repository_configs: Repository configuration dictionary
|
|
39
|
+
"""
|
|
40
|
+
cls._provider_configs = provider_configs
|
|
41
|
+
cls._repository_configs = repository_configs
|
|
42
|
+
|
|
43
|
+
def __init__(self, request, client_address, server):
|
|
44
|
+
"""Initialize request handler
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
request: HTTP request
|
|
48
|
+
client_address: Client address
|
|
49
|
+
server: Server instance
|
|
50
|
+
"""
|
|
51
|
+
# Copy class-level config to instance (avoid shared state)
|
|
52
|
+
self._provider_configs = self.__class__._provider_configs.copy()
|
|
53
|
+
self._repository_configs = self.__class__._repository_configs.copy()
|
|
54
|
+
super().__init__(request, client_address, server)
|
|
55
|
+
|
|
56
|
+
def do_GET(self) -> None:
|
|
57
|
+
"""Handle GET request
|
|
58
|
+
|
|
59
|
+
All GET requests return 403 Forbidden
|
|
60
|
+
"""
|
|
61
|
+
self.send_error(HTTP_FORBIDDEN, MESSAGE_FORBIDDEN)
|
|
62
|
+
|
|
63
|
+
def do_POST(self) -> None:
|
|
64
|
+
"""Handle POST request (webhook endpoint)
|
|
65
|
+
|
|
66
|
+
Implementation flow:
|
|
67
|
+
1. Parse request body
|
|
68
|
+
2. Identify provider and event
|
|
69
|
+
3. Create corresponding handler
|
|
70
|
+
4. Verify signature
|
|
71
|
+
5. Extract repository identifier
|
|
72
|
+
6. Execute deployment command
|
|
73
|
+
7. Send response
|
|
74
|
+
|
|
75
|
+
All errors are caught and appropriate HTTP status codes are returned
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
# Step 1: Parse request body
|
|
79
|
+
try:
|
|
80
|
+
request = self._parse_request()
|
|
81
|
+
except RequestParseError as e:
|
|
82
|
+
logging.warning('Request parse failed: %s', e)
|
|
83
|
+
self._send_error(HTTP_BAD_REQUEST, MESSAGE_BAD_REQUEST)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if request.post_data is None:
|
|
87
|
+
logging.warning('Unsupported request format')
|
|
88
|
+
self._send_error(HTTP_BAD_REQUEST, MESSAGE_BAD_REQUEST)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Step 2: Identify provider
|
|
92
|
+
provider = self._identify_provider(request)
|
|
93
|
+
if provider is None:
|
|
94
|
+
logging.warning('Unknown provider')
|
|
95
|
+
self._send_error(HTTP_PRECONDITION_FAILED, MESSAGE_PRECONDITION_FAILED)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Step 3: Create handler
|
|
99
|
+
handler = HandlerFactory.from_handler_type(provider)
|
|
100
|
+
|
|
101
|
+
# Steps 4-6: Process request
|
|
102
|
+
try:
|
|
103
|
+
provider_config = self._provider_configs.get(provider)
|
|
104
|
+
if not provider_config:
|
|
105
|
+
raise UnsupportedProviderError(f'{provider} configuration not found')
|
|
106
|
+
|
|
107
|
+
repo_name = handler.handle_request(request, provider_config)
|
|
108
|
+
|
|
109
|
+
except (SignatureValidationError, UnsupportedEventError,
|
|
110
|
+
UnsupportedProviderError) as e:
|
|
111
|
+
logging.warning('Webhook processing error: %s', e)
|
|
112
|
+
if isinstance(e, SignatureValidationError):
|
|
113
|
+
self._send_error(HTTP_UNAUTHORIZED, MESSAGE_UNAUTHORIZED)
|
|
114
|
+
elif isinstance(e, UnsupportedEventError):
|
|
115
|
+
self._send_error(HTTP_NOT_ACCEPTABLE, MESSAGE_NOT_ACCEPTABLE)
|
|
116
|
+
else:
|
|
117
|
+
self._send_error(HTTP_PRECONDITION_FAILED, MESSAGE_PRECONDITION_FAILED)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Step 7: Execute deployment command
|
|
121
|
+
if repo_name:
|
|
122
|
+
repo_config = self._repository_configs.get(repo_name)
|
|
123
|
+
if not repo_config:
|
|
124
|
+
logging.warning('No repository configuration: %s', repo_name)
|
|
125
|
+
self._send_error(HTTP_NOT_FOUND, MESSAGE_NOT_FOUND)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
execute_deployment(repo_name, repo_config.cwd, repo_config.cmd)
|
|
129
|
+
self._send_response(HTTP_OK)
|
|
130
|
+
else:
|
|
131
|
+
logging.warning('Repository information missing from payload')
|
|
132
|
+
self._send_error(HTTP_NOT_FOUND, MESSAGE_NOT_FOUND)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logging.error('Unexpected error processing webhook: %s', e)
|
|
136
|
+
self._send_error(HTTP_INTERNAL_SERVER_ERROR, MESSAGE_INTERNAL_SERVER_ERROR)
|
|
137
|
+
|
|
138
|
+
def _parse_request(self) -> WebhookRequest:
|
|
139
|
+
"""Parse HTTP request
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
WebhookRequest instance
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
RequestParseError: Parsing failed
|
|
146
|
+
"""
|
|
147
|
+
# Get Content-Type and Content-Length
|
|
148
|
+
content_type = self.headers.get(HEADER_CONTENT_TYPE, '')
|
|
149
|
+
content_length = int(self.headers.get(HEADER_CONTENT_LENGTH, 0))
|
|
150
|
+
|
|
151
|
+
# Read request body
|
|
152
|
+
if content_length == 0:
|
|
153
|
+
raise RequestParseError('Empty request body')
|
|
154
|
+
|
|
155
|
+
payload = self.rfile.read(content_length)
|
|
156
|
+
|
|
157
|
+
# Parse POST data
|
|
158
|
+
post_data = None
|
|
159
|
+
if CONTENT_TYPE_JSON in content_type:
|
|
160
|
+
try:
|
|
161
|
+
post_data = json.loads(payload.decode('utf-8'))
|
|
162
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
163
|
+
raise RequestParseError(f'Invalid JSON: {e}')
|
|
164
|
+
elif CONTENT_TYPE_FORM_URLENCODED in content_type:
|
|
165
|
+
try:
|
|
166
|
+
post_data = {k: v[0] if v else None for k, v in parse_qs(payload.decode('utf-8')).items()}
|
|
167
|
+
except UnicodeDecodeError as e:
|
|
168
|
+
raise RequestParseError(f'Invalid form data: {e}')
|
|
169
|
+
|
|
170
|
+
# Identify provider and event
|
|
171
|
+
provider, event = self._parse_provider_and_event()
|
|
172
|
+
|
|
173
|
+
return WebhookRequest(
|
|
174
|
+
provider=provider,
|
|
175
|
+
event=event,
|
|
176
|
+
payload=payload,
|
|
177
|
+
headers=dict(self.headers),
|
|
178
|
+
post_data=post_data,
|
|
179
|
+
content_type=content_type,
|
|
180
|
+
content_length=content_length
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _parse_provider_and_event(self) -> tuple:
|
|
184
|
+
"""Parse provider and event from request headers
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (provider, event)
|
|
188
|
+
"""
|
|
189
|
+
headers_dict = dict(self.headers)
|
|
190
|
+
|
|
191
|
+
# GitHub
|
|
192
|
+
if HEADER_GITHUB_EVENT in headers_dict:
|
|
193
|
+
return Provider.GITHUB, headers_dict.get(HEADER_GITHUB_EVENT)
|
|
194
|
+
|
|
195
|
+
# Gitee
|
|
196
|
+
if HEADER_GITEE_EVENT in headers_dict:
|
|
197
|
+
return Provider.GITEE, headers_dict.get(HEADER_GITEE_EVENT)
|
|
198
|
+
|
|
199
|
+
# GitLab
|
|
200
|
+
if HEADER_GITLAB_EVENT in headers_dict:
|
|
201
|
+
return Provider.GITLAB, headers_dict.get(HEADER_GITLAB_EVENT)
|
|
202
|
+
|
|
203
|
+
# Custom
|
|
204
|
+
custom_config = self._provider_configs.get(Provider.CUSTOM)
|
|
205
|
+
if custom_config and custom_config.header_name:
|
|
206
|
+
header_value = headers_dict.get(custom_config.header_name, '')
|
|
207
|
+
if header_value.startswith(custom_config.header_value):
|
|
208
|
+
event = headers_dict.get(custom_config.header_event) if custom_config.header_event else None
|
|
209
|
+
return Provider.CUSTOM, event
|
|
210
|
+
|
|
211
|
+
# Unable to identify
|
|
212
|
+
return None, None
|
|
213
|
+
|
|
214
|
+
def _identify_provider(self, request: WebhookRequest) -> Optional[Provider]:
|
|
215
|
+
"""Identify provider from request
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
request: Webhook request
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Provider enum value, None if unable to identify
|
|
222
|
+
"""
|
|
223
|
+
return request.provider
|
|
224
|
+
|
|
225
|
+
def _send_response(self, status_code: int, message: bytes = MESSAGE_OK) -> None:
|
|
226
|
+
"""Send HTTP response
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
status_code: HTTP status code
|
|
230
|
+
message: Response body
|
|
231
|
+
"""
|
|
232
|
+
self.send_response(status_code)
|
|
233
|
+
self.send_header('Content-Type', 'text/plain')
|
|
234
|
+
self.end_headers()
|
|
235
|
+
self.wfile.write(message)
|
|
236
|
+
|
|
237
|
+
def _send_error(self, status_code: int, message: str = '') -> None:
|
|
238
|
+
"""Send HTTP error response
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
status_code: HTTP status code
|
|
242
|
+
message: Error message
|
|
243
|
+
"""
|
|
244
|
+
self.send_error(status_code, message)
|
|
245
|
+
|
|
246
|
+
def log_message(self, format: str, *args) -> None:
|
|
247
|
+
"""Override to prevent duplicate logging"""
|
|
248
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""日志配置
|
|
2
|
+
|
|
3
|
+
配置应用程序日志系统。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def setup_logging(log_file: str = None):
|
|
11
|
+
"""配置日志系统
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
log_file: 日志文件路径(空字符串表示不记录到文件)
|
|
15
|
+
|
|
16
|
+
根据配置设置日志记录:
|
|
17
|
+
- 如果 log_file 为空,只输出到 stdout
|
|
18
|
+
- 如果 log_file 非空,同时输出到文件和 stdout
|
|
19
|
+
|
|
20
|
+
Note:
|
|
21
|
+
Python 3.8+ 使用 force 参数确保重新配置,
|
|
22
|
+
早期版本手动清除现有 handlers。
|
|
23
|
+
"""
|
|
24
|
+
handlers = []
|
|
25
|
+
|
|
26
|
+
# 文件处理器
|
|
27
|
+
if log_file:
|
|
28
|
+
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
29
|
+
file_handler.setFormatter(logging.Formatter(
|
|
30
|
+
'%(asctime)s %(message)s',
|
|
31
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
32
|
+
))
|
|
33
|
+
handlers.append(file_handler)
|
|
34
|
+
|
|
35
|
+
# 控制台处理器
|
|
36
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
37
|
+
console_handler.setFormatter(logging.Formatter(
|
|
38
|
+
'%(asctime)s %(message)s',
|
|
39
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
40
|
+
))
|
|
41
|
+
handlers.append(console_handler)
|
|
42
|
+
|
|
43
|
+
# 配置根日志记录器
|
|
44
|
+
# Python 3.8+ 支持 force 参数
|
|
45
|
+
basic_config_kwargs = {
|
|
46
|
+
'level': logging.DEBUG,
|
|
47
|
+
'handlers': handlers,
|
|
48
|
+
}
|
|
49
|
+
if sys.version_info >= (3, 8):
|
|
50
|
+
basic_config_kwargs['force'] = True
|
|
51
|
+
else:
|
|
52
|
+
# 早期版本:手动清除现有 handlers
|
|
53
|
+
root_logger = logging.getLogger()
|
|
54
|
+
for handler in root_logger.handlers[:]:
|
|
55
|
+
root_logger.removeHandler(handler)
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(**basic_config_kwargs)
|