jQuery file upload to S3 (and rails) with CORS headers

1k views Asked by At

Im trying to upload a file directly to S3, and displaying a progress bar while it uploads. But when I submit the form, I get the following error message:

OPTIONS https://s3-eu-west-1.amazonaws.com/my-bucket 403 (Forbidden) 9:1 XMLHttpRequest cannot load

https://s3-eu-west-1.amazonaws.com/my-bucket. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://localhost:3000' is therefore not allowed access. The response had HTTP status code 403. 9:223 fail

This is my form:

%form#file_upload(action=@aws_s3_url method="post" enctype="multipart/form-data")
  -# order is important!
  -# also, the things that are not filled in right now *will* be filled in soon.  See below.
  %input{:type => :hidden, :name => :key}
  %input{:type => :hidden, :name => "AWSAccessKeyId", :value => "ACCESS_KEY"}
  %input{:type => :hidden, :name => :acl,  :value => :private}
  %input{:type => :hidden, :name => :success_action_redirect}
  %input{:type => :hidden, :name => :policy}
  %input{:type => :hidden, :name => :signature}

  .fileupload-content
    .fileupload-progress
  .file-upload
    %label.fileinput-button
      %span Upload Document
      %input{:type => :file, :name => :file}

And this is my javascript code, using the jquery-file-upload plugin:

$(function() {
  $('#file_upload').fileupload({
    //forceIframeTransport: true,    // VERY IMPORTANT.  you will get 405 Method Not Allowed if you don't add this.
    autoUpload: true,
    type: 'POST',
    dataType: 'xml',
    url: $(this).attr('action'),
    add: function (event, data) {
      $.ajax({
        url: "/projects/9/create_file",
        type: 'GET',
        dataType: 'json',
        data: {doc: {title: data.files[0].name}},
        async: false,
        success: function(retdata) {
          // after we created our document in rails, it is going to send back JSON of they key,
          // policy, and signature.  We will put these into our form before it gets submitted to amazon.
          $('#file_upload').find('input[name=key]').val(retdata.key);
          $('#file_upload').find('input[name=policy]').val(retdata.policy);
          $('#file_upload').find('input[name=signature]').val(retdata.signature);
          $('#file_upload').find('input[name=success_action_redirect]').val(retdata.success_action_redirect);
        }

      });

      console.log(data)
      file = data.files[0]
      data.context = $(tmpl("template-upload", file))
      $('#file_upload').append(data.context)

      data.submit();
    },
    progress: function (event, data) {
      progress = parseInt(data.loaded / data.total * 100, 10)
      console.log("Progress")
      data.context.find('.bar').css('width', progress + '%')
    },
    send: function(e, data) {
      // show a loading spinner because now the form will be submitted to amazon,
      // and the file will be directly uploaded there, via an iframe in the background.
      //$('#loading').show();
      console.log("Loading")
    },
    fail: function(e, data) {
      console.log('fail');
      console.log(data);
    },
    done: function (event, data) {
      // here you can perform an ajax call to get your documents to display on the screen.
      $('#your_documents').load("/documents?for_item=1234");

      // hide the loading spinner that we turned on earlier.
      $('#loading').hide();
    },
  });
});

I know the code works because when I uncoment forceIframeTransport it processes the request. The problem is that using that method you only get a progress event once (when is completed), so it defeats the purpose of having a progress bar. I read that to do without the forceIframeTransport, you have to set the CORS headers, which I do in rails like this:

before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers

#, DELETE, OPTIONS
#DELETE, OPTIONS

def cors_set_access_control_headers
  headers['Access-Control-Allow-Origin'] = 'https://0.0.0.0:3000/*'
  headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT'
  headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token'
  headers['Access-Control-Max-Age'] = '3000'
end

def cors_preflight_check
  if request.method == 'OPTIONS'
    headers['Access-Control-Allow-Origin'] = 'https://0.0.0.0:3000/*'
    headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT'
    headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Token'
    headers['Access-Control-Max-Age'] = '3000'

    render :text => '', :content_type => 'text/plain'
  end
end

And they are indeed sent, because when I manually do the GET "/projects/9/create_file" (With the advance rest plugin for example) i get the following response headers:

Access-Control-Allow-Origin: https://0.0.0.0:3000/*

Access-Control-Allow-Methods: POST, GET, PUT

Access-Control-Allow-Headers: Origin, Content-Type, Accept,

Authorization, Token Access-Control-Max-Age: 3000

Also, this is my CORS configuration in amazon:

<CORSRule>
    <AllowedOrigin>https://0.0.0.0:3000/*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>

Any ideas about what could be wrong?

1

There are 1 answers

4
max On BEST ANSWER

To allow any subpath on https://example.com you would do:

<CORSRule>
    <AllowedOrigin>https://example.com</AllowedOrigin>
</CORSRule>

The origin header field can either be a wildcard (*) or a url containing one or no wildcard. Ex:

https://*.example.com

However adding the wildcard on the end https://0.0.0.0:3000/* will not allow any path as one could guess.