v2.31.1 f37fe67

targets.go RawHistoryFolder

1package publictls
2
3import (
4	"context"
5	"fmt"
6	"sort"
7	"strings"
8
9	"github.com/bnema/gordon/internal/boundaries/out"
10	"github.com/bnema/gordon/internal/domain"
11)
12
13// CertificateTarget represents a single certificate to obtain or renew.
14type CertificateTarget struct {
15	ID        string
16	Names     []string
17	Challenge domain.ACMEChallengeMode
18}
19
20// DeriveCertificateTargets derives certificate targets from routes and external
21// routes based on the given ACME challenge mode.
22func DeriveCertificateTargets(
23	ctx context.Context,
24	mode domain.ACMEChallengeMode,
25	routes []domain.Route,
26	external map[string]string,
27	additionalHosts []string,
28	resolver out.CloudflareZoneResolver,
29) ([]CertificateTarget, error) {
30	hosts := routeHosts(routes, external, additionalHosts)
31
32	switch mode {
33	case domain.ACMEChallengeHTTP01:
34		return deriveHTTP01Targets(hosts), nil
35	case domain.ACMEChallengeCloudflareDNS01:
36		return deriveDNS01Targets(ctx, hosts, resolver)
37	default:
38		return nil, fmt.Errorf("unsupported challenge mode: %s", mode)
39	}
40}
41
42// routeHosts collects all unique canonical hosts from routes and external keys,
43// sorted alphabetically. Trailing dots are stripped before canonicalization.
44func routeHosts(routes []domain.Route, external map[string]string, additionalHosts []string) []string {
45	seen := make(map[string]struct{})
46	var hosts []string
47
48	addHost := func(raw string) {
49		raw = strings.TrimSuffix(raw, ".")
50		canonical, ok := domain.CanonicalRouteDomain(raw)
51		if !ok {
52			return
53		}
54		if _, exists := seen[canonical]; !exists {
55			seen[canonical] = struct{}{}
56			hosts = append(hosts, canonical)
57		}
58	}
59
60	for _, r := range routes {
61		addHost(r.Domain)
62	}
63	for h := range external {
64		addHost(h)
65	}
66	for _, h := range additionalHosts {
67		addHost(h)
68	}
69
70	sort.Strings(hosts)
71	return hosts
72}
73
74// deriveHTTP01Targets creates one target per host for HTTP-01 challenge.
75func deriveHTTP01Targets(hosts []string) []CertificateTarget {
76	targets := make([]CertificateTarget, len(hosts))
77	for i, host := range hosts {
78		targets[i] = CertificateTarget{
79			ID:        "http01-" + host,
80			Names:     []string{host},
81			Challenge: domain.ACMEChallengeHTTP01,
82		}
83	}
84	return targets
85}
86
87// deriveDNS01Targets creates targets grouped by wildcard base for DNS-01 challenge.
88func deriveDNS01Targets(ctx context.Context, hosts []string, resolver out.CloudflareZoneResolver) ([]CertificateTarget, error) {
89	if resolver == nil {
90		return nil, fmt.Errorf("dns-01 resolver is nil: cannot resolve DNS zones")
91	}
92	baseSeen := make(map[string]struct{})
93	var bases []string
94
95	for _, host := range hosts {
96		zone, err := resolver.FindZone(ctx, host)
97		if err != nil {
98			return nil, fmt.Errorf("find zone for %s: %w", host, err)
99		}
100		base, err := wildcardBase(host, zone.Name)
101		if err != nil {
102			return nil, fmt.Errorf("find wildcard base for %s: %w", host, err)
103		}
104		if _, exists := baseSeen[base]; !exists {
105			baseSeen[base] = struct{}{}
106			bases = append(bases, base)
107		}
108	}
109
110	sort.Strings(bases)
111
112	targets := make([]CertificateTarget, len(bases))
113	for i, base := range bases {
114		targets[i] = CertificateTarget{
115			ID:        "dns01-" + base,
116			Names:     []string{base, "*." + base},
117			Challenge: domain.ACMEChallengeCloudflareDNS01,
118		}
119	}
120	return targets, nil
121}
122
123// wildcardBase determines the wildcard base for a host within a zone.
124// This is used by deriveDNS01Targets to group hosts under a common
125// wildcard certificate (e.g. example.com + *.example.com). Hosts that
126// share the same wildcard base are combined into a single DNS-01 target.
127//
128// If host == zone, returns zone.
129// If host is a direct child of zone, returns zone.
130// If host is deeper (e.g. api.prod.example.com within example.com),
131// returns the parent label under the zone (prod.example.com).
132func wildcardBase(host, zone string) (string, error) {
133	host = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")
134	zone = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(zone)), ".")
135	if host == "" || zone == "" {
136		return "", fmt.Errorf("host and zone must be non-empty")
137	}
138	if host == zone {
139		return zone, nil
140	}
141	if !strings.HasSuffix(host, "."+zone) {
142		return "", fmt.Errorf("zone %q does not match host %q", zone, host)
143	}
144
145	// Remove the zone suffix (prefixed by a dot) to isolate subdomain parts.
146	trimmed := strings.TrimSuffix(host, "."+zone)
147
148	if !strings.Contains(trimmed, ".") {
149		// Direct child of the zone (e.g. app.example.com).
150		return zone, nil
151	}
152
153	// Deeper than direct child: extract the parent under the zone.
154	// e.g. "api.prod.example.com" -> trimmed "api.prod" -> parent "prod"
155	_, parent, _ := strings.Cut(trimmed, ".")
156	return parent + "." + zone, nil
157}

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