v2.31.0-dev18 7b9923f

handler.go RawHistoryFolder

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 &#x2192;</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 &amp; 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 &amp; 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}

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 8

Tags 106

↑↓ moveenter openesc close