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
gitwebhooks/__init__.py
ADDED
|
@@ -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
|
+
]
|
gitwebhooks/__main__.py
ADDED
|
@@ -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}')
|