Quantcast
Channel: Active questions tagged python - Stack Overflow
Viewing all articles
Browse latest Browse all 13981

How to set Content-Type header with boto3 presigned url multipart upload

$
0
0

Is there any way to allow Content-Type header with multipart uploads to presigned s3 url?

Let's begin with the following code:

import boto3import requestsBUCKET_NAME = "foo"# No, it's global in this MRE onlyclient = boto3.client('s3')def create(key):    response = client.create_multipart_upload(Bucket=BUCKET_NAME, Key=key)    return response['UploadId']def get_url(key, upload_id, chunk_number):    signed_url = client.generate_presigned_url(        ClientMethod='upload_part',        Params={"Bucket": BUCKET_NAME,"Key": key,"PartNumber": chunk_number,"UploadId": upload_id,            # "ContentType": "application/x-www-form-urlencoded",        },        ExpiresIn=60 * 60,  # seconds    )    return signed_urldef complete(key, upload_id, parts):    client.complete_multipart_upload(        Bucket=BUCKET_NAME,        Key=key,        UploadId=upload_id,        MultipartUpload={"Parts": parts}    )def test_upload():    key = 'data/foo.bar'    upload_id = create(key)    url = get_url(key, upload_id, 1)    with open("/tmp/foo.bar", "rb") as src:        response = requests.put(url, data=src.read())    etag = response.headers["ETag"]    complete(key, upload_id, {"ETag": etag, "PartNumber": 1})

Hooray, this works. However, let's try to do the same from frontend, replacing requests call with

fetch(uploadTo, {    method: 'PUT',    body: blob,})

(no matter how blob is defined here, this is irrelevant to our problem).

And this fails, returning 403 SignatureDoesNotMatch. Why? Because Content-Type header is set (and fetchcannot do without it), and this is part of S3-side backend signature verification. Content-Type is not a part of generated URL, so with any content type fetch tries to set this will not match. I know this is the case, because here's what response looks like (with only URL being different, ignore this incompatibilities, uploadId=1 is just a fake - same thing happens with real URL; pay attention to StringToSign tag):

<?xml version="1.0" encoding="UTF-8"?><Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you    provided. Check your key and signing method.</Message><AWSAccessKeyId>...</AWSAccessKeyId><StringToSign>PUT    application/x-www-form-urlencoded    1684064749    x-amz-security-token:FwoGZXIvYXdzEOT//////////wEaDE+2gqVw4NTt1c2eOCKGAVTf4uDCD+GJ8P6lG2vBg8yQ2dqyU7/6aHg4hXMljyDFByT7hJ1/F/GPwBi84eAMDZqGzXpIySe8PhU80ak5C4vg7vcGOOSaB3cXk7TtQ2q0pWb8MB0AYb3LGAJ6sahySjHSdArFFADB60u6SskWhq9HHSijilW9hKIiUgdceZAPhLH1J59oKITngqMGMijigFpTERPtZLB+MOjIqJIpHvPJrrfRg4mzwAmZbk+rropyYha4rBNP    /sim-cal-bucket/temp/foo.mp4?partNumber=1&amp;uploadId=1</StringToSign><SignatureProvided>miwrnxtxoPdGnuAEqiP52ZMscBQ=</SignatureProvided><StringToSignBytes>50 55 54 0a 0a 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 2d    77 77 77 2d 66 6f 72 6d 2d 75 72 6c 65 6e 63 6f 64 65 64 0a 31 36 38 34 30    36 34 37 34 39 0a 78 2d 61 6d 7a 2d 73 65 63 75 72 69 74 79 2d 74 6f 6b 65    6e 3a 46 77 6f 47 5a 58 49 76 59 58 64 7a 45 4f 54 2f 2f 2f 2f 2f 2f 2f 2f    2f 2f 77 45 61 44 45 2b 32 67 71 56 77 34 4e 54 74 31 63 32 65 4f 43 4b 47    41 56 54 66 34 75 44 43 44 2b 47 4a 38 50 36 6c 47 32 76 42 67 38 79 51 32    64 71 79 55 37 2f 36 61 48 67 34 68 58 4d 6c 6a 79 44 46 42 79 54 37 68 4a    31 2f 46 2f 47 50 77 42 69 38 34 65 41 4d 44 5a 71 47 7a 58 70 49 79 53 65    38 50 68 55 38 30 61 6b 35 43 34 76 67 37 76 63 47 4f 4f 53 61 42 33 63 58    6b 37 54 74 51 32 71 30 70 57 62 38 4d 42 30 41 59 62 33 4c 47 41 4a 36 73    61 68 79 53 6a 48 53 64 41 72 46 46 41 44 42 36 30 75 36 53 73 6b 57 68 71    39 48 48 53 69 6a 69 6c 57 39 68 4b 49 69 55 67 64 63 65 5a 41 50 68 4c 48    31 4a 35 39 6f 4b 49 54 6e 67 71 4d 47 4d 69 6a 69 67 46 70 54 45 52 50 74    5a 4c 42 2b 4d 4f 6a 49 71 4a 49 70 48 76 50 4a 72 72 66 52 67 34 6d 7a 77    41 6d 5a 62 6b 2b 72 72 6f 70 79 59 68 61 34 72 42 4e 50 0a 2f 73 69 6d 2d    63 61 6c 2d 62 75 63 6b 65 74 2f 74 65 6d 70 2f 66 6f 6f 2e 6d 70 34 3f 70    61 72 74 4e 75 6d 62 65 72 3d 31 26 75 70 6c 6f 61 64 49 64 3d 31</StringToSignBytes><RequestId>073V6QJXMA0XAKWS</RequestId><HostId>1pR1Pz4RSnRilgjUbb0AVDcMWiqCq05dMrAVU+0t4a0HF5ytfXmNiIecxH80urVoiKtxtHhxS2o=</HostId></Error>

