@marcel91200 / caddy Mirror · read-only / commits / 76b198f5
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 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 +}