package api import ( "fmt" "net/http" "github.com/owen/vibeproxy/internal/database" "github.com/owen/vibeproxy/internal/models" ) func (a *API) listProxyHosts(w http.ResponseWriter, r *http.Request) { hosts, err := database.ListProxyHosts(a.db) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, hosts) } func (a *API) getProxyHost(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } h, err := database.GetProxyHost(a.db, id) if err != nil { writeError(w, http.StatusNotFound, "proxy host not found") return } writeJSON(w, http.StatusOK, h) } func (a *API) createProxyHost(w http.ResponseWriter, r *http.Request) { var h models.ProxyHost if !decodeJSON(w, r, &h) { return } if err := validateProxyHost(&h); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } h.ID = 0 id, err := database.SaveProxyHost(a.db, &h) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } a.reload(r) audit(a.db, r, "proxy_created", h.Name, fmt.Sprintf("id=%d domains=%v", id, h.Domains)) writeJSON(w, http.StatusCreated, h) } func (a *API) updateProxyHost(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } var h models.ProxyHost if !decodeJSON(w, r, &h) { return } h.ID = id if err := validateProxyHost(&h); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if _, err := database.SaveProxyHost(a.db, &h); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } a.reload(r) audit(a.db, r, "proxy_changed", h.Name, fmt.Sprintf("id=%d", id)) writeJSON(w, http.StatusOK, h) } func (a *API) deleteProxyHost(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } if err := database.DeleteProxyHost(a.db, id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } a.reload(r) audit(a.db, r, "proxy_deleted", fmt.Sprintf("id=%d", id), "") w.WriteHeader(http.StatusNoContent) } func (a *API) enableProxyHost(w http.ResponseWriter, r *http.Request) { a.setProxyHostEnabled(w, r, true) } func (a *API) disableProxyHost(w http.ResponseWriter, r *http.Request) { a.setProxyHostEnabled(w, r, false) } func (a *API) setProxyHostEnabled(w http.ResponseWriter, r *http.Request, enabled bool) { id, err := pathID(r) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } if err := database.SetProxyHostEnabled(a.db, id, enabled); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } a.reload(r) audit(a.db, r, "proxy_changed", fmt.Sprintf("id=%d", id), fmt.Sprintf("enabled=%v", enabled)) w.WriteHeader(http.StatusNoContent) } func (a *API) reload(r *http.Request) { if err := a.store.Reload(r.Context()); err != nil { a.log.Error("config reload failed", "err", err) } } func validateProxyHost(h *models.ProxyHost) error { if h.Name == "" { return fmt.Errorf("name is required") } if h.Type == "" { h.Type = models.ProxyHostTypeProxy } if len(h.Domains) == 0 { return fmt.Errorf("at least one domain is required") } if h.Type == models.ProxyHostTypeProxy { if len(h.Backends) == 0 { return fmt.Errorf("at least one backend is required for a proxy host") } for i, b := range h.Backends { if b.Host == "" { return fmt.Errorf("backend %d: host is required", i) } if b.Port <= 0 || b.Port > 65535 { return fmt.Errorf("backend %d: invalid port", i) } if b.Weight <= 0 { h.Backends[i].Weight = 1 } if b.HealthCheckType == "" { h.Backends[i].HealthCheckType = models.HealthCheckTCP } if b.IntervalSeconds <= 0 { h.Backends[i].IntervalSeconds = 10 } if b.FailureThreshold <= 0 { h.Backends[i].FailureThreshold = 3 } if b.RecoveryThreshold <= 0 { h.Backends[i].RecoveryThreshold = 2 } } if h.ForwardScheme == "" { h.ForwardScheme = "http" } if h.LoadBalancingMethod == "" { h.LoadBalancingMethod = "round_robin" } } else if h.Type == models.ProxyHostTypeRedirect { if h.RedirectTargetURL == "" { return fmt.Errorf("redirect target URL is required") } switch h.RedirectCode { case 301, 302, 307, 308: default: h.RedirectCode = 301 } } else { return fmt.Errorf("invalid proxy host type %q", h.Type) } if h.ConnectTimeoutMS <= 0 { h.ConnectTimeoutMS = 5000 } if h.ReadTimeoutMS <= 0 { h.ReadTimeoutMS = 60000 } if h.WriteTimeoutMS <= 0 { h.WriteTimeoutMS = 60000 } if h.IdleTimeoutMS <= 0 { h.IdleTimeoutMS = 120000 } for _, hd := range h.CustomHeaders { if hd.Name == "" { return fmt.Errorf("custom header name is required") } if hd.Direction != models.HeaderDirectionRequest && hd.Direction != models.HeaderDirectionResponse { return fmt.Errorf("custom header direction must be request or response") } if hd.Action != models.HeaderActionSet && hd.Action != models.HeaderActionRemove { return fmt.Errorf("custom header action must be set or remove") } } return nil }