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