Implement code word based remote authentication #1

Merged
LeoVasanko merged 16 commits from remote-auth into main 2025-12-08 23:56:48 +00:00
Showing only changes of commit 8f0cb8d80a - Show all commits
+95 -44
View File
@@ -240,19 +240,73 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None:
stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n")
return None
# Helper to parse Caddy log line (JSON or plain text) into (level, logger, msg)
def parse_caddy_log(line: str) -> tuple[str, str, str] | None:
"""Parse a Caddy log line, return (level, logger, msg) or None if unparseable."""
line = line.rstrip("\n")
if not line:
return None
# Try JSON format first
try:
log = json.loads(line)
return (
log.get("level", ""),
log.get("logger", ""),
log.get("msg", ""),
)
except json.JSONDecodeError:
pass
# Plain text format: "2025/12/06 22:59:41.390 INFO logger msg..."
# or "2025/12/06 22:59:41.390 INFO msg..." (no logger)
parts = line.split("\t")
if len(parts) >= 2:
# First part is "timestamp LEVEL", rest are logger and/or message
first = parts[0].rsplit(None, 1) # split off the level from timestamp
if len(first) == 2:
level = first[1].lower()
if len(parts) == 2:
return (level, "", parts[1])
else:
return (level, parts[1], "\t".join(parts[2:]))
# Unparseable - return as-is with no level/logger
return ("", "", line)
def strip_caddy_verbose(msg: str) -> str:
"""Remove verbose prefixes from Caddy error messages."""
return msg.replace("loading initial config: loading new config: ", "")
def format_caddy_log(level: str, logger: str, msg: str) -> str:
"""Format a parsed Caddy log for display."""
msg = strip_caddy_verbose(msg)
if logger:
return f"┃ [{level.upper()}] {logger}: {msg}\n"
else:
return f"┃ [{level.upper()}] {msg}\n"
# Read stderr line by line until Caddy signals it's ready or exits
# Caddy outputs JSON logs; "serving initial configuration" means it's ready
# Caddy outputs logs; "serving initial configuration" means it's ready
ready = False
while True:
exit_code = caddy_process.poll()
if exit_code is not None:
# Process exited - read remaining stderr and report failure
remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else ""
stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n")
if remaining:
for line in remaining.splitlines():
if line:
stderr.write(f"{line}\n")
parsed = parse_caddy_log(line)
if parsed:
level, logger, msg = parsed
if level:
stderr.write(format_caddy_log(level, logger, msg))
else:
stderr.write(f"{strip_caddy_verbose(msg)}\n")
else:
stderr.write(f"{strip_caddy_verbose(line)}\n")
stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n")
return None
# Read one line from stderr (blocks until data available)
@@ -260,55 +314,52 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None:
if not line:
continue
# Try to parse as JSON log
try:
log = json.loads(line)
msg = log.get("msg", "")
level = log.get("level", "")
# Check for ready signal
if "serving initial configuration" in msg:
ready = True
break
# Show errors/warnings to user
if level in ("error", "fatal"):
logger = log.get("logger", "")
if logger:
stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n")
else:
stderr.write(f"┃ [{level.upper()}] {msg}\n")
except json.JSONDecodeError:
# Not JSON - might be sudo password prompt or other output
# Pass through to user
stderr.write(line)
stderr.flush()
if ready:
# Check for ready signal
if "serving initial configuration" in line:
ready = True
break
parsed = parse_caddy_log(line)
if not parsed:
continue
level, logger, msg = parsed
# Filter out info-level and admin messages
if level == "info" or logger == "admin":
continue
# Show errors/fatal to user
if level in ("error", "fatal"):
stderr.write(format_caddy_log(level, logger, msg))
elif not level:
# Unparseable non-empty line (e.g., sudo prompt) - pass through with prefix
stderr.write(f"{strip_caddy_verbose(msg)}\n")
stderr.flush()
# Start a background thread to drain stderr and show errors
def drain_stderr():
while True:
line = caddy_process.stderr.readline().decode()
if not line:
break
try:
log = json.loads(line)
level = log.get("level", "")
if level in ("error", "fatal", "warn"):
msg = log.get("msg", "")
logger = log.get("logger", "")
# Skip expected warnings
if logger == "admin" and "admin endpoint disabled" in msg:
continue
if logger:
stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n")
else:
stderr.write(f"┃ [{level.upper()}] {msg}\n")
except json.JSONDecodeError:
stderr.write(line)
parsed = parse_caddy_log(line)
if not parsed:
continue
level, logger, msg = parsed
# Filter out info-level and admin messages
if level == "info" or logger == "admin":
continue
# Show errors/warnings to user
if level in ("error", "fatal", "warn"):
stderr.write(format_caddy_log(level, logger, msg))
elif not level:
# Unparseable line - pass through with prefix
stderr.write(f"{strip_caddy_verbose(msg)}\n")
drain_thread = Thread(target=drain_stderr, daemon=True)
drain_thread.start()