gitwebhooks 2.4.1__py3-none-any.whl → 2.4.2__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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Automated deployment server supporting GitHub, Gitee, GitLab, and custom webhooks.
4
4
  """
5
5
 
6
- __version__ = '2.4.1'
6
+ __version__ = '2.4.2'
7
7
 
8
8
  # Export main classes for convenient import paths
9
9
  from .server import WebhookServer
@@ -34,6 +34,12 @@ class CustomTokenVerifier(SignatureVerifier):
34
34
  if not self.verify_enabled:
35
35
  return SignatureVerificationResult.success()
36
36
 
37
+ # An empty secret means verification is not actually configured.
38
+ # Without this guard an empty token would match and let an
39
+ # unauthenticated request through (which would run the deploy command).
40
+ if not secret:
41
+ return SignatureVerificationResult.failure('Secret not configured')
42
+
37
43
  if signature == secret:
38
44
  return SignatureVerificationResult.success()
39
45
  else:
@@ -4,12 +4,10 @@ Creates corresponding verifier instances based on provider type.
4
4
  """
5
5
 
6
6
  from gitwebhooks.models.provider import Provider
7
- from gitwebhooks.auth.verifier import SignatureVerifier
8
7
  from gitwebhooks.auth.github import GithubSignatureVerifier
9
8
  from gitwebhooks.auth.gitee import GiteeSignatureVerifier
10
9
  from gitwebhooks.auth.gitlab import GitlabTokenVerifier
11
10
  from gitwebhooks.auth.custom import CustomTokenVerifier
12
- from gitwebhooks.utils.exceptions import UnsupportedProviderError
13
11
 
14
12
 
15
13
  class VerifierFactory:
@@ -25,29 +23,6 @@ class VerifierFactory:
25
23
  Provider.CUSTOM: None, # Custom verifier created dynamically
26
24
  }
27
25
 
28
- @classmethod
29
- def get_verifier(cls, provider: Provider,
30
- verify_enabled: bool = True) -> SignatureVerifier:
31
- """Get verifier for the specified provider
32
-
33
- Args:
34
- provider: Provider type
35
- verify_enabled: Whether verification is enabled (CUSTOM only)
36
-
37
- Returns:
38
- SignatureVerifier instance
39
-
40
- Raises:
41
- UnsupportedProviderError: Unsupported provider
42
- """
43
- if provider == Provider.CUSTOM:
44
- return CustomTokenVerifier(verify_enabled=verify_enabled)
45
-
46
- verifier = cls._verifiers.get(provider)
47
- if verifier is None:
48
- raise UnsupportedProviderError(f'No verifier for provider: {provider}')
49
- return verifier
50
-
51
26
  @classmethod
52
27
  def create_github_verifier(cls) -> GithubSignatureVerifier:
53
28
  """Create GitHub verifier (convenience method)"""
gitwebhooks/auth/gitee.py CHANGED
@@ -51,6 +51,12 @@ class GiteeSignatureVerifier(SignatureVerifier):
51
51
  if signature is None:
52
52
  return SignatureVerificationResult.failure('Missing signature or password')
53
53
 
54
+ # An empty secret means verification is not actually configured.
55
+ # Without this guard an empty token would match and let an
56
+ # unauthenticated request through (which would run the deploy command).
57
+ if not secret:
58
+ return SignatureVerificationResult.failure('Secret not configured')
59
+
54
60
  # Step 1: Try password validation first (direct comparison)
55
61
  if hmac.compare_digest(signature, secret):
56
62
  return SignatureVerificationResult.success()
@@ -23,6 +23,12 @@ class GitlabTokenVerifier(SignatureVerifier):
23
23
  Returns:
24
24
  SignatureVerificationResult instance
