detached 0986ec3

refactor: resolve pre-release hexarch follow-ups

commit
0986ec35b6c61d526994d2d3f1271f3c65118c84
author
brice · · 2026-04-08 15:31 UTC
parent
a8338c0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +173 -89
M internal/adapters/in/cli/ca.go +4 -3
@@ -13,6 +13,7 @@ import (
1313 "github.com/spf13/cobra"
1414
1515 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
16+ boundaryout "github.com/bnema/gordon/internal/boundaries/out"
1617 )
1718
1819 func newCACmd() *cobra.Command {
@@ -108,7 +109,7 @@ func resolveCADataDir() (string, error) {
108109 // loadCAFromDataDir creates a CA adapter directly (bypassing the usecase layer).
109110 // This is intentional: CLI commands like 'ca export' and 'ca info' are standalone
110111 // admin utilities that run outside the server lifecycle and don't need business logic.
111-func loadCAFromDataDir(dataDir string) (*pkiadapter.CA, error) {
112+func loadCAFromDataDir(dataDir string) (boundaryout.CertificateAuthority, error) {
112113 ca, err := pkiadapter.NewCA(dataDir, cliLogger())
113114 if err != nil {
114115 return nil, fmt.Errorf("failed to load CA from %s: %w", dataDir, err)
@@ -155,7 +156,7 @@ func runCAInstall(_ context.Context, out io.Writer, dataDir string, uninstall, j
155156 }
156157
157158 if uninstall {
158- if err := pkiadapter.UninstallRoot(cert); err != nil {
159+ if err := ca.UninstallRoot(cert); err != nil {
159160 return fmt.Errorf("failed to uninstall root CA: %w", err)
160161 }
161162 if jsonOut {
@@ -164,7 +165,7 @@ func runCAInstall(_ context.Context, out io.Writer, dataDir string, uninstall, j
164165 return cliWriteLine(out, cliRenderSuccess("Root CA removed from system trust stores"))
165166 }
166167
167- if err := pkiadapter.InstallRoot(cert); err != nil {
168+ if err := ca.InstallRoot(cert); err != nil {
168169 return fmt.Errorf("failed to install root CA: %w", err)
169170 }
170171 if jsonOut {
@@ -13,6 +13,7 @@ import (
13 "github.com/spf13/cobra"13 "github.com/spf13/cobra"
14 14
15 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"15 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
16+ boundaryout "github.com/bnema/gordon/internal/boundaries/out"
16 )17 )
17 18
18 func newCACmd() *cobra.Command {19 func newCACmd() *cobra.Command {
@@ -108,7 +109,7 @@ func resolveCADataDir() (string, error) {
108 // loadCAFromDataDir creates a CA adapter directly (bypassing the usecase layer).109 // loadCAFromDataDir creates a CA adapter directly (bypassing the usecase layer).
109 // This is intentional: CLI commands like 'ca export' and 'ca info' are standalone110 // This is intentional: CLI commands like 'ca export' and 'ca info' are standalone
110 // admin utilities that run outside the server lifecycle and don't need business logic.111 // admin utilities that run outside the server lifecycle and don't need business logic.
111-func loadCAFromDataDir(dataDir string) (*pkiadapter.CA, error) {112+func loadCAFromDataDir(dataDir string) (boundaryout.CertificateAuthority, error) {
112 ca, err := pkiadapter.NewCA(dataDir, cliLogger())113 ca, err := pkiadapter.NewCA(dataDir, cliLogger())
113 if err != nil {114 if err != nil {
114 return nil, fmt.Errorf("failed to load CA from %s: %w", dataDir, err)115 return nil, fmt.Errorf("failed to load CA from %s: %w", dataDir, err)
@@ -155,7 +156,7 @@ func runCAInstall(_ context.Context, out io.Writer, dataDir string, uninstall, j
155 }156 }
156 157
157 if uninstall {158 if uninstall {
158- if err := pkiadapter.UninstallRoot(cert); err != nil {159+ if err := ca.UninstallRoot(cert); err != nil {
159 return fmt.Errorf("failed to uninstall root CA: %w", err)160 return fmt.Errorf("failed to uninstall root CA: %w", err)
160 }161 }
161 if jsonOut {162 if jsonOut {
@@ -164,7 +165,7 @@ func runCAInstall(_ context.Context, out io.Writer, dataDir string, uninstall, j
164 return cliWriteLine(out, cliRenderSuccess("Root CA removed from system trust stores"))165 return cliWriteLine(out, cliRenderSuccess("Root CA removed from system trust stores"))
165 }166 }
166 167
167- if err := pkiadapter.InstallRoot(cert); err != nil {168+ if err := ca.InstallRoot(cert); err != nil {
168 return fmt.Errorf("failed to install root CA: %w", err)169 return fmt.Errorf("failed to install root CA: %w", err)
169 }170 }
170 if jsonOut {171 if jsonOut {
M internal/adapters/in/http/auth/handler.go +2 -2
@@ -12,7 +12,7 @@ import (
1212 "github.com/bnema/zerowrap"
1313
1414 "github.com/bnema/gordon/internal/adapters/dto"
15- "github.com/bnema/gordon/internal/adapters/in/http/httputil"
15+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1616 "github.com/bnema/gordon/internal/boundaries/in"
1717 "github.com/bnema/gordon/internal/domain"
1818 )
@@ -166,7 +166,7 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
166166 }
167167
168168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {
169- if httputil.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {
169+ if httphelper.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {
170170 log.Debug().Str("username", username).Msg("internal registry auth accepted")
171171 return true, &domain.TokenClaims{
172172 Subject: username,
@@ -12,7 +12,7 @@ import (
12 "github.com/bnema/zerowrap"12 "github.com/bnema/zerowrap"
13 13
14 "github.com/bnema/gordon/internal/adapters/dto"14 "github.com/bnema/gordon/internal/adapters/dto"
15- "github.com/bnema/gordon/internal/adapters/in/http/httputil"15+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
16 "github.com/bnema/gordon/internal/boundaries/in"16 "github.com/bnema/gordon/internal/boundaries/in"
17 "github.com/bnema/gordon/internal/domain"17 "github.com/bnema/gordon/internal/domain"
18 )18 )
@@ -166,7 +166,7 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
166 }166 }
167 167
168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {
169- if httputil.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {169+ if httphelper.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {
170 log.Debug().Str("username", username).Msg("internal registry auth accepted")170 log.Debug().Str("username", username).Msg("internal registry auth accepted")
171 return true, &domain.TokenClaims{171 return true, &domain.TokenClaims{
172 Subject: username,172 Subject: username,
M internal/adapters/in/http/auth/handler_test.go +2 -2
@@ -13,7 +13,7 @@ import (
1313 "github.com/stretchr/testify/assert"
1414 "github.com/stretchr/testify/mock"
1515
16- "github.com/bnema/gordon/internal/adapters/in/http/httputil"
16+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1717 "github.com/bnema/gordon/internal/boundaries/in/mocks"
1818 "github.com/bnema/gordon/internal/domain"
1919 )
@@ -358,7 +358,7 @@ func TestIsLocalhostRequest(t *testing.T) {
358358 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)
359359 req.RemoteAddr = tt.remoteAddr
360360
361- result := httputil.IsLocalhostRequest(req)
361+ result := httphelper.IsLocalhostRequest(req)
362362
363363 assert.Equal(t, tt.want, result)
364364 })
@@ -13,7 +13,7 @@ import (
13 "github.com/stretchr/testify/assert"13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"14 "github.com/stretchr/testify/mock"
15 15
16- "github.com/bnema/gordon/internal/adapters/in/http/httputil"16+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
17 "github.com/bnema/gordon/internal/boundaries/in/mocks"17 "github.com/bnema/gordon/internal/boundaries/in/mocks"
18 "github.com/bnema/gordon/internal/domain"18 "github.com/bnema/gordon/internal/domain"
19 )19 )
@@ -358,7 +358,7 @@ func TestIsLocalhostRequest(t *testing.T) {
358 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)358 req := httptest.NewRequest(http.MethodGet, "/auth/token", nil)
359 req.RemoteAddr = tt.remoteAddr359 req.RemoteAddr = tt.remoteAddr
360 360
361- result := httputil.IsLocalhostRequest(req)361+ result := httphelper.IsLocalhostRequest(req)
362 362
363 assert.Equal(t, tt.want, result)363 assert.Equal(t, tt.want, result)
364 })364 })
R internal/adapters/in/http/httputil/localhost.go → internal/adapters/in/http/httphelper/localhost.go +1 -1 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package httputil
1+package httphelper
22
33 import (
44 "net"
@@ -1,4 +1,4 @@
1-package httputil1+package httphelper
2 2
3 import (3 import (
4 "net"4 "net"
R internal/adapters/in/http/httputil/localhost_test.go → internal/adapters/in/http/httphelper/localhost_test.go +3 -3 renamed, 82% similar
@@ -1,11 +1,11 @@
1-package httputil_test
1+package httphelper_test
22
33 import (
44 "net/http"
55 "net/http/httptest"
66 "testing"
77
8- "github.com/bnema/gordon/internal/adapters/in/http/httputil"
8+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
99 )
1010
1111 func TestIsLocalhostRequest(t *testing.T) {
@@ -25,7 +25,7 @@ func TestIsLocalhostRequest(t *testing.T) {
2525 t.Run(tt.name, func(t *testing.T) {
2626 req := httptest.NewRequest(http.MethodGet, "/", nil)
2727 req.RemoteAddr = tt.remoteAddr
28- if got := httputil.IsLocalhostRequest(req); got != tt.want {
28+ if got := httphelper.IsLocalhostRequest(req); got != tt.want {
2929 t.Errorf("IsLocalhostRequest(%q) = %v, want %v", tt.remoteAddr, got, tt.want)
3030 }
3131 })
@@ -1,11 +1,11 @@
1-package httputil_test1+package httphelper_test
2 2
3 import (3 import (
4 "net/http"4 "net/http"
5 "net/http/httptest"5 "net/http/httptest"
6 "testing"6 "testing"
7 7
8- "github.com/bnema/gordon/internal/adapters/in/http/httputil"8+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
9 )9 )
10 10
11 func TestIsLocalhostRequest(t *testing.T) {11 func TestIsLocalhostRequest(t *testing.T) {
@@ -25,7 +25,7 @@ func TestIsLocalhostRequest(t *testing.T) {
25 t.Run(tt.name, func(t *testing.T) {25 t.Run(tt.name, func(t *testing.T) {
26 req := httptest.NewRequest(http.MethodGet, "/", nil)26 req := httptest.NewRequest(http.MethodGet, "/", nil)
27 req.RemoteAddr = tt.remoteAddr27 req.RemoteAddr = tt.remoteAddr
28- if got := httputil.IsLocalhostRequest(req); got != tt.want {28+ if got := httphelper.IsLocalhostRequest(req); got != tt.want {
29 t.Errorf("IsLocalhostRequest(%q) = %v, want %v", tt.remoteAddr, got, tt.want)29 t.Errorf("IsLocalhostRequest(%q) = %v, want %v", tt.remoteAddr, got, tt.want)
30 }30 }
31 })31 })
A internal/adapters/in/http/httphelper/trust.go +110 -0
@@ -0,0 +1,110 @@
1+package httphelper
2+
3+import (
4+ "net"
5+ "net/http"
6+ "strconv"
7+ "strings"
8+)
9+
10+// localhostNets contains IPv4 and IPv6 loopback ranges.
11+var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
12+
13+// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to [net.IPNet].
14+// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
15+// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
16+func ParseTrustedProxies(proxies []string) []*net.IPNet {
17+ var nets []*net.IPNet
18+ for _, proxy := range proxies {
19+ // Try parsing as CIDR
20+ _, ipNet, err := net.ParseCIDR(proxy)
21+ if err == nil {
22+ nets = append(nets, ipNet)
23+ continue
24+ }
25+ // Try parsing as single IP
26+ ip := net.ParseIP(proxy)
27+ if ip != nil {
28+ // Convert single IP to /32 or /128 CIDR
29+ bits := 32
30+ if ip.To4() == nil {
31+ bits = 128
32+ }
33+ nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
34+ }
35+ }
36+ return nets
37+}
38+
39+// IsTrustedProxy reports whether the given IP address falls within any of the
40+// trusted networks. Returns false if trustedNets is empty or the IP cannot be parsed.
41+func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
42+ if len(trustedNets) == 0 {
43+ return false
44+ }
45+ parsedIP := net.ParseIP(ip)
46+ if parsedIP == nil {
47+ return false
48+ }
49+ for _, ipNet := range trustedNets {
50+ if ipNet.Contains(parsedIP) {
51+ return true
52+ }
53+ }
54+ return false
55+}
56+
57+// IsTrustedOrLocal reports whether an IP belongs to localhost or the given
58+// trusted proxy networks.
59+func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {
60+ return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets)
61+}
62+
63+// ExtractRemoteIP returns the IP portion of an [http.Request.RemoteAddr],
64+// stripping the port. When RemoteAddr lacks a port (e.g. Unix sockets or
65+// certain proxy setups), it is returned as-is.
66+func ExtractRemoteIP(remoteAddr string) string {
67+ host, _, err := net.SplitHostPort(remoteAddr)
68+ if err != nil {
69+ return remoteAddr
70+ }
71+ return host
72+}
73+
74+// IsTrustedSource reports whether the request's remote address is in trustedNets.
75+// When RemoteAddr lacks a port, IPv6 brackets are stripped before checking.
76+func IsTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
77+ if len(trustedNets) == 0 {
78+ return false
79+ }
80+ host, _, err := net.SplitHostPort(r.RemoteAddr)
81+ if err != nil {
82+ // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
83+ // or certain proxy setups). Fall back to using it directly.
84+ host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
85+ }
86+ return IsTrustedProxy(host, trustedNets)
87+}
88+
89+// HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into
90+// the correct HTTPS authority (host or host:port).
91+//
92+// Rules:
93+// - No port in Host: omit TLS port (public reverse-proxy assumed on :443).
94+// - Host port == httpPort: map to tlsPort.
95+// - Any other explicit port: preserve it.
96+func HTTPSAuthority(host string, httpPort, tlsPort int) string {
97+ hostname, portStr, err := net.SplitHostPort(host)
98+ if err != nil {
99+ // No port in Host header — omit TLS port from URL.
100+ return host
101+ }
102+
103+ port, err := strconv.Atoi(portStr)
104+ if err == nil && port == httpPort {
105+ return net.JoinHostPort(hostname, strconv.Itoa(tlsPort))
106+ }
107+
108+ // Unknown explicit port — preserve it.
109+ return net.JoinHostPort(hostname, portStr)
110+}
@@ -0,0 +1,110 @@
1+package httphelper
2+
3+import (
4+ "net"
5+ "net/http"
6+ "strconv"
7+ "strings"
8+)
9+
10+// localhostNets contains IPv4 and IPv6 loopback ranges.
11+var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
12+
13+// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to [net.IPNet].
14+// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
15+// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
16+func ParseTrustedProxies(proxies []string) []*net.IPNet {
17+ var nets []*net.IPNet
18+ for _, proxy := range proxies {
19+ // Try parsing as CIDR
20+ _, ipNet, err := net.ParseCIDR(proxy)
21+ if err == nil {
22+ nets = append(nets, ipNet)
23+ continue
24+ }
25+ // Try parsing as single IP
26+ ip := net.ParseIP(proxy)
27+ if ip != nil {
28+ // Convert single IP to /32 or /128 CIDR
29+ bits := 32
30+ if ip.To4() == nil {
31+ bits = 128
32+ }
33+ nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
34+ }
35+ }
36+ return nets
37+}
38+
39+// IsTrustedProxy reports whether the given IP address falls within any of the
40+// trusted networks. Returns false if trustedNets is empty or the IP cannot be parsed.
41+func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
42+ if len(trustedNets) == 0 {
43+ return false
44+ }
45+ parsedIP := net.ParseIP(ip)
46+ if parsedIP == nil {
47+ return false
48+ }
49+ for _, ipNet := range trustedNets {
50+ if ipNet.Contains(parsedIP) {
51+ return true
52+ }
53+ }
54+ return false
55+}
56+
57+// IsTrustedOrLocal reports whether an IP belongs to localhost or the given
58+// trusted proxy networks.
59+func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {
60+ return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets)
61+}
62+
63+// ExtractRemoteIP returns the IP portion of an [http.Request.RemoteAddr],
64+// stripping the port. When RemoteAddr lacks a port (e.g. Unix sockets or
65+// certain proxy setups), it is returned as-is.
66+func ExtractRemoteIP(remoteAddr string) string {
67+ host, _, err := net.SplitHostPort(remoteAddr)
68+ if err != nil {
69+ return remoteAddr
70+ }
71+ return host
72+}
73+
74+// IsTrustedSource reports whether the request's remote address is in trustedNets.
75+// When RemoteAddr lacks a port, IPv6 brackets are stripped before checking.
76+func IsTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
77+ if len(trustedNets) == 0 {
78+ return false
79+ }
80+ host, _, err := net.SplitHostPort(r.RemoteAddr)
81+ if err != nil {
82+ // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
83+ // or certain proxy setups). Fall back to using it directly.
84+ host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
85+ }
86+ return IsTrustedProxy(host, trustedNets)
87+}
88+
89+// HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into
90+// the correct HTTPS authority (host or host:port).
91+//
92+// Rules:
93+// - No port in Host: omit TLS port (public reverse-proxy assumed on :443).
94+// - Host port == httpPort: map to tlsPort.
95+// - Any other explicit port: preserve it.
96+func HTTPSAuthority(host string, httpPort, tlsPort int) string {
97+ hostname, portStr, err := net.SplitHostPort(host)
98+ if err != nil {
99+ // No port in Host header — omit TLS port from URL.
100+ return host
101+ }
102+
103+ port, err := strconv.Atoi(portStr)
104+ if err == nil && port == httpPort {
105+ return net.JoinHostPort(hostname, strconv.Itoa(tlsPort))
106+ }
107+
108+ // Unknown explicit port — preserve it.
109+ return net.JoinHostPort(hostname, portStr)
110+}
M internal/adapters/in/http/middleware/auth.go +3 -3
@@ -12,7 +12,7 @@ import (
1212 "github.com/bnema/zerowrap"
1313
1414 "github.com/bnema/gordon/internal/adapters/dto"
15- "github.com/bnema/gordon/internal/adapters/in/http/httputil"
15+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1616 "github.com/bnema/gordon/internal/boundaries/in"
1717 "github.com/bnema/gordon/internal/domain"
1818 )
@@ -59,7 +59,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, t
5959 return func(next http.Handler) http.Handler {
6060 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6161 // Allow localhost requests only with internal instance credentials.
62- if httputil.IsLocalhostRequest(r) && isInternalRegistryAuth(r, internalAuth) {
62+ if httphelper.IsLocalhostRequest(r) && isInternalRegistryAuth(r, internalAuth) {
6363 log.Debug().
6464 Str(zerowrap.FieldLayer, "adapter").
6565 Str(zerowrap.FieldAdapter, "http").
@@ -72,7 +72,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, t
7272 }
7373
7474 // Warn if not using TLS (skip for localhost — internal proxy traffic)
75- if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" && !httputil.IsLocalhostRequest(r) {
75+ if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" && !httphelper.IsLocalhostRequest(r) {
7676 log.Warn().
7777 Str(zerowrap.FieldLayer, "adapter").
7878 Str(zerowrap.FieldAdapter, "http").
@@ -12,7 +12,7 @@ import (
12 "github.com/bnema/zerowrap"12 "github.com/bnema/zerowrap"
13 13
14 "github.com/bnema/gordon/internal/adapters/dto"14 "github.com/bnema/gordon/internal/adapters/dto"
15- "github.com/bnema/gordon/internal/adapters/in/http/httputil"15+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
16 "github.com/bnema/gordon/internal/boundaries/in"16 "github.com/bnema/gordon/internal/boundaries/in"
17 "github.com/bnema/gordon/internal/domain"17 "github.com/bnema/gordon/internal/domain"
18 )18 )
@@ -59,7 +59,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, t
59 return func(next http.Handler) http.Handler {59 return func(next http.Handler) http.Handler {
60 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {60 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61 // Allow localhost requests only with internal instance credentials.61 // Allow localhost requests only with internal instance credentials.
62- if httputil.IsLocalhostRequest(r) && isInternalRegistryAuth(r, internalAuth) {62+ if httphelper.IsLocalhostRequest(r) && isInternalRegistryAuth(r, internalAuth) {
63 log.Debug().63 log.Debug().
64 Str(zerowrap.FieldLayer, "adapter").64 Str(zerowrap.FieldLayer, "adapter").
65 Str(zerowrap.FieldAdapter, "http").65 Str(zerowrap.FieldAdapter, "http").
@@ -72,7 +72,7 @@ func RegistryAuthV2(authSvc in.AuthService, internalAuth InternalRegistryAuth, t
72 }72 }
73 73
74 // Warn if not using TLS (skip for localhost — internal proxy traffic)74 // Warn if not using TLS (skip for localhost — internal proxy traffic)
75- if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" && !httputil.IsLocalhostRequest(r) {75+ if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" && !httphelper.IsLocalhostRequest(r) {
76 log.Warn().76 log.Warn().
77 Str(zerowrap.FieldLayer, "adapter").77 Str(zerowrap.FieldLayer, "adapter").
78 Str(zerowrap.FieldAdapter, "http").78 Str(zerowrap.FieldAdapter, "http").
M internal/adapters/in/http/middleware/cidr.go +13 -25
@@ -5,16 +5,16 @@ import (
55 "fmt"
66 "net"
77 "net/http"
8- "strconv"
98 "strings"
109
1110 "github.com/bnema/zerowrap"
1211
1312 "github.com/bnema/gordon/internal/adapters/dto"
13+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1414 )
1515
1616 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.
17-var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
17+var localhostNets = httphelper.ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
1818
1919 // cidrAllowlist is the shared implementation for CIDR-based access control middleware.
2020 // ipExtractor determines how the client IP is obtained from the request.
@@ -52,40 +52,28 @@ func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) str
5252 }
5353
5454 // ExtractRemoteIP returns the IP portion of a RemoteAddr, stripping the port.
55+//
56+// Deprecated: use httphelper.ExtractRemoteIP directly. This wrapper preserves
57+// the existing middleware API for callers that have not migrated yet.
5558 func ExtractRemoteIP(remoteAddr string) string {
56- host, _, err := net.SplitHostPort(remoteAddr)
57- if err != nil {
58- return remoteAddr
59- }
60- return host
59+ return httphelper.ExtractRemoteIP(remoteAddr)
6160 }
6261
6362 // IsTrustedOrLocal reports whether an IP belongs to localhost or trusted proxy nets.
63+//
64+// Deprecated: use httphelper.IsTrustedOrLocal directly. This wrapper preserves
65+// the existing middleware API for callers that have not migrated yet.
6466 func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {
65- return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets)
67+ return httphelper.IsTrustedOrLocal(ip, proxyNets)
6668 }
6769
6870 // HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into the
6971 // correct HTTPS authority (host or host:port).
7072 //
71-// Rules:
72-// - No port in Host → omit TLS port (public reverse-proxy assumed on :443).
73-// - Host port == httpPort → map to tlsPort.
74-// - Any other explicit port → preserve it.
73+// Deprecated: use httphelper.HTTPSAuthority directly. This wrapper preserves
74+// the existing middleware API for callers that have not migrated yet.
7575 func HTTPSAuthority(host string, httpPort, tlsPort int) string {
76- hostname, portStr, err := net.SplitHostPort(host)
77- if err != nil {
78- // No port in Host header — omit TLS port from URL.
79- return host
80- }
81-
82- port, err := strconv.Atoi(portStr)
83- if err == nil && port == httpPort {
84- return net.JoinHostPort(hostname, strconv.Itoa(tlsPort))
85- }
86-
87- // Unknown explicit port — preserve it.
88- return net.JoinHostPort(hostname, portStr)
76+ return httphelper.HTTPSAuthority(host, httpPort, tlsPort)
8977 }
9078
9179 // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.
@@ -5,16 +5,16 @@ import (
5 "fmt"5 "fmt"
6 "net"6 "net"
7 "net/http"7 "net/http"
8- "strconv"
9 "strings"8 "strings"
10 9
11 "github.com/bnema/zerowrap"10 "github.com/bnema/zerowrap"
12 11
13 "github.com/bnema/gordon/internal/adapters/dto"12 "github.com/bnema/gordon/internal/adapters/dto"
13+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
14 )14 )
15 15
16 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.16 // localhostNets contains IPv4 and IPv6 loopback ranges that are always allowed.
17-var localhostNets = ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})17+var localhostNets = httphelper.ParseTrustedProxies([]string{"127.0.0.0/8", "::1"})
18 18
19 // cidrAllowlist is the shared implementation for CIDR-based access control middleware.19 // cidrAllowlist is the shared implementation for CIDR-based access control middleware.
20 // ipExtractor determines how the client IP is obtained from the request.20 // ipExtractor determines how the client IP is obtained from the request.
@@ -52,40 +52,28 @@ func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) str
52 }52 }
53 53
54 // ExtractRemoteIP returns the IP portion of a RemoteAddr, stripping the port.54 // ExtractRemoteIP returns the IP portion of a RemoteAddr, stripping the port.
55+//
56+// Deprecated: use httphelper.ExtractRemoteIP directly. This wrapper preserves
57+// the existing middleware API for callers that have not migrated yet.
55 func ExtractRemoteIP(remoteAddr string) string {58 func ExtractRemoteIP(remoteAddr string) string {
56- host, _, err := net.SplitHostPort(remoteAddr)59+ return httphelper.ExtractRemoteIP(remoteAddr)
57- if err != nil {
58- return remoteAddr
59- }
60- return host
61 }60 }
62 61
63 // IsTrustedOrLocal reports whether an IP belongs to localhost or trusted proxy nets.62 // IsTrustedOrLocal reports whether an IP belongs to localhost or trusted proxy nets.
63+//
64+// Deprecated: use httphelper.IsTrustedOrLocal directly. This wrapper preserves
65+// the existing middleware API for callers that have not migrated yet.
64 func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {66 func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool {
65- return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets)67+ return httphelper.IsTrustedOrLocal(ip, proxyNets)
66 }68 }
67 69
68 // HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into the70 // HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into the
69 // correct HTTPS authority (host or host:port).71 // correct HTTPS authority (host or host:port).
70 //72 //
71-// Rules:73+// Deprecated: use httphelper.HTTPSAuthority directly. This wrapper preserves
72-// - No port in Host → omit TLS port (public reverse-proxy assumed on :443).74+// the existing middleware API for callers that have not migrated yet.
73-// - Host port == httpPort → map to tlsPort.
74-// - Any other explicit port → preserve it.
75 func HTTPSAuthority(host string, httpPort, tlsPort int) string {75 func HTTPSAuthority(host string, httpPort, tlsPort int) string {
76- hostname, portStr, err := net.SplitHostPort(host)76+ return httphelper.HTTPSAuthority(host, httpPort, tlsPort)
77- if err != nil {
78- // No port in Host header — omit TLS port from URL.
79- return host
80- }
81-
82- port, err := strconv.Atoi(portStr)
83- if err == nil && port == httpPort {
84- return net.JoinHostPort(hostname, strconv.Itoa(tlsPort))
85- }
86-
87- // Unknown explicit port — preserve it.
88- return net.JoinHostPort(hostname, portStr)
89 }77 }
90 78
91 // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.79 // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges.
M internal/adapters/in/http/middleware/clientip.go +11 -34
@@ -4,12 +4,14 @@ import (
44 "net"
55 "net/http"
66 "strings"
7+
8+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
79 )
810
911 // cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.
1012 // Cf-Connecting-Ip is only trusted when the immediate upstream matches these.
1113 // Source: https://www.cloudflare.com/ips/
12-var cloudflareNets = ParseTrustedProxies([]string{
14+var cloudflareNets = httphelper.ParseTrustedProxies([]string{
1315 // IPv4
1416 "173.245.48.0/20",
1517 "103.21.244.0/22",
@@ -57,45 +59,20 @@ func normalizeIP(raw string) string {
5759 // ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.
5860 // It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
5961 // Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
62+//
63+// Deprecated: use httphelper.ParseTrustedProxies directly. This wrapper preserves
64+// the existing middleware API for callers that have not migrated yet.
6065 func ParseTrustedProxies(proxies []string) []*net.IPNet {
61- var nets []*net.IPNet
62- for _, proxy := range proxies {
63- // Try parsing as CIDR
64- _, ipNet, err := net.ParseCIDR(proxy)
65- if err == nil {
66- nets = append(nets, ipNet)
67- continue
68- }
69- // Try parsing as single IP
70- ip := net.ParseIP(proxy)
71- if ip != nil {
72- // Convert single IP to /32 or /128 CIDR
73- bits := 32
74- if ip.To4() == nil {
75- bits = 128
76- }
77- nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
78- }
79- }
80- return nets
66+ return httphelper.ParseTrustedProxies(proxies)
8167 }
8268
8369 // IsTrustedProxy checks if the given IP address is from a trusted proxy.
8470 // Returns false if trustedNets is empty or the IP cannot be parsed.
71+//
72+// Deprecated: use httphelper.IsTrustedProxy directly. This wrapper preserves
73+// the existing middleware API for callers that have not migrated yet.
8574 func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
86- if len(trustedNets) == 0 {
87- return false
88- }
89- parsedIP := net.ParseIP(ip)
90- if parsedIP == nil {
91- return false
92- }
93- for _, ipNet := range trustedNets {
94- if ipNet.Contains(parsedIP) {
95- return true
96- }
97- }
98- return false
75+ return httphelper.IsTrustedProxy(ip, trustedNets)
9976 }
10077
10178 // GetClientIP extracts the client IP address from the request.
@@ -4,12 +4,14 @@ import (
4 "net"4 "net"
5 "net/http"5 "net/http"
6 "strings"6 "strings"
7+
8+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
7 )9 )
8 10
9 // cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.11 // cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.
10 // Cf-Connecting-Ip is only trusted when the immediate upstream matches these.12 // Cf-Connecting-Ip is only trusted when the immediate upstream matches these.
11 // Source: https://www.cloudflare.com/ips/13 // Source: https://www.cloudflare.com/ips/
12-var cloudflareNets = ParseTrustedProxies([]string{14+var cloudflareNets = httphelper.ParseTrustedProxies([]string{
13 // IPv415 // IPv4
14 "173.245.48.0/20",16 "173.245.48.0/20",
15 "103.21.244.0/22",17 "103.21.244.0/22",
@@ -57,45 +59,20 @@ func normalizeIP(raw string) string {
57 // ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.59 // ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.
58 // It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").60 // It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
59 // Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.61 // Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
62+//
63+// Deprecated: use httphelper.ParseTrustedProxies directly. This wrapper preserves
64+// the existing middleware API for callers that have not migrated yet.
60 func ParseTrustedProxies(proxies []string) []*net.IPNet {65 func ParseTrustedProxies(proxies []string) []*net.IPNet {
61- var nets []*net.IPNet66+ return httphelper.ParseTrustedProxies(proxies)
62- for _, proxy := range proxies {
63- // Try parsing as CIDR
64- _, ipNet, err := net.ParseCIDR(proxy)
65- if err == nil {
66- nets = append(nets, ipNet)
67- continue
68- }
69- // Try parsing as single IP
70- ip := net.ParseIP(proxy)
71- if ip != nil {
72- // Convert single IP to /32 or /128 CIDR
73- bits := 32
74- if ip.To4() == nil {
75- bits = 128
76- }
77- nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
78- }
79- }
80- return nets
81 }67 }
82 68
83 // IsTrustedProxy checks if the given IP address is from a trusted proxy.69 // IsTrustedProxy checks if the given IP address is from a trusted proxy.
84 // Returns false if trustedNets is empty or the IP cannot be parsed.70 // Returns false if trustedNets is empty or the IP cannot be parsed.
71+//
72+// Deprecated: use httphelper.IsTrustedProxy directly. This wrapper preserves
73+// the existing middleware API for callers that have not migrated yet.
85 func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {74 func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
86- if len(trustedNets) == 0 {75+ return httphelper.IsTrustedProxy(ip, trustedNets)
87- return false
88- }
89- parsedIP := net.ParseIP(ip)
90- if parsedIP == nil {
91- return false
92- }
93- for _, ipNet := range trustedNets {
94- if ipNet.Contains(parsedIP) {
95- return true
96- }
97- }
98- return false
99 }76 }
100 77
101 // GetClientIP extracts the client IP address from the request.78 // GetClientIP extracts the client IP address from the request.
M internal/adapters/in/http/onboarding/handler.go +2 -2
@@ -6,7 +6,7 @@ import (
66 "net"
77 "net/http"
88
9- "github.com/bnema/gordon/internal/adapters/in/http/middleware"
9+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1010 )
1111
1212 // Handler serves CA onboarding endpoints for direct/Tailnet clients.
@@ -49,7 +49,7 @@ func (h *Handler) ServeMobileconfig(w http.ResponseWriter, _ *http.Request) {
4949 // publicHTTPSURL derives the public HTTPS URL from the request Host header.
5050 // The result is HTML-escaped to prevent XSS via a crafted Host header.
5151 func (h *Handler) publicHTTPSURL(r *http.Request) string {
52- authority := middleware.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort)
52+ authority := httphelper.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort)
5353 return html.EscapeString("https://" + authority + "/")
5454 }
5555
@@ -6,7 +6,7 @@ import (
6 "net"6 "net"
7 "net/http"7 "net/http"
8 8
9- "github.com/bnema/gordon/internal/adapters/in/http/middleware"9+ "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
10 )10 )
11 11
12 // Handler serves CA onboarding endpoints for direct/Tailnet clients.12 // Handler serves CA onboarding endpoints for direct/Tailnet clients.
@@ -49,7 +49,7 @@ func (h *Handler) ServeMobileconfig(w http.ResponseWriter, _ *http.Request) {
49 // publicHTTPSURL derives the public HTTPS URL from the request Host header.49 // publicHTTPSURL derives the public HTTPS URL from the request Host header.
50 // The result is HTML-escaped to prevent XSS via a crafted Host header.50 // The result is HTML-escaped to prevent XSS via a crafted Host header.
51 func (h *Handler) publicHTTPSURL(r *http.Request) string {51 func (h *Handler) publicHTTPSURL(r *http.Request) string {
52- authority := middleware.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort)52+ authority := httphelper.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort)
53 return html.EscapeString("https://" + authority + "/")53 return html.EscapeString("https://" + authority + "/")
54 }54 }
55 55
M internal/adapters/in/http/proxy/forwarder.go +2 -11
@@ -15,7 +15,7 @@ import (
1515
1616 "github.com/bnema/zerowrap"
1717
18- "github.com/bnema/gordon/internal/adapters/in/http/middleware"
18+ gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1919 "github.com/bnema/gordon/internal/domain"
2020 )
2121
@@ -214,16 +214,7 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
214214
215215 // isTrustedSource reports whether the request's remote address is in trustedNets.
216216 func isTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
217- if len(trustedNets) == 0 {
218- return false
219- }
220- host, _, err := net.SplitHostPort(r.RemoteAddr)
221- if err != nil {
222- // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
223- // or certain proxy setups). Fall back to using it directly.
224- host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
225- }
226- return middleware.IsTrustedProxy(host, trustedNets)
217+ return gordonhttp.IsTrustedSource(r, trustedNets)
227218 }
228219
229220 // modifyResponse returns a function that adds proxy headers and enforces response size limits.
@@ -15,7 +15,7 @@ import (
15 15
16 "github.com/bnema/zerowrap"16 "github.com/bnema/zerowrap"
17 17
18- "github.com/bnema/gordon/internal/adapters/in/http/middleware"18+ gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
19 "github.com/bnema/gordon/internal/domain"19 "github.com/bnema/gordon/internal/domain"
20 )20 )
21 21
@@ -214,16 +214,7 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
214 214
215 // isTrustedSource reports whether the request's remote address is in trustedNets.215 // isTrustedSource reports whether the request's remote address is in trustedNets.
216 func isTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {216 func isTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
217- if len(trustedNets) == 0 {217+ return gordonhttp.IsTrustedSource(r, trustedNets)
218- return false
219- }
220- host, _, err := net.SplitHostPort(r.RemoteAddr)
221- if err != nil {
222- // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
223- // or certain proxy setups). Fall back to using it directly.
224- host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
225- }
226- return middleware.IsTrustedProxy(host, trustedNets)
227 }218 }
228 219
229 // modifyResponse returns a function that adds proxy headers and enforces response size limits.220 // modifyResponse returns a function that adds proxy headers and enforces response size limits.
M internal/adapters/in/http/proxy/forwarder_test.go +3 -3
@@ -14,7 +14,7 @@ import (
1414 "github.com/stretchr/testify/mock"
1515 "github.com/stretchr/testify/require"
1616
17- "github.com/bnema/gordon/internal/adapters/in/http/middleware"
17+ gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
1818 "github.com/bnema/gordon/internal/boundaries/in"
1919 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
2020 "github.com/bnema/gordon/internal/domain"
@@ -201,7 +201,7 @@ func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
201201 assert.Equal(t, http.StatusOK, w.Code)
202202 }
203203 func TestNewReverseProxy_XForwardedProto(t *testing.T) {
204- trustedNets := middleware.ParseTrustedProxies([]string{"10.0.0.0/8"})
204+ trustedNets := gordonhttp.ParseTrustedProxies([]string{"10.0.0.0/8"})
205205
206206 tests := []struct {
207207 name string
@@ -250,7 +250,7 @@ func TestNewReverseProxy_XForwardedProto(t *testing.T) {
250250 remoteAddr: "[::1]",
251251 incomingProto: "https",
252252 wantProto: "https",
253- nets: middleware.ParseTrustedProxies([]string{"::1/128"}),
253+ nets: gordonhttp.ParseTrustedProxies([]string{"::1/128"}),
254254 },
255255 {
256256 name: "bare IPv4 no port untrusted source strips proto",
@@ -14,7 +14,7 @@ import (
14 "github.com/stretchr/testify/mock"14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
16 16
17- "github.com/bnema/gordon/internal/adapters/in/http/middleware"17+ gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
18 "github.com/bnema/gordon/internal/boundaries/in"18 "github.com/bnema/gordon/internal/boundaries/in"
19 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"19 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
20 "github.com/bnema/gordon/internal/domain"20 "github.com/bnema/gordon/internal/domain"
@@ -201,7 +201,7 @@ func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
201 assert.Equal(t, http.StatusOK, w.Code)201 assert.Equal(t, http.StatusOK, w.Code)
202 }202 }
203 func TestNewReverseProxy_XForwardedProto(t *testing.T) {203 func TestNewReverseProxy_XForwardedProto(t *testing.T) {
204- trustedNets := middleware.ParseTrustedProxies([]string{"10.0.0.0/8"})204+ trustedNets := gordonhttp.ParseTrustedProxies([]string{"10.0.0.0/8"})
205 205
206 tests := []struct {206 tests := []struct {
207 name string207 name string
@@ -250,7 +250,7 @@ func TestNewReverseProxy_XForwardedProto(t *testing.T) {
250 remoteAddr: "[::1]",250 remoteAddr: "[::1]",
251 incomingProto: "https",251 incomingProto: "https",
252 wantProto: "https",252 wantProto: "https",
253- nets: middleware.ParseTrustedProxies([]string{"::1/128"}),253+ nets: gordonhttp.ParseTrustedProxies([]string{"::1/128"}),
254 },254 },
255 {255 {
256 name: "bare IPv4 no port untrusted source strips proto",256 name: "bare IPv4 no port untrusted source strips proto",
M internal/adapters/out/pki/ca.go +10 -0
@@ -449,6 +449,16 @@ func (ca *CA) IntermediateLifetime() time.Duration {
449449 return intermediateLifetime
450450 }
451451
452+// InstallRoot installs the root CA certificate into the system trust store.
453+func (ca *CA) InstallRoot(cert *x509.Certificate) error {
454+ return InstallRoot(cert)
455+}
456+
457+// UninstallRoot removes the root CA certificate from the system trust store.
458+func (ca *CA) UninstallRoot(cert *x509.Certificate) error {
459+ return UninstallRoot(cert)
460+}
461+
452462 func randomSerial() (*big.Int, error) {
453463 serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
454464 if err != nil {
@@ -449,6 +449,16 @@ func (ca *CA) IntermediateLifetime() time.Duration {
449 return intermediateLifetime449 return intermediateLifetime
450 }450 }
451 451
452+// InstallRoot installs the root CA certificate into the system trust store.
453+func (ca *CA) InstallRoot(cert *x509.Certificate) error {
454+ return InstallRoot(cert)
455+}
456+
457+// UninstallRoot removes the root CA certificate from the system trust store.
458+func (ca *CA) UninstallRoot(cert *x509.Certificate) error {
459+ return UninstallRoot(cert)
460+}
461+
452 func randomSerial() (*big.Int, error) {462 func randomSerial() (*big.Int, error) {
453 serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))463 serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
454 if err != nil {464 if err != nil {
M internal/boundaries/out/pki.go +7 -0
@@ -3,6 +3,7 @@ package out
33 import (
44 "context"
55 "crypto/tls"
6+ "crypto/x509"
67 "time"
78
89 "github.com/bnema/gordon/internal/domain"
@@ -49,4 +50,10 @@ type CertificateAuthority interface {
4950
5051 // IntermediateLifetime returns the configured intermediate CA lifetime.
5152 IntermediateLifetime() time.Duration
53+
54+ // InstallRoot installs the root CA certificate into the system trust store.
55+ InstallRoot(cert *x509.Certificate) error
56+
57+ // UninstallRoot removes the root CA certificate from the system trust store.
58+ UninstallRoot(cert *x509.Certificate) error
5259 }
@@ -3,6 +3,7 @@ package out
3 import (3 import (
4 "context"4 "context"
5 "crypto/tls"5 "crypto/tls"
6+ "crypto/x509"
6 "time"7 "time"
7 8
8 "github.com/bnema/gordon/internal/domain"9 "github.com/bnema/gordon/internal/domain"
@@ -49,4 +50,10 @@ type CertificateAuthority interface {
49 50
50 // IntermediateLifetime returns the configured intermediate CA lifetime.51 // IntermediateLifetime returns the configured intermediate CA lifetime.
51 IntermediateLifetime() time.Duration52 IntermediateLifetime() time.Duration
53+
54+ // InstallRoot installs the root CA certificate into the system trust store.
55+ InstallRoot(cert *x509.Certificate) error
56+
57+ // UninstallRoot removes the root CA certificate from the system trust store.
58+ UninstallRoot(cert *x509.Certificate) error
52 }59 }

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