v3.3.1 31f1eec

handler.go RawHistoryFolder

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	// registryForwarding permits registry-domain requests to reach the
29	// internal registry through this public proxy. It is enabled only
30	// when registry authentication is on: with auth disabled the
31	// registry is a local-only service reached by direct loopback
32	// connection, never through public ingress.
33	registryForwarding bool
34	activeConns        atomic.Int64
35}
36
37// NewHandler creates a new proxy HTTP handler. Registry forwarding is
38// denied until explicitly enabled with WithRegistryForwarding.
39func NewHandler(proxySvc in.ProxyService, trustedNets []*net.IPNet, log zerowrap.Logger) *Handler {
40	return &Handler{
41		proxySvc:          proxySvc,
42		log:               log,
43		trustedNets:       trustedNets,
44		appTransport:      newAppTransport(),
45		h2cTransport:      newH2CTransport(),
46		registryTransport: newRegistryTransport(),
47	}
48}
49
50// WithRegistryForwarding enables forwarding public registry-domain requests
51// to the internal registry. Callers pass the installation auth state: a
52// disabled-auth registry must stay unreachable through public ingress.
53func (h *Handler) WithRegistryForwarding(enabled bool) *Handler {
54	h.registryForwarding = enabled
55	return h
56}
57
58// ServeHTTP handles incoming HTTP requests and proxies them to the appropriate backend.
59func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
60	cfg := h.proxySvc.ProxyConfig()
61
62	// SECURITY: Limit concurrent connections to prevent resource exhaustion.
63	if cfg.MaxConcurrentConns > 0 {
64		current := h.activeConns.Add(1)
65		if current > int64(cfg.MaxConcurrentConns) {
66			h.activeConns.Add(-1)
67			proxyError(w, "Service Unavailable", http.StatusServiceUnavailable)
68			return
69		}
70		defer h.activeConns.Add(-1)
71	}
72
73	// SECURITY: Limit request body size to prevent resource exhaustion.
74	if cfg.MaxBodySize > 0 {
75		r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxBodySize)
76	}
77
78	// Enrich request context with fields for downstream logging
79	ctx := zerowrap.CtxWithFields(r.Context(), map[string]any{
80		zerowrap.FieldLayer:    "adapter",
81		zerowrap.FieldAdapter:  "http",
82		zerowrap.FieldMethod:   r.Method,
83		zerowrap.FieldPath:     r.URL.Path,
84		zerowrap.FieldHost:     r.Host,
85		zerowrap.FieldClientIP: middleware.GetClientIP(r, h.trustedNets),
86	})
87	r = r.WithContext(ctx)
88	log := zerowrap.FromCtx(ctx)
89
90	host := normalizeRequestHost(r.Host)
91	if host == "" {
92		proxyError(w, "Bad Request", http.StatusBadRequest)
93		return
94	}
95
96	// Check if this is the registry domain
97	if h.proxySvc.IsRegistryDomain(host) {
98		if !h.registryForwarding {
99			// Auth is disabled: the registry is local-only. Forwarding
100			// here would present the proxy's loopback peer to the
101			// registry, which trusts loopback, exposing anonymous
102			// reads and writes on public ingress.
103			log.Warn().Str(zerowrap.FieldHost, host).Msg("registry forwarding denied: authentication disabled")
104			proxyError(w, "404 page not found", http.StatusNotFound)
105			return
106		}
107		log.Debug().Msg("routing request to registry")
108		h.forwardToRegistry(w, r, cfg.RegistryPort)
109		return
110	}
111
112	// Get target for this domain
113	log.Debug().Str("resolving_target_for", host).Msg("looking up proxy target")
114	target, err := h.proxySvc.GetTarget(ctx, host)
115	if err != nil {
116		log.Warn().Err(err).Msg("no route found for domain")
117		proxyError(w, "404 page not found", http.StatusNotFound)
118		return
119	}
120
121	log.Debug().
122		Str("host", target.Host).
123		Int("port", target.Port).
124		Str("container_id", target.ContainerID).
125		Msg("resolved proxy target")
126
127	h.forwardToTarget(w, r, target, cfg.MaxResponseSize)
128}
129
130func normalizeRequestHost(host string) string {
131	host = strings.TrimSpace(host)
132	if h, port, err := net.SplitHostPort(host); err == nil {
133		portNum, err := strconv.Atoi(port)
134		if err != nil || portNum < 1 || portNum > 65535 {
135			return ""
136		}
137		host = h
138	} else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
139		inner := strings.TrimSuffix(strings.TrimPrefix(host, "["), "]")
140		if ip := net.ParseIP(inner); ip == nil || ip.To4() != nil {
141			return ""
142		}
143		host = inner
144	} else if strings.Contains(host, ":") {
145		return ""
146	}
147	host = strings.ToLower(host)
148	host = strings.TrimSuffix(host, ".")
149	return host
150}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close