package server import ( "encoding/json" "errors" "log/slog" "net/http" "time" "github.com/wzray/dns/internal/resolv" ) type Server struct { mgr *resolv.Manager log *slog.Logger mux *http.ServeMux } func New(mgr *resolv.Manager, log *slog.Logger) *Server { s := &Server{mgr: mgr, log: log, mux: http.NewServeMux()} s.routes() return s } func (s *Server) Handler() http.Handler { return s.withLogging(s.mux) } func (s *Server) routes() { s.mux.HandleFunc("GET /health", s.handleHealth) s.mux.HandleFunc("GET /dns", s.handleList) s.mux.HandleFunc("POST /dns", s.handleAdd) s.mux.HandleFunc("DELETE /dns/{address}", s.handleRemove) } type listResponse struct { Servers []string `json:"servers"` } type addRequest struct { Address string `json:"address"` } type errorResponse struct { Error string `json:"error"` } func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleList(w http.ResponseWriter, r *http.Request) { servers, err := s.mgr.List() if err != nil { s.log.Error("list nameservers", "err", err) writeError(w, http.StatusInternalServerError, "failed to read nameservers") return } if servers == nil { servers = []string{} } writeJSON(w, http.StatusOK, listResponse{Servers: servers}) } func (s *Server) handleAdd(w http.ResponseWriter, r *http.Request) { var req addRequest dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } if req.Address == "" { writeError(w, http.StatusBadRequest, "address is required") return } err := s.mgr.Add(req.Address) switch { case err == nil: writeJSON(w, http.StatusCreated, map[string]string{"address": req.Address}) case errors.Is(err, resolv.ErrInvalidAddress): writeError(w, http.StatusBadRequest, err.Error()) case errors.Is(err, resolv.ErrAlreadyExists): writeError(w, http.StatusConflict, err.Error()) default: s.log.Error("add nameserver", "addr", req.Address, "err", err) writeError(w, http.StatusInternalServerError, "failed to add nameserver") } } func (s *Server) handleRemove(w http.ResponseWriter, r *http.Request) { addr := r.PathValue("address") if addr == "" { writeError(w, http.StatusBadRequest, "address is required") return } err := s.mgr.Remove(addr) switch { case err == nil: w.WriteHeader(http.StatusNoContent) case errors.Is(err, resolv.ErrInvalidAddress): writeError(w, http.StatusBadRequest, err.Error()) case errors.Is(err, resolv.ErrNotFound): writeError(w, http.StatusNotFound, err.Error()) default: s.log.Error("remove nameserver", "addr", addr, "err", err) writeError(w, http.StatusInternalServerError, "failed to remove nameserver") } } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, errorResponse{Error: msg}) } type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func (s *Server) withLogging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) s.log.Info("http request", "method", r.Method, "path", r.URL.Path, "status", rec.status, "duration_ms", time.Since(start).Milliseconds(), "remote", r.RemoteAddr, ) }) }