diff --git a/tests/drivers/get_bucket_cors/get_bucket_cors_rest.sh b/tests/drivers/get_bucket_cors/get_bucket_cors_rest.sh index 9a639734..887b22ba 100644 --- a/tests/drivers/get_bucket_cors/get_bucket_cors_rest.sh +++ b/tests/drivers/get_bucket_cors/get_bucket_cors_rest.sh @@ -86,4 +86,4 @@ get_bucket_cors_check_valid_data() { return 1 fi return 0 -} \ No newline at end of file +} diff --git a/tests/drivers/rest.sh b/tests/drivers/rest.sh index 33b51239..95affcfc 100644 --- a/tests/drivers/rest.sh +++ b/tests/drivers/rest.sh @@ -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 +} diff --git a/tests/rest_scripts/command/putBucketCorsCommand.go b/tests/rest_scripts/command/putBucketCorsCommand.go new file mode 100644 index 00000000..31fd929e --- /dev/null +++ b/tests/rest_scripts/command/putBucketCorsCommand.go @@ -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 = "\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 +} diff --git a/tests/rest_scripts/command/s3Command.go b/tests/rest_scripts/command/s3Command.go index 43858c40..79a2d9df 100644 --- a/tests/rest_scripts/command/s3Command.go +++ b/tests/rest_scripts/command/s3Command.go @@ -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) diff --git a/tests/rest_scripts/generateCommand.go b/tests/rest_scripts/generateCommand.go index 94397f11..65d12dbf 100644 --- a/tests/rest_scripts/generateCommand.go +++ b/tests/rest_scripts/generateCommand.go @@ -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() diff --git a/tests/test_rest.sh b/tests/test_rest.sh new file mode 100755 index 00000000..426b1387 --- /dev/null +++ b/tests/test_rest.sh @@ -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' > '$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 +} diff --git a/tests/test_rest_cors.sh b/tests/test_rest_cors.sh index 00817c7a..2c65349a 100755 --- a/tests/test_rest_cors.sh +++ b/tests/test_rest_cors.sh @@ -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 +}