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 }