mirror of
https://github.com/versity/versitygw.git
synced 2026-02-07 10:50:43 +00:00
There is some desire to have a web dashboard for the gateway. So that we dont have to proxy all requests through the webserver and expose credentials over the wire, the better approach would be to enable CORS headers to allow browser requests directly to the s3/admin service. The default for these headers is off, so that they are only enabled for instances that specfically want to support this workload.
264 lines
11 KiB
Go
264 lines
11 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package integration
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
func PreflightOPTIONS_non_existing_bucket(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_non_existing_bucket"
|
|
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
res, err := makeOPTIONSRequest(s, "non-existing-bucket", "http://localhost:7070", http.MethodPost, "X-Amz-Date")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return checkApiErr(res.err, s3err.GetAPIError(s3err.ErrNoSuchBucket))
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_missing_origin(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_missing_origin"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
res, err := makeOPTIONSRequest(s, bucket, "", http.MethodGet, "X-Custom-Header")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return checkApiErr(res.err, s3err.GetAPIError(s3err.ErrMissingCORSOrigin))
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_invalid_request_method(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_invalid_request_method"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
for _, method := range []string{
|
|
// should be case sensitive, all with capital letters
|
|
"get", "Get", "GEt", "geT",
|
|
"post", "Post", "POSt", "posT",
|
|
"put", "Put", "pUt", "puT",
|
|
"head", "Head", "HEAd", "heAD",
|
|
// unsupported methods
|
|
"PATCH", "CONNECT", "OPTIONS",
|
|
// nonsense strings
|
|
"something", "invalid_method", "method",
|
|
} {
|
|
res, err := makeOPTIONSRequest(s, bucket, "www.my-origin.com", method, "X-Custom-Header")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkApiErr(res.err, s3err.GetInvalidCORSMethodErr(method)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_invalid_request_headers(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_invalid_request_headers"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
for _, test := range []struct {
|
|
invalidHeader string
|
|
headers string
|
|
}{
|
|
{"invalid header", "X-Amz-Date,X-Amz-Content-Sha256,invalid header"}, // invalid 'space' in header name
|
|
{"X-Custom:Header", "Authorization,X-Custom:Header"}, // invalid char :
|
|
{"X(Custom)", "Content-Length,X(Custom)"}, // invalid char ()
|
|
{" Bad/Header", "Content-Encoding, Bad/Header"}, // extra 'space', invalid char /
|
|
{"X[Key]", "Date,X[Key]"}, // invalid char '[]'
|
|
{"Bad=Name", "X-Amz-Custome-Header,Bad=Name"}, // invalid char =
|
|
{`X"Quote"`, `X"Quote"`}, // invalid quote "
|
|
{"NonAsciiŁ", "Content-Length,NonAsciiŁ"}, // non-ASCII character
|
|
{"Emoji😀", "X-Emoji,Emoji😀"}, // emoji invalid
|
|
{"bad@char", "Accept-Encoding,bad@char"}, // @ is invalid
|
|
{"tab\tchar", "tab\tchar,X-Something-Valid"}, // invalid encodign \t
|
|
} {
|
|
res, err := makeOPTIONSRequest(s, bucket, "www.my-origin.com", http.MethodGet, test.headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkApiErr(res.err, s3err.GetInvalidCORSRequestHeaderErr(test.invalidHeader)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_unset_bucket_cors(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_unset_bucket_cors"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
res, err := makeOPTIONSRequest(s, bucket, "http://example.com", http.MethodPost, "X-Amz-Date,Date")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return checkApiErr(res.err, s3err.GetAPIError(s3err.ErrCORSIsNotEnabled))
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_access_forbidden(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_access_forbidden"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"http://example.com", "https://example.com"},
|
|
AllowedMethods: []string{http.MethodGet},
|
|
AllowedHeaders: []string{"X-Amz-Date", "X-Amz-Content-Sha256"},
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{http.MethodHead},
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"http://origin*"},
|
|
AllowedMethods: []string{http.MethodPost},
|
|
AllowedHeaders: []string{"Authorization"},
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"http://something.com"},
|
|
AllowedMethods: []string{http.MethodPut},
|
|
AllowedHeaders: []string{"X-Amz-*"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
origin string
|
|
method string
|
|
headers string
|
|
}{
|
|
// origin deson't match
|
|
{"http://non-matching-origin.net", http.MethodGet, "X-Amz-Date"},
|
|
// method doesn't match
|
|
{"http://example.com", http.MethodPut, "X-Amz-Content-Sha256"},
|
|
// header doesn't match
|
|
{"http://example.com", http.MethodGet, "X-Amz-Expected-Bucket-Owner"},
|
|
// extra header
|
|
{"http://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256,Extra-Header"},
|
|
// extra header (2nd rule)
|
|
{"https://any-origin.com", http.MethodHead, "X-Amz-Extra-Header"},
|
|
// origin match, method not (2nd rule)
|
|
{"https://any-origin.com", http.MethodPost, ""},
|
|
// third rule: headers doesn't match
|
|
{"https://origin.com", http.MethodPost, "Content-Length"},
|
|
// third rule: extra header
|
|
{"https://origin.com", http.MethodPost, "Authorization,Content-Disposition"},
|
|
// third rule: origin doesn't match
|
|
{"https://www.origin.com", http.MethodPost, "Authorization"},
|
|
// forth rule: header doesn't match the wildcard
|
|
{"https://something.com", http.MethodPut, "Authorization"},
|
|
{"https://something.com", http.MethodPut, "X-Amz"},
|
|
{"https://something.com", http.MethodPut, "X-Amz-Date,Content-Length"},
|
|
} {
|
|
res, err := makeOPTIONSRequest(s, bucket, test.origin, test.method, test.headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkApiErr(res.err, s3err.GetAPIError(s3err.ErrCORSForbidden)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PreflightOPTIONS_access_granted(s *S3Conf) error {
|
|
testName := "PreflightOPTIONS_access_granted"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"http://example.com", "https://example.com"},
|
|
AllowedMethods: []string{http.MethodGet, http.MethodHead},
|
|
AllowedHeaders: []string{"X-Amz-Date", "X-Amz-Content-Sha256"},
|
|
ExposeHeaders: []string{"Content-Type", "Content-Length"},
|
|
MaxAgeSeconds: getPtr(int32(100)),
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{http.MethodHead},
|
|
AllowedHeaders: []string{"X-Amz-Meta-Something"},
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"something.net"},
|
|
AllowedMethods: []string{http.MethodPost, http.MethodPut},
|
|
AllowedHeaders: []string{"Authorization"},
|
|
ExposeHeaders: []string{"Content-Disposition", "Content-Encoding"},
|
|
MaxAgeSeconds: getPtr(int32(3000)),
|
|
ID: getPtr("unique_id"),
|
|
},
|
|
{
|
|
AllowedOrigins: []string{"http://www*"},
|
|
AllowedMethods: []string{http.MethodGet},
|
|
AllowedHeaders: []string{"x-amz-server-side-encryption"},
|
|
ExposeHeaders: []string{"X-Amz-Expected-Bucket-Owner"},
|
|
MaxAgeSeconds: getPtr(int32(5000)),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
varyHdr := "Origin, Access-Control-Request-Headers, Access-Control-Request-Method"
|
|
|
|
for _, test := range []struct {
|
|
origin string
|
|
method string
|
|
headers string
|
|
result PreflightResult
|
|
}{
|
|
// first rule matches
|
|
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
|
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-content-sha256", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
|
{"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
|
{"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
|
// second rule matches: origin is a wildcard
|
|
{"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "ETag", "", "false", varyHdr, nil}},
|
|
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "ETag", "", "false", varyHdr, nil}},
|
|
// third rule matches
|
|
{"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding, ETag", "3000", "true", varyHdr, nil}},
|
|
{"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding, ETag", "3000", "true", varyHdr, nil}},
|
|
// forth rule matches: origin contains wildcard
|
|
{"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "", "X-Amz-Expected-Bucket-Owner, ETag", "5000", "true", varyHdr, nil}},
|
|
{"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "x-amz-server-side-encryption", "X-Amz-Expected-Bucket-Owner, ETag", "5000", "true", varyHdr, nil}},
|
|
} {
|
|
err := testOPTIONSEdnpoint(s, bucket, test.origin, test.method, test.headers, &test.result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|