1// Package proxy implements the HTTP adapter for the reverse proxy.
2package proxy
3
4import (
5 "net"
6 "net/http"
7 "sync/atomic"
8
9 "github.com/bnema/zerowrap"
10
11 "github.com/bnema/gordon/internal/adapters/in/http/middleware"
12 "github.com/bnema/gordon/internal/boundaries/in"
13)
14
15// Handler implements http.Handler for the reverse proxy.
16// It owns all HTTP-level concerns: concurrency limiting, body size enforcement,
17// transport selection, and reverse proxy execution.
18// Routing decisions are delegated to the ProxyService usecase.
19type Handler struct {
20 proxySvc in.ProxyService
21 log zerowrap.Logger
22 trustedNets []*net.IPNet
23 appTransport http.RoundTripper
24 h2cTransport http.RoundTripper
25 registryTransport http.RoundTripper
26 activeConns atomic.Int64
27}
28
29// NewHandler creates a new proxy HTTP handler.
30func NewHandler(proxySvc in.ProxyService, trustedNets []*net.IPNet, log zerowrap.Logger) *Handler {
31 return &Handler{
32 proxySvc: proxySvc,
33 log: log,
34 trustedNets: trustedNets,
35 appTransport: newAppTransport(),
36 h2cTransport: newH2CTransport(),
37 registryTransport: newRegistryTransport(),
38 }
39}
40
41// ServeHTTP handles incoming HTTP requests and proxies them to the appropriate backend.
42func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
43 cfg := h.proxySvc.ProxyConfig()
44
45 // SECURITY: Limit concurrent connections to prevent resource exhaustion.
46 if cfg.MaxConcurrentConns > 0 {
47 current := h.activeConns.Add(1)
48 if current > int64(cfg.MaxConcurrentConns) {
49 h.activeConns.Add(-1)
50 proxyError(w, "Service Unavailable", http.StatusServiceUnavailable)
51 return
52 }
53 defer h.activeConns.Add(-1)
54 }
55
56 // SECURITY: Limit request body size to prevent resource exhaustion.
57 if cfg.MaxBodySize > 0 {
58 r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxBodySize)
59 }
60
61 // Enrich request context with fields for downstream logging
62 ctx := zerowrap.CtxWithFields(r.Context(), map[string]any{
63 zerowrap.FieldLayer: "adapter",
64 zerowrap.FieldAdapter: "http",
65 zerowrap.FieldMethod: r.Method,
66 zerowrap.FieldPath: r.URL.Path,
67 zerowrap.FieldHost: r.Host,
68 zerowrap.FieldClientIP: middleware.GetClientIP(r, h.trustedNets),
69 })
70 r = r.WithContext(ctx)
71 log := zerowrap.FromCtx(ctx)
72
73 // Check if this is the registry domain
74 if h.proxySvc.IsRegistryDomain(r.Host) {
75 log.Debug().Msg("routing request to registry")
76 h.forwardToRegistry(w, r, cfg.RegistryPort)
77 return
78 }
79
80 // Get target for this domain
81 log.Debug().Str("resolving_target_for", r.Host).Msg("looking up proxy target")
82 target, err := h.proxySvc.GetTarget(ctx, r.Host)
83 if err != nil {
84 log.Warn().Err(err).Msg("no route found for domain")
85 proxyError(w, "404 page not found", http.StatusNotFound)
86 return
87 }
88
89 log.Debug().
90 Str("host", target.Host).
91 Int("port", target.Port).
92 Str("container_id", target.ContainerID).
93 Msg("resolved proxy target")
94
95 h.forwardToTarget(w, r, target, cfg.MaxResponseSize)
96}