-
Notifications
You must be signed in to change notification settings - Fork 271
Expand file tree
/
Copy pathwebhook.py
More file actions
203 lines (153 loc) · 6.41 KB
/
Copy pathwebhook.py
File metadata and controls
203 lines (153 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import base64
import hmac
from hashlib import sha256
from typing import (
TYPE_CHECKING,
Dict,
Optional,
overload,
)
from replicate.resource import Namespace, Resource
if TYPE_CHECKING:
import httpx
class WebhookSigningSecret(Resource):
"""
A webhook signing secret.
"""
key: str
class WebhookValidationError(ValueError):
"""Base exception for webhook validation errors."""
class MissingWebhookHeaderError(WebhookValidationError):
"""Exception raised when a required webhook header is missing."""
class InvalidSecretKeyError(WebhookValidationError):
"""Exception raised when the secret key format is invalid."""
class MissingWebhookBodyError(WebhookValidationError):
"""Exception raised when the webhook body is missing."""
class InvalidTimestampError(WebhookValidationError):
"""Exception raised when the webhook timestamp is invalid or outside the tolerance."""
class InvalidSignatureError(WebhookValidationError):
"""Exception raised when the webhook signature is invalid."""
class Webhooks(Namespace):
"""
Namespace for operations related to webhooks.
"""
@property
def default(self) -> "Webhooks.Default":
"""
Namespace for operations related to the default webhook.
"""
return self.Default(self._client)
class Default(Namespace):
"""
Namespace for operations related to the default webhook.
"""
def secret(self) -> WebhookSigningSecret:
"""
Get the default webhook signing secret.
Returns:
WebhookSigningSecret: The default webhook signing secret.
"""
resp = self._client._request("GET", "/v1/webhooks/default/secret")
return WebhookSigningSecret(**resp.json())
async def async_secret(self) -> WebhookSigningSecret:
"""
Get the default webhook signing secret.
Returns:
WebhookSigningSecret: The default webhook signing secret.
"""
resp = await self._client._async_request(
"GET", "/v1/webhooks/default/secret"
)
return WebhookSigningSecret(**resp.json())
@overload
@staticmethod
def validate(
request: "httpx.Request",
secret: WebhookSigningSecret,
tolerance: Optional[int] = None,
) -> bool: ...
@overload
@staticmethod
def validate(
headers: Dict[str, str],
body: str,
secret: WebhookSigningSecret,
tolerance: Optional[int] = None,
) -> bool: ...
@staticmethod
def validate( # type: ignore # pylint: disable=too-many-branches,too-many-locals
request: Optional["httpx.Request"] = None,
headers: Optional[Dict[str, str]] = None,
body: Optional[str] = None,
secret: Optional[WebhookSigningSecret] = None,
tolerance: Optional[int] = None,
) -> None:
"""
Validate the signature from an incoming webhook request using the provided secret.
Args:
request (httpx.Request): The request object.
headers (Dict[str, str]): The request headers.
body (str): The request body.
secret (WebhookSigningSecret): The webhook signing secret.
tolerance (Optional[int]): Maximum allowed time difference (in seconds) between the current time and the webhook timestamp.
Returns:
None: If the request is valid.
Raises:
MissingWebhookHeaderError: If required webhook headers are missing.
InvalidSecretKeyError: If the secret key format is invalid.
MissingWebhookBodyError: If the webhook body is missing.
InvalidTimestampError: If the webhook timestamp is invalid or outside the tolerance.
InvalidSignatureError: If the webhook signature is invalid.
"""
if not secret:
raise ValueError("Missing webhook signing secret")
if request and any([headers, body]):
raise ValueError("Only one of request or headers/body can be provided")
if request and request.headers:
webhook_id = request.headers.get("webhook-id")
timestamp = request.headers.get("webhook-timestamp")
signature = request.headers.get("webhook-signature")
body = request.content.decode("utf-8")
else:
if not headers:
raise MissingWebhookHeaderError("Missing webhook headers")
# Convert headers to case-insensitive dictionary
headers = {k.lower(): v for k, v in headers.items()}
webhook_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signature = headers.get("webhook-signature")
if not webhook_id:
raise MissingWebhookHeaderError("Missing webhook id")
if not timestamp:
raise MissingWebhookHeaderError("Missing webhook timestamp")
if not signature:
raise MissingWebhookHeaderError("Missing webhook signature")
if not body:
raise MissingWebhookBodyError("Missing webhook body")
if tolerance is not None:
import time # pylint: disable=import-outside-toplevel
current_time = int(time.time())
webhook_time = int(timestamp)
time_difference = abs(current_time - webhook_time)
if time_difference > tolerance:
raise InvalidTimestampError(
f"Webhook timestamp is outside the allowed tolerance of {tolerance} seconds"
)
signed_content = f"{webhook_id}.{timestamp}.{body}"
key_parts = secret.key.split("_")
if len(key_parts) != 2:
raise InvalidSecretKeyError(f"Invalid secret key format: {secret.key}")
secret_bytes = base64.b64decode(key_parts[1])
h = hmac.new(secret_bytes, signed_content.encode(), sha256)
computed_signature = h.digest()
valid = False
for sig in signature.split():
sig_parts = sig.split(",")
if len(sig_parts) < 2:
raise InvalidSignatureError(f"Invalid signature format: {sig}")
sig_bytes = base64.b64decode(sig_parts[1])
if hmac.compare_digest(sig_bytes, computed_signature):
valid = True
break
if not valid:
raise InvalidSignatureError("Webhook signature is invalid")