Files
vibeproxy/internal/proxy/server.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

121 lines
3.1 KiB
Go

package proxy
import (
"context"
"crypto/tls"
"database/sql"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/owen/vibeproxy/internal/certificates"
"github.com/owen/vibeproxy/internal/logging"
"github.com/owen/vibeproxy/internal/metrics"
"github.com/owen/vibeproxy/internal/runtime"
)
// Server owns the plain-HTTP and TLS listeners that serve all proxy hosts.
type Server struct {
httpSrv *http.Server
httpsSrv *http.Server
log *slog.Logger
}
type Options struct {
HTTPAddr string // e.g. ":80"
HTTPSAddr string // e.g. ":443"
}
func NewServer(opts Options, store *runtime.Store, certMgr *certificates.Manager, db *sql.DB, log *slog.Logger) *Server {
reqLog := logging.NewRequestLogger(db, log)
go reqLog.Run(context.Background())
handler := NewHandler(store, certMgr, reqLog, db, log)
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{"h2", "http/1.1"},
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := certMgr.GetCertificateForDomain(hello.ServerName)
if err != nil {
metrics.TLSHandshakesTotal.WithLabelValues("error").Inc()
return nil, err
}
metrics.TLSHandshakesTotal.WithLabelValues("ok").Inc()
return cert, nil
},
}
httpAddr := opts.HTTPAddr
if httpAddr == "" {
httpAddr = ":80"
}
httpsAddr := opts.HTTPSAddr
if httpsAddr == "" {
httpsAddr = ":443"
}
return &Server{
log: log,
httpSrv: &http.Server{
Addr: httpAddr,
Handler: handler,
ReadTimeout: 0, // per-request timeouts are enforced inside the handler
WriteTimeout: 0,
IdleTimeout: 120 * time.Second,
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn),
},
httpsSrv: &http.Server{
Addr: httpsAddr,
Handler: handler,
TLSConfig: tlsConfig,
IdleTimeout: 120 * time.Second,
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn),
},
}
}
// Start runs both listeners; it blocks until either fails or ctx is done.
func (s *Server) Start(ctx context.Context) error {
errCh := make(chan error, 2)
go func() {
s.log.Info("http listener starting", "addr", s.httpSrv.Addr)
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- fmt.Errorf("http listener: %w", err)
}
}()
go func() {
s.log.Info("https listener starting", "addr", s.httpsSrv.Addr)
// Certificates are served dynamically via GetCertificate, so we
// don't pass cert/key file paths here.
if err := s.httpsSrv.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- fmt.Errorf("https listener: %w", err)
}
}()
select {
case <-ctx.Done():
return s.Shutdown(context.Background())
case err := <-errCh:
s.Shutdown(context.Background())
return err
}
}
// Shutdown gracefully drains in-flight requests/connections before
// closing the listeners.
func (s *Server) Shutdown(ctx context.Context) error {
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
err1 := s.httpSrv.Shutdown(shutdownCtx)
err2 := s.httpsSrv.Shutdown(shutdownCtx)
if err1 != nil {
return err1
}
return err2
}