mirror of
https://github.com/versity/versitygw.git
synced 2026-01-05 03:24:04 +00:00
Fixes #1597 S3 returns a specific error when calling an object GET operation (e.g., `bucket/object/key?uploads`) with the `?uploads` query parameter. It’s not the standard `MethodNotAllowed` error. This PR adds support for handling this specific error route.
378 lines
11 KiB
Go
378 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"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
var (
|
|
shortTimeout = 30 * time.Second
|
|
longTimeout = 60 * time.Second
|
|
iso8601Format = "20060102T150405Z"
|
|
timefmt = "Mon, 02 Jan 2006 15:04:05 GMT"
|
|
nullVersionId = "null"
|
|
)
|
|
|
|
// router tests
|
|
func RouterPutPartNumberWithoutUploadId(s *S3Conf) error {
|
|
testName := "RouterPutPartNumberWithoutUploadId"
|
|
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
req, err := http.NewRequest(http.MethodPut, s.endpoint+"/bucket/object", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
query := req.URL.Query()
|
|
query.Add("partNumber", "1")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMissingUploadId)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func RouterPostRoot(s *S3Conf) error {
|
|
testName := "RouterPostRoot"
|
|
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
req, err := http.NewRequest(http.MethodPost, s.endpoint+"/", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func RouterPostObjectWithoutQuery(s *S3Conf) error {
|
|
testName := "RouterPostObjectWithoutQuery"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
req, err := http.NewRequest(http.MethodPost, s.endpoint+"/bucket/object", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func RouterPUTObjectOnlyUploadId(s *S3Conf) error {
|
|
testName := "RouterPUTObjectOnlyUploadId"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
req, err := http.NewRequest(http.MethodPut, s.endpoint+"/bucket/object", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
query := req.URL.Query()
|
|
query.Add("uploadId", "my-upload-id")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func RouterGetUploadsWithKey(s *S3Conf) error {
|
|
testName := "RouterGetUploadsWithKey"
|
|
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
req, err := http.NewRequest(http.MethodGet, s.endpoint+"/bucket/object?uploads", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrGetUploadsWithKey))
|
|
})
|
|
}
|
|
|
|
// CORS middleware tests
|
|
func CORSMiddleware_invalid_method(s *S3Conf) error {
|
|
testName := "CORSMiddleware_invalid_method"
|
|
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://www.example.com"},
|
|
AllowedMethods: []string{http.MethodPut},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a PutObject signed request
|
|
req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{
|
|
"Origin": "http://www.example.com",
|
|
"Access-Control-Request-Method": "invalid_method",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkApiErr(result.err, s3err.GetInvalidCORSMethodErr("invalid_method"))
|
|
})
|
|
}
|
|
|
|
func CORSMiddleware_invalid_headers(s *S3Conf) error {
|
|
testName := "CORSMiddleware_invalid_headers"
|
|
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://www.example.com"},
|
|
AllowedMethods: []string{http.MethodPut},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create a PutObject signed request
|
|
req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{
|
|
"Origin": "http://www.example.com",
|
|
"Access-Control-Request-Headers": "invalid header",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkApiErr(result.err, s3err.GetInvalidCORSRequestHeaderErr("invalid header"))
|
|
})
|
|
}
|
|
|
|
func CORSMiddleware_access_forbidden(s *S3Conf) error {
|
|
testName := "CORSMiddleware_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},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
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, ""},
|
|
} {
|
|
req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{
|
|
"Origin": test.origin,
|
|
"Access-Control-Request-Headers": test.headers,
|
|
"Access-Control-Request-Method": test.method,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// no error expected, all the headers should be empty
|
|
if err := comparePreflightResult(&PreflightResult{}, res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func CORSMiddleware_access_granted(s *S3Conf) error {
|
|
testName := "CORSMiddleware_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"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
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, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
|
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
|
{"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length", "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", "100", "true", varyHdr, nil}},
|
|
// second rule matches
|
|
{"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "", "", "false", varyHdr, nil}},
|
|
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "", "false", varyHdr, nil}},
|
|
// third rule matches
|
|
{"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}},
|
|
{"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}},
|
|
} {
|
|
req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{
|
|
"Origin": test.origin,
|
|
"Access-Control-Request-Headers": test.headers,
|
|
"Access-Control-Request-Method": test.method,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := comparePreflightResult(&test.result, res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|