0

I'm trying to create a signed URL of a file that can let the user download file from the S3 bucket. I'm following this way written in docs. The generated signature is working perfectly and it gets to download the file as well when a user visits the URL. The problem is that I want the name of the downloaded file to be custom. For example: FileKey: tg6AbybBgFMidmKy5dz4vVXZ FileName: test.xlsx

The file is saved on S3 with the key and without the file extension.

So, when a user downloads the file, it's extensionless and hence unrecognized by OS. I want it to be downloaded with the FileName. I tried adding response-content-disposition in the query but it throws the error:

enter image description here

Below is the code I'm using right now.

    # Create a date for headers and the credential string
    time = datetime.datetime.utcnow()
    amz_date = time.strftime('%Y%m%dT%H%M%SZ')
    datestamp = time.strftime('%Y%m%d') # Date w/o time, used in credential scope

    # **CREATE A CANONICAL REQUEST**
    canonical_uri = '/' + document_key
    canonical_headers = 'host:' + bucket_url + '\n'

    algorithm = 'AWS4-HMAC-SHA256'
    credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request'

    # Create the canonical query string in URL-encoded (space=%20).
    canonical_querystring = 'response-content-disposition=attachment; filename="' + document_name + '"; filename*=UTF-8\'\'' + document_name
    canonical_querystring += '&response-content-type=' + document_type
    canonical_querystring += '&X-Amz-Algorithm=AWS4-HMAC-SHA256'
    canonical_querystring += '&X-Amz-Credential=' + quote_plus(access_key + '/' + credential_scope, safe='')
    canonical_querystring += '&X-Amz-Date=' + amz_date
    canonical_querystring += '&X-Amz-Expires=' + url_expiry
    canonical_querystring += '&X-Amz-SignedHeaders=' + signed_headers

    canonical_request = request_method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers.lower() + '\n' + signed_headers.lower()

    # CREATE THE STRING TO SIGN
    string_to_sign = algorithm + '\n' +  amz_date + '\n' +  credential_scope + '\n' +  hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
    
    # CALCULATE THE SIGNATURE
    signing_key = getSignatureKey(secret_key, datestamp, region, service)
    signature = hmac.new(signing_key, (string_to_sign).encode("utf-8"), hashlib.sha256).hexdigest()

    # ADD SIGNING INFORMATION TO THE REQUEST
    canonical_querystring += '&X-Amz-Signature=' + signature
    
    # Merge for downloadable URL
    download_url = 'https://' + bucket_url + canonical_uri +'?' + canonical_querystring

I'm not using AWS SDK. I'm trying to do it on my own!

EDIT: Even after adding & which was missing, I was getting a signature mismatch.

3
  • Check my answer, i have done it using php stackoverflow.com/questions/46862880/… you can use this algo in python Commented Dec 23, 2020 at 7:47
  • @UmairHamid, Is that working for custom file name? Commented Dec 24, 2020 at 7:51
  • this will gets the file you have to rename that file using server-side logic or client-side logic. I did it using server-side logic Commented Dec 29, 2020 at 9:14

1 Answer 1

1

The issue was with encoding and I missed a & in canonical_querystring = 'X-Amz-Algorithm! It killed my 3 days looking around all forums and trying different things! Here's the working piece of code!


  # Create a date for headers and the credential string
    time = datetime.datetime.utcnow()
    amz_date = time.strftime('%Y%m%dT%H%M%SZ')
    datestamp = time.strftime('%Y%m%d') # Date w/o time, used in credential scope

    # **CREATE A CANONICAL REQUEST**
    canonical_uri = '/' + document_key
    canonical_headers = 'host:' + bucket_url + '\n'

    algorithm = 'AWS4-HMAC-SHA256'
    credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request'

    # Create the canonical query string in URL-encoded (space=%20).
    canonical_querystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256'
    canonical_querystring += '&X-Amz-Credential=' + quote_plus(access_key + '/' + credential_scope, safe='')
    canonical_querystring += '&X-Amz-Date=' + amz_date
    canonical_querystring += '&X-Amz-Expires=' + url_expiry
    canonical_querystring += '&X-Amz-SignedHeaders=' + signed_headers
    canonical_querystring += '&response-content-disposition=' + quote('attachment; filename=\"' + document_name + '\"; filename*=UTF-8\'\'' + document_name, safe='')
    canonical_querystring += '&response-content-type=' + quote(document_type, safe='')

    canonical_request = request_method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers.lower() + '\n' + signed_headers.lower() + '\n' + 'UNSIGNED-PAYLOAD'

    # CREATE THE STRING TO SIGN
    string_to_sign = algorithm + '\n' +  amz_date + '\n' +  credential_scope + '\n' +  hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()

    # CALCULATE THE SIGNATURE
    signing_key = getSignatureKey(secret_key, datestamp, region, service)
    signature = hmac.new(signing_key, (string_to_sign).encode("utf-8"), hashlib.sha256).hexdigest()

    # ADD SIGNING INFORMATION TO THE REQUEST
    canonical_querystring += '&X-Amz-Signature=' + signature
    
    # Merge for downloadable URL
    download_url = 'https://' + bucket_url + canonical_uri +'?' + canonical_querystring

I was using

quote_plus('attachment; filename=\"' + document_name + '\"; filename*=UTF-8\'\'' + document_name,)

instead of which I had to use

quote('attachment; filename=\"' + document_name + '\"; filename*=UTF-8\'\'' + document_name, safe='')

Using this method/logic you can generate link of S3 Object with custom filename without using aws-sdk

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.