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