25
25
  """
26
+ # An empty secret means verification is not actually configured.
27
+ # Without this guard an empty token would match and let an
28
+ # unauthenticated request through (which would run the deploy command).
29
+ if not secret:
30
+ return SignatureVerificationResult.failure('Secret not configured')
31
+
26
32
  if signature == secret:
27
33
  return SignatureVerificationResult.success()
28
34
  else:
@@ -16,7 +16,7 @@ Validation Functions:
16
16
  - _get_platform_default_event: Returns platform-specific default event
17
17
 
18
18
  TECHNICAL DEBT:
19
- This module (768 lines) exceeds the Constitution's recommended 400 line limit.
19
+ This module (785 lines) exceeds the Constitution's recommended 400 line limit.
20
20
  This is acceptable because the module contains the complete interactive wizard
21
21
  logic including platform selection, repository configuration, event configuration,
22
22
  and file generation.
@@ -30,9 +30,15 @@ Refactoring Plan (future task):
30
30
  import os
31
31
  import re
32
32
  from dataclasses import dataclass
33
- from typing import Optional, List, Dict
33
+ from typing import Dict, Optional, Tuple
34
34
  import configparser
35
35
 
36
+ from gitwebhooks.utils.constants import (
37
+ CONFIG_PATH_LOCAL,
38
+ CONFIG_PATH_SYSTEM,
39
+ CONFIG_PATH_USER,
40
+ )
41
+
36
42
 
37
43
  # =============================================================================
38
44
  # Module Constants
@@ -40,15 +46,15 @@ import configparser
40
46
 
41
47
  CONFIG_LEVELS = {
42
48
  'system': {
43
- 'path': '/etc/gitwebhooks.ini',
49
+ 'path': CONFIG_PATH_SYSTEM,
44
50
  'requires_root': True
45
51
  },
46
52
  'local': {
47
- 'path': '/usr/local/etc/gitwebhooks.ini',
53
+ 'path': CONFIG_PATH_LOCAL,
48
54
  'requires_root': True
49
55
  },
50
56
  'user': {
51
- 'path': '~/.gitwebhooks.ini',
57
+ 'path': CONFIG_PATH_USER,
52
58
  'requires_root': False
53
59
  }
54
60
  }
@@ -201,7 +207,7 @@ def validate_port(value: str) -> bool:
201
207
  return False
202
208
 
203
209
 
204
- def validate_event_input(value: str) -> tuple[bool, str]:
210
+ def validate_event_input(value: str) -> Tuple[bool, str]:
205
211
  """
206
212
  验证事件名称输入。
207
213
 
