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.
Files changed (40) hide show
  1. gitwebhooks/__init__.py +18 -0
  2. gitwebhooks/__main__.py +10 -0
  3. gitwebhooks/auth/__init__.py +20 -0
  4. gitwebhooks/auth/custom.py +40 -0
  5. gitwebhooks/auth/factory.py +73 -0
  6. gitwebhooks/auth/gitee.py +86 -0
  7. gitwebhooks/auth/github.py +52 -0
  8. gitwebhooks/auth/gitlab.py +29 -0
  9. gitwebhooks/auth/verifier.py +34 -0
  10. gitwebhooks/cli.py +107 -0
  11. gitwebhooks/config/__init__.py +17 -0
  12. gitwebhooks/config/loader.py +170 -0
  13. gitwebhooks/config/models.py +145 -0
  14. gitwebhooks/config/registry.py +104 -0
  15. gitwebhooks/config/server.py +85 -0
  16. gitwebhooks/handlers/__init__.py +22 -0
  17. gitwebhooks/handlers/base.py +107 -0
  18. gitwebhooks/handlers/custom.py +73 -0
  19. gitwebhooks/handlers/factory.py +83 -0
  20. gitwebhooks/handlers/gitee.py +113 -0
  21. gitwebhooks/handlers/github.py +43 -0
  22. gitwebhooks/handlers/gitlab.py +43 -0
  23. gitwebhooks/handlers/request.py +248 -0
  24. gitwebhooks/logging/__init__.py +10 -0
  25. gitwebhooks/logging/setup.py +57 -0
  26. gitwebhooks/models/__init__.py +14 -0
  27. gitwebhooks/models/provider.py +22 -0
  28. gitwebhooks/models/request.py +61 -0
  29. gitwebhooks/models/result.py +40 -0
  30. gitwebhooks/server.py +128 -0
  31. gitwebhooks/utils/__init__.py +14 -0
  32. gitwebhooks/utils/constants.py +42 -0
  33. gitwebhooks/utils/exceptions.py +34 -0
  34. gitwebhooks/utils/executor.py +34 -0
  35. gitwebhooks-2.0.0.dist-info/METADATA +312 -0
  36. gitwebhooks-2.0.0.dist-info/RECORD +40 -0
  37. gitwebhooks-2.0.0.dist-info/WHEEL +5 -0
  38. gitwebhooks-2.0.0.dist-info/entry_points.txt +2 -0
  39. gitwebhooks-2.0.0.dist-info/licenses/LICENSE +21 -0
  40. gitwebhooks-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,18 @@
