mirror of
https://github.com/versity/versitygw.git
synced 2026-07-27 02:22:46 +00:00
Merge pull request #1492 from versity/sis/bucket-cors-allow-headers
fix: adds Acces-Control-Allow-Headers to cors responses
This commit is contained in:
+34
-2
@@ -45,13 +45,23 @@ func (ch CORSHeader) String() string {
|
||||
return string(ch)
|
||||
}
|
||||
|
||||
// ToLower converts the header to lower case
|
||||
func (ch CORSHeader) ToLower() string {
|
||||
return strings.ToLower(string(ch))
|
||||
}
|
||||
|
||||
// IsValid validates the cors http request method:
|
||||
// the methods are case sensitive
|
||||
func (cm CORSHTTPMethod) IsValid() bool {
|
||||
return cm == http.MethodGet || cm == http.MethodHead || cm == http.MethodPut ||
|
||||
return cm.IsEmpty() || cm == http.MethodGet || cm == http.MethodHead || cm == http.MethodPut ||
|
||||
cm == http.MethodPost || cm == http.MethodDelete
|
||||
}
|
||||
|
||||
// IsEmpty checks if the cors method is an empty string
|
||||
func (cm CORSHTTPMethod) IsEmpty() bool {
|
||||
return cm == ""
|
||||
}
|
||||
|
||||
// String converts the method value to 'string'
|
||||
func (cm CORSHTTPMethod) String() string {
|
||||
return string(cm)
|
||||
@@ -88,12 +98,19 @@ type CORSAllowanceConfig struct {
|
||||
Methods string
|
||||
ExposedHeaders string
|
||||
AllowCredentials string
|
||||
AllowHeaders string
|
||||
MaxAge *int32
|
||||
}
|
||||
|
||||
// IsAllowed walks through the CORS rules and finds the first one allowing access.
|
||||
// If no rule grants access, returns 'AccessForbidden'
|
||||
func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, headers []CORSHeader) (*CORSAllowanceConfig, error) {
|
||||
// if method is empty, anyways cors is forbidden
|
||||
// skip, without going through the rules
|
||||
if method.IsEmpty() {
|
||||
debuglogger.Logf("empty Access-Control-Request-Method")
|
||||
return nil, s3err.GetAPIError(s3err.ErrCORSForbidden)
|
||||
}
|
||||
for _, rule := range cc.Rules {
|
||||
// find the first rule granting access
|
||||
if isAllowed, wilcardOrigin := rule.Match(origin, method, headers); isAllowed {
|
||||
@@ -109,6 +126,7 @@ func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, hea
|
||||
AllowCredentials: allowCredentials,
|
||||
Methods: rule.GetAllowedMethods(),
|
||||
ExposedHeaders: rule.GetExposeHeaders(),
|
||||
AllowHeaders: buildAllowedHeaders(headers),
|
||||
MaxAge: rule.MaxAgeSeconds,
|
||||
}, nil
|
||||
}
|
||||
@@ -189,7 +207,7 @@ func (cr *CORSRule) Match(origin string, method CORSHTTPMethod, headers []CORSHe
|
||||
for _, reqHeader := range headers {
|
||||
match := false
|
||||
for _, header := range cr.AllowedHeaders {
|
||||
if wildcardMatch(header.String(), reqHeader.String()) {
|
||||
if wildcardMatch(header.ToLower(), reqHeader.ToLower()) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
@@ -217,6 +235,20 @@ func (cr *CORSRule) GetExposeHeaders() string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// buildAllowedHeaders builds a comma separated string from []CORSHeader
|
||||
func buildAllowedHeaders(headers []CORSHeader) string {
|
||||
var result strings.Builder
|
||||
|
||||
for i, h := range headers {
|
||||
if i > 0 {
|
||||
result.WriteString(", ")
|
||||
}
|
||||
result.WriteString(h.ToLower())
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// GetAllowedMethods returns comma separated CORS allowed methods
|
||||
func (cr *CORSRule) GetAllowedMethods() string {
|
||||
var result strings.Builder
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestCORSHTTPMethod_IsValid(t *testing.T) {
|
||||
method CORSHTTPMethod
|
||||
want bool
|
||||
}{
|
||||
{"empty valid", "", true},
|
||||
{"GET valid", http.MethodGet, true},
|
||||
{"HEAD valid", http.MethodHead, true},
|
||||
{"PUT valid", http.MethodPut, true},
|
||||
@@ -71,6 +72,83 @@ func TestCORSHTTPMethod_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSHeader_ToLower(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header CORSHeader
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "already lowercase",
|
||||
header: CORSHeader("content-type"),
|
||||
want: "content-type",
|
||||
},
|
||||
{
|
||||
name: "mixed case",
|
||||
header: CORSHeader("X-CuStOm-HeAdEr"),
|
||||
want: "x-custom-header",
|
||||
},
|
||||
{
|
||||
name: "uppercase",
|
||||
header: CORSHeader("AUTHORIZATION"),
|
||||
want: "authorization",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
header: CORSHeader(""),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "numeric and symbols",
|
||||
header: CORSHeader("X-123-HEADER"),
|
||||
want: "x-123-header",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.header.ToLower()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSHTTPMethod_IsEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method CORSHTTPMethod
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty string is empty",
|
||||
method: CORSHTTPMethod(""),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "GET method is not empty",
|
||||
method: CORSHTTPMethod("GET"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "random string is not empty",
|
||||
method: CORSHTTPMethod("FOO"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "lowercase get is not empty (case sensitive)",
|
||||
method: CORSHTTPMethod("get"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.method.IsEmpty()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSConfiguration_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -130,6 +208,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) {
|
||||
Origin: "http://allowed.com",
|
||||
AllowCredentials: "true",
|
||||
Methods: http.MethodGet,
|
||||
AllowHeaders: "x-test",
|
||||
ExposedHeaders: "",
|
||||
MaxAge: nil,
|
||||
},
|
||||
@@ -152,6 +231,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) {
|
||||
result: &CORSAllowanceConfig{
|
||||
Origin: "*",
|
||||
AllowCredentials: "false",
|
||||
AllowHeaders: "x-test",
|
||||
Methods: http.MethodGet,
|
||||
ExposedHeaders: "",
|
||||
MaxAge: nil,
|
||||
@@ -415,6 +495,52 @@ func TestGetExposeHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAllowedHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers []CORSHeader
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty slice returns empty string",
|
||||
headers: []CORSHeader{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single header lowercase",
|
||||
headers: []CORSHeader{"Content-Type"},
|
||||
want: "content-type",
|
||||
},
|
||||
{
|
||||
name: "multiple headers lowercased with commas",
|
||||
headers: []CORSHeader{"Content-Type", "X-Custom-Header", "Authorization"},
|
||||
want: "content-type, x-custom-header, authorization",
|
||||
},
|
||||
{
|
||||
name: "already lowercase header",
|
||||
headers: []CORSHeader{"accept"},
|
||||
want: "accept",
|
||||
},
|
||||
{
|
||||
name: "mixed case headers",
|
||||
headers: []CORSHeader{"ACCEPT", "x-Powered-By"},
|
||||
want: "accept, x-powered-by",
|
||||
},
|
||||
{
|
||||
name: "empty header value",
|
||||
headers: []CORSHeader{""},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildAllowedHeaders(tt.headers)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllowedMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -102,6 +102,7 @@ func (s S3ApiController) CORSOptions(ctx *fiber.Ctx) (*Response, error) {
|
||||
"Access-Control-Allow-Methods": &allowConfig.Methods,
|
||||
"Access-Control-Expose-Headers": &allowConfig.ExposedHeaders,
|
||||
"Access-Control-Allow-Credentials": &allowConfig.AllowCredentials,
|
||||
"Access-Control-Allow-Headers": &allowConfig.AllowHeaders,
|
||||
"Access-Control-Max-Age": utils.ConvertPtrToStringPtr(allowConfig.MaxAge),
|
||||
"Vary": &middlewares.VaryHdr,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
// 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 controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
func TestS3ApiController_CORSOptions(t *testing.T) {
|
||||
maxAge := int32(10000)
|
||||
cors, err := xml.Marshal(auth.CORSConfiguration{
|
||||
Rules: []auth.CORSRule{
|
||||
{
|
||||
AllowedOrigins: []string{"example.com"},
|
||||
AllowedMethods: []auth.CORSHTTPMethod{http.MethodGet, http.MethodPost},
|
||||
AllowedHeaders: []auth.CORSHeader{"Content-Type", "Content-Disposition"},
|
||||
ExposeHeaders: []auth.CORSHeader{"Content-Encoding", "date"},
|
||||
MaxAgeSeconds: &maxAge,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input testInput
|
||||
output testOutput
|
||||
}{
|
||||
{
|
||||
name: "missing origin",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMissingCORSOrigin),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "invalid_method",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetInvalidCORSMethodErr("invalid_method"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid headers",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "Content Type",
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetInvalidCORSRequestHeaderErr("Content Type"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fails to get bucket cors",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
beRes: []byte{},
|
||||
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bucket cors is not enabled",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
beRes: []byte{},
|
||||
beErr: s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrCORSIsNotEnabled),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fails to parse bucket cors",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
beRes: []byte("invalid_cors"),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: errors.New("failed to parse cors config:"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cors is not allowed",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "PUT",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
},
|
||||
beRes: cors,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrCORSForbidden),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: cors is allowed",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"Origin": "example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "content-type, Content-Disposition",
|
||||
},
|
||||
beRes: cors,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
Headers: map[string]*string{
|
||||
"Access-Control-Allow-Origin": utils.GetStringPtr("example.com"),
|
||||
"Access-Control-Allow-Methods": utils.GetStringPtr("GET, POST"),
|
||||
"Access-Control-Expose-Headers": utils.GetStringPtr("Content-Encoding, date"),
|
||||
"Access-Control-Allow-Credentials": utils.GetStringPtr("true"),
|
||||
"Access-Control-Allow-Headers": utils.GetStringPtr("content-type, content-disposition"),
|
||||
"Access-Control-Max-Age": utils.ConvertToStringPtr(maxAge),
|
||||
"Vary": &middlewares.VaryHdr,
|
||||
},
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
be := &BackendMock{
|
||||
GetBucketCorsFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
return tt.input.beRes.([]byte), tt.input.beErr
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := S3ApiController{
|
||||
be: be,
|
||||
}
|
||||
|
||||
testController(
|
||||
t,
|
||||
ctrl.CORSOptions,
|
||||
tt.output.response,
|
||||
tt.output.err,
|
||||
ctxInputs{
|
||||
locals: tt.input.locals,
|
||||
headers: tt.input.headers,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler {
|
||||
|
||||
// if request method is not specified with Access-Control-Request-Method
|
||||
// override it with the actual request method
|
||||
if method == "" {
|
||||
if method.IsEmpty() {
|
||||
method = auth.CORSHTTPMethod(ctx.Request().Header.Method())
|
||||
} else if !method.IsValid() {
|
||||
// check if allowed method is valid
|
||||
@@ -92,6 +92,7 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler {
|
||||
"Access-Control-Allow-Methods": allowConfig.Methods,
|
||||
"Access-Control-Expose-Headers": allowConfig.ExposedHeaders,
|
||||
"Access-Control-Allow-Credentials": allowConfig.AllowCredentials,
|
||||
"Access-Control-Allow-Headers": allowConfig.AllowHeaders,
|
||||
"Vary": VaryHdr,
|
||||
} {
|
||||
if val != "" {
|
||||
|
||||
+20
-20
@@ -14217,22 +14217,22 @@ func PreflightOPTIONS_access_granted(s *S3Conf) error {
|
||||
result PreflightResult
|
||||
}{
|
||||
// first rule matches
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "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", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date", "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-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: origin is a wildcard
|
||||
{"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}},
|
||||
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}},
|
||||
{"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", "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}},
|
||||
{"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}},
|
||||
// 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", "5000", "true", varyHdr, nil}},
|
||||
{"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}},
|
||||
{"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "", "X-Amz-Expected-Bucket-Owner", "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", "5000", "true", varyHdr, nil}},
|
||||
// fifth rule matches: allowed headers contains wildcard
|
||||
{"http://uniquie-origin.net", http.MethodPost, "X-Amz-anything-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
{"http://uniquie-origin.net", http.MethodPut, "X-Amz-yyy-xxx-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
{"http://uniquie-origin.net", http.MethodPost, "X-Amz-anything-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-anything-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
{"http://uniquie-origin.net", http.MethodPut, "X-Amz-yyy-xxx-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-yyy-xxx-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
} {
|
||||
err := testOPTIONSEdnpoint(s, bucket, test.origin, test.method, test.headers, &test.result)
|
||||
if err != nil {
|
||||
@@ -14439,16 +14439,16 @@ func CORSMiddleware_access_granted(s *S3Conf) error {
|
||||
result PreflightResult
|
||||
}{
|
||||
// first rule matches
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "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", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"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", "", "", "false", varyHdr, nil}},
|
||||
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "false", varyHdr, nil}},
|
||||
{"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", "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}},
|
||||
{"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,
|
||||
|
||||
@@ -1685,6 +1685,7 @@ func compareCorsConfig(expected, got []types.CORSRule) error {
|
||||
type PreflightResult struct {
|
||||
Origin string
|
||||
Methods string
|
||||
AllowHeaders string
|
||||
ExposeHeaders string
|
||||
MaxAge string
|
||||
AllowCredentials string
|
||||
@@ -1715,6 +1716,7 @@ func extractCORSHeaders(resp *http.Response) (*PreflightResult, error) {
|
||||
Methods: resp.Header.Get("Access-Control-Allow-Methods"),
|
||||
ExposeHeaders: resp.Header.Get("Access-Control-Expose-Headers"),
|
||||
MaxAge: resp.Header.Get("Access-Control-Max-Age"),
|
||||
AllowHeaders: resp.Header.Get("Access-Control-Allow-Headers"),
|
||||
AllowCredentials: resp.Header.Get("Access-Control-Allow-Credentials"),
|
||||
Vary: resp.Header.Get("Vary"),
|
||||
}, nil
|
||||
@@ -1769,6 +1771,9 @@ func comparePreflightResult(expected, got *PreflightResult) error {
|
||||
if expected.Methods != got.Methods {
|
||||
return fmt.Errorf("expected the allowed methods to be %v, instead got %v", expected.Methods, got.Methods)
|
||||
}
|
||||
if expected.AllowHeaders != got.AllowHeaders {
|
||||
return fmt.Errorf("expected the allow headers to be %v, instead got %v", expected.AllowHeaders, got.AllowHeaders)
|
||||
}
|
||||
if expected.ExposeHeaders != got.ExposeHeaders {
|
||||
return fmt.Errorf("expected the expose headers to be %v, instead got %v", expected.ExposeHeaders, got.ExposeHeaders)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user