@@ -4,143 +4,19 @@ Provides functions for interactive user input and validation.
4
4
  """
5
5
 
6
6
  import sys
7
- from typing import Any, Callable, Optional, Dict, List, Tuple
8
7
 
9
- from gitwebhooks.utils.constants import ConfigLevel
8
+ from gitwebhooks.utils.constants import (
9
+ CONFIG_PATH_LOCAL,
10
+ CONFIG_PATH_SYSTEM,
11
+ CONFIG_PATH_USER,
12
+ ConfigLevel,
13
+ )
10
14
 
11
15
 
12
16
  # Question template for configuration initialization
13
- QUESTIONS = [
14
- # Server configuration
15
- {
16
- 'key': 'server_address',
17
- 'prompt': 'Server listen address [default: 0.0.0.0]: ',
18
- 'default': '0.0.0.0',
19
- 'validator': None,
20
- 'required': True
21
- },
22
- {
23
- 'key': 'server_port',
24
- 'prompt': 'Server port [default: 6789]: ',
25
- 'default': '6789',
26
- 'validator': 'validate_port',
27
- 'required': True
28
- },
29
- {
30
- 'key': 'log_file',
31
- 'prompt': 'Log file path [default: ~/.gitwebhook.log]: ',
32
- 'default': '~/.gitwebhook.log',
33
- 'validator': None,
34
- 'required': False
35
- },
36
- ]
37
17
 
38
18
 
39
19
  # Platform-specific questions
40
- PLATFORM_QUESTIONS = {
41
- 'github': [
42
- {
43
- 'section': 'github',
44
- 'key': 'verify',
45
- 'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
46
- 'default': 'true',
47
- 'validator': None,
48
- 'required': False
49
- },
50
- {
51
- 'section': 'github',
52
- 'key': 'secret',
53
- 'prompt': ' Webhook secret (leave empty to skip): ',
54
- 'default': '',
55
- 'validator': None,
56
- 'required': False
57
- },
58
- ],
59
- 'gitee': [
60
- {
61
- 'section': 'gitee',
62
- 'key': 'verify',
63
- 'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
64
- 'default': 'true',
65
- 'validator': None,
66
- 'required': False
67
- },
68
- {
69
- 'section': 'gitee',
70
- 'key': 'secret',
71
- 'prompt': ' Webhook secret (leave empty to skip): ',
72
- 'default': '',
73
- 'validator': None,
74
- 'required': False
75
- },
76
- ],
77
- 'gitlab': [
78
- {
79
- 'section': 'gitlab',
80
- 'key': 'verify',
81
- 'prompt': ' Enable token verification? (Y/n) [default: Y]: ',
82
- 'default': 'true',
83
- 'validator': None,
84
- 'required': False
85
- },
86
- {
87
- 'section': 'gitlab',
88
- 'key': 'secret',
89
- 'prompt': ' Webhook token (leave empty to skip): ',
90
- 'default': '',
91
- 'validator': None,
92
- 'required': False
93
- },
94
- ],
95
- }
96
-
97
-
98
- def ask_question(prompt: str,
99
- validator: Optional[Callable[[str], Any]] = None,
100
- default: Optional[Any] = None,
101
- required: bool = True) -> Any:
102
- """Ask a question with optional validation and default value
103
-
104
- Args:
105
- prompt: Question prompt to display
106
- validator: Optional validation function that raises ValueError on invalid input
107
- default: Optional default value if user provides empty input
108
- required: Whether a value is required (defaults to True)
109
-
110
- Returns:
111
- Validated user input or default value
112
-
113
- Raises:
114
- KeyboardInterrupt: If user confirms exit on Ctrl+C
115
- """
116
- while True:
117
- try:
118
- response = input(prompt).strip()
119
-
120
- # Use default if empty input provided
121
- if not response and default is not None:
122
- return default
123
-
124
- # Validate if validator provided
125
- if validator:
126
- if isinstance(validator, str):
127
- # Validator is a string name, look up function
128
- validator_func = globals().get(validator)
129
- if validator_func:
130
- return validator_func(response)
131
- else:
132
- return validator(response)
133
-
134
- return response
135
-
136
- except KeyboardInterrupt:
137
- # Ask for confirmation before exiting
138
- if ask_yes_no('Confirm exit? Unsaved changes will be lost (y/N) ', default=False):
139
- print('Configuration cancelled')
140
- sys.exit(0)
141
- except ValueError as e:
142
- print(f'Invalid input: {e}')
143
- print('Please try again')
144
20
 
145
21
 
146
22
  def ask_yes_no(prompt: str, default: bool = False) -> bool:
@@ -205,78 +81,6 @@ def validate_port(value: str) -> int:
205
81
  raise ValueError('Port must be a number')
206
82
 
207
83
 
208
- def validate_address(value: str) -> str:
209
- """Validate server address
210
-
211
- Args:
212
- value: String input to validate
213
-
214
- Returns:
215
- Validated address string
216
-
217
- Raises:
218
- ValueError: If address is empty
219
- """
220
- if not value:
221
- raise ValueError('Server address cannot be empty')
222
- return value
223
-
224
-
225
- def validate_path(value: str) -> str:
226
- """Validate file path
227
-
228
- Args:
229
- value: String input to validate
230
-
231
- Returns:
232
- Validated path string
233
-
234
- Raises:
235
- ValueError: If path is empty
236
- """
237
- if not value:
238
- raise ValueError('Path cannot be empty')
239
- return value
240
-
241
-
242
- def validate_bool(value: str) -> bool:
243
- """Validate boolean input
244
-
245
- Args:
246
- value: String input to validate
247
-
248
- Returns:
249
- True or False
250
-
251
- Raises:
252
- ValueError: If input is not a boolean value
253
- """
254
- value_lower = value.lower()
255
- if value_lower in ('y', 'yes', 'true', '1'):
256
- return True
257
- elif value_lower in ('n', 'no', 'false', '0'):
258
- return False
259
- else:
260
- raise ValueError('Please enter yes/no or y/n')
261
-
262
-
263
- def validate_text(value: str) -> str:
264
- """Validate text input (non-empty)
265
-
266
- Args:
267
- value: String input to validate
268
-
269
- Returns:
270
- Validated text string
271
-
272
- Raises:
273
- ValueError: If text is empty
274
- """
275
- if not value:
276
- raise ValueError('Input cannot be empty')
277
- return value
278
-
279
-
280
84
  def ask_config_level() -> ConfigLevel:
281
85
  """Ask user to select configuration file level
