Portal link: send the instance credential as Authorization: Bearer
Not ?token= in the tunnel URL, which access logs keep. The hub has to be upgraded first; websockets>=14 for additional_headers on the asyncio client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
@@ -191,9 +191,12 @@ class CloudConnector:
|
||||
import websockets
|
||||
|
||||
attached = False
|
||||
url = f"{config.ws_url}?token={config.token}"
|
||||
# A header rather than `?token=`, since a URL ends up in access logs.
|
||||
# A hub from before the header reads only the query: upgrade the hub
|
||||
# before its instances.
|
||||
async with websockets.connect(
|
||||
url,
|
||||
config.ws_url,
|
||||
additional_headers={"Authorization": f"Bearer {config.token}"},
|
||||
max_size=MAX_WS_MESSAGE,
|
||||
ping_interval=PING_INTERVAL_S,
|
||||
ping_timeout=PING_TIMEOUT_S,
|
||||
|
||||
@@ -74,8 +74,9 @@ dependencies = [
|
||||
"textual>=1.0",
|
||||
# That dashboard subscribes to the engine's event bus over the same socket
|
||||
# the browser uses. It arrives under `fastapi[standard]` either way, and is
|
||||
# named here because this code imports it.
|
||||
"websockets>=13",
|
||||
# named here because this code imports it. 14 is where the asyncio client
|
||||
# the portal link dials with takes `additional_headers`.
|
||||
"websockets>=14",
|
||||
# The encrypted-payload half of web push (RFC 8291). It brings only
|
||||
# cryptography, which is already here. `pywebpush` would do the signing and
|
||||
# the request too, at the cost of `requests` *and* `aiohttp` — two HTTP
|
||||
|
||||
@@ -611,3 +611,33 @@ def test_the_tunnel_opens_the_live_socket_with_a_ticket(
|
||||
assert asyncio.run(opened())[0] == "ws_open_ok"
|
||||
# Used once: the same ticket again is refused.
|
||||
assert asyncio.run(opened()) == ["ws_close"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_the_link_sends_its_credential_as_a_header() -> None:
|
||||
"""Not in the URL, which access logs keep."""
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
from websockets.asyncio.server import ServerConnection, serve
|
||||
|
||||
from fluksio.cloud.connector import CloudConnector
|
||||
|
||||
seen: dict[str, str | None] = {}
|
||||
|
||||
async def hub(socket: ServerConnection) -> None:
|
||||
assert socket.request is not None
|
||||
seen["path"] = socket.request.path
|
||||
seen["auth"] = socket.request.headers.get("Authorization")
|
||||
await socket.recv()
|
||||
await socket.send(json.dumps({"op": "refused", "reason": "test hub"}))
|
||||
|
||||
async with serve(hub, "127.0.0.1", 0) as server:
|
||||
port = next(iter(server.sockets)).getsockname()[1]
|
||||
config = SimpleNamespace(
|
||||
ws_url=f"ws://127.0.0.1:{port}/api/v1/tunnel/attach", token="t0k"
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="test hub"):
|
||||
await CloudConnector(Mock())._session(config) # type: ignore[arg-type]
|
||||
|
||||
assert seen == {"path": "/api/v1/tunnel/attach", "auth": "Bearer t0k"}
|
||||
|
||||
@@ -49,6 +49,12 @@ Once enrolled, the instance appears under **Instances** with its
|
||||
status, when it was last seen and its version. **Open** takes you to its
|
||||
dashboard at `${DOMAIN}/i/{instance-id}`.
|
||||
|
||||
**Upgrade a self-hosted portal before its instances.** An instance dials the
|
||||
tunnel with its credential in an `Authorization` header, where it stays out of
|
||||
access logs, and a portal from before that reads it only from the URL. The
|
||||
portal still takes the old `?token=` for one release, so older instances keep
|
||||
attaching meanwhile.
|
||||
|
||||
## What the portal can and cannot do
|
||||
|
||||
The portal holds one credential for your instance and proxies requests down
|
||||
|
||||
@@ -999,7 +999,7 @@ requires-dist = [
|
||||
{ name = "sqlmodel", specifier = ">=0.0.21,<1.0.0" },
|
||||
{ name = "textual", specifier = ">=1.0" },
|
||||
{ name = "uv", specifier = ">=0.5" },
|
||||
{ name = "websockets", specifier = ">=13" },
|
||||
{ name = "websockets", specifier = ">=14" },
|
||||
]
|
||||
provides-extras = ["parquet", "server"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user