diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index 6d7341a98..8056da6e5 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -684,9 +684,12 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) { routers = append(routers, apiRouter.Host( fmt.Sprintf("%s.%s", "{bucket:.+}", virtualHost)).Subrouter()) } - } else { - routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter()) } + // Always register a Host-less path-style catch-all last so requests that + // arrive via an IP, an unlisted hostname, or a reverse proxy that rewrites + // the Host header still match bucket routes. Host-specific routers above + // take precedence because they were registered first. + routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter()) // Get CORS middleware instance with caching corsMiddleware := s3a.getCORSMiddleware() diff --git a/weed/s3api/s3api_server_routing_test.go b/weed/s3api/s3api_server_routing_test.go index 0b0f96697..6194abf60 100644 --- a/weed/s3api/s3api_server_routing_test.go +++ b/weed/s3api/s3api_server_routing_test.go @@ -296,3 +296,43 @@ func TestRouting_IAMMatcherLogic(t *testing.T) { }) } } + +// TestRouting_BucketRouteMatchesAnyHost is a regression test for issue #9539. +// When DomainName is configured, bucket-prefix routes must still match for +// requests whose Host header does not match a configured domain (for example, +// requests forwarded by a reverse proxy that rewrites Host to an internal +// upstream address). A Host-less path-style catch-all is registered after the +// host-specific routers so it only fires when no Host matcher applies. +func TestRouting_BucketRouteMatchesAnyHost(t *testing.T) { + cases := []struct { + name string + domainName string + host string + }{ + {"virtual-host domain configured, request via IP", "s3.example.com", "10.0.0.5:8333"}, + {"virtual-host domain configured, request via unrelated host", "s3.example.com", "internal-upstream:8333"}, + {"path-style + virtual-host configured, request via IP", "s3.example.com,api.s3.example.com", "10.0.0.5:8333"}, + {"domain configured, request via the bare configured host (no bucket subdomain)", "s3.example.com", "s3.example.com"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + router := mux.NewRouter() + s3a := setupRoutingTestServer(t) + s3a.option.DomainName = tc.domainName + s3a.registerRouter(router) + + req, _ := http.NewRequest(http.MethodHead, "http://"+tc.host+"/some-bucket", nil) + req.Host = tc.host + + var match mux.RouteMatch + if !router.Match(req, &match) { + t.Fatalf("expected HEAD /some-bucket with Host=%q to match a bucket route (domainName=%q); got no match (err=%v)", + tc.host, tc.domainName, match.MatchErr) + } + if match.MatchErr != nil { + t.Fatalf("route matched but reported error: %v", match.MatchErr) + } + }) + } +}