@marcel91200 / caddy Mirror · read-only / commits / b129ed6b
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 07:00 UTC

httpcaddyfile: Fixes for `prefer_wildcard` mode (#6636)

* httpcaddyfile: Fixes for prefer_wildcard mode The wildcard hosts need to be collected first, then considered after, because there's no guarantee that all non-wildcards will appear after all wildcards when looping. Also we should not add a domain to Skip if it doesn't qualify for TLS anyway. * Alternate solution by avoiding adding APs altogether if covered by wildcard

Francis Lavoie committed Oct 30, 2024 at 13:09 UTC b129ed6be88e40667a843bfab74abb3e5239bc8f
4 files changed +353 -51
caddyconfig/httpcaddyfile/httptype.go
+23 -19
@@ -706,6 +706,16 @@ func (st *ServerType) serversFromPairings(
706 return specificity(iLongestHost) > specificity(jLongestHost)
707 })
708
709 + // collect all hosts that have a wildcard in them
710 + wildcardHosts := []string{}
711 + for _, sblock := range p.serverBlocks {
712 + for _, addr := range sblock.parsedKeys {
713 + if strings.HasPrefix(addr.Host, "*.") {
714 + wildcardHosts = append(wildcardHosts, addr.Host[2:])
715 + }
716 + }
717 + }
718 +
719 var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
720 autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
721
@@ -791,13 +801,6 @@ func (st *ServerType) serversFromPairings(
801 }
802 }
803
794 - wildcardHosts := []string{}
795 - for _, addr := range sblock.parsedKeys {
796 - if strings.HasPrefix(addr.Host, "*.") {
797 - wildcardHosts = append(wildcardHosts, addr.Host[2:])
798 - }
799 - }
800 -
804 for _, addr := range sblock.parsedKeys {
805 // if server only uses HTTP port, auto-HTTPS will not apply
806 if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
@@ -813,18 +816,6 @@ func (st *ServerType) serversFromPairings(
816 }
817 }
818
816 - // If prefer wildcard is enabled, then we add hosts that are
817 - // already covered by the wildcard to the skip list
818 - if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" {
819 - baseDomain := addr.Host
820 - if idx := strings.Index(baseDomain, "."); idx != -1 {
821 - baseDomain = baseDomain[idx+1:]
822 - }
823 - if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
824 - srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
825 - }
826 - }
827 -
819 // If TLS is specified as directive, it will also result in 1 or more connection policy being created
820 // Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
821 // specifying prefix "https://"
@@ -841,6 +832,19 @@ func (st *ServerType) serversFromPairings(
832 (addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
833 addressQualifiesForTLS = true
834 }
835 +
836 + // If prefer wildcard is enabled, then we add hosts that are
837 + // already covered by the wildcard to the skip list
838 + if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
839 + baseDomain := addr.Host
840 + if idx := strings.Index(baseDomain, "."); idx != -1 {
841 + baseDomain = baseDomain[idx+1:]
842 + }
843 + if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
844 + srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
845 + }
846 + }
847 +
848 // predict whether auto-HTTPS will add the conn policy for us; if so, we
849 // may not need to add one for this server
850 autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
caddyconfig/httpcaddyfile/tlsapp.go
+59 -32
@@ -92,6 +92,25 @@ func (st ServerType) buildTLSApp(
92 tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
93 }
94
95 + // collect all hosts that have a wildcard in them, and arent HTTP
96 + wildcardHosts := []string{}
97 + for _, p := range pairings {
98 + var addresses []string
99 + for _, addressWithProtocols := range p.addressesWithProtocols {
100 + addresses = append(addresses, addressWithProtocols.address)
101 + }
102 + if !listenersUseAnyPortOtherThan(addresses, httpPort) {
103 + continue
104 + }
105 + for _, sblock := range p.serverBlocks {
106 + for _, addr := range sblock.parsedKeys {
107 + if strings.HasPrefix(addr.Host, "*.") {
108 + wildcardHosts = append(wildcardHosts, addr.Host[2:])
109 + }
110 + }
111 + }
112 + }
113 +
114 for _, p := range pairings {
115 // avoid setting up TLS automation policies for a server that is HTTP-only
116 var addresses []string
@@ -115,6 +134,12 @@ func (st ServerType) buildTLSApp(
134 return nil, warnings, err
135 }
136
137 + // make a plain copy so we can compare whether we made any changes
138 + apCopy, err := newBaseAutomationPolicy(options, warnings, true)
139 + if err != nil {
140 + return nil, warnings, err
141 + }
142 +
143 sblockHosts := sblock.hostsFromKeys(false)
144 if len(sblockHosts) == 0 && catchAllAP != nil {
145 ap = catchAllAP
@@ -217,9 +242,21 @@ func (st ServerType) buildTLSApp(
242 catchAllAP = ap
243 }
244
245 + hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
246 + sort.Strings(hostsNotHTTP) // solely for deterministic test results
247 +
248 + // if the we prefer wildcards and the AP is unchanged,
249 + // then we can skip this AP because it should be covered
250 + // by an AP with a wildcard
251 + if slices.Contains(autoHTTPS, "prefer_wildcard") {
252 + if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
253 + reflect.DeepEqual(ap, apCopy) {
254 + continue
255 + }
256 + }
257 +
258 // associate our new automation policy with this server block's hosts
221 - ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
222 - sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
259 + ap.SubjectsRaw = hostsNotHTTP
260
261 // if a combination of public and internal names were given
262 // for this same server block and no issuer was specified, we
@@ -258,6 +295,7 @@ func (st ServerType) buildTLSApp(
295 ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
296 }
297 }
298 +
299 if tlsApp.Automation == nil {
300 tlsApp.Automation = new(caddytls.AutomationConfig)
301 }
@@ -418,10 +456,7 @@ func (st ServerType) buildTLSApp(
456 }
457
458 // consolidate automation policies that are the exact same
421 - tlsApp.Automation.Policies = consolidateAutomationPolicies(
422 - tlsApp.Automation.Policies,
423 - slices.Contains(autoHTTPS, "prefer_wildcard"),
424 - )
459 + tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
460
461 // ensure automation policies don't overlap subjects (this should be
462 // an error at provision-time as well, but catch it in the adapt phase
@@ -567,7 +602,7 @@ func newBaseAutomationPolicy(
602
603 // consolidateAutomationPolicies combines automation policies that are the same,
604 // for a cleaner overall output.
570 -func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
605 +func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
606 // sort from most specific to least specific; we depend on this ordering
607 sort.SliceStable(aps, func(i, j int) bool {
608 if automationPolicyIsSubset(aps[i], aps[j]) {
@@ -652,31 +687,6 @@ outer:
687 j--
688 }
689 }
655 -
656 - if preferWildcard {
657 - // remove subjects from i if they're covered by a wildcard in j
658 - iSubjs := aps[i].SubjectsRaw
659 - for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
660 - for jSubj := range aps[j].SubjectsRaw {
661 - if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
662 - continue
663 - }
664 - if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
665 - iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1)
666 - iSubj--
667 - break
668 - }
669 - }
670 - }
671 - aps[i].SubjectsRaw = iSubjs
672 -
673 - // remove i if it has no subjects left
674 - if len(aps[i].SubjectsRaw) == 0 {
675 - aps = slices.Delete(aps, i, i+1)
676 - i--
677 - continue outer
678 - }
679 - }
690 }
691 }
692
@@ -748,3 +758,20 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
758 func isTailscaleDomain(name string) bool {
759 return strings.HasSuffix(strings.ToLower(name), ".ts.net")
760 }
761 +
762 +func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
763 + if len(hosts) == 0 || len(wildcards) == 0 {
764 + return false
765 + }
766 + for _, host := range hosts {
767 + for _, wildcard := range wildcards {
768 + if strings.HasPrefix(host, "*.") {
769 + continue
770 + }
771 + if certmagic.MatchWildcard(host, "*."+wildcard) {
772 + return true
773 + }
774 + }
775 + }
776 + return false
777 +}
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
+3
@@ -74,6 +74,9 @@ foo.example.com {
74 }
75 ],
76 "automatic_https": {
77 + "skip_certificates": [
78 + "foo.example.com"
79 + ],
80 "prefer_wildcard": true
81 }
82 }
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest new
+268
@@ -0,0 +1,268 @@
1 +{
2 + auto_https prefer_wildcard
3 +}
4 +
5 +# Covers two domains
6 +*.one.example.com {
7 + tls {
8 + dns mock
9 + }
10 + respond "one fallback"
11 +}
12 +
13 +# Is covered, should not get its own AP
14 +foo.one.example.com {
15 + respond "foo one"
16 +}
17 +
18 +# This one has its own tls config so it doesn't get covered (escape hatch)
19 +bar.one.example.com {
20 + respond "bar one"
21 + tls bar@bar.com
22 +}
23 +
24 +# Covers nothing but AP gets consolidated with the first
25 +*.two.example.com {
26 + tls {
27 + dns mock
28 + }
29 + respond "two fallback"
30 +}
31 +
32 +# Is HTTP so it should not cover
33 +http://*.three.example.com {
34 + respond "three fallback"
35 +}
36 +
37 +# Has no wildcard coverage so it gets an AP
38 +foo.three.example.com {
39 + respond "foo three"
40 +}
41 +----------
42 +{
43 + "apps": {
44 + "http": {
45 + "servers": {
46 + "srv0": {
47 + "listen": [
48 + ":443"
49 + ],
50 + "routes": [
51 + {
52 + "match": [
53 + {
54 + "host": [
55 + "foo.three.example.com"
56 + ]
57 + }
58 + ],
59 + "handle": [
60 + {
61 + "handler": "subroute",
62 + "routes": [
63 + {
64 + "handle": [
65 + {
66 + "body": "foo three",
67 + "handler": "static_response"
68 + }
69 + ]
70 + }
71 + ]
72 + }
73 + ],
74 + "terminal": true
75 + },
76 + {
77 + "match": [
78 + {
79 + "host": [
80 + "foo.one.example.com"
81 + ]
82 + }
83 + ],
84 + "handle": [
85 + {
86 + "handler": "subroute",
87 + "routes": [
88 + {
89 + "handle": [
90 + {
91 + "body": "foo one",
92 + "handler": "static_response"
93 + }
94 + ]
95 + }
96 + ]
97 + }
98 + ],
99 + "terminal": true
100 + },
101 + {
102 + "match": [
103 + {
104 + "host": [
105 + "bar.one.example.com"
106 + ]
107 + }
108 + ],
109 + "handle": [
110 + {
111 + "handler": "subroute",
112 + "routes": [
113 + {
114 + "handle": [
115 + {
116 + "body": "bar one",
117 + "handler": "static_response"
118 + }
119 + ]
120 + }
121 + ]
122 + }
123 + ],
124 + "terminal": true
125 + },
126 + {
127 + "match": [
128 + {
129 + "host": [
130 + "*.one.example.com"
131 + ]
132 + }
133 + ],
134 + "handle": [
135 + {
136 + "handler": "subroute",
137 + "routes": [
138 + {
139 + "handle": [
140 + {
141 + "body": "one fallback",
142 + "handler": "static_response"
143 + }
144 + ]
145 + }
146 + ]
147 + }
148 + ],
149 + "terminal": true
150 + },
151 + {
152 + "match": [
153 + {
154 + "host": [
155 + "*.two.example.com"
156 + ]
157 + }
158 + ],
159 + "handle": [
160 + {
161 + "handler": "subroute",
162 + "routes": [
163 + {
164 + "handle": [
165 + {
166 + "body": "two fallback",
167 + "handler": "static_response"
168 + }
169 + ]
170 + }
171 + ]
172 + }
173 + ],
174 + "terminal": true
175 + }
176 + ],
177 + "automatic_https": {
178 + "skip_certificates": [
179 + "foo.one.example.com",
180 + "bar.one.example.com"
181 + ],
182 + "prefer_wildcard": true
183 + }
184 + },
185 + "srv1": {
186 + "listen": [
187 + ":80"
188 + ],
189 + "routes": [
190 + {
191 + "match": [
192 + {
193 + "host": [
194 + "*.three.example.com"
195 + ]
196 + }
197 + ],
198 + "handle": [
199 + {
200 + "handler": "subroute",
201 + "routes": [
202 + {
203 + "handle": [
204 + {
205 + "body": "three fallback",
206 + "handler": "static_response"
207 + }
208 + ]
209 + }
210 + ]
211 + }
212 + ],
213 + "terminal": true
214 + }
215 + ],
216 + "automatic_https": {
217 + "prefer_wildcard": true
218 + }
219 + }
220 + }
221 + },
222 + "tls": {
223 + "automation": {
224 + "policies": [
225 + {
226 + "subjects": [
227 + "foo.three.example.com"
228 + ]
229 + },
230 + {
231 + "subjects": [
232 + "bar.one.example.com"
233 + ],
234 + "issuers": [
235 + {
236 + "email": "bar@bar.com",
237 + "module": "acme"
238 + },
239 + {
240 + "ca": "https://acme.zerossl.com/v2/DV90",
241 + "email": "bar@bar.com",
242 + "module": "acme"
243 + }
244 + ]
245 + },
246 + {
247 + "subjects": [
248 + "*.one.example.com",
249 + "*.two.example.com"
250 + ],
251 + "issuers": [
252 + {
253 + "challenges": {
254 + "dns": {
255 + "provider": {
256 + "name": "mock"
257 + }
258 + }
259 + },
260 + "module": "acme"
261 + }
262 + ]
263 + }
264 + ]
265 + }
266 + }
267 + }
268 +}
\ No newline at end of file