Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions tests/routes/desktop_browser/test_proxy_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,41 @@ def test_empty(self):
from tinyagentos.routes.desktop_browser.proxy import _strip_port

assert _strip_port("") == ""


class TestRequestScheme:
"""_request_scheme honours x-forwarded-proto and clamps to http/https."""

@staticmethod
def _req(*, xff=None, scheme="http"):
from types import SimpleNamespace
headers = {"x-forwarded-proto": xff} if xff is not None else {}
return SimpleNamespace(headers=headers, url=SimpleNamespace(scheme=scheme))

def test_plain_http(self):
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(scheme="http")) == "http"

def test_direct_https(self):
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(scheme="https")) == "https"

def test_forwarded_proto_https_wins_over_http_url(self):
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(xff="https", scheme="http")) == "https"

def test_forwarded_proto_comma_list_takes_first(self):
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(xff="https, http", scheme="http")) == "https"

def test_bogus_forwarded_proto_falls_back_to_request_scheme(self):
# A junk x-forwarded-proto must NOT downgrade a genuinely HTTPS
# request — fall back to the request's own scheme.
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(xff="javascript", scheme="https")) == "https"
assert _request_scheme(self._req(xff="javascript", scheme="http")) == "http"

def test_bogus_request_scheme_clamped_to_http(self):
# No valid scheme anywhere → safe default of http.
from tinyagentos.routes.desktop_browser.proxy import _request_scheme
assert _request_scheme(self._req(xff="javascript", scheme="ftp")) == "http"
16 changes: 13 additions & 3 deletions tinyagentos/routes/desktop_browser/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,17 @@ def _request_scheme(request: Request) -> str:

Honours ``x-forwarded-proto`` (which may be a comma list behind chained
proxies — take the first) so a hostile/odd value can't deform the CSP.
A malformed forwarded scheme falls back to the request's own scheme
rather than hard-coding ``http`` — otherwise a genuinely HTTPS request
carrying a junk header would be downgraded to ``ws://`` and lose the
HTTPS CSP path.
"""
scheme = (
request.headers.get("x-forwarded-proto") or request.url.scheme or "http"
forwarded = (
request.headers.get("x-forwarded-proto") or ""
).split(",")[0].strip().lower()
if forwarded in ("http", "https"):
return forwarded
scheme = (request.url.scheme or "http").lower()
return scheme if scheme in ("http", "https") else "http"


Expand Down Expand Up @@ -302,7 +309,10 @@ def _proxy_url(absolute: str) -> str:
charset=charset,
)

ws_scheme = "wss" if request.url.scheme == "https" else "ws"
# Use the effective scheme (honours x-forwarded-proto behind a TLS
# terminator), matching the CSP below — otherwise a reverse-proxied
# HTTPS deploy injects ws:// and the copilot socket fails.
ws_scheme = "wss" if _request_scheme(request) == "https" else "ws"
ws_url = (
f"{ws_scheme}://{request.url.netloc}/api/desktop/browser/copilot"
f"?profile_id={quote(profile_id, safe='')}"
Expand Down
Loading