// Package httpapi exposes the Control API's HTTP surface — the only way // validator-agents, probers, and operators interact with the system. All // business logic lives in internal/orchestrator; handlers here do request // parsing/validation, call into the orchestrator or db package, and shape // the JSON response. package httpapi import ( "encoding/json" "log/slog" "net" "net/http" "cloudipvalidator/internal/db" "cloudipvalidator/internal/orchestrator" ) type Server struct { DB *db.DB Orch *orchestrator.Orchestrator Log *slog.Logger } func New(d *db.DB, o *orchestrator.Orchestrator, log *slog.Logger) *Server { return &Server{DB: d, Orch: o, Log: log} } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() s.routes(mux) return loggingMiddleware(s.Log, mux) } func loggingMiddleware(log *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) log.Debug("request", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr) }) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if v != nil { _ = json.NewEncoder(w).Encode(v) } } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, errorResponse{Error: msg}) } func readJSON(r *http.Request, v interface{}) error { if r.Body == nil || r.ContentLength == 0 { return nil } dec := json.NewDecoder(r.Body) return dec.Decode(v) } // remoteIP returns the caller's source IP with any port stripped. Used by // /whatsmyip — the validator-agent's self-check mechanism relies on this // being the actual TCP peer address (as SNAT'd by the newly associated // FIP), never a client-supplied header. func remoteIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host }