1
+ """Git Webhooks Server - Modular package structure.
2
+
3
+ Automated deployment server supporting GitHub, Gitee, GitLab, and custom webhooks.
4
+ """
5
+
6
+ __version__ = '2.0.0'
7
+
8
+ # Export main classes for convenient import paths
9
+ from .server import WebhookServer
10
+ from .models.provider import Provider
11
+ from .models.request import WebhookRequest
12
+
13
+ __all__ = [
14
+ '__version__',
15
+ 'WebhookServer',
16
+ 'Provider',
17
+ 'WebhookRequest',
18
+ ]
@@ -0,0 +1,10 @@
1
+ """Python module entry point
2
+
3
+ Supports running the server using `python3 -m gitwebhooks.cli`.
4
+ """
5
+
6
+ from gitwebhooks.cli import main
7
+ import sys
8
+
9
+ if __name__ == '__main__':
10
+ sys.exit(main())
@@ -0,0 +1,20 @@
1
+ """Authentication and signature verification module
2
+
3
+ Verifies signatures and tokens for webhook requests.
4
+ """
5
+
6
+ from .verifier import SignatureVerifier
7
+ from .github import GithubSignatureVerifier
8
+ from .gitee import GiteeSignatureVerifier
9
+ from .gitlab import GitlabTokenVerifier
10
+ from .custom import CustomTokenVerifier
11
+ from .factory import VerifierFactory
12
+
13
+ __all__ = [
14
+ 'SignatureVerifier',
15
+ 'GithubSignatureVerifier',
16
+ 'GiteeSignatureVerifier',
17
+ 'GitlabTokenVerifier',
18
+ 'CustomTokenVerifier',
19
+ 'VerifierFactory',
20
+ ]
@@ -0,0 +1,40 @@
1
+ """Custom token verifier
2
+
3
+ Verifies tokens from custom webhooks.
4
+ """
5
+
6
+ from gitwebhooks.auth.verifier import SignatureVerifier
7
+ from gitwebhooks.models.result import SignatureVerificationResult
8
+
9
+
10
+ class CustomTokenVerifier(SignatureVerifier):
11
+ """Custom token verifier"""
12
+
13
+ def __init__(self, verify_enabled: bool = True):
14
+ """Initialize verifier
15
+
16
+ Args:
17
+ verify_enabled: Whether verification is enabled (False = always return success)
18
+ """
19
+ self.verify_enabled = verify_enabled
20
+
21
+ def verify(self, payload: bytes, signature: str, secret: str,
22
+ **kwargs) -> SignatureVerificationResult:
23
+ """Verify custom token
24
+
25
+ Args:
26
+ payload: Raw request body bytes (unused)
27
+ signature: Token value from custom header
28
+ secret: Configured secret key
29
+ **kwargs: Unused
30
+
31
+ Returns:
32
+ SignatureVerificationResult instance
33
+ """
34
+ if not self.verify_enabled:
35
+ return SignatureVerificationResult.success()
36
+
37
+ if signature == secret:
38
+ return SignatureVerificationResult.success()
39
+ else:
40
+ return SignatureVerificationResult.failure('Invalid token')
@@ -0,0 +1,73 @@
1
+ """Verifier factory class
2
+
3
+ Creates corresponding verifier instances based on provider type.
4
+ """
5
+
6
+ from gitwebhooks.models.provider import Provider
7
+ from gitwebhooks.auth.verifier import SignatureVerifier
8
+ from gitwebhooks.auth.github import GithubSignatureVerifier
9
+ from gitwebhooks.auth.gitee import GiteeSignatureVerifier
10
+ from gitwebhooks.auth.gitlab import GitlabTokenVerifier
11
+ from gitwebhooks.auth.custom import CustomTokenVerifier
12
+ from gitwebhooks.utils.exceptions import UnsupportedProviderError
13
+
14
+
15
+ class VerifierFactory:
16
+ """Verifier factory class
17
+
18
+ Creates corresponding verifier instances based on provider type
19
+ """
20
+
21
+ _verifiers = {
22
+ Provider.GITHUB: GithubSignatureVerifier(),
23
+ Provider.GITEE: GiteeSignatureVerifier(),
24
+ Provider.GITLAB: GitlabTokenVerifier(),
25
+ Provider.CUSTOM: None, # Custom verifier created dynamically
26
+ }
27
+
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
+ @classmethod
52
+ def create_github_verifier(cls) -> GithubSignatureVerifier:
53
+ """Create GitHub verifier (convenience method)"""
54
+ return GithubSignatureVerifier()
55
+
56
+ @classmethod
57
+ def create_gitee_verifier(cls) -> GiteeSignatureVerifier:
58
+ """Create Gitee verifier (convenience method)"""
59
+ return GiteeSignatureVerifier()
60
+
61
+ @classmethod
62
+ def create_gitlab_verifier(cls) -> GitlabTokenVerifier:
63
+ """Create GitLab verifier (convenience method)"""
64
+ return GitlabTokenVerifier()
65
+
66
+ @classmethod
67
+ def create_custom_verifier(cls, verify_enabled: bool = True) -> CustomTokenVerifier:
68
+ """Create custom verifier (convenience method)
69
+
70
+ Args:
71
+ verify_enabled: Whether verification is enabled
72
+ """
73
+ return CustomTokenVerifier(verify_enabled=verify_enabled)
@@ -0,0 +1,86 @@
1
+ """Gitee HMAC-SHA256/password verifier
2
+
3
+ Verifies signatures or passwords from Gitee webhooks.
4
+
5
+ According to Gitee official documentation:
6
+ https://help.gitee.com/webhook/how-to-verify-webhook-keys
7
+
8
+ Signature generation algorithm:
9
+ 1. sign_string = timestamp + "\\n" + secret
10
+ 2. hmac_sha256 = HmacSHA256(sign_string)
11
+ 3. signature = base64(urlEncode(hmac_sha256))
12
+
13
+ Note: The Gitee signature does NOT include the request payload.
14
+ """
15
+
16
+ import base64
17
+ import hashlib
18
+ import hmac
19
+
20
+ from gitwebhooks.auth.verifier import SignatureVerifier
21
+ from gitwebhooks.models.result import SignatureVerificationResult
22
+
23
+
24
+ class GiteeSignatureVerifier(SignatureVerifier):
25
+ """Gitee HMAC-SHA256/password verifier"""
26
+
27
+ HASH_ALGORITHM = hashlib.sha256
28
+
29
+ def verify(self, payload: bytes, signature: str, secret: str,
30
+ **kwargs) -> SignatureVerificationResult:
31
+ """Verify Gitee signature or password
32
+
33
+ Gitee sends X-Gitee-Timestamp in both signature and password modes.
34
+ To distinguish between them:
35
+ 1. First try password validation (direct comparison)
36
+ 2. If password fails and timestamp exists, try signature validation
37
+
38
+ This approach works because:
39
+ - Passwords are typically short plaintext without special chars
40
+ - Signatures are Base64 encoded, containing chars like '+', '/', '='
41
+
42
+ Args:
43
+ payload: Raw request body bytes (not used in Gitee signature)
44
+ signature: X-Gitee-Token header value
45
+ secret: Webhook secret/password
46
+ **kwargs: May contain 'timestamp' key
47
+
48
+ Returns:
49
+ SignatureVerificationResult instance
50
+ """
51
+ if signature is None:
52
+ return SignatureVerificationResult.failure('Missing signature or password')
53
+
54
+ # Step 1: Try password validation first (direct comparison)
55
+ if hmac.compare_digest(signature, secret):
56
+ return SignatureVerificationResult.success()
57
+
58
+ # Step 2: If password failed, try signature validation
59
+ timestamp = kwargs.get('timestamp')
60
+ if timestamp is not None:
61
+ # Verify signature
62
+ try:
63
+ timestamp_int = int(timestamp)
64
+ except ValueError:
65
+ return SignatureVerificationResult.failure('Invalid timestamp format')
66
+
67
+ # Gitee signature: timestamp + "\n" + secret
68
+ # Reference: https://help.gitee.com/webhook/how-to-verify-webhook-keys
69
+ sign_string = f'{timestamp_int}\n{secret}'
70
+ secret_bytes = secret.encode('utf-8')
71
+
72
+ signature_bytes = hmac.new(
73
+ secret_bytes,
74
+ sign_string.encode('utf-8'),
75
+ self.HASH_ALGORITHM
76
+ ).digest()
77
+ # Gitee sends Base64 encoded signature (NOT URL encoded)
78
+ expected_signature = base64.b64encode(signature_bytes).decode('utf-8')
79
+
80
+ if hmac.compare_digest(signature, expected_signature):
81
+ return SignatureVerificationResult.success()
82
+ else:
83
+ return SignatureVerificationResult.failure('Invalid signature')
84
+
85
+ # Password validation failed and no timestamp for signature validation
86
+ return SignatureVerificationResult.failure('Invalid password')
@@ -0,0 +1,52 @@
1
+ """GitHub HMAC-SHA1 signature verifier
2
+
3
+ Verifies HMAC-SHA1 signatures from GitHub webhooks.
4
+ """
5
+
6
+ import hashlib
7
+ import hmac
8
+
9
+ from gitwebhooks.auth.verifier import SignatureVerifier
10
+ from gitwebhooks.models.result import SignatureVerificationResult
11
+
12
+
13
+ class GithubSignatureVerifier(SignatureVerifier):
14
+ """GitHub HMAC-SHA1 signature verifier"""
15
+
16
+ PREFIX = 'sha1='
17
+ HASH_ALGORITHM = hashlib.sha1
18
+
19
+ def verify(self, payload: bytes, signature: str, secret: str,
20
+ **kwargs) -> SignatureVerificationResult:
21
+ """Verify GitHub HMAC-SHA1 signature
22
+
23
+ Args:
24
+ payload: Raw request body bytes
25
+ signature: X-Hub-Signature header value
26
+ secret: Webhook secret
27
+ **kwargs: Unused
28
+
29
+ Returns:
30
+ SignatureVerificationResult instance
31
+ """
32
+ if signature is None:
33
+ return SignatureVerificationResult.failure('Missing signature')
34
+
35
+ if not secret:
36
+ return SignatureVerificationResult.failure('Secret not configured')
37
+
38
+ if not signature.startswith(self.PREFIX):
39
+ return SignatureVerificationResult.failure('Invalid signature format')
40
+
41
+ secret_bytes = secret.encode('utf-8')
42
+ expected_signature = self.PREFIX + hmac.new(
43
+ secret_bytes,
44
+ payload,
45
+ self.HASH_ALGORITHM
46
+ ).hexdigest()
47
+
48
+ # Use constant-time comparison
49
+ if hmac.compare_digest(signature, expected_signature):
50
+ return SignatureVerificationResult.success()
51
+ else:
52
+ return SignatureVerificationResult.failure('Invalid signature')
@@ -0,0 +1,29 @@
1
+ """GitLab token verifier
2
+
3
+ Verifies tokens from GitLab webhooks.
4
+ """
5
+
6
+ from gitwebhooks.auth.verifier import SignatureVerifier
7
+ from gitwebhooks.models.result import SignatureVerificationResult
8
+
9
+
10
+ class GitlabTokenVerifier(SignatureVerifier):
11
+ """GitLab token verifier"""
12
+
13
+ def verify(self, payload: bytes, signature: str, secret: str,
14
+ **kwargs) -> SignatureVerificationResult:
15
+ """Verify GitLab token
16
+
17
+ Args:
18
+ payload: Raw request body bytes (unused)
19
+ signature: X-Gitlab-Token header value
20
+ secret: Webhook token
21
+ **kwargs: Unused
22
+
23
+ Returns:
24
+ SignatureVerificationResult instance
25
+ """
26
+ if signature == secret:
27
+ return SignatureVerificationResult.success()
28
+ else:
29
+ return SignatureVerificationResult.failure('Invalid token')
@@ -0,0 +1,34 @@
1
+ """Signature verifier base class
2
+
3
+ Defines the abstract interface for signature verification.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ from gitwebhooks.models.result import SignatureVerificationResult
9
+
10
+
11
+ class SignatureVerifier(ABC):
12
+ """Signature verifier base class
13
+
14
+ All platform-specific verifiers must inherit from this class and implement the verification method.
15
+ """
16
+
17
+ @abstractmethod
18
+ def verify(self, payload: bytes, signature: str, secret: str,
19
+ **kwargs) -> SignatureVerificationResult:
20
+ """Verify signature
21
+
22
+ Args:
23
+ payload: Raw request body bytes
24
+ signature: Signature string from request
25
+ secret: Configured secret key
26
+ **kwargs: Platform-specific additional parameters (e.g., timestamp)
27
+
28
+ Returns:
29
+ SignatureVerificationResult instance
30
+
31
+ Raises:
32
+ SignatureValidationError: When verification fails (optional)
33
+ """
34
+ pass
gitwebhooks/cli.py ADDED
@@ -0,0 +1,107 @@
1
+ """CLI main function
2
+
3
+ Command-line interface module providing compatibility with the original git-webhooks-server.py.
4
+ """
5
+
6
+ import getopt
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ from gitwebhooks.server import WebhookServer
12
+ from gitwebhooks.utils.exceptions import ConfigurationError
13
+
14
+
15
+ def main(argv: List[str] = None) -> int:
16
+ """Main entry point
17
+
18
+ Args:
19
+ argv: Command-line argument list (defaults to sys.argv[1:])
20
+
21
+ Returns:
22
+ Exit code (0 = success, 1 = error)
23
+
24
+ Raises:
25
+ SystemExit: Exit on error
26
+
27
+ Command Line Arguments:
28
+ -h, --help Show help message and exit
29
+ -c, --config Specify configuration file path
30
+
31
+ Default Config Path:
32
+ /usr/local/etc/git-webhooks-server.ini
33
+
34
+ Examples:
35
+ python3 -m gitwebhooks.cli -c /path/to/config.ini
36
+ gitwebhooks-cli --config /etc/webhooks.ini
37
+ """
38
+ if argv is None:
39
+ argv = sys.argv[1:]
40
+
41
+ # Default configuration file path
42
+ default_config = '/usr/local/etc/git-webhooks-server.ini'
43
+ config_file = default_config
44
+
45
+ # Parse command-line arguments
46
+ try:
47
+ opts, _ = getopt.getopt(argv, 'hc:', ['config=', 'help'])
48
+ except getopt.GetoptError:
49
+ print_help()
50
+ return 1
51
+
52
+ for opt, value in opts:
53
+ if opt in ('-h', '--help'):
54
+ print_help()
55
+ return 0
56
+ elif opt in ('-c', '--config'):
57
+ config_file = value
58
+
59
+ # Check configuration file exists
60
+ if not Path(config_file).exists():
61
+ print(f'Error: Configuration file not found: {config_file}', file=sys.stderr)
62
+ return 1
63
+
64
+ # Create and run server
65
+ try:
66
+ server = WebhookServer(config_path=config_file)
67
+ server.run()
68
+ return 0
69
+ except ConfigurationError as e:
70
+ print(f'Configuration error: {e}', file=sys.stderr)
71
+ return 1
72
+ except KeyboardInterrupt:
73
+ print('\nServer stopped by user')
74
+ return 0
75
+ except Exception as e:
76
+ print(f'Error: {e}', file=sys.stderr)
77
+ return 1
78
+
79
+
80
+ def print_help() -> None:
81
+ """Print help information
82
+
83
+ Display command-line usage instructions
84
+ """
85
+ print('Git Webhooks Server - Automated deployment webhook handler')
86
+ print()
87
+ print('Usage:')
88
+ print(' gitwebhooks-cli -c <configuration_file>')
89
+ print(' gitwebhooks-cli --config=<configuration_file>')
90
+ print(' gitwebhooks-cli --help')
91
+ print()
92
+ print('Options:')
93
+ print(' -h, --help Show this help message and exit')
94
+ print(' -c, --config Path to INI configuration file')
95
+ print()
96
+ print('Default configuration path:')
97
+ print(' /usr/local/etc/git-webhooks-server.ini')
98
+ print()
99
+ print('Examples:')
100
+ print(' gitwebhooks-cli -c /etc/webhooks.ini')
101
+ print(' gitwebhooks-cli --config=./my-config.ini')
102
+ print()
103
+ print('Report bugs to: https://github.com/yourusername/git-webhooks-server/issues')
104
+
105
+
106
+ if __name__ == '__main__':
107
+ sys.exit(main())
@@ -0,0 +1,17 @@
1
+ """Configuration management module
2
+
3
+ Responsible for loading, parsing, and managing INI format configuration files.
4
+ """
5
+
6
+ from .loader import ConfigLoader
7
+ from .models import ProviderConfig, RepositoryConfig
8
+ from .registry import ConfigurationRegistry
9
+ from .server import ServerConfig
10
+
11
+ __all__ = [
12
+ 'ConfigLoader',
13
+ 'ProviderConfig',
14
+ 'RepositoryConfig',
15
+ 'ServerConfig',
16
+ 'ConfigurationRegistry',
17
+ ]
@@ -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}')