|
27 | 27 | # RFC 9110 auth-param: token BWS "=" BWS ( token / quoted-string ) |
28 | 28 | # Matches: key="value" or key=token, handles escaped quotes in quoted strings |
29 | 29 | _AUTH_PARAM_RE = re.compile(r'([a-zA-Z_][\w-]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))') |
| 30 | +# Syntax-level Payment Auth grammar. Supported-method dispatch is handled after parsing. |
| 31 | +_PAYMENT_METHOD_ID_RE = re.compile(r"^[a-z]+$") |
30 | 32 |
|
31 | 33 |
|
32 | 34 | class ParseError(Exception): |
@@ -75,6 +77,12 @@ def _unescape_quoted(s: str) -> str: |
75 | 77 | return re.sub(r"\\(.)", r"\1", s) |
76 | 78 |
|
77 | 79 |
|
| 80 | +def _validate_payment_method_id(method: str) -> None: |
| 81 | + """Validate payment-method-id = 1*LOWERALPHA.""" |
| 82 | + if not _PAYMENT_METHOD_ID_RE.fullmatch(method): |
| 83 | + raise ParseError(f"Invalid payment method id: {method!r}") |
| 84 | + |
| 85 | + |
78 | 86 | def _parse_auth_params(params_str: str) -> dict[str, str]: |
79 | 87 | """Parse RFC 9110 auth-params: key="value" or key=token pairs.""" |
80 | 88 | params: dict[str, str] = {} |
@@ -119,6 +127,7 @@ def parse_www_authenticate(header: str) -> Challenge: |
119 | 127 | method = params.get("method") |
120 | 128 | if not method: |
121 | 129 | raise ParseError("Missing 'method' field") |
| 130 | + _validate_payment_method_id(method) |
122 | 131 |
|
123 | 132 | intent = params.get("intent") |
124 | 133 | if not intent: |
@@ -214,10 +223,13 @@ def parse_authorization(header: str) -> Credential: |
214 | 223 | if "id" not in challenge_data: |
215 | 224 | raise ParseError("Credential challenge missing required field: id") |
216 | 225 |
|
| 226 | + method = str(challenge_data.get("method", "")) |
| 227 | + _validate_payment_method_id(method) |
| 228 | + |
217 | 229 | echo = ChallengeEcho( |
218 | 230 | id=str(challenge_data["id"]), |
219 | 231 | realm=str(challenge_data.get("realm", "")), |
220 | | - method=str(challenge_data.get("method", "")), |
| 232 | + method=method, |
221 | 233 | intent=str(challenge_data.get("intent", "")), |
222 | 234 | request=str(challenge_data.get("request", "")), |
223 | 235 | expires=str(challenge_data["expires"]) if challenge_data.get("expires") else None, |
@@ -302,14 +314,16 @@ def parse_payment_receipt(header: str) -> Receipt: |
302 | 314 | raise ParseError("Invalid receipt status") |
303 | 315 |
|
304 | 316 | timestamp = _parse_timestamp(str(data["timestamp"])) |
| 317 | + method = str(data["method"]) |
| 318 | + _validate_payment_method_id(method) |
305 | 319 |
|
306 | 320 | extra = data.get("extra") |
307 | 321 |
|
308 | 322 | return Receipt( |
309 | 323 | status=status, |
310 | 324 | timestamp=timestamp, |
311 | 325 | reference=str(data["reference"]), |
312 | | - method=str(data.get("method", "")), |
| 326 | + method=method, |
313 | 327 | external_id=str(data["externalId"]) if data.get("externalId") else None, |
314 | 328 | extra=extra if isinstance(extra, dict) else None, |
315 | 329 | ) |
|
0 commit comments