v2.31.0-dev20 020cc00

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

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