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.
This commit is contained in:
+7
-5
@@ -760,7 +760,9 @@ async def federation_terminal_ws_proxy(websocket: WebSocket, remote_id: int) ->
|
|||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
ws_url,
|
ws_url,
|
||||||
subprotocols=[Subprotocol("tty")],
|
subprotocols=[Subprotocol("tty")],
|
||||||
additional_headers={"Authorization": f"Bearer {remote_key}"},
|
additional_headers={"Authorization": f"Bearer {remote_key}"}
|
||||||
|
if remote_key
|
||||||
|
else {},
|
||||||
) as remote_ws:
|
) as remote_ws:
|
||||||
|
|
||||||
async def client_to_remote() -> None:
|
async def client_to_remote() -> None:
|
||||||
@@ -926,7 +928,7 @@ async def federation_sessions(request: Request) -> list[dict]:
|
|||||||
try:
|
try:
|
||||||
resp = await http_client.get(
|
resp = await http_client.get(
|
||||||
f"{url.rstrip('/')}/api/sessions",
|
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):
|
if resp.status_code in (401, 403):
|
||||||
return [
|
return [
|
||||||
@@ -1026,7 +1028,7 @@ async def federation_connect(
|
|||||||
try:
|
try:
|
||||||
resp = await http_client.post(
|
resp = await http_client.post(
|
||||||
url,
|
url,
|
||||||
headers={"Authorization": f"Bearer {remote_key}"},
|
headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
@@ -1072,7 +1074,7 @@ async def federation_bell_clear(
|
|||||||
try:
|
try:
|
||||||
resp = await http_client.post(
|
resp = await http_client.post(
|
||||||
url,
|
url,
|
||||||
headers={"Authorization": f"Bearer {remote_key}"},
|
headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
@@ -1119,7 +1121,7 @@ async def federation_create_session(
|
|||||||
try:
|
try:
|
||||||
resp = await http_client.post(
|
resp = await http_client.post(
|
||||||
url,
|
url,
|
||||||
headers={"Authorization": f"Bearer {remote_key}"},
|
headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {},
|
||||||
json={"name": payload.name},
|
json={"name": payload.name},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|||||||
@@ -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"})
|
response = client.post("/api/federation/0/sessions", json={"name": "new-session"})
|
||||||
assert response.status_code == 502
|
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)
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user