diff --git a/DECISIONS.md b/DECISIONS.md index 5a1e53d..440741a 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -83,3 +83,4 @@ - 2026-02-28 m+git@andri.dk — Cert cache now evicts expired certificates. `GetCert` checks the cached cert's expiry time before returning it; expired entries are regenerated on demand. Previously, certs were cached indefinitely — after 24h of uptime, cached certs would be stale and clients would get TLS errors. The cache stores `expiresAt` alongside each cert. No background goroutine; expiry is checked lazily on access. - 2026-02-28 m+git@andri.dk — Structured logging with `log/slog` (stdlib, zero new dependencies). Replaced all `fmt.Fprintf(os.Stderr)` calls with `slog` at appropriate levels. Added `--log-level` flag (env: `UBLPROXY_LOG_LEVEL`, default: `info`). Every log line includes `ip` (client source IP) and `user` (first 8 chars of credential ID, or `anon`). Per-request traffic logging and passthrough tunnels are Debug level — only visible with `--log-level=debug`. Blocked requests, startup, and errors are Info/Error level and always visible. Blocklist loading failures are Warn level. Output is `slog.TextHandler` (structured key=value pairs to stderr). - 2026-02-28 m+git@andri.dk — Normalize client IP addresses via `normalizeIP()` to unwrap IPv4-mapped IPv6 (e.g. `::ffff:192.168.1.5` → `192.168.1.5`). Go's `net` package can represent the same IPv4 address differently depending on whether the connection arrived via IPv4 or IPv6. Without normalization, the session map key from portal auth could differ from the proxy lookup key, causing `user=anon` despite an active session. All `RemoteAddr` extraction points now go through normalization. Debug logging added to `sessionMap.Set()`, `Get()`, and `Delete()` for diagnosing session lookup issues. +- 2026-02-28 m+git@andri.dk — Lazy session restore in `authenticate()`. The in-memory `sessionMap` (IP → credential) is lost on server restart. Now, when a valid Bearer token is validated against SQLite, `authenticate()` also populates the `sessionMap` for that client IP. The proxy knows the user as soon as their browser makes any authenticated API call (e.g. `GET /api/whoami` on portal page load). No schema change needed. diff --git a/api.go b/api.go index e966e71..86e8232 100644 --- a/api.go +++ b/api.go @@ -141,6 +141,8 @@ func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // authenticate extracts and validates the session token from the Authorization // header. Returns nil if the token is missing or invalid. +// On success, ensures the in-memory sessionMap has an entry for this client +// IP so the proxy can resolve the credential for proxied requests. func (a *apiHandler) authenticate(r *http.Request) *store.Session { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { @@ -151,6 +153,19 @@ func (a *apiHandler) authenticate(r *http.Request) *store.Session { if err != nil { return nil } + // Lazily restore the IP → session mapping. After a server restart the + // in-memory sessionMap is empty; re-populating it here means the proxy + // knows the user as soon as their browser makes any authenticated API + // call (e.g. GET /api/whoami on portal page load). + if a.sessions != nil { + clientIP := clientIPFromRequest(r) + if existing := a.sessions.Get(clientIP); existing == nil || existing.Token != sess.Token { + a.sessions.Set(clientIP, sessionEntry{ + Token: sess.Token, + CredentialID: sess.CredentialID, + }) + } + } return sess }