1package onboarding
2
3import (
4 "fmt"
5 "html"
6 "net"
7 "net/http"
8
9 "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
10)
11
12// Handler serves CA onboarding endpoints for direct/Tailnet clients.
13type Handler struct {
14 rootPEM []byte
15 mobileconfig []byte
16 fingerprint string
17 httpPort int
18 tlsPort int
19}
20
21// NewHandler creates an onboarding handler.
22// fingerprint is the SHA-256 fingerprint of the root CA certificate.
23// httpPort is the configured HTTP listener port (used to map to tlsPort).
24// tlsPort is the configured HTTPS listener port.
25func NewHandler(rootPEM, mobileconfig []byte, fingerprint string, httpPort, tlsPort int) *Handler {
26 return &Handler{
27 rootPEM: rootPEM,
28 mobileconfig: mobileconfig,
29 fingerprint: fingerprint,
30 httpPort: httpPort,
31 tlsPort: tlsPort,
32 }
33}
34
35// ServeCACert serves the root CA certificate as PEM.
36func (h *Handler) ServeCACert(w http.ResponseWriter, _ *http.Request) {
37 w.Header().Set("Content-Type", "application/x-x509-ca-cert")
38 w.Header().Set("Content-Disposition", "attachment; filename=gordon-ca.crt")
39 _, _ = w.Write(h.rootPEM)
40}
41
42// ServeMobileconfig serves an iOS configuration profile with the root CA.
43func (h *Handler) ServeMobileconfig(w http.ResponseWriter, _ *http.Request) {
44 w.Header().Set("Content-Type", "application/x-apple-aspen-config")
45 w.Header().Set("Content-Disposition", "attachment; filename=gordon-ca.mobileconfig")
46 _, _ = w.Write(h.mobileconfig)
47}
48
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.
51func (h *Handler) publicHTTPSURL(r *http.Request) string {
52 authority := httphelper.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort)
53 return html.EscapeString("https://" + authority + "/")
54}
55
56// isRawIP reports whether the host (without port) is a raw IP literal.
57func isRawIP(hostHeader string) bool {
58 host, _, err := net.SplitHostPort(hostHeader)
59 if err != nil {
60 host = hostHeader
61 }
62 if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
63 host = host[1 : len(host)-1]
64 }
65 return net.ParseIP(host) != nil
66}
67
68// ServeOnboardingPage serves the CA trust onboarding HTML page.
69func (h *Handler) ServeOnboardingPage(w http.ResponseWriter, r *http.Request) {
70 w.Header().Set("Content-Type", "text/html; charset=utf-8")
71
72 redirectURL := h.publicHTTPSURL(r)
73 showSiteLink := !isRawIP(r.Host)
74
75 siteLink := ""
76 if showSiteLink {
77 siteLink = fmt.Sprintf(
78 `<div class="skip"><p>Already installed? <a href="%s">Go to site →</a></p></div>`,
79 redirectURL,
80 )
81 }
82
83 _, _ = fmt.Fprintf(w, `<!DOCTYPE html>
84<html lang="en">
85<head>
86<meta charset="utf-8">
87<meta name="viewport" content="width=device-width, initial-scale=1">
88<title>Gordon — Trust CA Certificate</title>
89<style>
90 body { font-family: -apple-system, system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; color: #333; }
91 h1 { font-size: 1.5em; }
92 .fingerprint { background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px 16px; margin: 16px 0; font-family: monospace; font-size: 0.85em; word-break: break-all; }
93 .fingerprint strong { display: block; margin-bottom: 4px; font-family: -apple-system, system-ui, sans-serif; }
94 .platform { background: #f5f5f5; border-radius: 8px; padding: 16px; margin: 12px 0; }
95 .platform h2 { font-size: 1.1em; margin: 0 0 8px; }
96 a.btn { display: inline-block; background: #2563eb; color: white; padding: 10px 20px; border-radius: 6px; text-decoration: none; margin: 4px 0; }
97 a.btn:hover { background: #1d4ed8; }
98 .skip { margin-top: 24px; font-size: 0.9em; }
99 .skip a { color: #666; }
100 code { background: #e5e7eb; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
101 .note { margin-top: 20px; font-size: 0.85em; color: #666; }
102</style>
103</head>
104<body>
105<h1>Gordon — Trust CA Certificate</h1>
106<p>This Gordon instance uses its own internal CA for HTTPS. Import the CA certificate into your device or browser to connect securely.</p>
107
108<div class="fingerprint">
109 <strong>CA Fingerprint (SHA-256)</strong>
110 %s
111</div>
112<p style="font-size:0.85em;color:#666;">Verify this fingerprint matches the output of <code>gordon ca info</code> on the server.</p>
113
114<div class="platform" id="ios">
115 <h2>iOS / iPadOS</h2>
116 <p><a class="btn" href="/.well-known/gordon/ca.mobileconfig">Download Profile</a></p>
117 <p>After downloading, go to <strong>Settings → General → VPN & Device Management</strong> to install, then <strong>Settings → General → About → Certificate Trust Settings</strong> to enable full trust.</p>
118</div>
119
120<div class="platform" id="android">
121 <h2>Android</h2>
122 <p><a class="btn" href="/.well-known/gordon/ca.crt">Download Certificate</a></p>
123 <p>Go to <strong>Settings → Security → Install a certificate → CA certificate</strong> and select the downloaded file.</p>
124</div>
125
126<div class="platform" id="desktop">
127 <h2>macOS / Linux / Windows</h2>
128 <p><a class="btn" href="/.well-known/gordon/ca.crt">Download Certificate</a></p>
129 <p>Double-click the downloaded file and add it to your system trust store, or import it via your OS certificate manager.</p>
130</div>
131
132<div class="platform" id="firefox">
133 <h2>Firefox / Zen</h2>
134 <p><a class="btn" href="/.well-known/gordon/ca.crt">Download Certificate</a></p>
135 <p>Firefox and Zen use their own certificate store (NSS). Go to <strong>Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import</strong> and select the downloaded file. Check <strong>Trust this CA to identify websites</strong>.</p>
136</div>
137
138%s
139
140<p class="note">The <code>gordon ca install</code> command is for the Gordon server host only, not for client devices.</p>
141</body>
142</html>`, html.EscapeString(h.fingerprint), siteLink)
143}