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