Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 14:45 UTC
http: Sort auto-HTTPS redirect routes by host specificity (fixes #7390) (#7502)
Paulo Henrique committed
Feb 21, 2026 at 23:42 UTC
76b198f586e4e2482a0278ba52c176cff70af8cf
2 files changed
+80
caddytest/integration/autohttps_test.go
+23
@@ -143,3 +143,26 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
143
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
144
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
145
}
146
+
147
+func TestAutoHTTPSRedirectSortingExactMatchOverWildcard(t *testing.T) {
148
+ tester := caddytest.NewTester(t)
149
+ tester.InitServer(`
150
+ {
151
+ skip_install_trust
152
+ admin localhost:2999
153
+ http_port 9080
154
+ https_port 9443
155
+ local_certs
156
+ }
157
+ *.localhost:10443 {
158
+ respond "Wildcard"
159
+ }
160
+ dev.localhost {
161
+ respond "Exact"
162
+ }
163
+ `, "caddyfile")
164
+
165
+ tester.AssertRedirect("http://dev.localhost:9080/", "https://dev.localhost/", http.StatusPermanentRedirect)
166
+
167
+ tester.AssertRedirect("http://foo.localhost:9080/", "https://foo.localhost:10443/", http.StatusPermanentRedirect)
168
+}
modules/caddyhttp/autohttps.go
+57
@@ -424,6 +424,40 @@ redirServersLoop:
424
// we'll create a new server for all the listener addresses
425
// that are unused and serve the remaining redirects from it
426
427
+ // Sort redirect routes by host specificity to ensure exact matches
428
+ // take precedence over wildcards, preventing ambiguous routing.
429
+ slices.SortFunc(routes, func(a, b Route) int {
430
+ hostA := getFirstHostFromRoute(a)
431
+ hostB := getFirstHostFromRoute(b)
432
+
433
+ // Catch-all routes (empty host) have the lowest priority
434
+ if hostA == "" && hostB != "" {
435
+ return 1
436
+ }
437
+ if hostB == "" && hostA != "" {
438
+ return -1
439
+ }
440
+
441
+ hasWildcardA := strings.Contains(hostA, "*")
442
+ hasWildcardB := strings.Contains(hostB, "*")
443
+
444
+ // Exact domains take precedence over wildcards
445
+ if !hasWildcardA && hasWildcardB {
446
+ return -1
447
+ }
448
+ if hasWildcardA && !hasWildcardB {
449
+ return 1
450
+ }
451
+
452
+ // If both are exact or both are wildcards, the longer one is more specific
453
+ if len(hostA) != len(hostB) {
454
+ return len(hostB) - len(hostA)
455
+ }
456
+
457
+ // Tie-breaker: alphabetical order to ensure determinism
458
+ return strings.Compare(hostA, hostB)
459
+ })
460
+
461
// Use the sorted srvNames to consistently find the target server
462
for _, srvName := range srvNames {
463
srv := app.Servers[srvName]
@@ -793,3 +827,26 @@ func isTailscaleDomain(name string) bool {
827
}
828
829
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
830
+
831
+// getFirstHostFromRoute traverses a route's matchers to find the Host rule.
832
+// Since we are dealing with internally generated redirect routes, the host
833
+// is typically the first string within the MatchHost.
834
+func getFirstHostFromRoute(r Route) string {
835
+ for _, matcherSet := range r.MatcherSets {
836
+ for _, m := range matcherSet {
837
+ // Check if the matcher is of type MatchHost (value or pointer)
838
+ switch hm := m.(type) {
839
+ case MatchHost:
840
+ if len(hm) > 0 {
841
+ return hm[0]
842
+ }
843
+ case *MatchHost:
844
+ if len(*hm) > 0 {
845
+ return (*hm)[0]
846
+ }
847
+ }
848
+ }
849
+ }
850
+ // Return an empty string if it's a catch-all route (no specific host)
851
+ return ""
852
+}