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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user