Files
vibeproxy/internal/database/repo_proxyhosts.go
T
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

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
}