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
243 lines
8.4 KiB
Go
243 lines
8.4 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"github.com/owen/vibeproxy/internal/models"
|
|
)
|
|
|
|
func ListProxyHosts(db *sql.DB) ([]models.ProxyHost, error) {
|
|
rows, err := db.Query(`SELECT id, name, type, listen_addr, forward_scheme, preserve_host_header, websocket_support,
|
|
force_https, hsts_enabled, certificate_id, access_list_id, connect_timeout_ms, read_timeout_ms, write_timeout_ms,
|
|
idle_timeout_ms, load_balancing_method, redirect_target_url, redirect_code, redirect_preserve_path,
|
|
redirect_preserve_qs, enabled, comments, created_at, updated_at FROM proxy_hosts ORDER BY id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.ProxyHost
|
|
for rows.Next() {
|
|
h, err := scanProxyHost(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range out {
|
|
if err := hydrateProxyHost(db, &out[i]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type rowScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanProxyHost(rs rowScanner) (models.ProxyHost, error) {
|
|
var h models.ProxyHost
|
|
var certID, aclID sql.NullInt64
|
|
var preserveHost, ws, forceHTTPS, hsts, enabled, preservePath, preserveQS int
|
|
var created, updated string
|
|
err := rs.Scan(&h.ID, &h.Name, &h.Type, &h.ListenAddr, &h.ForwardScheme, &preserveHost, &ws,
|
|
&forceHTTPS, &hsts, &certID, &aclID, &h.ConnectTimeoutMS, &h.ReadTimeoutMS, &h.WriteTimeoutMS,
|
|
&h.IdleTimeoutMS, &h.LoadBalancingMethod, &h.RedirectTargetURL, &h.RedirectCode, &preservePath,
|
|
&preserveQS, &enabled, &h.Comments, &created, &updated)
|
|
if err != nil {
|
|
return h, err
|
|
}
|
|
h.PreserveHostHeader = preserveHost == 1
|
|
h.WebsocketSupport = ws == 1
|
|
h.ForceHTTPS = forceHTTPS == 1
|
|
h.HSTSEnabled = hsts == 1
|
|
h.RedirectPreservePath = preservePath == 1
|
|
h.RedirectPreserveQS = preserveQS == 1
|
|
h.Enabled = enabled == 1
|
|
h.CreatedAt = parseTime(created)
|
|
h.UpdatedAt = parseTime(updated)
|
|
if certID.Valid {
|
|
v := certID.Int64
|
|
h.CertificateID = &v
|
|
}
|
|
if aclID.Valid {
|
|
v := aclID.Int64
|
|
h.AccessListID = &v
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
func hydrateProxyHost(db *sql.DB, h *models.ProxyHost) error {
|
|
drows, err := db.Query(`SELECT domain FROM proxy_host_domains WHERE proxy_host_id = ? ORDER BY id`, h.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for drows.Next() {
|
|
var d string
|
|
if err := drows.Scan(&d); err != nil {
|
|
drows.Close()
|
|
return err
|
|
}
|
|
h.Domains = append(h.Domains, d)
|
|
}
|
|
drows.Close()
|
|
if err := drows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
brows, err := db.Query(`SELECT id, proxy_host_id, host, port, weight, enabled, health_check_type, health_check_path,
|
|
health_check_status, interval_seconds, failure_threshold, recovery_threshold FROM backends WHERE proxy_host_id = ? ORDER BY id`, h.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for brows.Next() {
|
|
var b models.Backend
|
|
var enabled int
|
|
if err := brows.Scan(&b.ID, &b.ProxyHostID, &b.Host, &b.Port, &b.Weight, &enabled, &b.HealthCheckType, &b.HealthCheckPath,
|
|
&b.HealthCheckStatus, &b.IntervalSeconds, &b.FailureThreshold, &b.RecoveryThreshold); err != nil {
|
|
brows.Close()
|
|
return err
|
|
}
|
|
b.Enabled = enabled == 1
|
|
b.Healthy = true
|
|
h.Backends = append(h.Backends, b)
|
|
}
|
|
brows.Close()
|
|
if err := brows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
hrows, err := db.Query(`SELECT id, proxy_host_id, direction, action, name, value FROM custom_headers WHERE proxy_host_id = ? ORDER BY id`, h.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for hrows.Next() {
|
|
var c models.CustomHeader
|
|
if err := hrows.Scan(&c.ID, &c.ProxyHostID, &c.Direction, &c.Action, &c.Name, &c.Value); err != nil {
|
|
hrows.Close()
|
|
return err
|
|
}
|
|
h.CustomHeaders = append(h.CustomHeaders, c)
|
|
}
|
|
hrows.Close()
|
|
return hrows.Err()
|
|
}
|
|
|
|
func GetProxyHost(db *sql.DB, id int64) (*models.ProxyHost, error) {
|
|
row := db.QueryRow(`SELECT id, name, type, listen_addr, forward_scheme, preserve_host_header, websocket_support,
|
|
force_https, hsts_enabled, certificate_id, access_list_id, connect_timeout_ms, read_timeout_ms, write_timeout_ms,
|
|
idle_timeout_ms, load_balancing_method, redirect_target_url, redirect_code, redirect_preserve_path,
|
|
redirect_preserve_qs, enabled, comments, created_at, updated_at FROM proxy_hosts WHERE id = ?`, id)
|
|
h, err := scanProxyHost(row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
if err := hydrateProxyHost(db, &h); err != nil {
|
|
return nil, err
|
|
}
|
|
return &h, nil
|
|
}
|
|
|
|
// SaveProxyHost inserts (id==0) or fully replaces (id!=0) a proxy host,
|
|
// including its domains, backends and custom headers, in one transaction.
|
|
func SaveProxyHost(db *sql.DB, h *models.ProxyHost) (int64, error) {
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if h.ID == 0 {
|
|
res, err := tx.Exec(`INSERT INTO proxy_hosts (name, type, listen_addr, forward_scheme, preserve_host_header,
|
|
websocket_support, force_https, hsts_enabled, certificate_id, access_list_id, connect_timeout_ms,
|
|
read_timeout_ms, write_timeout_ms, idle_timeout_ms, load_balancing_method, redirect_target_url,
|
|
redirect_code, redirect_preserve_path, redirect_preserve_qs, enabled, comments)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
h.Name, h.Type, h.ListenAddr, h.ForwardScheme, boolToInt(h.PreserveHostHeader),
|
|
boolToInt(h.WebsocketSupport), boolToInt(h.ForceHTTPS), boolToInt(h.HSTSEnabled), nullInt64(h.CertificateID),
|
|
nullInt64(h.AccessListID), h.ConnectTimeoutMS, h.ReadTimeoutMS, h.WriteTimeoutMS, h.IdleTimeoutMS,
|
|
h.LoadBalancingMethod, h.RedirectTargetURL, h.RedirectCode, boolToInt(h.RedirectPreservePath),
|
|
boolToInt(h.RedirectPreserveQS), boolToInt(h.Enabled), h.Comments)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
h.ID, err = res.LastInsertId()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
} else {
|
|
_, err := tx.Exec(`UPDATE proxy_hosts SET name=?, type=?, listen_addr=?, forward_scheme=?, preserve_host_header=?,
|
|
websocket_support=?, force_https=?, hsts_enabled=?, certificate_id=?, access_list_id=?, connect_timeout_ms=?,
|
|
read_timeout_ms=?, write_timeout_ms=?, idle_timeout_ms=?, load_balancing_method=?, redirect_target_url=?,
|
|
redirect_code=?, redirect_preserve_path=?, redirect_preserve_qs=?, enabled=?, comments=?,
|
|
updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
|
|
h.Name, h.Type, h.ListenAddr, h.ForwardScheme, boolToInt(h.PreserveHostHeader),
|
|
boolToInt(h.WebsocketSupport), boolToInt(h.ForceHTTPS), boolToInt(h.HSTSEnabled), nullInt64(h.CertificateID),
|
|
nullInt64(h.AccessListID), h.ConnectTimeoutMS, h.ReadTimeoutMS, h.WriteTimeoutMS, h.IdleTimeoutMS,
|
|
h.LoadBalancingMethod, h.RedirectTargetURL, h.RedirectCode, boolToInt(h.RedirectPreservePath),
|
|
boolToInt(h.RedirectPreserveQS), boolToInt(h.Enabled), h.Comments, h.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM proxy_host_domains WHERE proxy_host_id = ?`, h.ID); err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM backends WHERE proxy_host_id = ?`, h.ID); err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM custom_headers WHERE proxy_host_id = ?`, h.ID); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
for _, d := range h.Domains {
|
|
if _, err := tx.Exec(`INSERT INTO proxy_host_domains (proxy_host_id, domain) VALUES (?, ?)`, h.ID, d); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
for _, b := range h.Backends {
|
|
if _, err := tx.Exec(`INSERT INTO backends (proxy_host_id, host, port, weight, enabled, health_check_type,
|
|
health_check_path, health_check_status, interval_seconds, failure_threshold, recovery_threshold)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
|
h.ID, b.Host, b.Port, b.Weight, boolToInt(b.Enabled), b.HealthCheckType, b.HealthCheckPath,
|
|
b.HealthCheckStatus, b.IntervalSeconds, b.FailureThreshold, b.RecoveryThreshold); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
for _, c := range h.CustomHeaders {
|
|
if _, err := tx.Exec(`INSERT INTO custom_headers (proxy_host_id, direction, action, name, value) VALUES (?,?,?,?,?)`,
|
|
h.ID, c.Direction, c.Action, c.Name, c.Value); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return 0, err
|
|
}
|
|
return h.ID, nil
|
|
}
|
|
|
|
func DeleteProxyHost(db *sql.DB, id int64) error {
|
|
_, err := db.Exec(`DELETE FROM proxy_hosts WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func SetProxyHostEnabled(db *sql.DB, id int64, enabled bool) error {
|
|
_, err := db.Exec(`UPDATE proxy_hosts SET enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, boolToInt(enabled), id)
|
|
return err
|
|
}
|
|
|
|
func nullInt64(v *int64) any {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return *v
|
|
}
|