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,145 @@
|
|
|
1
|
+
"""Configuration data models
|
|
2
|
+
|
|
3
|
+
Defines configuration-related dataclasses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import configparser
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from gitwebhooks.models.provider import Provider
|
|
12
|
+
from gitwebhooks.utils.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ProviderConfig:
|
|
17
|
+
"""Git platform provider configuration
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
provider: Provider type
|
|
21
|
+
verify: Whether to verify signature/token
|
|
22
|
+
secret: Secret key for verification
|
|
23
|
+
handle_events: List of events to handle (empty list = handle all events)
|
|
24
|
+
header_name: Custom identification header (CUSTOM only)
|
|
25
|
+
header_value: Custom identification value (CUSTOM only)
|
|
26
|
+
header_event: Custom event header (CUSTOM only)
|
|
27
|
+
header_token: Custom token header (CUSTOM only)
|
|
28
|
+
identifier_path: Repository identifier JSON path (CUSTOM only)
|
|
29
|
+
"""
|
|
30
|
+
provider: Provider
|
|
31
|
+
verify: bool
|
|
32
|
+
secret: str
|
|
33
|
+
handle_events: List[str]
|
|
34
|
+
|
|
35
|
+
# Custom provider only
|
|
36
|
+
header_name: str = ''
|
|
37
|
+
header_value: str = ''
|
|
38
|
+
header_event: str = ''
|
|
39
|
+
header_token: str = ''
|
|
40
|
+
identifier_path: str = ''
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_config_parser(cls, parser: configparser.ConfigParser,
|
|
44
|
+
provider: Provider) -> 'ProviderConfig':
|
|
45
|
+
"""Load provider configuration from ConfigParser
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
parser: ConfigParser instance
|
|
49
|
+
provider: Provider type
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
ProviderConfig instance
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
configparser.NoSectionError: Configuration section does not exist
|
|
56
|
+
"""
|
|
57
|
+
section = provider.value
|
|
58
|
+
handle_events_str = parser.get(section, 'handle_events',
|
|
59
|
+
fallback='').strip()
|
|
60
|
+
handle_events = [e.strip() for e in handle_events_str.split(',')
|
|
61
|
+
if e.strip()] if handle_events_str else []
|
|
62
|
+
|
|
63
|
+
config = cls(
|
|
64
|
+
provider=provider,
|
|
65
|
+
verify=parser.getboolean(section, 'verify', fallback=False),
|
|
66
|
+
secret=parser.get(section, 'secret', fallback=''),
|
|
67
|
+
handle_events=handle_events
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Custom provider additional fields
|
|
71
|
+
if provider == Provider.CUSTOM:
|
|
72
|
+
config.header_name = parser.get(section, 'header_name',
|
|
73
|
+
fallback='X-Custom-Webhook')
|
|
74
|
+
config.header_value = parser.get(section, 'header_value',
|
|
75
|
+
fallback='custom')
|
|
76
|
+
config.header_event = parser.get(section, 'header_event',
|
|
77
|
+
fallback='X-Custom-Event')
|
|
78
|
+
config.header_token = parser.get(section, 'header_token',
|
|
79
|
+
fallback='X-Custom-Token')
|
|
80
|
+
config.identifier_path = parser.get(section, 'identifier_path',
|
|
81
|
+
fallback='')
|
|
82
|
+
|
|
83
|
+
return config
|
|
84
|
+
|
|
85
|
+
def allows_event(self, event: str) -> bool:
|
|
86
|
+
"""Check if an event is allowed to be handled
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
event: Event type (None means all events)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if event is allowed, False otherwise
|
|
93
|
+
"""
|
|
94
|
+
if not self.handle_events:
|
|
95
|
+
return True # Empty list = handle all events
|
|
96
|
+
return event in self.handle_events
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class RepositoryConfig:
|
|
101
|
+
"""Repository deployment configuration
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
name: Repository name (format: 'owner/repo' or 'group/project/repo')
|
|
105
|
+
cwd: Working directory for deployment command execution
|
|
106
|
+
cmd: Deployment command (shell string)
|
|
107
|
+
"""
|
|
108
|
+
name: str
|
|
109
|
+
cwd: str
|
|
110
|
+
cmd: str
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_config_parser(cls, parser: configparser.ConfigParser,
|
|
114
|
+
name: str):
|
|
115
|
+
"""Load repository configuration from ConfigParser
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
parser: ConfigParser instance
|
|
119
|
+
name: Configuration section name (repository name)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
RepositoryConfig instance, or None if section does not exist
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
configparser.NoOptionError: Section exists but missing required options
|
|
126
|
+
"""
|
|
127
|
+
if not parser.has_section(name):
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return cls(
|
|
131
|
+
name=name,
|
|
132
|
+
cwd=parser.get(name, 'cwd'),
|
|
133
|
+
cmd=parser.get(name, 'cmd')
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def validate(self) -> None:
|
|
137
|
+
"""Validate configuration
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ConfigurationError: Configuration is invalid
|
|
141
|
+
"""
|
|
142
|
+
if not self.cwd:
|
|
143
|
+
raise ConfigurationError(f'Repository {self.name}: cwd is required')
|
|
144
|
+
if not self.cmd:
|
|
145
|
+
raise ConfigurationError(f'Repository {self.name}: cmd is required')
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Configuration registry
|
|
2
|
+
|
|
3
|
+
Holds all configuration and provides access interface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
from gitwebhooks.config.loader import ConfigLoader
|
|
9
|
+
from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
|
|
10
|
+
from gitwebhooks.config.server import ServerConfig
|
|
11
|
+
from gitwebhooks.models.provider import Provider
|
|
12
|
+
from gitwebhooks.utils.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigurationRegistry:
|
|
16
|
+
"""Configuration registry
|
|
17
|
+
|
|
18
|
+
Holds all configuration and provides access interface
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, loader: ConfigLoader):
|
|
22
|
+
"""Initialize configuration registry
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
loader: Configuration loader instance
|
|
26
|
+
"""
|
|
27
|
+
self.loader = loader
|
|
28
|
+
self._server_config = None
|
|
29
|
+
self._provider_configs = None
|
|
30
|
+
self._repository_configs = None
|
|
31
|
+
self._load_all_configs()
|
|
32
|
+
|
|
33
|
+
def _load_all_configs(self) -> None:
|
|
34
|
+
"""Load all configurations
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ConfigurationError: Configuration loading or validation failed
|
|
38
|
+
"""
|
|
39
|
+
self._server_config = ServerConfig.from_loader(self.loader)
|
|
40
|
+
self._server_config.validate()
|
|
41
|
+
|
|
42
|
+
self._provider_configs = self.loader.load_all_provider_configs()
|
|
43
|
+
self._repository_configs = self.loader.load_all_repository_configs()
|
|
44
|
+
|
|
45
|
+
self.loader.validate_repository_configs()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def server_config(self) -> ServerConfig:
|
|
49
|
+
"""Get server configuration"""
|
|
50
|
+
if self._server_config is None:
|
|
51
|
+
raise ConfigurationError('Server config not loaded')
|
|
52
|
+
return self._server_config
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def provider_configs(self) -> Dict[Provider, ProviderConfig]:
|
|
56
|
+
"""Get provider configuration dictionary"""
|
|
57
|
+
if self._provider_configs is None:
|
|
58
|
+
raise ConfigurationError('Provider configs not loaded')
|
|
59
|
+
return self._provider_configs
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def repository_configs(self) -> Dict[str, RepositoryConfig]:
|
|
63
|
+
"""Get repository configuration dictionary"""
|
|
64
|
+
if self._repository_configs is None:
|
|
65
|
+
raise ConfigurationError('Repository configs not loaded')
|
|
66
|
+
return self._repository_configs
|
|
67
|
+
|
|
68
|
+
def get_provider_config(self, provider: Provider) -> ProviderConfig:
|
|
69
|
+
"""Get configuration for specified provider
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
provider: Provider type
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
ProviderConfig instance
|
|
76
|
+
"""
|
|
77
|
+
return self.provider_configs.get(provider, ProviderConfig(
|
|
78
|
+
provider=provider,
|
|
79
|
+
verify=False,
|
|
80
|
+
secret='',
|
|
81
|
+
handle_events=[]
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
def get_repository_config(self, name: str) -> RepositoryConfig:
|
|
85
|
+
"""Get configuration for specified repository
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: Repository name
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
RepositoryConfig instance, or None if not found
|
|
92
|
+
"""
|
|
93
|
+
return self.repository_configs.get(name)
|
|
94
|
+
|
|
95
|
+
def has_repository(self, name: str) -> bool:
|
|
96
|
+
"""Check if repository exists
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name: Repository name
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if exists, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
return name in self.repository_configs
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Server configuration
|
|
2
|
+
|
|
3
|
+
Defines server-related configuration dataclasses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
from gitwebhooks.utils.exceptions import ConfigurationError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ServerConfig:
|
|
16
|
+
"""Server configuration
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
address: Listen address
|
|
20
|
+
port: Listen port
|
|
21
|
+
log_file: Log file path (empty string means no file logging)
|
|
22
|
+
ssl_enabled: Whether SSL is enabled
|
|
23
|
+
ssl_key_file: SSL key file path
|
|
24
|
+
ssl_cert_file: SSL certificate file path
|
|
25
|
+
"""
|
|
26
|
+
address: str
|
|
27
|
+
port: int
|
|
28
|
+
log_file: str
|
|
29
|
+
ssl_enabled: bool = False
|
|
30
|
+
ssl_key_file: Optional[str] = None
|
|
31
|
+
ssl_cert_file: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_loader(cls, loader: 'ConfigLoader') -> 'ServerConfig':
|
|
35
|
+
"""Create server configuration from ConfigLoader
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
loader: Configuration loader instance
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
ServerConfig instance
|
|
42
|
+
"""
|
|
43
|
+
server_cfg = loader.get_server_config()
|
|
44
|
+
ssl_cfg = loader.get_ssl_config()
|
|
45
|
+
|
|
46
|
+
return cls(
|
|
47
|
+
address=server_cfg['address'],
|
|
48
|
+
port=server_cfg['port'],
|
|
49
|
+
log_file=server_cfg['log_file'],
|
|
50
|
+
ssl_enabled=ssl_cfg is not None,
|
|
51
|
+
ssl_key_file=ssl_cfg['key_file'] if ssl_cfg else None,
|
|
52
|
+
ssl_cert_file=ssl_cfg['cert_file'] if ssl_cfg else None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def validate(self) -> None:
|
|
56
|
+
"""Validate server configuration
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ConfigurationError: Configuration is invalid
|
|
60
|
+
"""
|
|
61
|
+
if self.ssl_enabled:
|
|
62
|
+
if not self.ssl_key_file:
|
|
63
|
+
raise ConfigurationError('SSL enabled but key_file not specified')
|
|
64
|
+
if not self.ssl_cert_file:
|
|
65
|
+
raise ConfigurationError('SSL enabled but cert_file not specified')
|
|
66
|
+
|
|
67
|
+
# Verify files exist
|
|
68
|
+
key_path = Path(self.ssl_key_file)
|
|
69
|
+
cert_path = Path(self.ssl_cert_file)
|
|
70
|
+
|
|
71
|
+
if not key_path.exists():
|
|
72
|
+
raise ConfigurationError(f'SSL key file not found: {self.ssl_key_file}')
|
|
73
|
+
if not cert_path.exists():
|
|
74
|
+
raise ConfigurationError(f'SSL cert file not found: {self.ssl_cert_file}')
|
|
75
|
+
|
|
76
|
+
# Validate port range
|
|
77
|
+
if not (1 <= self.port <= 65535):
|
|
78
|
+
raise ConfigurationError(f'Invalid port number: {self.port}')
|
|
79
|
+
|
|
80
|
+
# Validate log file writable (if specified)
|
|
81
|
+
if self.log_file:
|
|
82
|
+
log_path = Path(self.log_file)
|
|
83
|
+
parent_dir = log_path.parent
|
|
84
|
+
if parent_dir.exists() and not os.access(parent_dir, os.W_OK):
|
|
85
|
+
raise ConfigurationError(f'Log directory not writable: {parent_dir}')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Webhook handlers module
|
|
2
|
+
|
|
3
|
+
Handles webhook requests from different Git platforms.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .base import WebhookHandler
|
|
7
|
+
from .github import GithubHandler
|
|
8
|
+
from .gitee import GiteeHandler
|
|
9
|
+
from .gitlab import GitlabHandler
|
|
10
|
+
from .custom import CustomHandler
|
|
11
|
+
from .factory import HandlerFactory
|
|
12
|
+
from .request import WebhookRequestHandler
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'WebhookHandler',
|
|
16
|
+
'GithubHandler',
|
|
17
|
+
'GiteeHandler',
|
|
18
|
+
'GitlabHandler',
|
|
19
|
+
'CustomHandler',
|
|
20
|
+
'HandlerFactory',
|
|
21
|
+
'WebhookRequestHandler',
|
|
22
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Handler base class
|
|
2
|
+
|
|
3
|
+
Defines the abstract interface for webhook handlers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
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.utils.exceptions import SignatureValidationError, UnsupportedEventError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebhookHandler(ABC):
|
|
17
|
+
"""Webhook handler base class
|
|
18
|
+
|
|
19
|
+
All platform-specific handlers must inherit from this class and implement all abstract methods.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_provider(self) -> Provider:
|
|
24
|
+
"""Return the provider type supported by this handler
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Provider enum value
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def verify_signature(self, request: WebhookRequest,
|
|
33
|
+
config: ProviderConfig) -> SignatureVerificationResult:
|
|
34
|
+
"""Verify webhook signature
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
request: Webhook request data
|
|
38
|
+
config: Provider configuration (includes secret)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Signature verification result
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
SignatureValidationError: When verification fails
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def extract_repository(self, request: WebhookRequest,
|
|
50
|
+
config: ProviderConfig) -> Optional[str]:
|
|
51
|
+
"""Extract repository identifier from request
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
request: Webhook request data
|
|
55
|
+
config: Provider configuration (may include identifier_path)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Repository identifier (e.g., 'owner/repo'), None if not found
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def is_event_allowed(self, event: Optional[str],
|
|
64
|
+
config: ProviderConfig) -> bool:
|
|
65
|
+
"""Check if event type is allowed to be handled
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
event: Event type (e.g., 'push', 'merge_request')
|
|
69
|
+
config: Provider configuration (includes handle_events list)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if event is allowed, False otherwise
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def handle_request(self, request: WebhookRequest,
|
|
77
|
+
config: ProviderConfig) -> Optional[str]:
|
|
78
|
+
"""Handle complete webhook request
|
|
79
|
+
|
|
80
|
+
This is a template method that defines the fixed flow of request processing:
|
|
81
|
+
1. Check if event is allowed
|
|
82
|
+
2. Verify signature
|
|
83
|
+
3. Extract repository identifier
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
request: Webhook request data
|
|
87
|
+
config: Provider configuration
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Repository identifier, None if processing failed
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
SignatureValidationError: Signature verification failed
|
|
94
|
+
UnsupportedEventError: Event type not allowed
|
|
95
|
+
"""
|
|
96
|
+
# Step 1: Check event
|
|
97
|
+
if not self.is_event_allowed(request.event, config):
|
|
98
|
+
raise UnsupportedEventError(f'Event not configured: {request.event}')
|
|
99
|
+
|
|
100
|
+
# Step 2: Verify signature (if required)
|
|
101
|
+
if config.verify:
|
|
102
|
+
result = self.verify_signature(request, config)
|
|
103
|
+
if not result.is_valid:
|
|
104
|
+
raise SignatureValidationError(result.error_message or 'Verification failed')
|
|
105
|
+
|
|
106
|
+
# Step 3: Extract repository identifier
|
|
107
|
+
return self.extract_repository(request, config)
|
|
@@ -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()
|