So, we need to pass a Content-Type to signed url somehow. Neither generate_signed_url nor its Params (that must match params of upload_part) accept ContentType option. This looks like a dead end... To double confirm, here's what works in JS - ContentType is passed to the signer.

Well, for now I'm just monkeypatching botocore to allow passing ContentType parameter to upload_part (cloned botocore/data/s3/2006-03-01/service-2.json and added this parameter to UploadPartRequest definition, patching this file in venv in Dockerfile), but it's certainly not what I want. However, this confirms that I really need to pass ContentType, and no other solution can allow setting this header. After uncommenting ContentType key in the sample above, everything is fine.

Just to compare, below are urls without and with content type - this arg is included in url directly. The latter URL works with frontend fetch flawlessly.

https://sim-cal-bucket.s3.amazonaws.com/temp/foo.mp4?partNumber=1&uploadId=1&AWSAccessKeyId=...&Signature=miwrnxtxoPdGnuAEqiP52ZMscBQ%3D&x-amz-security-token=FwoGZXIvYXdzEOT%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDE%2B2gqVw4NTt1c2eOCKGAVTf4uDCD%2BGJ8P6lG2vBg8yQ2dqyU7%2F6aHg4hXMljyDFByT7hJ1%2FF%2FGPwBi84eAMDZqGzXpIySe8PhU80ak5C4vg7vcGOOSaB3cXk7TtQ2q0pWb8MB0AYb3LGAJ6sahySjHSdArFFADB60u6SskWhq9HHSijilW9hKIiUgdceZAPhLH1J59oKITngqMGMijigFpTERPtZLB%2BMOjIqJIpHvPJrrfRg4mzwAmZbk%2BrropyYha4rBNP&Expires=1684064749https://sim-cal-bucket.s3.amazonaws.com/temp/foo.mp4?partNumber=1&uploadId=1&AWSAccessKeyId=...&Signature=1FeHhXi7QRtL0wCT7kJ%2BVEcBeso%3D&content-type=application%2Fx-www-form-urlencoded&x-amz-security-token=FwoGZXIvYXdzEOT%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDE%2B2gqVw4NTt1c2eOCKGAVTf4uDCD%2BGJ8P6lG2vBg8yQ2dqyU7%2F6aHg4hXMljyDFByT7hJ1%2FF%2FGPwBi84eAMDZqGzXpIySe8PhU80ak5C4vg7vcGOOSaB3cXk7TtQ2q0pWb8MB0AYb3LGAJ6sahySjHSdArFFADB60u6SskWhq9HHSijilW9hKIiUgdceZAPhLH1J59oKITngqMGMijigFpTERPtZLB%2BMOjIqJIpHvPJrrfRg4mzwAmZbk%2BrropyYha4rBNP&Expires=1684065651

Solutions suggesting to make a request without Content-Type are unacceptable, because this is a part of public API, and I do not want to make customers jump through hoops trying to send such request.


Viewing all articles
Browse latest Browse all 13981

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>