From 90edc2e2fcf7fb50bf8175ce8aee9f91561a5a12 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Thu, 12 Oct 2023 22:58:47 +0000 Subject: [PATCH] feat: make webhook headers case insensitive --- src/finch/_utils/__init__.py | 1 + src/finch/_utils/_utils.py | 22 +++++++++++++++++++++- src/finch/resources/webhooks.py | 27 +++++++-------------------- tests/api_resources/test_webhooks.py | 4 ++-- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/finch/_utils/__init__.py b/src/finch/_utils/__init__.py index 6d13a362..26dc560b 100644 --- a/src/finch/_utils/__init__.py +++ b/src/finch/_utils/__init__.py @@ -22,6 +22,7 @@ from ._utils import is_required_type as is_required_type from ._utils import is_annotated_type as is_annotated_type from ._utils import maybe_coerce_float as maybe_coerce_float +from ._utils import get_required_header as get_required_header from ._utils import maybe_coerce_boolean as maybe_coerce_boolean from ._utils import maybe_coerce_integer as maybe_coerce_integer from ._utils import strip_annotated_type as strip_annotated_type diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index e43ef6f8..d4eafd4c 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -1,13 +1,14 @@ from __future__ import annotations import os +import re import inspect import functools from typing import Any, Mapping, TypeVar, Callable, Iterable, Sequence, cast, overload from pathlib import Path from typing_extensions import Required, Annotated, TypeGuard, get_args, get_origin -from .._types import NotGiven, FileTypes, NotGivenOr +from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike from .._compat import is_union as _is_union from .._compat import parse_date as parse_date from .._compat import parse_datetime as parse_datetime @@ -351,3 +352,22 @@ def file_from_path(path: str) -> FileTypes: contents = Path(path).read_bytes() file_name = os.path.basename(path) return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if isinstance(headers, Mapping): + headers = cast(Headers, headers) + for k, v in headers.items(): + if k.lower() == lower_header and isinstance(v, str): + return v + + """ to deal with the case where the header looks like Finch-Event-Id """ + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") diff --git a/src/finch/resources/webhooks.py b/src/finch/resources/webhooks.py index f5388ae5..2948150e 100644 --- a/src/finch/resources/webhooks.py +++ b/src/finch/resources/webhooks.py @@ -10,6 +10,7 @@ from datetime import datetime, timezone, timedelta from .._types import HeadersLike +from .._utils import get_required_header from .._resource import SyncAPIResource, AsyncAPIResource __all__ = ["Webhooks", "AsyncWebhooks"] @@ -51,13 +52,8 @@ def verify_signature( except Exception: raise ValueError("Bad secret") - msg_id = headers.get("finch-event-id") - if not msg_id: - raise ValueError("Could not find finch-event-id header") - - msg_timestamp = headers.get("finch-timestamp") - if not msg_timestamp: - raise ValueError("Could not find finch-timestamp header") + msg_id = get_required_header(headers, "finch-event-id") + msg_timestamp = get_required_header(headers, "finch-timestamp") # validate the timestamp webhook_tolerance = timedelta(minutes=5) @@ -88,9 +84,7 @@ def verify_signature( to_sign = f"{msg_id}.{timestamp_str}.{body}".encode() expected_signature = hmac.new(parsedSecret, to_sign, hashlib.sha256).digest() - msg_signature = headers.get("finch-signature") - if not msg_signature: - raise ValueError("Could not find finch-signature header") + msg_signature = get_required_header(headers, "finch-signature") # Signature header can contain multiple signatures delimited by spaces passed_sigs = msg_signature.split(" ") @@ -151,13 +145,8 @@ def verify_signature( except Exception: raise ValueError("Bad secret") - msg_id = headers.get("finch-event-id") - if not msg_id: - raise ValueError("Could not find finch-event-id header") - - msg_timestamp = headers.get("finch-timestamp") - if not msg_timestamp: - raise ValueError("Could not find finch-timestamp header") + msg_id = get_required_header(headers, "finch-event-id") + msg_timestamp = get_required_header(headers, "finch-timestamp") # validate the timestamp webhook_tolerance = timedelta(minutes=5) @@ -188,9 +177,7 @@ def verify_signature( to_sign = f"{msg_id}.{timestamp_str}.{body}".encode() expected_signature = hmac.new(parsedSecret, to_sign, hashlib.sha256).digest() - msg_signature = headers.get("finch-signature") - if not msg_signature: - raise ValueError("Could not find finch-signature header") + msg_signature = get_required_header(headers, "finch-signature") # Signature header can contain multiple signatures delimited by spaces passed_sigs = msg_signature.split(" ") diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py index 4496ea58..8a0a5ce1 100644 --- a/tests/api_resources/test_webhooks.py +++ b/tests/api_resources/test_webhooks.py @@ -27,7 +27,7 @@ class TestWebhooks: payload = """{"company_id":"720be419-0293-4d32-a707-32179b0827ab"}""" signature = "m7y0TV2C+hlHxU42wCieApTSTaA8/047OAplBqxIV/s=" headers = { - "finch-event-id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj", + "Finch-Event-Id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj", "finch-timestamp": timestamp, "finch-signature": f"v1,{signature}", } @@ -127,7 +127,7 @@ class TestAsyncWebhooks: payload = """{"company_id":"720be419-0293-4d32-a707-32179b0827ab"}""" signature = "m7y0TV2C+hlHxU42wCieApTSTaA8/047OAplBqxIV/s=" headers = { - "finch-event-id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj", + "Finch-Event-Id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj", "finch-timestamp": timestamp, "finch-signature": f"v1,{signature}", }