7

I am creating an API for a backend service with Rails 4. The service needs to upload an image file to an amazon s3 bucket.

I'd like to use a direct upload url, so that the clients manage the uploads to s3 and the server is not kept busy.

Currently I have the following prototypical rails action

def create
  filename = params[:filename]
  s3_direct_post = S3_BUCKET.presigned_post(key: "offers/#{SecureRandom.uuid}/#{filename}", acl: 'public-read')
  s3p = s3_direct_post.fields
  url = "#{s3_direct_post.url}/#{filename}?X-Amz-Algorithm=#{s3p['x-amz-algorithm']}&X-Amz-Credential=#{s3p['x-amz-credential']}&X-Amz-Date=#{s3p['x-amz-date']}&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=#{s3p['x-amz-signature']}"
  render json: {success: true, url: url}, status: :ok
end

This generates such an url:

https://my-bucket.s3.eu-central-1.amazonaws.com/test.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYKEY/20150420/eu-central-1/s3/aws4_request&X-Amz-Date=20150420T162603Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=MYSIGNATURE

Now I try to post the test.png to this url with the following:

curl -v -T test.png "url"

and I get the following error response:

<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>MYKEY</AWSAccessKeyId>...

I believe the problem comes from the fact, that the specified X-Amz-SignedHeaders Header is wrong. I am not sure which headers are used by default from the amazon rails sdk gem.

How should I change my url generation, so that a mobile client can just take the url and post a file to it?

peshkira
  • 6,069
  • 1
  • 33
  • 46

1 Answers1

11

Here is a solution:

In config/initializers/aws.rb:

AWS_CREDS = Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])

Aws.config.update({
  region: 'eu-central-1',
  credentials: AWS_CREDS
})

S3 = Aws::S3::Resource.new('eu-central-1')
S3_BUCKET_NAME = ENV['S3_BUCKET_NAME']
S3_BUCKET = S3.bucket(S3_BUCKET_NAME)

In your model/controller/concern/or whatever:

obj = S3_BUCKET.object("offers/#{user.id}/#{self.id}")
url = obj.presigned_url(:put) # obj.presigned_url(:put, acl: 'public-read') #if you want to make the file public

Then to upload you can use a mobile client or curl:

curl -X PUT -T file_to_upload "url from above"

Note that you will have to add the x-amz-acl: public-read header if you used the public-read acl option:

curl -H "x-amz-acl: public-read" -X PUT -T file_to_upload "url from above"
user664833
  • 18,397
  • 19
  • 91
  • 140
peshkira
  • 6,069
  • 1
  • 33
  • 46
  • 1
    `Ok no one seems to care about this`...with SO that is hardly ever the case :). Needed to access my server-side presigned post credentials from a mobile client and was a bit confused till I came across this. Thanks. – divergent Oct 07 '15 at 20:49
  • 1
    Yep, this is great. Just want to add that though the question was asked regarding aws-sdk version 1, this solution is appropriate for aws-sdk version 2 (note that Aws::S3::Resource.new in this solution uses *Aws*, not *AWS* - version 2 of the sdk uses *Aws*. Docs and examples particularly for version 2 sdk are hard to come by, so thanks for this. – pontiac_ventura Apr 01 '16 at 03:02
  • 1
    I did need to change the variable names from all uppercase to mixed case to avoid a dynamic constant assignment error, however. – pontiac_ventura Apr 01 '16 at 03:14
  • 1
    It was pretty hard to find this info so thank you for sharing. I was able to set the `public-read` permission in the `presigned_url` method and not include it as a header (just as you're sending the other headers as query params). Also there is more info at this url: http://docs.aws.amazon.com/AmazonS3/latest/dev/UploadObjectPreSignedURLRubySDK.html#UploadObjectPreSignedURLRubySDKV2 – Tony Jan 07 '17 at 23:41