-
Notifications
You must be signed in to change notification settings - Fork 174
feat(BA-2257): Forward client Date header through Webserver proxy #11335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Forward the client's `Date` header through `Webserver` proxy by accepting per-call header overrides on `Request.fetch()`, fixing signature-based authentication that binds the signature to the `Date` header. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |||||||||||||||||
| import appdirs | ||||||||||||||||||
| import attrs | ||||||||||||||||||
| from aiohttp.client import _RequestContextManager, _WSRequestContextManager | ||||||||||||||||||
| from dateutil.parser import parse as parse_datetime | ||||||||||||||||||
| from dateutil.tz import tzutc | ||||||||||||||||||
| from multidict import CIMultiDict | ||||||||||||||||||
| from yarl import URL | ||||||||||||||||||
|
|
@@ -277,6 +278,20 @@ def _pack_content(self) -> RequestContent | aiohttp.FormData: | |||||||||||||||||
| return data | ||||||||||||||||||
| return self._content | ||||||||||||||||||
|
|
||||||||||||||||||
| def _apply_header_overrides(self, overrides: Mapping[str, str]) -> None: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Apply caller-supplied header overrides on top of the auto-set headers. | ||||||||||||||||||
|
|
||||||||||||||||||
| If ``Date`` is overridden, parse it back into ``self.date`` so that | ||||||||||||||||||
| request signing uses the same value the upstream server will see. | ||||||||||||||||||
| Required for use cases like proxying a pre-signed request where the | ||||||||||||||||||
| original ``Date`` must be preserved end-to-end. | ||||||||||||||||||
| """ | ||||||||||||||||||
| for key, value in overrides.items(): | ||||||||||||||||||
| self.headers[key] = value | ||||||||||||||||||
| if key.lower() == "date": | ||||||||||||||||||
| self.date = parse_datetime(value) | ||||||||||||||||||
|
||||||||||||||||||
| self.date = parse_datetime(value) | |
| try: | |
| parsed_date = parse_datetime(value) | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError(f"Invalid Date header override: {value!r}") from exc | |
| if parsed_date.tzinfo is None: | |
| parsed_date = parsed_date.replace(tzinfo=tzutc()) | |
| self.date = parsed_date |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New behavior was added for connect_websocket(..., headers=...) and connect_events(..., headers=...), but the unit tests added here only cover fetch(). Since these are public entry points and apply overrides (including special-casing Date), consider adding small unit tests that assert header overrides are applied and self.date is updated (or not) for WebSocket/SSE as well.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -255,7 +255,13 @@ async def web_handler( | |||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
| if (value := frontend_rqst.headers.get(key)) is not None: | ||||||||||||||||||||||||||||||||||||||||||
| backend_rqst.headers[key] = value | ||||||||||||||||||||||||||||||||||||||||||
| async with backend_rqst.fetch() as backend_resp: | ||||||||||||||||||||||||||||||||||||||||||
| # Preserve the client's original Date header so that signature-based | ||||||||||||||||||||||||||||||||||||||||||
| # auth schemes that bind the signature to Date keep working through | ||||||||||||||||||||||||||||||||||||||||||
| # the proxy. fetch() otherwise refreshes Date unconditionally. | ||||||||||||||||||||||||||||||||||||||||||
| fetch_header_overrides: dict[str, str] = {} | ||||||||||||||||||||||||||||||||||||||||||
| if (client_date := frontend_rqst.headers.get("Date")) is not None: | ||||||||||||||||||||||||||||||||||||||||||
| fetch_header_overrides["Date"] = client_date | ||||||||||||||||||||||||||||||||||||||||||
| async with backend_rqst.fetch(headers=fetch_header_overrides) as backend_resp: | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| # Preserve the client's original Date header so that signature-based | |
| # auth schemes that bind the signature to Date keep working through | |
| # the proxy. fetch() otherwise refreshes Date unconditionally. | |
| fetch_header_overrides: dict[str, str] = {} | |
| if (client_date := frontend_rqst.headers.get("Date")) is not None: | |
| fetch_header_overrides["Date"] = client_date | |
| async with backend_rqst.fetch(headers=fetch_header_overrides) as backend_resp: | |
| def _build_fetch_header_overrides(headers: CIMultiDict[str]) -> dict[str, str]: | |
| # Preserve the client's original Date header so that signature-based | |
| # auth schemes that bind the signature to Date keep working through | |
| # the proxy. fetch() otherwise refreshes Date unconditionally. | |
| fetch_header_overrides: dict[str, str] = {} | |
| if (client_date := headers.get("Date")) is not None: | |
| fetch_header_overrides["Date"] = client_date | |
| return fetch_header_overrides | |
| async with backend_rqst.fetch( | |
| headers=_build_fetch_header_overrides(frontend_rqst.headers) | |
| ) as backend_resp: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overriding the on-the-wire
Dateheader can break HMAC signing when the override is not already in ISO-8601 format:generate_signature()signs withdate.isoformat(), while the Manager verifies signatures using the rawDateheader string (seesrc/ai/backend/manager/api/rest/middleware/auth.py:396-405, which usesrequest["raw_date"]). With the current override behavior, a value like RFC1123 (Tue, 02 Sep ... GMT) will produce a different signed string than what the server reconstructs. Consider adjusting the signing path to use the exact header value that will be sent (e.g., thread the raw Date string into signing), or normalize the overridden header to the canonical format expected by the signature scheme.