v2.31.1 f37fe67

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	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}

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