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
This commit is contained in:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
package app
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/owen/vibeproxy/internal/metrics"
)
type adminServer struct {
srv *http.Server
}
func newAdminServer(addr string, handler http.Handler) *adminServer {
return &adminServer{srv: &http.Server{
Addr: addr,
Handler: handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
}}
}
func (a *adminServer) Start(ctx context.Context) error {
errCh := make(chan error, 1)
go func() {
if err := a.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- fmt.Errorf("admin listener: %w", err)
}
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
return a.srv.Shutdown(shutdownCtx)
case err := <-errCh:
return err
}
}
func healthzHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
}
func readyzHandler(db *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := db.PingContext(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("database unavailable"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
})
}
func metricsHandler() http.Handler {
return metrics.Handler()
}
+166
View File
@@ -0,0 +1,166 @@
// Package app wires together every subsystem (database, runtime store,
// certificate manager, HTTP/TCP/UDP proxying, web UI, REST API) into a
// single running vibeproxy process, and exposes the same wiring pieces
// individually for CLI subcommands that don't need the full server.
package app
import (
"context"
"database/sql"
"fmt"
"log/slog"
"os"
"path/filepath"
"github.com/owen/vibeproxy/internal/api"
"github.com/owen/vibeproxy/internal/auth"
"github.com/owen/vibeproxy/internal/backup"
"github.com/owen/vibeproxy/internal/certificates"
"github.com/owen/vibeproxy/internal/config"
"github.com/owen/vibeproxy/internal/database"
"github.com/owen/vibeproxy/internal/logging"
"github.com/owen/vibeproxy/internal/proxy"
"github.com/owen/vibeproxy/internal/runtime"
"github.com/owen/vibeproxy/internal/streams"
"github.com/owen/vibeproxy/internal/web"
)
type App struct {
Cfg config.Config
DB *sql.DB
Log *slog.Logger
Store *runtime.Store
Certs *certificates.Manager
Streams *streams.Manager
}
// Open initializes the database, runtime store, and certificate manager.
// It does not start any listeners — used by both `serve` and one-shot CLI
// commands like `database migrate`.
func Open(cfg config.Config, log *slog.Logger) (*App, error) {
if err := os.MkdirAll(cfg.DataDir, 0o700); err != nil {
return nil, fmt.Errorf("create data dir: %w", err)
}
db, err := database.Open(cfg.DBPath)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
if err := ensureAdminUser(db, log); err != nil {
db.Close()
return nil, err
}
certStoragePath := database.GetSetting(db, "cert_storage_path", filepath.Join(cfg.DataDir, "certs"))
certMgr, err := certificates.NewManager(db, certStoragePath, log)
if err != nil {
db.Close()
return nil, fmt.Errorf("init certificate manager: %w", err)
}
if err := certMgr.LoadAll(); err != nil {
log.Warn("failed loading certificates", "err", err)
}
store := runtime.NewStore(db)
if err := store.Reload(context.Background()); err != nil {
db.Close()
return nil, fmt.Errorf("initial config load: %w", err)
}
streamMgr := streams.NewManager(db, log)
return &App{Cfg: cfg, DB: db, Log: log, Store: store, Certs: certMgr, Streams: streamMgr}, nil
}
func (a *App) Close() error {
return a.DB.Close()
}
// ensureAdminUser creates the single `admin` account with a generated
// temporary password on first run, per the "no administrator exists yet"
// requirement.
func ensureAdminUser(db *sql.DB, log *slog.Logger) error {
n, err := database.CountUsers(db)
if err != nil {
return err
}
if n > 0 {
return nil
}
password, err := auth.GenerateRandomPassword()
if err != nil {
return err
}
hash, err := auth.HashPassword(password)
if err != nil {
return err
}
if err := database.CreateUser(db, "admin", hash, true); err != nil {
return err
}
database.InsertAuditLog(db, "system", "admin_created", "admin", "initial administrator account created")
fmt.Println("========================================================")
fmt.Println(" vibeproxy: created initial administrator account")
fmt.Println(" username: admin")
fmt.Printf(" password: %s\n", password)
fmt.Println(" Change this password after logging in (Settings > Admin Account).")
fmt.Println("========================================================")
log.Info("created initial admin account", "username", "admin")
return nil
}
// Run starts every listener (HTTP/HTTPS proxy, TCP/UDP streams, admin web
// UI + API) and background loops (health checks are started by the
// runtime store on reload; certificate renewal, log retention, backups),
// blocking until ctx is cancelled, then shutting everything down
// gracefully.
func (a *App) Run(ctx context.Context) error {
if err := a.Streams.Reload(ctx); err != nil {
return fmt.Errorf("initial stream load: %w", err)
}
settings := a.Store.Current().Settings
a.Certs.StartRenewalLoop(ctx, settings.ACMERenewalWindowDays, settings.ACMEEmail, settings.ACMEStaging)
logging.StartRetentionLoop(ctx, a.DB, a.Log, func() int {
return a.Store.Current().Settings.LogRetentionDays
})
backup.StartScheduledBackups(ctx, a.DB, a.Log,
func() string { return a.Store.Current().Settings.BackupPath },
func() int { return a.Store.Current().Settings.BackupIntervalHours },
func() int { return a.Store.Current().Settings.BackupRetentionCount },
func() bool { return a.Store.Current().Settings.BackupEnabled },
)
proxySrv := proxy.NewServer(proxy.Options{HTTPAddr: a.Cfg.HTTPAddr, HTTPSAddr: a.Cfg.HTTPSAddr}, a.Store, a.Certs, a.DB, a.Log)
csrf := auth.NewCSRF()
adminMux := web.NewRouter(a.DB, a.Store, a.Certs, a.Streams, csrf, a.Log)
apiMux := api.NewRouter(a.DB, a.Store, a.Certs, a.Streams, a.Log)
adminMux.Handle("/api/v1/", apiMux)
adminMux.Handle("/healthz", healthzHandler())
adminMux.Handle("/readyz", readyzHandler(a.DB))
adminMux.Handle("/metrics", metricsHandler())
adminSrv := newAdminServer(a.Cfg.AdminAddr, adminMux)
errCh := make(chan error, 4)
go func() {
if err := proxySrv.Start(ctx); err != nil {
errCh <- err
}
}()
go func() {
if err := adminSrv.Start(ctx); err != nil {
errCh <- err
}
}()
select {
case <-ctx.Done():
case err := <-errCh:
a.Log.Error("fatal listener error", "err", err)
}
a.Streams.StopAll()
return nil
}