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
202 lines
5.2 KiB
Go
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
|
|
}
|