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,170 @@
|
|
|
1
|
+
"""Configuration loader
|
|
2
|
+
|
|
3
|
+
Load and parse configuration from INI files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import configparser
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
|
|
11
|
+
from gitwebhooks.models.provider import Provider
|
|
12
|
+
from gitwebhooks.utils.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigLoader:
|
|
16
|
+
"""Configuration loader
|
|
17
|
+
|
|
18
|
+
Responsible for loading and validating configuration from INI files
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config_path: str):
|
|
22
|
+
"""Initialize configuration loader
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config_path: INI configuration file path
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ConfigurationError: File does not exist or cannot be parsed
|
|
29
|
+
"""
|
|
30
|
+
self.config_path = Path(config_path)
|
|
31
|
+
self.parser = configparser.ConfigParser()
|
|
32
|
+
self._load_file()
|
|
33
|
+
|
|
34
|
+
def _load_file(self) -> None:
|
|
35
|
+
"""Load configuration file
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ConfigurationError: File does not exist or parsing failed
|
|
39
|
+
"""
|
|
40
|
+
if not self.config_path.exists():
|
|
41
|
+
raise ConfigurationError(f'Config file not found: {self.config_path}')
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
self.parser.read(str(self.config_path))
|
|
45
|
+
except (configparser.Error, UnicodeDecodeError) as e:
|
|
46
|
+
raise ConfigurationError(f'Failed to parse config: {e}')
|
|
47
|
+
|
|
48
|
+
def load_provider_config(self, provider: Provider) -> ProviderConfig:
|
|
49
|
+
"""Load configuration for specified provider
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
provider: Provider type
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ProviderConfig instance
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ConfigurationError: Configuration section does not exist
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return ProviderConfig.from_config_parser(self.parser, provider)
|
|
62
|
+
except configparser.NoSectionError:
|
|
63
|
+
# Return default configuration
|
|
64
|
+
return ProviderConfig(
|
|
65
|
+
provider=provider,
|
|
66
|
+
verify=False,
|
|
67
|
+
secret='',
|
|
68
|
+
handle_events=[]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def load_all_provider_configs(self) -> Dict[Provider, ProviderConfig]:
|
|
72
|
+
"""Load all provider configurations
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Mapping of provider to configuration
|
|
76
|
+
"""
|
|
77
|
+
configs = {}
|
|
78
|
+
for provider in Provider:
|
|
79
|
+
configs[provider] = self.load_provider_config(provider)
|
|
80
|
+
return configs
|
|
81
|
+
|
|
82
|
+
def load_repository_config(self, name: str) -> Optional[RepositoryConfig]:
|
|
83
|
+
"""Load configuration for specified repository
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Repository name (configuration section name)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
RepositoryConfig instance, or None if section does not exist
|
|
90
|
+
"""
|
|
91
|
+
return RepositoryConfig.from_config_parser(self.parser, name)
|
|
92
|
+
|
|
93
|
+
def load_all_repository_configs(self) -> Dict[str, RepositoryConfig]:
|
|
94
|
+
"""Load all repository configurations
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Mapping of repository name to configuration
|
|
98
|
+
|
|
99
|
+
Note:
|
|
100
|
+
Skips reserved section names: server, ssl, github, gitee, gitlab, custom
|
|
101
|
+
"""
|
|
102
|
+
configs = {}
|
|
103
|
+
reserved_sections = self._get_reserved_sections()
|
|
104
|
+
|
|
105
|
+
for section in self.parser.sections():
|
|
106
|
+
if section not in reserved_sections:
|
|
107
|
+
repo_config = self.load_repository_config(section)
|
|
108
|
+
if repo_config is not None:
|
|
109
|
+
configs[section] = repo_config
|
|
110
|
+
|
|
111
|
+
return configs
|
|
112
|
+
|
|
113
|
+
def _get_reserved_sections(self) -> set:
|
|
114
|
+
"""Get reserved configuration section names
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Set of reserved section names
|
|
118
|
+
"""
|
|
119
|
+
return {'server', 'ssl', 'github', 'gitee', 'gitlab', 'custom'}
|
|
120
|
+
|
|
121
|
+
def get_server_config(self) -> Dict[str, any]:
|
|
122
|
+
"""Get server configuration
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dictionary containing server configuration, keys include:
|
|
126
|
+
- address: Listen address
|
|
127
|
+
- port: Listen port
|
|
128
|
+
- log_file: Log file path
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
'address': self.parser.get('server', 'address', fallback='0.0.0.0'),
|
|
132
|
+
'port': self.parser.getint('server', 'port', fallback=6789),
|
|
133
|
+
'log_file': self.parser.get('server', 'log_file', fallback='').strip(),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def get_ssl_config(self) -> Optional[Dict[str, str]]:
|
|
137
|
+
"""Get SSL configuration
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dictionary containing SSL configuration, or None if not enabled
|
|
141
|
+
Keys include:
|
|
142
|
+
- enable: Whether SSL is enabled
|
|
143
|
+
- key_file: SSL key file path
|
|
144
|
+
- cert_file: SSL certificate file path
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
if not self.parser.getboolean('ssl', 'enable'):
|
|
148
|
+
return None
|
|
149
|
+
except (configparser.NoSectionError, ValueError):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
'enable': True,
|
|
154
|
+
'key_file': self.parser.get('ssl', 'key_file'),
|
|
155
|
+
'cert_file': self.parser.get('ssl', 'cert_file'),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def validate_repository_configs(self) -> None:
|
|
159
|
+
"""Validate all repository configurations
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ConfigurationError: Configuration is invalid
|
|
163
|
+
"""
|
|
164
|
+
configs = self.load_all_repository_configs()
|
|
165
|
+
|
|
166
|
+
for name, config in configs.items():
|
|
167
|
+
try:
|
|
168
|
+
config.validate()
|
|
169
|
+
except ConfigurationError as e:
|
|
170
|
+
raise ConfigurationError(f'Repository {name}: {e}')
|
|
@@ -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)
|