Files
owenandClaude Sonnet 5 ba1f7c4c03 Initial vibeproxy implementation
Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go,
with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS
framework), and a REST API under /api/v1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
2026-08-17 10:28:59 -05:00

202 lines
5.2 KiB
Go

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
}