1package publictls
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/gordon/internal/boundaries/out"
8 "github.com/bnema/gordon/internal/domain"
9)
10
11// Config holds the resolved configuration for public ACME TLS.
12type Config struct {
13 Enabled bool
14 Email string
15 Challenge string
16 HTTPPort int
17 TLSPort int
18 DataDir string
19 ObtainBatchSize int
20}
21
22// EffectiveChallenge represents the resolved ACME challenge configuration.
23type EffectiveChallenge struct {
24 ConfiguredMode domain.ACMEChallengeMode
25 Mode domain.ACMEChallengeMode
26 Token string
27 TokenSource domain.ACMETokenSource
28 Reason string
29}
30
31// validPort checks that port is in the valid TCP port range 1..65535.
32func validPort(p int) bool {
33 return p >= 1 && p <= 65535
34}
35
36// ResolveEffectiveChallenge resolves the effective ACME challenge configuration
37// from the given Config and optional token resolver.
38func ResolveEffectiveChallenge(ctx context.Context, cfg Config, resolver out.SecretResolver) (EffectiveChallenge, error) {
39 if !cfg.Enabled {
40 configuredMode := domain.ACMEChallengeAuto
41 if parsed, err := domain.ParseACMEChallengeMode(cfg.Challenge); err == nil {
42 configuredMode = parsed
43 }
44 return EffectiveChallenge{
45 ConfiguredMode: configuredMode,
46 Mode: domain.ACMEChallengeAuto,
47 TokenSource: domain.ACMETokenSourceNone,
48 Reason: "ACME disabled",
49 }, nil
50 }
51
52 if cfg.Email == "" {
53 return EffectiveChallenge{}, domain.ErrACMEEmailRequired
54 }
55
56 if !validPort(cfg.TLSPort) {
57 return EffectiveChallenge{}, fmt.Errorf("%w: tls_port must be in range 1..65535", domain.ErrACMEChallengeInvalid)
58 }
59
60 parsedMode, err := domain.ParseACMEChallengeMode(cfg.Challenge)
61 if err != nil {
62 return EffectiveChallenge{}, err
63 }
64
65 switch parsedMode {
66 case domain.ACMEChallengeHTTP01:
67 if !validPort(cfg.HTTPPort) {
68 return EffectiveChallenge{}, fmt.Errorf("%w: http_port must be in range 1..65535 for http-01 challenge", domain.ErrACMEChallengeInvalid)
69 }
70 return EffectiveChallenge{
71 ConfiguredMode: domain.ACMEChallengeHTTP01,
72 Mode: domain.ACMEChallengeHTTP01,
73 TokenSource: domain.ACMETokenSourceNone,
74 Reason: "http-01 challenge selected",
75 }, nil
76
77 case domain.ACMEChallengeCloudflareDNS01:
78 token, source, err := resolveToken(ctx, resolver, true)
79 if err != nil {
80 return EffectiveChallenge{}, fmt.Errorf("resolve Cloudflare token: %w", err)
81 }
82 if token == "" {
83 return EffectiveChallenge{}, domain.ErrCloudflareTokenMissing
84 }
85 return EffectiveChallenge{
86 ConfiguredMode: domain.ACMEChallengeCloudflareDNS01,
87 Mode: domain.ACMEChallengeCloudflareDNS01,
88 Token: token,
89 TokenSource: source,
90 Reason: "cloudflare dns-01 challenge selected",
91 }, nil
92
93 case domain.ACMEChallengeAuto:
94 token, source, _ := resolveToken(ctx, resolver, false)
95 if token != "" {
96 return EffectiveChallenge{
97 ConfiguredMode: domain.ACMEChallengeAuto,
98 Mode: domain.ACMEChallengeCloudflareDNS01,
99 Token: token,
100 TokenSource: source,
101 Reason: "auto selected cloudflare dns-01 (token available)",
102 }, nil
103 }
104 if !validPort(cfg.HTTPPort) {
105 return EffectiveChallenge{}, fmt.Errorf("%w: http_port must be in range 1..65535 for http-01 fallback in auto mode", domain.ErrACMEChallengeInvalid)
106 }
107 return EffectiveChallenge{
108 ConfiguredMode: domain.ACMEChallengeAuto,
109 Mode: domain.ACMEChallengeHTTP01,
110 TokenSource: domain.ACMETokenSourceNone,
111 Reason: "auto selected http-01 (no cloudflare token)",
112 }, nil
113
114 default:
115 return EffectiveChallenge{}, fmt.Errorf("%w: %q", domain.ErrACMEChallengeInvalid, parsedMode)
116 }
117}
118
119// resolveToken resolves the Cloudflare token via the resolver. When strict is
120// false, resolver errors are ignored so auto mode can fall back to HTTP-01.
121func resolveToken(ctx context.Context, resolver out.SecretResolver, strict bool) (string, domain.ACMETokenSource, error) {
122 if resolver == nil {
123 return "", domain.ACMETokenSourceNone, nil
124 }
125 sv, err := resolver.ResolveCloudflareToken(ctx)
126 if err != nil {
127 if strict {
128 return "", domain.ACMETokenSourceNone, err
129 }
130 return "", domain.ACMETokenSourceNone, nil
131 }
132 if sv.Value == "" {
133 return "", domain.ACMETokenSourceNone, nil
134 }
135 return sv.Value, sv.Source, nil
136}