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
121 lines
3.1 KiB
Go
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
|
|
}
|