// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "net" "net/http" "strconv" "time" ) // AuditRecorder is the audit-store dependency of the HTTP layer type AuditRecorder interface { RecordOperation(ctx context.Context, token, origin, operation string) error } // Fixed JSON error bodies const ( bodyUnauthorized = `{"error":"unauthorized"}` bodyNotFound = `{"error":"not found"}` bodyMethodNotAllowed = `{"error":"method not allowed"}` bodyInternalError = `{"error":"internal server error"}` ) // writeBody writes a complete JSON response with an explicit Content-Length func writeBody(w http.ResponseWriter, status int, body []byte) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Length", strconv.Itoa(len(body))) w.WriteHeader(status) w.Write(body) } // writeError writes one of the fixed JSON error bodies func writeError(w http.ResponseWriter, status int, body string) { writeBody(w, status, []byte(body)) } // clientIP returns the IP part of the connection's remote address // Forwarding headers (X-Forwarded-For, Forwarded, X-Real-IP) are deliberately ignored func clientIP(remoteAddr string) string { host, _, err := net.SplitHostPort(remoteAddr) if err != nil { return remoteAddr } return host } // handleContainers serves GET /api/v1/containers: authenticate, audit, list containers, respond func (s *WebServer) handleContainers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() defer traceFunc(ctx, s.log, "WebServer.handleContainers")() // Authenticate tok, class, err := authenticate(ctx, s.log, s.tokens, r.Header) if err != nil { s.log.DebugContext(ctx, "authentication result", "result", class) s.log.ErrorContext(ctx, "token lookup failed", "error", err) writeError(w, http.StatusInternalServerError, bodyInternalError) return } if class != authOK { s.log.DebugContext(ctx, "authentication result", "result", class) w.Header().Set("WWW-Authenticate", "Bearer") writeError(w, http.StatusUnauthorized, bodyUnauthorized) return } id := tokenID(tok.Token) s.log.DebugContext(ctx, "authentication result", "result", class, "token_id", id) // Audit, before touching Docker so failed Docker calls are still recorded origin := clientIP(r.RemoteAddr) if err := s.audit.RecordOperation(ctx, tok.Token, origin, operationGetContainers); err != nil { s.log.ErrorContext(ctx, "audit insert failed", "token_id", id, "error", err) writeError(w, http.StatusInternalServerError, bodyInternalError) return } s.log.DebugContext(ctx, "audit row inserted", "token_id", id, "origin", origin, "operation", operationGetContainers) // Query Docker s.log.DebugContext(ctx, "docker request started") start := time.Now() containers, err := s.docker.ListRunningContainers(ctx) if err != nil { s.log.DebugContext(ctx, "docker request finished", "ok", false, "duration", time.Since(start)) s.log.ErrorContext(ctx, "docker request failed", "error", err) writeError(w, http.StatusInternalServerError, bodyInternalError) return } s.log.DebugContext(ctx, "docker request finished", "ok", true, "duration", time.Since(start), "containers", len(containers)) // Serialize fully before writing anything body, err := s.encode(containers) if err != nil { s.log.ErrorContext(ctx, "response serialization failed", "error", err) writeError(w, http.StatusInternalServerError, bodyInternalError) return } writeBody(w, http.StatusOK, body) } // handleNotFound serves every unknown path func (s *WebServer) handleNotFound(w http.ResponseWriter, r *http.Request) { defer traceFunc(r.Context(), s.log, "WebServer.handleNotFound")() writeError(w, http.StatusNotFound, bodyNotFound) } // handleMethodNotAllowed serves known paths requested with an unsupported method func (s *WebServer) handleMethodNotAllowed(w http.ResponseWriter, r *http.Request) { defer traceFunc(r.Context(), s.log, "WebServer.handleMethodNotAllowed")() w.Header().Set("Allow", http.MethodGet) writeError(w, http.StatusMethodNotAllowed, bodyMethodNotAllowed) } // statusRecorder captures the status code written by the wrapped handler type statusRecorder struct { http.ResponseWriter status int wroteHeader bool } // WriteHeader records the first status code func (r *statusRecorder) WriteHeader(code int) { if !r.wroteHeader { r.status = code r.wroteHeader = true } r.ResponseWriter.WriteHeader(code) } // Write records an implicit 200 when no status was set func (r *statusRecorder) Write(b []byte) (int, error) { if !r.wroteHeader { r.status = http.StatusOK r.wroteHeader = true } return r.ResponseWriter.Write(b) } // Unwrap exposes the underlying writer to http.ResponseController func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter } // logRequests logs every request reaching the application at INFO func (s *WebServer) logRequests(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() defer traceFunc(ctx, s.log, "WebServer.logRequests")() start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) s.log.InfoContext(ctx, "this request has been made", "method", r.Method, "path", r.URL.Path, "status", rec.status, "duration", time.Since(start), "origin", clientIP(r.RemoteAddr), ) }) }