gitwebhooks 2.0.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.py +107 -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/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 +42 -0
- gitwebhooks/utils/exceptions.py +34 -0
- gitwebhooks/utils/executor.py +34 -0
- gitwebhooks-2.0.0.dist-info/METADATA +312 -0
- gitwebhooks-2.0.0.dist-info/RECORD +40 -0
- gitwebhooks-2.0.0.dist-info/WHEEL +5 -0
- gitwebhooks-2.0.0.dist-info/entry_points.txt +2 -0
- gitwebhooks-2.0.0.dist-info/licenses/LICENSE +21 -0
- gitwebhooks-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Data models module
|
|
2
|
+
|
|
3
|
+
Defines core data types and enumerations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .provider import Provider
|
|
7
|
+
from .request import WebhookRequest
|
|
8
|
+
from .result import SignatureVerificationResult
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'Provider',
|
|
12
|
+
'WebhookRequest',
|
|
13
|
+
'SignatureVerificationResult',
|
|
14
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Git platform provider enumeration
|
|
2
|
+
|
|
3
|
+
Defines supported Git hosting platform types.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum, unique
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@unique
|
|
10
|
+
class Provider(Enum):
|
|
11
|
+
"""Git platform provider type
|
|
12
|
+
|
|
13
|
+
Supported Git hosting platforms:
|
|
14
|
+
- GITHUB: GitHub platform
|
|
15
|
+
- GITEE: Gitee platform (China)
|
|
16
|
+
- GITLAB: GitLab platform
|
|
17
|
+
- CUSTOM: Custom webhook platform
|
|
18
|
+
"""
|
|
19
|
+
GITHUB = 'github'
|
|
20
|
+
GITEE = 'gitee'
|
|
21
|
+
GITLAB = 'gitlab'
|
|
22
|
+
CUSTOM = 'custom'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Webhook request data wrapper
|
|
2
|
+
|
|
3
|
+
Represents a parsed HTTP webhook request.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from gitwebhooks.models.provider import Provider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class WebhookRequest:
|
|
14
|
+
"""Webhook request data wrapper
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
provider: Git platform provider
|
|
18
|
+
event: Event type (e.g., 'push', 'merge_request')
|
|
19
|
+
payload: Raw request body bytes
|
|
20
|
+
headers: HTTP request headers
|
|
21
|
+
post_data: Parsed JSON/form data
|
|
22
|
+
content_type: Content-Type header value
|
|
23
|
+
content_length: Content-Length header value
|
|
24
|
+
"""
|
|
25
|
+
provider: Provider
|
|
26
|
+
event: Optional[str]
|
|
27
|
+
payload: bytes
|
|
28
|
+
headers: Dict[str, str]
|
|
29
|
+
post_data: Optional[Dict[str, Any]]
|
|
30
|
+
content_type: str
|
|
31
|
+
content_length: int
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def repo_identifier(self) -> Optional[str]:
|
|
35
|
+
"""Extract repository identifier from post_data
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Repository identifier (e.g., 'owner/repo'), None if not found
|
|
39
|
+
|
|
40
|
+
Extraction rules:
|
|
41
|
+
- GitHub: repository.full_name
|
|
42
|
+
- Gitee: repository.full_name
|
|
43
|
+
- GitLab: project.path_with_namespace
|
|
44
|
+
- Custom: Uses ProviderConfig.identifier_path
|
|
45
|
+
"""
|
|
46
|
+
if self.post_data is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
extractors = {
|
|
50
|
+
Provider.GITHUB: lambda d: d.get('repository', {}).get('full_name'),
|
|
51
|
+
Provider.GITEE: lambda d: d.get('repository', {}).get('full_name'),
|
|
52
|
+
Provider.GITLAB: lambda d: d.get('project', {}).get('path_with_namespace'),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
extractor = extractors.get(self.provider)
|
|
56
|
+
if extractor:
|
|
57
|
+
result = extractor(self.post_data)
|
|
58
|
+
return result if isinstance(result, str) else None
|
|
59
|
+
|
|
60
|
+
# Custom provider uses identifier_path (handled in handler)
|
|
61
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Signature verification result dataclass
|
|
2
|
+
|
|
3
|
+
Represents the result status of signature verification.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SignatureVerificationResult:
|
|
12
|
+
"""Signature verification result
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
is_valid: Whether verification passed
|
|
16
|
+
error_message: Error description when verification fails
|
|
17
|
+
"""
|
|
18
|
+
is_valid: bool
|
|
19
|
+
error_message: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def success(cls) -> 'SignatureVerificationResult':
|
|
23
|
+
"""Create a successful verification result
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Result object with is_valid=True
|
|
27
|
+
"""
|
|
28
|
+
return cls(is_valid=True)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def failure(cls, message: str) -> 'SignatureVerificationResult':
|
|
32
|
+
"""Create a failed verification result
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Error description
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Result object with is_valid=False and error message
|
|
39
|
+
"""
|
|
40
|
+
return cls(is_valid=False, error_message=message)
|