package handler import ( "encoding/base64" "encoding/xml" "fmt" "net/http" "testing" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/data" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) func TestPreflight(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-preflight" cnrID, cnr, err := hc.prepareContainer(bktName, acl.Private) require.NoError(t, err) hc.frostfs.SetContainer(cnrID, cnr) var epoch uint64 t.Run("CORS object", func(t *testing.T) { for _, tc := range []struct { name string corsConfig *data.CORSConfiguration requestHeaders map[string]string expectedHeaders map[string]string status int }{ { name: "no CORS configuration", expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", fasthttp.HeaderAccessControlExposeHeaders: "", fasthttp.HeaderAccessControlMaxAge: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", }, status: fasthttp.StatusNotFound, }, { name: "specific allowed origin", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"http://example.com"}, AllowedMethods: []string{"GET", "HEAD"}, AllowedHeaders: []string{"Content-Type"}, ExposeHeaders: []string{"x-amz-*", "X-Amz-*"}, MaxAgeSeconds: 900, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "Content-Type", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://example.com", fasthttp.HeaderAccessControlAllowMethods: "GET, HEAD", fasthttp.HeaderAccessControlAllowHeaders: "Content-Type", fasthttp.HeaderAccessControlExposeHeaders: "x-amz-*, X-Amz-*", fasthttp.HeaderAccessControlMaxAge: "900", fasthttp.HeaderAccessControlAllowCredentials: "true", }, status: fasthttp.StatusOK, }, { name: "wildcard allowed origin", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, AllowedHeaders: []string{"Content-Type"}, ExposeHeaders: []string{"x-amz-*", "X-Amz-*"}, MaxAgeSeconds: 900, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://example.com", fasthttp.HeaderAccessControlAllowMethods: "GET, HEAD", fasthttp.HeaderAccessControlAllowHeaders: "", fasthttp.HeaderAccessControlExposeHeaders: "x-amz-*, X-Amz-*", fasthttp.HeaderAccessControlMaxAge: "900", fasthttp.HeaderAccessControlAllowCredentials: "", }, status: fasthttp.StatusOK, }, { name: "not allowed header", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, AllowedHeaders: []string{"Content-Type"}, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAccessControlRequestMethod: "GET", fasthttp.HeaderAccessControlRequestHeaders: "Authorization", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", fasthttp.HeaderAccessControlExposeHeaders: "", fasthttp.HeaderAccessControlMaxAge: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, status: fasthttp.StatusForbidden, }, { name: "empty Origin header", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, }, }, }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", fasthttp.HeaderAccessControlExposeHeaders: "", fasthttp.HeaderAccessControlMaxAge: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, status: fasthttp.StatusBadRequest, }, { name: "empty Access-Control-Request-Method header", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", fasthttp.HeaderAccessControlExposeHeaders: "", fasthttp.HeaderAccessControlMaxAge: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, status: fasthttp.StatusBadRequest, }, } { t.Run(tc.name, func(t *testing.T) { if tc.corsConfig != nil { epoch++ setCORSObject(t, hc, cnrID, tc.corsConfig, epoch) } r := prepareCORSRequest(t, bktName, tc.requestHeaders) hc.Handler().Preflight(r) require.Equal(t, tc.status, r.Response.StatusCode()) for k, v := range tc.expectedHeaders { require.Equal(t, v, string(r.Response.Header.Peek(k))) } }) } }) t.Run("CORS config", func(t *testing.T) { hc.cfg.cors = &data.CORSRule{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, AllowedHeaders: []string{"Content-Type", "Content-Encoding"}, ExposeHeaders: []string{"x-amz-*", "X-Amz-*"}, MaxAgeSeconds: 900, AllowedCredentials: true, } r := prepareCORSRequest(t, bktName, map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAccessControlRequestMethod: "GET", }) hc.Handler().Preflight(r) require.Equal(t, fasthttp.StatusOK, r.Response.StatusCode()) require.Equal(t, "900", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlMaxAge))) require.Equal(t, "*", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowOrigin))) require.Equal(t, "GET, HEAD", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowMethods))) require.Equal(t, "Content-Type, Content-Encoding", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowHeaders))) require.Equal(t, "x-amz-*, X-Amz-*", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlExposeHeaders))) require.Equal(t, "true", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowCredentials))) }) } func TestSetCORSHeaders(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-set-cors-headers" cnrID, cnr, err := hc.prepareContainer(bktName, acl.Private) require.NoError(t, err) hc.frostfs.SetContainer(cnrID, cnr) var epoch uint64 t.Run("CORS object", func(t *testing.T) { for _, tc := range []struct { name string corsConfig *data.CORSConfiguration requestHeaders map[string]string expectedHeaders map[string]string }{ { name: "empty Origin header", expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderVary: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, }, { name: "no CORS configuration", expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderVary: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", }, }, { name: "specific allowed origin", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"http://example.com"}, AllowedMethods: []string{"GET", "HEAD"}, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://example.com", fasthttp.HeaderAccessControlAllowMethods: "GET, HEAD", fasthttp.HeaderVary: fasthttp.HeaderOrigin, fasthttp.HeaderAccessControlAllowCredentials: "true", }, }, { name: "wildcard allowed origin, with credentials", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, }, }, }, requestHeaders: func() map[string]string { tkn := new(bearer.Token) err = tkn.Sign(hc.key.PrivateKey) require.NoError(t, err) t64 := base64.StdEncoding.EncodeToString(tkn.Marshal()) require.NotEmpty(t, t64) return map[string]string{ fasthttp.HeaderOrigin: "http://example.com", fasthttp.HeaderAuthorization: "Bearer " + t64, } }(), expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://example.com", fasthttp.HeaderAccessControlAllowMethods: "GET, HEAD", fasthttp.HeaderVary: fasthttp.HeaderOrigin, fasthttp.HeaderAccessControlAllowCredentials: "true", }, }, { name: "wildcard allowed origin, without credentials", corsConfig: &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, }, }, }, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://example.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "*", fasthttp.HeaderAccessControlAllowMethods: "GET, HEAD", fasthttp.HeaderVary: "", fasthttp.HeaderAccessControlAllowCredentials: "", }, }, } { t.Run(tc.name, func(t *testing.T) { epoch++ setCORSObject(t, hc, cnrID, tc.corsConfig, epoch) r := prepareCORSRequest(t, bktName, tc.requestHeaders) hc.Handler().SetCORSHeaders(r) require.Equal(t, fasthttp.StatusOK, r.Response.StatusCode()) for k, v := range tc.expectedHeaders { require.Equal(t, v, string(r.Response.Header.Peek(k))) } }) } }) t.Run("CORS config", func(t *testing.T) { hc.cfg.cors = &data.CORSRule{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD"}, AllowedHeaders: []string{"Content-Type", "Content-Encoding"}, ExposeHeaders: []string{"x-amz-*", "X-Amz-*"}, MaxAgeSeconds: 900, AllowedCredentials: true, } r := prepareCORSRequest(t, bktName, map[string]string{fasthttp.HeaderOrigin: "http://example.com"}) hc.Handler().SetCORSHeaders(r) require.Equal(t, "900", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlMaxAge))) require.Equal(t, "*", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowOrigin))) require.Equal(t, "GET, HEAD", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowMethods))) require.Equal(t, "Content-Type, Content-Encoding", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowHeaders))) require.Equal(t, "x-amz-*, X-Amz-*", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlExposeHeaders))) require.Equal(t, "true", string(r.Response.Header.Peek(fasthttp.HeaderAccessControlAllowCredentials))) }) } func TestCheckSubslice(t *testing.T) { for _, tc := range []struct { name string allowed []string actual []string expected bool }{ { name: "empty allowed slice", allowed: []string{}, actual: []string{"str1", "str2", "str3"}, expected: false, }, { name: "empty actual slice", allowed: []string{"str1", "str2", "str3"}, actual: []string{}, expected: true, }, { name: "allowed wildcard", allowed: []string{"str", "*"}, actual: []string{"str1", "str2", "str3"}, expected: true, }, { name: "similar allowed and actual", allowed: []string{"str1", "str2", "str3"}, actual: []string{"str1", "str2", "str3"}, expected: true, }, { name: "allowed actual", allowed: []string{"str", "str1", "str2", "str4"}, actual: []string{"str1", "str2"}, expected: true, }, { name: "not allowed actual", allowed: []string{"str", "str1", "str2", "str4"}, actual: []string{"str1", "str5"}, expected: false, }, { name: "wildcard in allowed", allowed: []string{"str*"}, actual: []string{"str", "str5"}, expected: true, }, } { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.expected, checkSubslice(tc.allowed, tc.actual)) }) } } func TestAllowedOriginWildcards(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-allowed-origin-wildcards" cnrID, cnr, err := hc.prepareContainer(bktName, acl.Private) require.NoError(t, err) hc.frostfs.SetContainer(cnrID, cnr) cfg := &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"*suffix.example"}, AllowedMethods: []string{"GET"}, }, { AllowedOrigins: []string{"https://*example"}, AllowedMethods: []string{"GET"}, }, { AllowedOrigins: []string{"prefix.example*"}, AllowedMethods: []string{"GET"}, }, }, } setCORSObject(t, hc, cnrID, cfg, 1) for _, tc := range []struct { name string handler func(*fasthttp.RequestCtx) requestHeaders map[string]string expectedHeaders map[string]string expectedStatus int }{ { name: "set cors headers, empty request cors headers", handler: hc.Handler().SetCORSHeaders, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, invalid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://origin.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, first rule, no symbols in place of wildcard", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "suffix.example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "suffix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, first rule, valid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://suffix.example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://suffix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, first rule, invalid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://suffix-example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, second rule, no symbols in place of wildcard", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, second rule, valid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://www.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, second rule, invalid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, third rule, no symbols in place of wildcard", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "prefix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, third rule, valid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example.com", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "prefix.example.com", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "set cors headers, third rule, invalid origin", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "www.prefix.example", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, third rule, invalid request method in header", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example.com", fasthttp.HeaderAccessControlRequestMethod: "PUT", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, }, { name: "set cors headers, third rule, valid request method in header", handler: hc.Handler().SetCORSHeaders, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example.com", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "prefix.example.com", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, empty request cors headers", handler: hc.Handler().Preflight, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusBadRequest, }, { name: "preflight, invalid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://origin.com", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusForbidden, }, { name: "preflight, first rule, no symbols in place of wildcard", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "suffix.example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "suffix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "prelight, first rule, valid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://suffix.example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "http://suffix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, first rule, invalid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "http://suffix-example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusForbidden, }, { name: "preflight, second rule, no symbols in place of wildcard", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, second rule, valid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://www.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, second rule, invalid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusForbidden, }, { name: "preflight, third rule, no symbols in place of wildcard", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "prefix.example", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, third rule, valid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example.com", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "prefix.example.com", fasthttp.HeaderAccessControlAllowMethods: "GET", }, }, { name: "preflight, third rule, invalid origin", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "www.prefix.example", fasthttp.HeaderAccessControlRequestMethod: "GET", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusForbidden, }, { name: "preflight, third rule, invalid request method in header", handler: hc.Handler().Preflight, requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "prefix.example.com", fasthttp.HeaderAccessControlRequestMethod: "PUT", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", }, expectedStatus: http.StatusForbidden, }, } { t.Run(tc.name, func(t *testing.T) { r := prepareCORSRequest(t, bktName, tc.requestHeaders) tc.handler(r) expectedStatus := fasthttp.StatusOK if tc.expectedStatus != 0 { expectedStatus = tc.expectedStatus } require.Equal(t, expectedStatus, r.Response.StatusCode()) for k, v := range tc.expectedHeaders { require.Equal(t, v, string(r.Response.Header.Peek(k))) } }) } } func TestAllowedHeaderWildcards(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-allowed-header-wildcards" cnrID, cnr, err := hc.prepareContainer(bktName, acl.Private) require.NoError(t, err) hc.frostfs.SetContainer(cnrID, cnr) cfg := &data.CORSConfiguration{ CORSRules: []data.CORSRule{ { AllowedOrigins: []string{"https://www.example.com"}, AllowedMethods: []string{"HEAD"}, AllowedHeaders: []string{"*-suffix"}, }, { AllowedOrigins: []string{"https://www.example.com"}, AllowedMethods: []string{"HEAD"}, AllowedHeaders: []string{"start-*-end"}, }, { AllowedOrigins: []string{"https://www.example.com"}, AllowedMethods: []string{"HEAD"}, AllowedHeaders: []string{"X-Amz-*"}, }, }, } setCORSObject(t, hc, cnrID, cfg, 1) for _, tc := range []struct { name string requestHeaders map[string]string expectedHeaders map[string]string expectedStatus int }{ { name: "first rule, valid headers", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "header-suffix, -suffix", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://www.example.com", fasthttp.HeaderAccessControlAllowMethods: "HEAD", fasthttp.HeaderAccessControlAllowHeaders: "header-suffix, -suffix", }, }, { name: "first rule, invalid headers", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "header-suffix-*", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", }, expectedStatus: http.StatusForbidden, }, { name: "second rule, valid headers", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "start--end, start-header-end", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://www.example.com", fasthttp.HeaderAccessControlAllowMethods: "HEAD", fasthttp.HeaderAccessControlAllowHeaders: "start--end, start-header-end", }, }, { name: "second rule, invalid header ending", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "start-header-end-*", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", }, expectedStatus: http.StatusForbidden, }, { name: "second rule, invalid header beginning", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "*-start-header-end", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", }, expectedStatus: http.StatusForbidden, }, { name: "third rule, valid headers", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "X-Amz-Date, X-Amz-Content-Sha256", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "https://www.example.com", fasthttp.HeaderAccessControlAllowMethods: "HEAD", fasthttp.HeaderAccessControlAllowHeaders: "X-Amz-Date, X-Amz-Content-Sha256", }, }, { name: "third rule, invalid headers", requestHeaders: map[string]string{ fasthttp.HeaderOrigin: "https://www.example.com", fasthttp.HeaderAccessControlRequestMethod: "HEAD", fasthttp.HeaderAccessControlRequestHeaders: "Authorization", }, expectedHeaders: map[string]string{ fasthttp.HeaderAccessControlAllowOrigin: "", fasthttp.HeaderAccessControlAllowMethods: "", fasthttp.HeaderAccessControlAllowHeaders: "", }, expectedStatus: http.StatusForbidden, }, } { t.Run(tc.name, func(t *testing.T) { r := prepareCORSRequest(t, bktName, tc.requestHeaders) hc.Handler().Preflight(r) expectedStatus := http.StatusOK if tc.expectedStatus != 0 { expectedStatus = tc.expectedStatus } require.Equal(t, expectedStatus, r.Response.StatusCode()) for k, v := range tc.expectedHeaders { require.Equal(t, v, string(r.Response.Header.Peek(k))) } }) } } func setCORSObject(t *testing.T, hc *handlerContext, cnrID cid.ID, corsConfig *data.CORSConfiguration, epoch uint64) { payload, err := xml.Marshal(corsConfig) require.NoError(t, err) a := object.NewAttribute() a.SetKey(object.AttributeFilePath) a.SetValue(fmt.Sprintf(corsFilePathTemplate, cnrID)) objID := oidtest.ID() obj := object.New() obj.SetAttributes(*a) obj.SetOwnerID(hc.owner) obj.SetPayload(payload) obj.SetPayloadSize(uint64(len(payload))) obj.SetContainerID(hc.corsCnr) obj.SetID(objID) obj.SetCreationEpoch(epoch) var addr oid.Address addr.SetObject(objID) addr.SetContainer(hc.corsCnr) hc.frostfs.SetObject(addr, obj) }