282
86
 
@@ -290,9 +94,9 @@ def ask_config_level() -> ConfigLevel:
290
94
  KeyboardInterrupt: If user confirms exit on Ctrl+C
291
95
  """
292
96
  print('Select configuration file level for the service:')
293
- print(' 1. User level (~/.gitwebhooks.ini) - Single user, highest priority')
294
- print(' 2. Local level (/usr/local/etc/gitwebhooks.ini) - Local system, medium priority')
295
- print(' 3. System level (/etc/gitwebhooks.ini) - Global system, lowest priority')
97
+ print(f' 1. User level ({CONFIG_PATH_USER}) - Single user, highest priority')
98
+ print(f' 2. Local level ({CONFIG_PATH_LOCAL}) - Local system, medium priority')
99
+ print(f' 3. System level ({CONFIG_PATH_SYSTEM}) - Global system, lowest priority')
296
100
  print()
297
101
 
298
102
  choice_map = {
gitwebhooks/cli/serve.py CHANGED
@@ -6,7 +6,6 @@ Provides server startup and systemd service management functionality.
6
6
  import argparse
7
7
  import sys
8
8
  from pathlib import Path
9
- from typing import Optional
10
9
 
11
10
  from gitwebhooks.server import WebhookServer
12
11
  from gitwebhooks.utils.exceptions import ConfigurationError
@@ -72,6 +71,10 @@ def start_server(args: argparse.Namespace) -> int:
72
71
  # Get config path from args
73
72
  config_file = getattr(args, 'config', None)
74
73
 
74
+ # Import here to avoid circular dependency (see gitwebhooks/cli/help.py).
75
+ # These live in gitwebhooks.main - do not re-implement them here.
76
+ from gitwebhooks.main import find_config_file, format_config_error
77
+
75
78
  # Print version
76
79
  print(f'gitwebhooks-cli {__version__}')
77
80
 
@@ -112,41 +115,3 @@ def start_server(args: argparse.Namespace) -> int:
112
115
  print(f'Error: {e}', file=sys.stderr)
113
116
  return 1
114
117
 
115
-
116
- def find_config_file() -> Optional[str]:
117
- """Find configuration file by priority order.
118
-
119
- Searches for configuration files in the following order:
120
- 1. User level: ~/.gitwebhooks.ini
121
- 2. Local level: /usr/local/etc/gitwebhooks.ini
122
- 3. System level: /etc/gitwebhooks.ini
123
-
124
- Returns:
125
- Path string of the first existing configuration file, or None if not found.
126
- """
127
- for config_path in CONFIG_SEARCH_PATHS:
128
- expanded_path = Path(config_path).expanduser().resolve()
129
- if expanded_path.exists():
130
- return str(expanded_path)
131
- return None
132
-
133
-
134
- def format_config_error(searched_paths: list) -> str:
135
- """Format user-friendly error message for missing configuration file.
136
-
137
- Args:
138
- searched_paths: List of paths that were searched.
139
-
140
- Returns:
141
- Formatted error message string.
142
- """
143
- lines = [
144
- 'Error: Configuration file not found.',
145
- 'Searched paths:'
146
- ]
147
- for i, path in enumerate(searched_paths, 1):
148
- lines.append(f' {i}. {path}')
149
- lines.append('')
150
- lines.append('You can create a configuration file using:')
151
- lines.append(' gitwebhooks-cli config init')
152
- return '\n'.join(lines)
@@ -9,21 +9,23 @@ from pathlib import Path
9
9
  from typing import Optional
10
10
 
11
11
  from gitwebhooks.utils.systemd import (
12
- check_systemd,
13
12
  check_root_permission,
14
13
  generate_service_file,
15
14
  get_service_path,
16
15
  detect_installation_type,
17
16
  InstallationType,
18
17
  )
19
- from gitwebhooks.utils.constants import ConfigLevel
18
+ from gitwebhooks.utils.constants import (
19
+ CONFIG_PATH_LOCAL,
20
+ CONFIG_PATH_SYSTEM,
21
+ CONFIG_PATH_USER,
22
+ ConfigLevel,
23
+ )
20
24
  from gitwebhooks.cli.prompts import ask_yes_no, ask_config_level
21
25
 
22
26
 
23
27
  # Error codes
24
28
  E_PERM = 1
25
- E_SYSTEMD = 1
26
- E_EXISTS = 1
27
29
 
28
30
 
29
31
  def install_service(
@@ -88,11 +90,11 @@ def install_service(
88
90
  config_path = config_path_override
89
91
  # Map path to config level for display purposes
90
92
  expanded_path = Path(config_path).expanduser()
91
- if str(expanded_path) == str(Path('~/.gitwebhooks.ini').expanduser()):
93
+ if str(expanded_path) == str(Path(CONFIG_PATH_USER).expanduser()):
92
94
  config_level = ConfigLevel.USER
93
- elif str(expanded_path) == '/usr/local/etc/gitwebhooks.ini':
95
+ elif str(expanded_path) == CONFIG_PATH_LOCAL:
94
96
  config_level = ConfigLevel.LOCAL
95
- elif str(expanded_path) == '/etc/gitwebhooks.ini':
97
+ elif str(expanded_path) == CONFIG_PATH_SYSTEM:
96
98
  config_level = ConfigLevel.SYSTEM
97
99
  else:
98
100
  # Custom path, default to USER for display
@@ -179,15 +181,6 @@ def check_service_exists() -> bool:
179
181
  return get_service_path().exists()
180
182
 
181
183
 
182
- def confirm_overwrite() -> bool:
183
- """Ask user to confirm overwrite
184
-
185
- Returns:
186
- True if user confirms, False otherwise
187
- """
188
- return ask_yes_no('Service already exists, overwrite? (y/N) ', default=False)
189
-
190
-
191
184
  def enable_and_start_service() -> int:
192
185
  """Enable and start the service
