61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
"""Where a websocket's credential comes from, and how it stays out of the URL.
|
|
|
|
A client that can set headers sends `Authorization: Bearer` on the handshake:
|
|
the worker and the SDK. A browser cannot, so it trades its token for a ticket
|
|
over an authenticated REST call and brings the ticket instead. A ticket is good
|
|
once, for thirty seconds, so the URL every proxy logs is worth nothing after.
|
|
|
|
The engine is one process, so the tickets live in its memory: the tunnel
|
|
connector, which serves the live socket inline, redeems them from the same
|
|
place.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import threading
|
|
from collections.abc import Mapping
|
|
from time import monotonic
|
|
|
|
#: How long a ticket waits to be used: the length of one connect.
|
|
TICKET_TTL_S = 30.0
|
|
|
|
_tickets: dict[str, tuple[str, float]] = {}
|
|
_lock = threading.Lock()
|
|
|
|
|
|
def issue_ticket(token: str) -> str:
|
|
"""A one-time ticket standing in for *token*."""
|
|
ticket = secrets.token_urlsafe(32)
|
|
now = monotonic()
|
|
with _lock:
|
|
# Swept here rather than on a timer: issuing is what fills the table.
|
|
for stale in [key for key, (_, due) in _tickets.items() if due <= now]:
|
|
del _tickets[stale]
|
|
_tickets[ticket] = (token, now + TICKET_TTL_S)
|
|
return ticket
|
|
|
|
|
|
def redeem_ticket(ticket: str) -> str:
|
|
"""The token a ticket stands for, once; empty if it is unknown or stale."""
|
|
with _lock:
|
|
entry = _tickets.pop(ticket, None)
|
|
if entry is None or entry[1] <= monotonic():
|
|
return ""
|
|
return entry[0]
|
|
|
|
|
|
def socket_token(headers: Mapping[str, str], ticket: str = "", token: str = "") -> str:
|
|
"""The credential a handshake carries: a header, then a ticket, then a token.
|
|
|
|
`token` is the query parameter clients sent before the other two existed.
|
|
It is read for one more release so an engine can be upgraded before its
|
|
workers; remove it after that (see NOTEPAD).
|
|
"""
|
|
header = headers.get("authorization") or ""
|
|
if header.lower().startswith("bearer "):
|
|
return header[7:]
|
|
if ticket:
|
|
return redeem_ticket(ticket)
|
|
return token
|