mirror of
https://github.com/versity/versitygw.git
synced 2026-08-17 20:56:21 +00:00
Merge pull request #1903 from versity/test/rest_cors_two
test: CORS - response header tests
This commit is contained in:
@@ -86,4 +86,4 @@ get_bucket_cors_check_valid_data() {
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
+189
-5
@@ -318,16 +318,31 @@ send_rest_go_command_callback() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# return 0 for key match, 1 for no key match, 2 for value mismatch
|
||||
check_key_and_value_pair_for_match() {
|
||||
if ! check_param_count_v2 "read key, read value, expected key, expected value" 4 $#; then
|
||||
return 2
|
||||
fi
|
||||
if [ "${1,,}" == "${3,,}" ]; then
|
||||
if [ "$2" != "$4" ]; then
|
||||
log 2 "expected value of '$4', was '$2'"
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
check_for_header_key_and_value() {
|
||||
if ! check_param_count_v2 "data file, header key, header value" 3 $#; then
|
||||
return 1
|
||||
fi
|
||||
while IFS=$': \r' read -r key value; do
|
||||
if [ "$key" == "$2" ]; then
|
||||
if [ "$value" != "$3" ]; then
|
||||
log 2 "expected value of '$3', was '$value'"
|
||||
return 1
|
||||
fi
|
||||
local check_result=0
|
||||
check_key_and_value_pair_for_match "$key" "$value" "$2" "$3" || check_result=$?
|
||||
if [ "$check_result" -eq 2 ]; then
|
||||
return 1
|
||||
elif [ "$check_result" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
done <<< "$(grep -E '^.+: .+$' "$1")"
|
||||
@@ -446,3 +461,172 @@ send_rest_go_command_write_response_to_file() {
|
||||
return 0
|
||||
}
|
||||
|
||||
check_header_keys_and_values() {
|
||||
if ! check_param_count_gt "data file, header/key value pairs" 1 $#; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local data_file="$1"
|
||||
local pairs=("${@:2}")
|
||||
local check_result=0
|
||||
local remaining_pairs=""
|
||||
local line=""
|
||||
local key=""
|
||||
local value=""
|
||||
|
||||
# Parse header lines until the blank line separator.
|
||||
# - Split on the first ':'
|
||||
# - Allow empty values (e.g. "X-Foo:")
|
||||
# - Preserve spaces in values
|
||||
while IFS= read -r line; do
|
||||
line="${line%$'\r'}"
|
||||
|
||||
# End of headers
|
||||
if [ -z "$line" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Require at least one ':' to treat as a header field
|
||||
case "$line" in
|
||||
*:*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
key="${line%%:*}"
|
||||
value="${line#*:}"
|
||||
value="${value# }"
|
||||
|
||||
check_result=0
|
||||
if remaining_pairs=$(check_for_key_and_value_within_pairs "$key" "$value" "${pairs[@]}"); then
|
||||
# match found; remaining_pairs contains the updated list
|
||||
pairs=()
|
||||
if [ -n "$remaining_pairs" ]; then
|
||||
while IFS= read -r line; do
|
||||
pairs+=("$line")
|
||||
done <<< "$remaining_pairs"
|
||||
fi
|
||||
|
||||
if [ "${#pairs[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
check_result=$?
|
||||
if [ "$check_result" -eq 2 ]; then
|
||||
log 2 "error checking pair"
|
||||
return 1
|
||||
fi
|
||||
# check_result==1 means no match; keep current pairs
|
||||
fi
|
||||
done < "$data_file"
|
||||
|
||||
if [ "${#pairs[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
log 2 "missing expected header key '${pairs[0]}'"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check that a specific header key/value pair (or pairs) is NOT present.
|
||||
# Returns:
|
||||
# 0 - none of the specified pairs are present
|
||||
# 1 - at least one specified pair is present
|
||||
# 2 - other error (param count, malformed pairs)
|
||||
check_header_keys_and_values_not_present() {
|
||||
if ! check_param_count_gt "data file, header/key value pairs" 1 $#; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
local data_file="$1"
|
||||
shift 1
|
||||
|
||||
if [ $(( $# % 2 )) -ne 0 ]; then
|
||||
log 2 "header key/value pairs must be even count"
|
||||
return 2
|
||||
fi
|
||||
|
||||
local expected_pairs=("$@")
|
||||
local line=""
|
||||
local key=""
|
||||
local value=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
line="${line%$'\r'}"
|
||||
|
||||
# End of headers
|
||||
if [ -z "$line" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
case "$line" in
|
||||
*:*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
key="${line%%:*}"
|
||||
value="${line#*:}"
|
||||
value="${value# }"
|
||||
|
||||
local idx
|
||||
for ((idx=0; idx<${#expected_pairs[@]}; idx+=2)); do
|
||||
local exp_key="${expected_pairs[$idx]}"
|
||||
local exp_val="${expected_pairs[$((idx+1))]}"
|
||||
|
||||
if [ "${key,,}" = "${exp_key,,}" ] && [ "$value" = "$exp_val" ]; then
|
||||
log 2 "unexpected header pair present: '$exp_key: $exp_val'"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
done < "$data_file"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
check_for_key_and_value_within_pairs() {
|
||||
if ! check_param_count_gt "read key, read value, full set of key/value pairs" 2 $#; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
local read_key="$1"
|
||||
local read_value="$2"
|
||||
shift 2
|
||||
|
||||
# Require an even number of remaining args (key/value pairs)
|
||||
if [ $(( $# % 2 )) -ne 0 ]; then
|
||||
log 2 "key/value pairs must be even count"
|
||||
return 2
|
||||
fi
|
||||
|
||||
local pairs=()
|
||||
local omit_idx=-1
|
||||
local idx=0
|
||||
while [ $# -gt 0 ]; do
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local check_result=0
|
||||
|
||||
pairs+=("$key" "$value")
|
||||
check_key_and_value_pair_for_match "$read_key" "$read_value" "$key" "$value" || check_result=$?
|
||||
if [ "$check_result" -eq 2 ]; then
|
||||
return 2
|
||||
elif [ "$check_result" -eq 0 ]; then
|
||||
# Omit the last matching pair (preserves previous behavior)
|
||||
omit_idx=$idx
|
||||
fi
|
||||
|
||||
idx=$((idx + 2))
|
||||
shift 2
|
||||
done
|
||||
|
||||
if [ "$omit_idx" -lt 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for ((idx=0; idx<${#pairs[@]}; idx+=2)); do
|
||||
if [ "$idx" -eq "$omit_idx" ]; then
|
||||
continue
|
||||
fi
|
||||
echo "${pairs[$idx]}"
|
||||
echo "${pairs[$((idx+1))]}"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
AllowedMethods = "allowedMethods"
|
||||
AllowedOrigins = "allowedOrigins"
|
||||
AllowedHeaders = "allowedHeaders"
|
||||
ExposedHeaders = "exposedHeaders"
|
||||
ID = "id"
|
||||
MaxAgeSeconds = "maxAgeSeconds"
|
||||
)
|
||||
|
||||
type CORSRule struct {
|
||||
XMLName xml.Name `xml:"CORSRule"`
|
||||
AllowedMethod []string `xml:"AllowedMethod"`
|
||||
AllowedOrigin []string `xml:"AllowedOrigin"`
|
||||
AllowedHeader []string `xml:"AllowedHeader,omitempty"`
|
||||
ExposeHeader []string `xml:"ExposeHeader,omitempty"`
|
||||
ID string `xml:"ID,omitempty"`
|
||||
MaxAgeSeconds string `xml:"MaxAgeSeconds,omitempty"`
|
||||
}
|
||||
|
||||
type CORSConfiguration struct {
|
||||
XMLName xml.Name `xml:"CORSConfiguration"`
|
||||
XMLNamespace string `xml:"xmlns,attr"`
|
||||
CORSRules []*CORSRule
|
||||
}
|
||||
|
||||
func NewPutBucketCORSCommand(command *S3Command, ruleStrings []string) (*S3Command, error) {
|
||||
command.Method = "PUT"
|
||||
command.Query = "cors"
|
||||
corsConfiguration := &CORSConfiguration{
|
||||
XMLNamespace: "https://s3.amazonaws.com/doc/2006-03-01/",
|
||||
}
|
||||
for _, ruleString := range ruleStrings {
|
||||
var corsRule *CORSRule
|
||||
var err error
|
||||
if corsRule, err = assembleCORSRule(ruleString); err != nil {
|
||||
return nil, fmt.Errorf("error assembling CORS rule: %w", err)
|
||||
}
|
||||
corsConfiguration.CORSRules = append(corsConfiguration.CORSRules, corsRule)
|
||||
}
|
||||
xmlData, err := xml.Marshal(corsConfiguration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling XML: %w", err)
|
||||
}
|
||||
command.Payload = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + string(xmlData)
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func assembleCORSRule(ruleString string) (*CORSRule, error) {
|
||||
corsRule := &CORSRule{}
|
||||
ruleComponents := strings.Split(ruleString, ";")
|
||||
for _, component := range ruleComponents {
|
||||
componentSegments := strings.Split(component, "=")
|
||||
switch componentSegments[0] {
|
||||
case AllowedMethods:
|
||||
corsRule.AllowedMethod = strings.Split(componentSegments[1], ",")
|
||||
case AllowedOrigins:
|
||||
corsRule.AllowedOrigin = strings.Split(componentSegments[1], ",")
|
||||
case AllowedHeaders:
|
||||
corsRule.AllowedHeader = strings.Split(componentSegments[1], ",")
|
||||
case ExposedHeaders:
|
||||
corsRule.ExposeHeader = strings.Split(componentSegments[1], ",")
|
||||
case ID:
|
||||
corsRule.ID = componentSegments[1]
|
||||
case MaxAgeSeconds:
|
||||
corsRule.MaxAgeSeconds = componentSegments[1]
|
||||
default:
|
||||
return nil, errors.New("invalid CORSRule component type: " + componentSegments[0])
|
||||
}
|
||||
}
|
||||
return corsRule, nil
|
||||
}
|
||||
@@ -41,25 +41,10 @@ const (
|
||||
|
||||
const SHA256HashZeroBytes = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
type S3RESTCommand struct {
|
||||
Method string
|
||||
Url string
|
||||
Queries map[string]string
|
||||
SignedParams map[string]string
|
||||
UnsignedParams map[string]string
|
||||
DataSource DataSource
|
||||
}
|
||||
|
||||
type S3CommandErrors struct {
|
||||
IncorrectSignature bool
|
||||
AuthorizationHeaderMalformed bool
|
||||
IncorrectCredential string
|
||||
IncorrectYearMonthDay bool
|
||||
InvalidYearMonthDay bool
|
||||
IncorrectContentMD5 bool
|
||||
MissingHostParam bool
|
||||
CustomHostParam string
|
||||
CustomHostParamSet bool
|
||||
type HeaderValue struct {
|
||||
Key string
|
||||
Value string
|
||||
Signed bool
|
||||
}
|
||||
|
||||
type S3Command struct {
|
||||
@@ -74,6 +59,7 @@ type S3Command struct {
|
||||
AwsSecretAccessKey string
|
||||
ServiceName string
|
||||
SignedParams map[string]string
|
||||
UnsignedParams map[string]string
|
||||
PayloadFile string
|
||||
IncorrectSignature bool
|
||||
AuthorizationHeaderMalformed bool
|
||||
@@ -100,7 +86,7 @@ type S3Command struct {
|
||||
currentDateTime string
|
||||
host string
|
||||
payloadHash string
|
||||
headerValues [][]string
|
||||
headerValues []*HeaderValue
|
||||
canonicalRequestHash string
|
||||
path string
|
||||
signedParamString string
|
||||
@@ -208,24 +194,24 @@ func (s *S3Command) initializeOpenSSLPayloadAndGetContentLength() error {
|
||||
}
|
||||
|
||||
func (s *S3Command) addHeaderValues() error {
|
||||
s.headerValues = [][]string{}
|
||||
s.headerValues = []*HeaderValue{}
|
||||
if s.MissingHostParam {
|
||||
s.headerValues = append(s.headerValues, []string{"host", ""})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{"host", "", true})
|
||||
} else if s.CustomHostParamSet {
|
||||
s.headerValues = append(s.headerValues, []string{"host", s.CustomHostParam})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{"host", s.CustomHostParam, true})
|
||||
} else {
|
||||
s.headerValues = append(s.headerValues, []string{"host", s.host})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{"host", s.host, true})
|
||||
}
|
||||
if s.PayloadType == StreamingAWS4HMACSHA256PayloadTrailer && s.ChecksumType != "" {
|
||||
s.headerValues = append(s.headerValues, []string{"x-amz-trailer", fmt.Sprintf("x-amz-checksum-%s", s.ChecksumType)})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{"x-amz-trailer", fmt.Sprintf("x-amz-checksum-%s", s.ChecksumType), true})
|
||||
}
|
||||
s.headerValues = append(s.headerValues,
|
||||
[]string{"x-amz-content-sha256", s.payloadHash},
|
||||
[]string{"x-amz-date", s.currentDateTime},
|
||||
&HeaderValue{"x-amz-content-sha256", s.payloadHash, true},
|
||||
&HeaderValue{"x-amz-date", s.currentDateTime, true},
|
||||
)
|
||||
if s.Client == OPENSSL && !s.OmitContentLength {
|
||||
s.headerValues = append(s.headerValues,
|
||||
[]string{"Content-Length", fmt.Sprintf("%d", s.contentLength)})
|
||||
&HeaderValue{"Content-Length", fmt.Sprintf("%d", s.contentLength), true})
|
||||
}
|
||||
if s.dataSource != nil && s.PayloadType != UnsignedPayload {
|
||||
payloadSize, err := s.dataSource.SourceDataByteSize()
|
||||
@@ -233,19 +219,22 @@ func (s *S3Command) addHeaderValues() error {
|
||||
return fmt.Errorf("error getting payload size: %w", err)
|
||||
}
|
||||
s.headerValues = append(s.headerValues,
|
||||
[]string{"x-amz-decoded-content-length", fmt.Sprintf("%d", payloadSize)})
|
||||
&HeaderValue{"x-amz-decoded-content-length", fmt.Sprintf("%d", payloadSize), true})
|
||||
}
|
||||
for key, value := range s.SignedParams {
|
||||
s.headerValues = append(s.headerValues, []string{key, value})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{key, value, true})
|
||||
}
|
||||
if s.ContentMD5 || s.IncorrectContentMD5 || s.CustomContentMD5 != "" {
|
||||
if err := s.addContentMD5Header(); err != nil {
|
||||
return fmt.Errorf("error adding Content-MD5 header: %w", err)
|
||||
}
|
||||
}
|
||||
for key, value := range s.UnsignedParams {
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{key, value, false})
|
||||
}
|
||||
sort.Slice(s.headerValues,
|
||||
func(i, j int) bool {
|
||||
return strings.ToLower(s.headerValues[i][0]) < strings.ToLower(s.headerValues[j][0])
|
||||
return strings.ToLower(s.headerValues[i].Key) < strings.ToLower(s.headerValues[j].Key)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -283,7 +272,7 @@ func (s *S3Command) addContentMD5Header() error {
|
||||
contentMD5 = base64.StdEncoding.EncodeToString(md5Hash)
|
||||
}
|
||||
|
||||
s.headerValues = append(s.headerValues, []string{"Content-MD5", contentMD5})
|
||||
s.headerValues = append(s.headerValues, &HeaderValue{"Content-MD5", contentMD5, true})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -308,9 +297,11 @@ func (s *S3Command) generateCanonicalRequestString() {
|
||||
|
||||
var signedParams []string
|
||||
for _, headerValue := range s.headerValues {
|
||||
key := strings.ToLower(headerValue[0])
|
||||
canonicalRequestLines = append(canonicalRequestLines, key+":"+headerValue[1])
|
||||
signedParams = append(signedParams, key)
|
||||
if headerValue.Signed {
|
||||
key := strings.ToLower(headerValue.Key)
|
||||
canonicalRequestLines = append(canonicalRequestLines, key+":"+headerValue.Value)
|
||||
signedParams = append(signedParams, key)
|
||||
}
|
||||
}
|
||||
|
||||
canonicalRequestLines = append(canonicalRequestLines, "")
|
||||
@@ -375,7 +366,7 @@ func (s *S3Command) buildCurlShellCommand() (string, error) {
|
||||
authorizationString := s.buildAuthorizationString()
|
||||
curlCommand = append(curlCommand, "-H", fmt.Sprintf("\"%s\"", authorizationString))
|
||||
for _, headerValue := range s.headerValues {
|
||||
headerString := fmt.Sprintf("\"%s: %s\"", headerValue[0], headerValue[1])
|
||||
headerString := fmt.Sprintf("\"%s: %s\"", headerValue.Key, headerValue.Value)
|
||||
curlCommand = append(curlCommand, "-H", headerString)
|
||||
}
|
||||
if s.PayloadFile != "" {
|
||||
@@ -407,10 +398,10 @@ func (s *S3Command) buildOpenSSLCommand() error {
|
||||
openSSLCommand := []string{fmt.Sprintf("%s %s HTTP/1.1", s.Method, s.path)}
|
||||
openSSLCommand = append(openSSLCommand, s.buildAuthorizationString())
|
||||
for _, headerValue := range s.headerValues {
|
||||
if headerValue[0] == "host" && s.MissingHostParam {
|
||||
if headerValue.Key == "host" && s.MissingHostParam {
|
||||
continue
|
||||
}
|
||||
openSSLCommand = append(openSSLCommand, fmt.Sprintf("%s:%s", headerValue[0], headerValue[1]))
|
||||
openSSLCommand = append(openSSLCommand, fmt.Sprintf("%s:%s", headerValue.Key, headerValue.Value))
|
||||
}
|
||||
|
||||
file, err := os.Create(s.FilePath)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
const (
|
||||
CreateBucket = "createBucket"
|
||||
PutBucketCors = "putBucketCORS"
|
||||
PutBucketTagging = "putBucketTagging"
|
||||
PutObject = "putObject"
|
||||
PutObjectTagging = "putObjectTagging"
|
||||
@@ -29,6 +30,7 @@ var awsSecretAccessKey *string
|
||||
var serviceName *string
|
||||
|
||||
var signedParamsMap restParams
|
||||
var unsignedParamsMap restParams
|
||||
var payloadFile *string
|
||||
var incorrectSignature *bool
|
||||
var incorrectCredential *string
|
||||
@@ -63,15 +65,19 @@ var omitContentLength *bool
|
||||
var locationConstraint *string
|
||||
var locationConstraintSet bool = false
|
||||
|
||||
var corsRules arrayFlags
|
||||
|
||||
type restParams map[string]string
|
||||
|
||||
var paramSeparator *string
|
||||
|
||||
func (r *restParams) String() string {
|
||||
return fmt.Sprintf("%v", *r)
|
||||
}
|
||||
|
||||
func (r *restParams) Set(value string) error {
|
||||
*r = make(map[string]string)
|
||||
pairs := strings.Split(value, ",")
|
||||
pairs := strings.Split(value, *paramSeparator)
|
||||
for _, pair := range pairs {
|
||||
kv := strings.SplitN(pair, ":", 2)
|
||||
if len(kv) != 2 {
|
||||
@@ -111,6 +117,7 @@ func main() {
|
||||
AwsSecretAccessKey: *awsSecretAccessKey,
|
||||
ServiceName: *serviceName,
|
||||
SignedParams: signedParamsMap,
|
||||
UnsignedParams: unsignedParamsMap,
|
||||
PayloadFile: *payloadFile,
|
||||
IncorrectSignature: *incorrectSignature,
|
||||
AuthorizationScheme: *authorizationScheme,
|
||||
@@ -149,7 +156,11 @@ func getS3CommandType(baseCommand *command.S3Command) (command.S3CommandConverte
|
||||
switch *commandType {
|
||||
case CreateBucket:
|
||||
if s3Command, err = command.NewCreateBucketCommand(baseCommand, *locationConstraint, locationConstraintSet); err != nil {
|
||||
return nil, fmt.Errorf("error setting up CreateBucket command: %v", err)
|
||||
return nil, fmt.Errorf("error setting up CreateBucket command: %w", err)
|
||||
}
|
||||
case PutBucketCors:
|
||||
if s3Command, err = command.NewPutBucketCORSCommand(baseCommand, corsRules); err != nil {
|
||||
return nil, fmt.Errorf("error setting up PutBucketCORS command: %w", err)
|
||||
}
|
||||
case PutBucketTagging:
|
||||
fields := command.TaggingFields{
|
||||
@@ -158,11 +169,11 @@ func getS3CommandType(baseCommand *command.S3Command) (command.S3CommandConverte
|
||||
TagValues: tagValues,
|
||||
}
|
||||
if s3Command, err = command.NewPutBucketTaggingCommand(baseCommand, &fields); err != nil {
|
||||
return nil, fmt.Errorf("error setting up PutBucketTagging command: %v", err)
|
||||
return nil, fmt.Errorf("error setting up PutBucketTagging command: %w", err)
|
||||
}
|
||||
case PutObject:
|
||||
if s3Command, err = command.NewPutObjectCommand(baseCommand); err != nil {
|
||||
return nil, fmt.Errorf("error setting up PutObject command: %v", err)
|
||||
return nil, fmt.Errorf("error setting up PutObject command: %w", err)
|
||||
}
|
||||
case PutObjectTagging:
|
||||
fields := command.TaggingFields{
|
||||
@@ -171,7 +182,7 @@ func getS3CommandType(baseCommand *command.S3Command) (command.S3CommandConverte
|
||||
TagValues: tagValues,
|
||||
}
|
||||
if s3Command, err = command.NewPutObjectTaggingCommand(baseCommand, &fields); err != nil {
|
||||
return nil, fmt.Errorf("error setting up PutBucketTagging command: %v", err)
|
||||
return nil, fmt.Errorf("error setting up PutBucketTagging command: %w", err)
|
||||
}
|
||||
default:
|
||||
s3Command = baseCommand
|
||||
@@ -198,6 +209,7 @@ func buildCommand(s3Command command.S3CommandConverter) error {
|
||||
}
|
||||
|
||||
func checkFlags() error {
|
||||
paramSeparator = flag.String("paramSeparator", ",", "What character to use to separate signed and unsigned params")
|
||||
method = flag.String("method", "GET", "HTTP method to use")
|
||||
url = flag.String("url", "https://localhost:7070", "S3 server URL")
|
||||
bucketName = flag.String("bucketName", "", "Bucket name")
|
||||
@@ -210,6 +222,7 @@ func checkFlags() error {
|
||||
logger.Debug = flag.Bool("debug", false, "Print debug statements")
|
||||
logger.LogFile = flag.String("logFile", "", "Log file, if any")
|
||||
flag.Var(&signedParamsMap, "signedParams", "Signed params, separated by comma")
|
||||
flag.Var(&unsignedParamsMap, "unsignedParams", "Unsigned params, separated by comma")
|
||||
payloadFile = flag.String("payloadFile", "", "Payload file path, if any")
|
||||
incorrectSignature = flag.Bool("incorrectSignature", false, "Simulate an incorrect signature")
|
||||
incorrectYearMonthDay = flag.Bool("incorrectYearMonthDay", false, "Simulate an incorrect year/month/day")
|
||||
@@ -235,6 +248,7 @@ func checkFlags() error {
|
||||
flag.Var(&tagKeys, "tagKey", "Tag key (can add multiple)")
|
||||
flag.Var(&tagValues, "tagValue", "Tag value (can add multiple)")
|
||||
locationConstraint = flag.String("locationConstraint", "", "Location constraint for bucket creation")
|
||||
flag.Var(&corsRules, "corsRule", "CORS rule for PutBucketCORS command (can add multiple)")
|
||||
// Parse the flags
|
||||
flag.Parse()
|
||||
|
||||
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Copyright 2026 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.
|
||||
|
||||
load ./bats-support/load
|
||||
load ./bats-assert/load
|
||||
|
||||
source ./tests/drivers/file.sh
|
||||
source ./tests/drivers/params.sh
|
||||
source ./tests/drivers/rest.sh
|
||||
source ./tests/logger.sh
|
||||
source ./tests/setup_unit.sh
|
||||
|
||||
@test "check_key_and_value_pair_for_match" {
|
||||
run check_key_and_value_pair_for_match "one" "two" "three"
|
||||
assert_failure 2
|
||||
|
||||
run check_key_and_value_pair_for_match "one" "two" "one" "three"
|
||||
assert_failure 2
|
||||
|
||||
run check_key_and_value_pair_for_match "one" "two" "three" "four"
|
||||
assert_failure 1
|
||||
|
||||
run check_key_and_value_pair_for_match "one" "two two" "one" "two two"
|
||||
assert_success
|
||||
|
||||
run check_key_and_value_pair_for_match "Content-Type" "application/xml" "content-type" "application/xml"
|
||||
assert_success
|
||||
|
||||
run check_key_and_value_pair_for_match "one" "two" "one" "two"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "check_for_key_and_value_within_pairs" {
|
||||
# match in middle, omit it
|
||||
run check_for_key_and_value_within_pairs "B" "2" "A" "1" "B" "2" "C" "3"
|
||||
assert_success
|
||||
assert_output $'A\n1\nC\n3'
|
||||
|
||||
# match first, omit it
|
||||
run check_for_key_and_value_within_pairs "A" "1" "A" "1" "B" "2" "C" "3"
|
||||
assert_success
|
||||
assert_output $'B\n2\nC\n3'
|
||||
|
||||
# match last, omit it
|
||||
run check_for_key_and_value_within_pairs "C" "3" "A" "1" "B" "2" "C" "3"
|
||||
assert_success
|
||||
assert_output $'A\n1\nB\n2'
|
||||
|
||||
# no match should fail
|
||||
run check_for_key_and_value_within_pairs "D" "9" "A" "1" "B" "2" "C" "3"
|
||||
assert_failure 1
|
||||
|
||||
# key match but value mismatch should fail
|
||||
run check_for_key_and_value_within_pairs "B" "999" "A" "1" "B" "2" "C" "3"
|
||||
assert_failure 2
|
||||
|
||||
# duplicate exact pairs; omit only the last matching pair
|
||||
run check_for_key_and_value_within_pairs "A" "2" "A" "2" "A" "2" "B" "3"
|
||||
assert_success
|
||||
assert_output $'A\n2\nB\n3'
|
||||
|
||||
# odd number of pair args should fail
|
||||
run check_for_key_and_value_within_pairs "A" "1" "A" "1" "B"
|
||||
assert_failure 2
|
||||
}
|
||||
|
||||
@test "check_header_keys_and_values" {
|
||||
run get_file_name
|
||||
assert_success
|
||||
resp_file="$TEST_FILE_FOLDER/$output"
|
||||
|
||||
# 1) Exact match, single header
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nContent-Type: application/xml\r\n\r\n<body/>' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Type" "application/xml"
|
||||
assert_success
|
||||
|
||||
# 2) Header value contains spaces
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nContent-Disposition: attachment; filename=\"a b.txt\"\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Disposition" 'attachment; filename="a b.txt"'
|
||||
assert_success
|
||||
|
||||
# 3) Empty header value allowed
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nX-Empty:\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "X-Empty" ""
|
||||
assert_success
|
||||
|
||||
# 4) Missing expected header should fail
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nContent-Type: application/xml\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "ETag" '"abc"'
|
||||
assert_failure
|
||||
|
||||
# 5) Value mismatch should fail
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Type" "application/xml"
|
||||
assert_failure
|
||||
|
||||
# 6) Multiple expected headers, any order
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nETag: \"abc\"\r\nContent-Type: application/xml\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Type" "application/xml" "ETag" '"abc"'
|
||||
assert_success
|
||||
|
||||
# 7) Stop parsing at blank line (body contains ':' should not be treated as header)
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\nContent-Type: application/xml\r\n\r\nNotAHeader: still body\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Type" "application/xml"
|
||||
assert_success
|
||||
|
||||
# 8) Key case-insensitivity
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\ncontent-type: application/xml\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Content-Type" "application/xml"
|
||||
assert_success
|
||||
|
||||
# middle header key/value
|
||||
run bash -c "printf 'HTTP/1.1 200 OK\r\ncontent-type: application/xml\r\ndummy-header: dummy-val\r\nanother-dummy-header: another-val\r\n\r\n' > '$resp_file'"
|
||||
assert_success
|
||||
run check_header_keys_and_values "$resp_file" "Dummy-Header" "dummy-val"
|
||||
assert_success
|
||||
}
|
||||
+152
-12
@@ -49,9 +49,6 @@ source ./tests/setup.sh
|
||||
}
|
||||
|
||||
@test "REST - CORS - empty CORS rule" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1863"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
@@ -65,9 +62,6 @@ source ./tests/setup.sh
|
||||
}
|
||||
|
||||
@test "REST - CORS - missing allowed origin" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1863"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
@@ -81,9 +75,6 @@ source ./tests/setup.sh
|
||||
}
|
||||
|
||||
@test "REST - CORS - missing allowed method" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1863"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
@@ -97,9 +88,6 @@ source ./tests/setup.sh
|
||||
}
|
||||
|
||||
@test "REST - CORS - empty allowed method" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1863"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
@@ -145,3 +133,155 @@ source ./tests/setup.sh
|
||||
run send_rest_go_command_expect_error "404" "NoSuchCORSConfiguration" "does not exist" "-query" "cors" "-bucketName" "$bucket_name"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - origin - ETag not returned in exposed headers" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1893"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedMethods=GET,PUT;allowedOrigins=http://example.com" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values_not_present" "-bucketName" "$bucket_name" "-signedParams" "origin:http://example.com" "--" "access-control-expose-headers" "ETag"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - non-allowed method" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET,POST" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values_not_present" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-signedParams" "origin:http://example.com" "-unsignedParams" "Access-Control-Request-Methods:GET,PUT" "--" "access-control-allow-methods" "GET"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - allowed method" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET,POST" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-signedParams" "origin:http://example.com" "-unsignedParams" "Access-Control-Request-Methods:GET,POST" "--" \
|
||||
"access-control-allow-methods" "GET, POST" "access-control-allow-origin" "http://example.com"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - non-allowed header" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET;allowedHeaders=dummy-header" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values_not_present" "-bucketName" "$bucket_name" "-unsignedParams" "Access-Control-Request-Headers:dummy-headers" "--" "access-control-expose-headers" "dummy-header"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - allowed headers" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET;allowedHeaders=dummy-header-one,dummy-header-two" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-unsignedParams" "origin:http://example.com,Access-Control-Request-Headers:dummy-header-one" "--" \
|
||||
"access-control-allow-headers" "dummy-header-one"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-unsignedParams" "origin:http://example.com;Access-Control-Request-Headers:dummy-header-one,dummy-header-two" "--" \
|
||||
"access-control-allow-headers" "dummy-header-one, dummy-header-two"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - invalid max age seconds" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_expect_error "400" "MalformedXML" "did not validate" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET;maxAgeSeconds=a" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - valid max age seconds" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET;maxAgeSeconds=3000" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-unsignedParams" "origin:http://example.com" "--" \
|
||||
"access-control-max-age" "3000"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - all fields" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1893"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET;allowedHeaders=dummy-header;exposedHeaders=dummy-header-two;maxAgeSeconds=60" \
|
||||
"-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-unsignedParams" "origin:http://example.com,access-control-request-methods:GET,access-control-request-headers:dummy-header" \
|
||||
"--" "access-control-allow-origin" "http://example.com" "access-control-allow-methods" "GET" "access-control-allow-headers" "dummy-header" "access-control-expose-headers" "dummy-header-two" "access-control-max-age" "60"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - CORS - two rules" {
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command "200" "-commandType" "putBucketCORS" "-paramSeparator" ";" "-corsRule" "allowedOrigins=http://example.com;allowedMethods=GET,POST" "-corsRule" "allowedOrigins=http://exampletwo.com;allowedMethods=GET,PUT" "-bucketName" "$bucket_name" "-contentMD5"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-unsignedParams" "origin:http://example.com;access-control-request-methods:GET,POST" \
|
||||
"--" "access-control-allow-origin" "http://example.com" "access-control-allow-methods" "GET, POST"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_callback "200" "check_header_keys_and_values" "-bucketName" "$bucket_name" "-paramSeparator" ";" "-unsignedParams" "origin:http://exampletwo.com;access-control-request-methods:GET,PUT" \
|
||||
"--" "access-control-allow-origin" "http://exampletwo.com" "access-control-allow-methods" "GET, PUT"
|
||||
assert_success
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user