From 39f076502ff45bf3e318bbdff5b40d20c8f1fa4b Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 6 Apr 2026 05:25:18 -0700 Subject: [PATCH] fix: skip Authorization header when federation key is empty Empty remote_instances[].key produced 'Bearer ' (illegal header value) causing httpx to reject the request entirely. Fix: only include the Authorization header when the key is non-empty. Remotes without keys still work if their auth allows it (e.g., localhost bypass). Fixes all 5 sites: fetch_remote GET, connect POST, bell/clear POST, create-session POST, and WebSocket proxy additional_headers. --- muxplex/main.py | 12 +++++---- muxplex/tests/test_api.py | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/muxplex/main.py b/muxplex/main.py index 7e02580..a43bc1c 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -760,7 +760,9 @@ async def federation_terminal_ws_proxy(websocket: WebSocket, remote_id: int) -> async with websockets.connect( ws_url, subprotocols=[Subprotocol("tty")], - additional_headers={"Authorization": f"Bearer {remote_key}"}, + additional_headers={"Authorization": f"Bearer {remote_key}"} + if remote_key + else {}, ) as remote_ws: async def client_to_remote() -> None: @@ -926,7 +928,7 @@ async def federation_sessions(request: Request) -> list[dict]: try: resp = await http_client.get( f"{url.rstrip('/')}/api/sessions", - headers={"Authorization": f"Bearer {key}"}, + headers={"Authorization": f"Bearer {key}"} if key else {}, ) if resp.status_code in (401, 403): return [ @@ -1026,7 +1028,7 @@ async def federation_connect( try: resp = await http_client.post( url, - headers={"Authorization": f"Bearer {remote_key}"}, + headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {}, ) resp.raise_for_status() return resp.json() @@ -1072,7 +1074,7 @@ async def federation_bell_clear( try: resp = await http_client.post( url, - headers={"Authorization": f"Bearer {remote_key}"}, + headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {}, ) resp.raise_for_status() return resp.json() @@ -1119,7 +1121,7 @@ async def federation_create_session( try: resp = await http_client.post( url, - headers={"Authorization": f"Bearer {remote_key}"}, + headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {}, json={"name": payload.name}, ) resp.raise_for_status() diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index 24c0b98..fd4714f 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -2624,3 +2624,57 @@ def test_federation_create_session_returns_502_when_remote_returns_error( response = client.post("/api/federation/0/sessions", json={"name": "new-session"}) assert response.status_code == 502 + + +# --------------------------------------------------------------------------- +# Federation Authorization header safety — guard against empty key +# --------------------------------------------------------------------------- + + +def test_federation_auth_headers_guard_empty_key(): + """Every federation Authorization header construction must guard against empty key. + + An empty remote_instances[].key produces 'Bearer ' (trailing space, empty + token). httpx rejects that with "Illegal header value b'Bearer '" which + silently makes the remote appear unreachable. + + The fix: every site that constructs the header must use the pattern + headers={"Authorization": f"Bearer {key}"} if key else {} + or an equivalent conditional, so an empty key simply omits the header. + + This is a source-inspection test — it catches regressions without spinning + up a live server. + """ + import inspect + + import muxplex.main as main_mod + + source = inspect.getsource(main_mod) + + # Collect every line that constructs an Authorization Bearer header (not + # comment or docstring lines — we only care about executable code lines). + offending: list[str] = [] + for line in source.splitlines(): + stripped = line.strip() + # Skip comment and docstring lines + if ( + stripped.startswith("#") + or stripped.startswith('"""') + or stripped.startswith("'\"'\"'") + ): + continue + # Match lines that build the header dict value + if ( + '"Authorization"' in stripped + and "Bearer" in stripped + and 'f"Bearer' in stripped + ): + # Must have an inline `if` guard — e.g. `{...} if key else {}` + if " if " not in stripped: + offending.append(stripped) + + assert not offending, ( + "Unguarded federation Bearer header(s) found in main.py — " + "use `{...} if key else {}` to skip the header when key is empty:\n" + + "\n".join(f" {line}" for line in offending) + )