package middleware import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) const ( FrostfsVHSHeader = "X-Frostfs-S3-VHS" FrostfsServernameHeader = "X-Frostfs-Servername" ) type VHSSettingsMock struct { domains []string } func (v *VHSSettingsMock) Domains() []string { return v.domains } func (v *VHSSettingsMock) GlobalVHS() bool { return false } func (v *VHSSettingsMock) VHSHeader() string { return FrostfsVHSHeader } func (v *VHSSettingsMock) ServernameHeader() string { return FrostfsServernameHeader } func (v *VHSSettingsMock) VHSNamespacesEnabled() map[string]bool { return make(map[string]bool) } func TestIsVHSAddress(t *testing.T) { for _, tc := range []struct { name string headerVHSEnabled string vhsEnabledFlag bool vhsNamespaced map[string]bool namespace string expected bool }{ { name: "vhs disabled", expected: false, }, { name: "vhs disabled for namespace", vhsEnabledFlag: true, vhsNamespaced: map[string]bool{ "kapusta": false, }, namespace: "kapusta", expected: false, }, { name: "vhs enabled (global vhs flag)", vhsEnabledFlag: true, expected: true, }, { name: "vhs enabled for namespace", vhsNamespaced: map[string]bool{ "kapusta": true, }, namespace: "kapusta", expected: true, }, { name: "vhs enabled (header)", headerVHSEnabled: "true", vhsEnabledFlag: false, vhsNamespaced: map[string]bool{ "kapusta": false, }, namespace: "kapusta", expected: true, }, { name: "vhs disabled (header)", headerVHSEnabled: "false", vhsEnabledFlag: true, vhsNamespaced: map[string]bool{ "kapusta": true, }, namespace: "kapusta", expected: false, }, } { t.Run(tc.name, func(t *testing.T) { actual := isVHSAddress(tc.headerVHSEnabled, tc.vhsEnabledFlag, tc.vhsNamespaced, tc.namespace) require.Equal(t, tc.expected, actual) }) } } func TestPreparePathStyleAddress(t *testing.T) { bkt, obj := "test-bucket", "test-object" for _, tc := range []struct { name string urlParams string expectedReqType ReqType expectedBktName string expectedObjName string }{ { name: "bucket request", urlParams: "/" + bkt, expectedReqType: bucketType, expectedBktName: bkt, }, { name: "bucket request with slash", urlParams: "/" + bkt + "/", expectedReqType: bucketType, expectedBktName: bkt, }, { name: "object request", urlParams: "/" + bkt + "/" + obj, expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj, }, { name: "object request with slash", urlParams: "/" + bkt + "/" + obj + "/", expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj + "/", }, { name: "none type request", urlParams: "/", expectedReqType: noneType, }, } { t.Run(tc.name, func(t *testing.T) { reqInfo := &ReqInfo{} r := httptest.NewRequest(http.MethodGet, tc.urlParams, nil) preparePathStyleAddress(reqInfo, r, reqLogOrDefault(r.Context(), zaptest.NewLogger(t))) require.Equal(t, tc.expectedReqType, reqInfo.RequestType) require.Equal(t, tc.expectedBktName, reqInfo.BucketName) require.Equal(t, tc.expectedObjName, reqInfo.ObjectName) }) } } func TestPrepareVHSAddress(t *testing.T) { bkt, obj, domain := "test-bucket", "test-object", "domain.com" for _, tc := range []struct { name string domains []string host string urlParams string expectedReqType ReqType expectedBktName string expectedObjName string }{ { name: "bucket request, the domain matched", domains: []string{domain}, host: bkt + "." + domain, urlParams: "/", expectedReqType: bucketType, expectedBktName: bkt, }, { name: "object request, the domain matched", domains: []string{domain}, host: bkt + "." + domain, urlParams: "/" + obj, expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj, }, { name: "object request with slash, the domain matched", domains: []string{domain}, host: bkt + "." + domain, urlParams: "/" + obj + "/", expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj + "/", }, { name: "list-buckets request, the domain matched", domains: []string{domain}, host: domain, urlParams: "/", expectedReqType: noneType, }, { name: "bucket request, the domain don't match", host: bkt + "." + domain, urlParams: "/", expectedReqType: bucketType, expectedBktName: bkt, }, { name: "object request, the domain don't match", host: bkt + "." + domain, urlParams: "/" + obj, expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj, }, { name: "object request with slash, the domain don't match", host: bkt + "." + domain, urlParams: "/" + obj + "/", expectedReqType: objectType, expectedBktName: bkt, expectedObjName: obj + "/", }, { name: "list-buckets request, the domain don't match (list-buckets isn't supported if the domains don't match)", host: domain, urlParams: "/", expectedReqType: bucketType, expectedBktName: strings.Split(domain, ".")[0], }, } { t.Run(tc.name, func(t *testing.T) { reqInfo := &ReqInfo{} vhsSettings := &VHSSettingsMock{domains: tc.domains} r := httptest.NewRequest(http.MethodGet, tc.urlParams, nil) r.Host = tc.host prepareVHSAddress(reqInfo, r, vhsSettings) require.Equal(t, tc.expectedReqType, reqInfo.RequestType) require.Equal(t, tc.expectedBktName, reqInfo.BucketName) require.Equal(t, tc.expectedObjName, reqInfo.ObjectName) }) } } func TestCheckDomains(t *testing.T) { for _, tc := range []struct { name string domains []string requestURL string expectedBktName string expectedMatch bool }{ { name: "valid url with bktName and namespace (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "bktA.s3.kapusta.domain.com", expectedBktName: "bktA", expectedMatch: true, }, { name: "valid url without bktName and namespace (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "s3.kapusta.domain.com", expectedBktName: "", expectedMatch: true, }, { name: "invalid url with invalid bktName (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "bktA.bktB.s3.kapusta.domain.com", expectedMatch: false, }, { name: "invalid url without namespace (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "bktA.s3.domain.com", expectedMatch: false, }, { name: "invalid url with invalid infix (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "bktA.s4.kapusta.domain.com", expectedMatch: false, }, { name: "invalid url with invalid postfix (wildcard after protocol infix)", domains: []string{"s3..domain.com"}, requestURL: "bktA.s3.kapusta.dom.su", expectedMatch: false, }, { name: "valid url with bktName and namespace (wildcard at the beginning of the domain)", domains: []string{".domain.com"}, requestURL: "bktA.kapusta.domain.com", expectedBktName: "bktA", expectedMatch: true, }, { name: "valid url without bktName and namespace (wildcard at the beginning of the domain)", domains: []string{".domain.com"}, requestURL: "kapusta.domain.com", expectedBktName: "", expectedMatch: true, }, { name: "invalid url with invalid bktName (wildcard at the beginning of the domain)", domains: []string{".domain.com"}, requestURL: "bktA.bktB.kapusta.domain.com", expectedMatch: false, }, { name: "collision test - true, because we cannot clearly distinguish a namespace from a bucket (wildcard at the beginning of the domain)", domains: []string{".domain.com"}, requestURL: "bktA.domain.com", expectedMatch: true, }, { name: "invalid url (fewer hosts)", domains: []string{".domain.com"}, requestURL: "domain.com", expectedMatch: false, }, { name: "invalid url with invalid postfix (wildcard at the beginning of the domain)", domains: []string{".domain.com"}, requestURL: "bktA.kapusta.dom.su", expectedMatch: false, }, { name: "valid url with bktName and without wildcard (root namaspace)", domains: []string{"domain.com"}, requestURL: "bktA.domain.com", expectedBktName: "bktA", expectedMatch: true, }, { name: "valid url without bktName and without wildcard (root namaspace)", domains: []string{"domain.com"}, requestURL: "domain.com", expectedBktName: "", expectedMatch: true, }, { name: "invalid url with bktName without wildcard (root namaspace)", domains: []string{"domain.com"}, requestURL: "bktA.dom.su", expectedMatch: false, }, { name: "invalid url without wildcard (root namaspace)", domains: []string{"domain.com"}, requestURL: "dom.su", expectedMatch: false, }, { name: "valid url, with a sorted list of domains", domains: []string{"s3..domain.com", ".domain.com", "domain.com"}, requestURL: "s3.kapusta.domain.com", expectedBktName: "", expectedMatch: true, }, { name: "valid url with bktName, multiple wildcards (wildcards at the beginning of the domain)", domains: []string{"..domain.com"}, requestURL: "bktA.s3.kapusta.domain.com", expectedBktName: "bktA", expectedMatch: true, }, { name: "valid url without bktName, multiple wildcards (wildcards at the beginning of the domain)", domains: []string{"..domain.com"}, requestURL: "s3.kapusta.domain.com", expectedBktName: "", expectedMatch: true, }, { name: "valid url with bktName, multiply wildcards", domains: []string{"s3..subdomain..com"}, requestURL: "bktA.s3.kapusta.subdomain.domain.com", expectedBktName: "bktA", expectedMatch: true, }, { name: "valid url without bktName, multiply wildcards", domains: []string{"s3..subdomain..com"}, requestURL: "s3.kapusta.subdomain.domain.com", expectedBktName: "", expectedMatch: true, }, { name: "invalid url without one wildcard", domains: []string{"..domain.com"}, requestURL: "kapusta.domain.com", expectedMatch: false, }, { name: "invalid url, multiply wildcards", domains: []string{"..domain.com"}, requestURL: "s3.kapusta.dom.com", expectedMatch: false, }, { name: "invalid url with invalid bktName, multiply wildcards", domains: []string{"..domain.com"}, requestURL: "bktA.bktB.s3.kapusta.domain.com", expectedMatch: false, }, } { t.Run(tc.name, func(t *testing.T) { bktName, match := checkDomain(tc.requestURL, tc.domains) require.Equal(t, tc.expectedBktName, bktName) require.Equal(t, tc.expectedMatch, match) }) } } func TestGetDomains(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) settings := &VHSSettingsMock{ domains: []string{ "s3.domain.com", "s3..domain.com", "domain.com", }, } t.Run("the request does not contain the X-Frostfs-Servername header", func(t *testing.T) { actualDomains := getDomains(req, settings) require.Equal(t, settings.domains, actualDomains) }) serverName := "domain.com" req.Header.Set(settings.ServernameHeader(), serverName) t.Run("the request contains the X-Frostfs-Servername header", func(t *testing.T) { actualDomains := getDomains(req, settings) require.Equal(t, []string{serverName}, actualDomains) }) }