Implement code word based remote authentication #1
+95
-44
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user