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