Repository: minio/minio (language: go)

Issue:
boto3 `upload_part_copy` allows empty string in `CopySourceRange` param
When using the boto3 client against a minio instance (as documented [here](https://docs.min.io/docs/how-to-use-aws-sdk-for-python-with-minio-server.html)), and trying to perform an object part copy (relevant boto3 [docs](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.upload_part_copy)), the `CopySourceRange` accepts the empty string as valid and equal to not including the header at all. However, against production S3, including the empty string here raises the following error:

```
ClientError('An error occurred (InvalidArgument) when calling the UploadPartCopy operation: The
x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the
zero-based offsets of the first and last bytes to copy')
```

## Expected Behavior
This error is also raised in minio.

## Current Behavior
This error is not raised, and no byte range is used.

## Steps to Reproduce
```python
client = boto3.client(
    's3',
    endpoint_url='http://localhost:9000',
    aws_access_key_id='minioAccessKey',
    aws_secret_access_key='minioSecretKey',
    region_name='us-east-1',
)

upload_id = client.create_multipart_upload(Bucket=dest_bucket,Key=dest_key)['UploadId']
client.upload_part_copy(
    Bucket=dest_bucket,
    Key=dest_key,
    UploadId=upload_id,
    CopySource=f'{source_bucket}/{source_key}',
    CopySourceRange='',
    PartNumber='1',
)
```



Discussion / hints:
If you send empty we just treat is fully Copy @AlmightyYakob 
We could return an error. 
> If you send empty we just treat is fully Copy @AlmightyYakob

Yes, which is contrary to what S3 actually does. This led to me writing code that passes an empty string if full copy is desired, which succeeded locally against minio, but failed against prod S3.

> We could return an error.

I think this is a good idea :+1: 

Fix patch:
diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go
index 484962f14c1f1..1efa2db20eaa4 100644
--- a/cmd/object-handlers.go
+++ b/cmd/object-handlers.go
@@ -2418,6 +2418,13 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
 	var parseRangeErr error
 	if rangeHeader := r.Header.Get(xhttp.AmzCopySourceRange); rangeHeader != "" {
 		rs, parseRangeErr = parseCopyPartRangeSpec(rangeHeader)
+	} else {
+		// This check is to see if client specified a header but the value
+		// is empty for 'x-amz-copy-source-range'
+		_, ok := r.Header[xhttp.AmzCopySourceRange]
+		if ok {
+			parseRangeErr = errInvalidRange
+		}
 	}
 
 	checkCopyPartPrecondFn := func(o ObjectInfo) bool {


Test patch:
diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go
index a7029f49ca8e2..44fbf25447fd8 100644
--- a/cmd/object-handlers_test.go
+++ b/cmd/object-handlers_test.go
@@ -1897,6 +1897,17 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
 			secretKey:          credentials.SecretKey,
 			expectedRespStatus: http.StatusNotFound,
 		},
+		// Test case - 16,  Test case with ivalid byte range empty value.
+		{
+			bucketName:       bucketName,
+			uploadID:         uploadID,
+			copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+			copySourceRange:  "empty",
+			accessKey:        credentials.AccessKey,
+			secretKey:        credentials.SecretKey,
+
+			expectedRespStatus: http.StatusBadRequest,
+		},
 	}
 
 	for i, testCase := range testCases {
@@ -1920,7 +1931,11 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
 			req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
 		}
 		if testCase.copySourceRange != "" {
-			req.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
+			if testCase.copySourceRange == "empty" {
+				req.Header.Set("X-Amz-Copy-Source-Range", "") // specifically test for S3 errors in this scenario.
+			} else {
+				req.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
+			}
 		}
 
 		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.