Add opt-in EMBED_KEY passwordless sign-in and iframe-friendly session cookie
build / check (push) Successful in 17s
build / image (push) Successful in 1m29s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 08:19:04 -04:00
1 parent 429a72f2c3
commit 77bd8389a2
2 files changed
+29 -2

No files matched your search

+2
View File
@@ -12,6 +12,8 @@ services:
OPENAUDIBLE_PASSWORD: ${OPENAUDIBLE_PASSWORD} OPENAUDIBLE_PASSWORD: ${OPENAUDIBLE_PASSWORD}
PLAYER_USER: ${PLAYER_USER} # default account, created on first start PLAYER_USER: ${PLAYER_USER} # default account, created on first start
PLAYER_PASSWORD: ${PLAYER_PASSWORD} PLAYER_PASSWORD: ${PLAYER_PASSWORD}
EMBED_KEY: ${EMBED_KEY:-} # optional: enables /auto?key=... passwordless sign-in (serve over HTTPS)
EMBED_USER: ${EMBED_USER:-} # which account it signs in as (default: first account)
volumes: volumes:
- player-data:/data # accounts, sessions, listening progress - player-data:/data # accounts, sessions, listening progress
+27 -2
View File
@@ -39,6 +39,9 @@ F_SESSIONS = os.path.join(DATA, "sessions.json") # token hash -> {user, exp}
F_PROGRESS = os.path.join(DATA, "progress_users.json") F_PROGRESS = os.path.join(DATA, "progress_users.json")
F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version
COOKIE = "oa_session" COOKIE = "oa_session"
# Optional passwordless entry for embedding in another app. Unset = disabled.
EMBED_KEY = os.environ.get("EMBED_KEY", "")
EMBED_USER = os.environ.get("EMBED_USER", "").strip().lower() # defaults to the first account
SESSION_DAYS = 90 SESSION_DAYS = 90
# Files the browser fetches without a session (home-screen icon and manifest). # Files the browser fetches without a session (home-screen icon and manifest).
@@ -281,6 +284,8 @@ class Handler(BaseHTTPRequestHandler):
# ----- routes ----- # ----- routes -----
def do_GET(self): def do_GET(self):
path = self.path.split("?")[0] path = self.path.split("?")[0]
if path == "/auto":
return self.auto_login(self.path.partition("?")[2])
if path == "/login": if path == "/login":
return self.send_body(200, LOGIN, "text/html; charset=utf-8") return self.send_body(200, LOGIN, "text/html; charset=utf-8")
if path == "/logout": if path == "/logout":
@@ -418,13 +423,33 @@ class Handler(BaseHTTPRequestHandler):
if not ok: if not ok:
self.fails[ip] = recent + [now] self.fails[ip] = recent + [now]
return self.json(401, {"error": "wrong"}) return self.json(401, {"error": "wrong"})
self.start_session(name, lambda cookie: self.json(200, {"user": name}, [("Set-Cookie", cookie)]))
def start_session(self, name, respond):
token = secrets.token_urlsafe(32) token = secrets.token_urlsafe(32)
now = time.time()
with lock: with lock:
sessions = {k: v for k, v in read(F_SESSIONS, {}).items() if v["exp"] > now} sessions = {k: v for k, v in read(F_SESSIONS, {}).items() if v["exp"] > now}
sessions[sha(token)] = {"user": name, "exp": now + SESSION_DAYS * 86400} sessions[sha(token)] = {"user": name, "exp": now + SESSION_DAYS * 86400}
write(F_SESSIONS, sessions, private=True) write(F_SESSIONS, sessions, private=True)
cookie = f"{COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_DAYS * 86400}" # EMBED mode: cookie must survive inside a cross-site iframe (needs HTTPS)
self.json(200, {"user": name}, [("Set-Cookie", cookie)]) same = "SameSite=None; Secure" if EMBED_KEY else "SameSite=Lax"
respond(f"{COOKIE}={token}; Path=/; HttpOnly; {same}; Max-Age={SESSION_DAYS * 86400}")
def auto_login(self, query):
"""GET /auto?key=SECRET -> signed in as EMBED_USER, no password. Off unless EMBED_KEY is set."""
key = (urllib.parse.parse_qs(query).get("key") or [""])[0]
users = read(F_USERS, {})
name = EMBED_USER or next(iter(users), "")
if not EMBED_KEY or name not in users or not secrets.compare_digest(key.encode(), EMBED_KEY.encode()):
time.sleep(1)
return self.send_error(404)
def go(cookie):
self.send_response(302)
self.send_header("Set-Cookie", cookie)
self.send_header("Location", "/")
self.end_headers()
self.start_session(name, go)
def logout(self): def logout(self):
c = SimpleCookie(self.headers.get("Cookie", "")) c = SimpleCookie(self.headers.get("Cookie", ""))