193
186
 
@@ -338,7 +331,10 @@ def handle_config_cleanup(purge: bool) -> None:
338
331
  Returns:
339
332
  None
340
333
  """
341
- config_path = Path('~/.gitwebhook.ini').expanduser()
334
+ # Single source of truth for the user-level config path (see constants.py).
335
+ # A hardcoded '~/.gitwebhook.ini' (missing "s") used to make
336
+ # `serve uninstall --purge` delete the wrong file - or nothing at all.
337
+ config_path = Path(CONFIG_PATH_USER).expanduser()
342
338
 
343
339
  if not purge:
344
340
  return
@@ -9,6 +9,7 @@ from typing import Dict, Optional
9
9
 
10
10
  from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
11
11
  from gitwebhooks.models.provider import Provider
12
+ from gitwebhooks.utils.constants import RESERVED_SECTIONS
12
13
  from gitwebhooks.utils.exceptions import ConfigurationError
13
14
 
14
15
 
@@ -122,7 +123,7 @@ class ConfigLoader:
122
123
  Returns:
123
124
  Set of reserved section names
124
125
  """
125
- return {'server', 'ssl', 'github', 'gitee', 'gitlab', 'custom'}
126
+ return RESERVED_SECTIONS
126
127
 
127
128
  def get_server_config(self) -> Dict[str, any]:
128
129
  """Get server configuration
@@ -133,9 +134,16 @@ class ConfigLoader:
133
134
  - port: Listen port
134
135
  - log_file: Log file path
135
136
  """
137
+ try:
138
+ port = self.parser.getint('server', 'port', fallback=6789)
139
+ except ValueError as e:
140
+ raise ConfigurationError(
141
+ f"Invalid [server] port: {self.parser.get('server', 'port')!r} is not an integer"
142
+ ) from e
143
+
136
144
  return {
137
145
  'address': self.parser.get('server', 'address', fallback='0.0.0.0'),
138
- 'port': self.parser.getint('server', 'port', fallback=6789),
146
+ 'port': port,
139
147
  'log_file': self.parser.get('server', 'log_file', fallback='').strip(),
140
148
  }
141
149
 
@@ -150,16 +158,26 @@ class ConfigLoader:
150
158
  - cert_file: SSL certificate file path
151
159
  """
152
160
  try:
153
- if not self.parser.getboolean('ssl', 'enable'):
154
- return None
155
- except (configparser.NoSectionError, ValueError):
161
+ ssl_enabled = self.parser.getboolean('ssl', 'enable')
162
+ except configparser.NoSectionError:
156
163
  return None
164
+ except ValueError as e:
165
+ raise ConfigurationError(
166
+ f"Invalid [ssl] enable: {self.parser.get('ssl', 'enable')!r} is not a boolean"
167
+ ) from e
157
168
 
158
- return {
159
- 'enable': True,
160
- 'key_file': self.parser.get('ssl', 'key_file'),
161
- 'cert_file': self.parser.get('ssl', 'cert_file'),
162
- }
169
+ if not ssl_enabled:
170
+ return None
171
+
172
+ ssl_config = {'enable': True}
173
+ for option in ('key_file', 'cert_file'):
174
+ value = self.parser.get('ssl', option, fallback='').strip()
175
+ if not value:
176
+ raise ConfigurationError(
177
+ f"SSL is enabled but '{option}' is missing in the [ssl] section"
178
+ )
179
+ ssl_config[option] = value
180
+ return ssl_config
163
181
 
164
182
  def validate_repository_configs(self) -> None:
165
183
  """Validate all repository configurations
@@ -5,7 +5,6 @@ Defines configuration-related dataclasses.
5
5
 
6
6
  import configparser
7
7
  from dataclasses import dataclass
8
- from pathlib import Path
9
8
  from typing import List
10
9
 
11
10
  from gitwebhooks.models.provider import Provider
@@ -64,41 +64,3 @@ class ConfigurationRegistry:
64
64
  if self._repository_configs is None:
65
65
  raise ConfigurationError('Repository configs not loaded')
66
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
@@ -6,7 +6,7 @@ Defines server-related configuration dataclasses.
6
6
  import os
7
7
  from dataclasses import dataclass
8
8
  from pathlib import Path
9
- from typing import Dict, Optional
9
+ from typing import Optional
10
10
 
11
11
  from gitwebhooks.utils.exceptions import ConfigurationError
12
12
 
@@ -6,7 +6,6 @@ Defines the abstract interface for webhook handlers.
6
6
  from abc import ABC, abstractmethod
7
7
  from typing import Optional
8
8
 
9
- from gitwebhooks.models.provider import Provider
10
9
  from gitwebhooks.models.request import WebhookRequest
11
10
  from gitwebhooks.models.result import SignatureVerificationResult
12
11
  from gitwebhooks.config.models import ProviderConfig
@@ -19,15 +18,6 @@ class WebhookHandler(ABC):
19
18
  All platform-specific handlers must inherit from this class and implement all abstract methods.
20
19
  """
21
20
 
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
21
  @abstractmethod
32
22
  def verify_signature(self, request: WebhookRequest,
33
23
  config: ProviderConfig) -> SignatureVerificationResult: