AWS CORS allows your users to upload files directly to an AWS bucket securely. There are many benefits to storing files on S3 rather than on your local web server storage, such as life-cycle management, cost and allowing horizontal scaling. If you already upload users files to S3 then CORS removes the need for your website server to be the proxy server which will reduce load and bandwidth.

Step 1: Create a new bucket
Go to the S3 console and ‘Create Bucket’. As the names for buckets are global, it can take a little while to find one that is sensible and short. For this example we are using bucket-upload but substitute that for yours.

Step 2: Associate CORS Policy
While you are in the bucket page, it is best to add a CORS policy before you forget. Click into your new bucket and then view the PropertiesPermissionsEdit CORS Configuration. You need to add a new policy there, this is the one I used, though you can be as restrictive or open as you like.

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Step 3: Create a new user
Whilst in the AWS Console, go to the Identity and Access Management (IAM) page. You can use an already created user credentials, but for security I would recommend creating a fresh one just for this purpose. Once you have created a user, make sure to generate an access key for them, you will need these later. Finally, you need to give the user access to upload to the bucket so select your user and choose Attach User Policy. You can just use the one I have below, remember to change it to your bucket name created in Step 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "Statement": [{
        "Action": [
            "s3:GetObject",
            "s3:PutObject",
            "s3:PutObjectAcl"
        ],
        "Effect": "Allow",
        "Resource": [
            "arn:aws:s3:::bucket-upload/*"
        ]
    }]
}

Step 4: Create your policy document
When you send a request to upload a file, you also need to send a JSON document about the incoming file. There is some good documentation on the AWS website about what you can include. Again this is up to how secure and restrictive you want to be, I would recommend the content-length option to prevent people uploading gigs worth of data. You need to put this in the MyPolicy field below in the next step, or use the one I have prepared earlier.

Step 5: Create a signed request
Now we are done with AWS, we can jump into our IDE and start coding. Below is a class I created that will calculate the base64 policy created in the previous step and the signature from your secret key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CorsUpload {
  public string Folder { get; set; }
  private string AwsAccessKeyID { get { return "ACCESS KEY"; } }
  private string AwsSecretKey { get { return "SECRET KEY"; } }
  private string Policy { get { return Convert.ToBase64String(Encoding.ASCII.GetBytes((MyPolicy))); } }
  private string Signature { get { return GetS3Signature(myPolicy); } }
  private static string MyPolicy {
   get {
    return "{\"expiration\": \"2020-01-01T00:00:00Z\", " +
        "\"conditions\": [ " +
        "{\"bucket\": \"bucket-upload\"}, " +
        "[\"starts-with\", \"$key\", \"news/\"]," +
        "{\"acl\": \"public-read\"}," +
        "[\"starts-with\", \"$Content-Type\", \"\"]" +
        "]}";
   }
  }
 
  private string GetS3Signature(string policyStr) {
   var b64Policy = Convert.ToBase64String(Encoding.ASCII.GetBytes(policyStr));
   var b64Key = Encoding.ASCII.GetBytes(AwsSecretKey);
   var hmacSha1 = new HMACSHA1(b64Key);
   return Convert.ToBase64String(hmacSha1.ComputeHash(Encoding.ASCII.GetBytes(b64Policy)));
  }
 
  public new string ToString() {
   var build = new StringBuilder();
   build.Append("<script type='text/javascript'>");
   build.AppendFormat("var access_key = '{0}';", AwsAccessKeyID);
   build.AppendFormat("var policy = '{0}';", Policy);
   build.AppendFormat("var signature = '{0}';", Signature);
   build.AppendFormat("var folder = '{0}';", Folder);
   build.Append("</script>");
   return build.ToString();
  }
}

Step 6: HTML time
Probably the easiest step by far, just use a normal HTML form with a file input. Make note that the button executes an uploadFile() method that we’ll create in the next step.

1
2
3
4
5
6
<form id="corsForm" enctype="multipart/form-data" method="post">
  <label for="file">Select a File to Upload</label><br />
  <input type="file" name="file" id="file"/>
  <input type="button" onclick="uploadFile()" value="Upload" />
  <div id="progressbar"></div>
 </form>

Step 7: JavaScript magic

You will need the variables from the CorsUpload class we created before, so you can include them into your page easily like this.

<%= new CorsUpload {Folder = "somefolder"}.ToString()%>

Then include a new .js script file with the contents below. The only thing you should change is the bucket name, but you can easily abstract that out and include in the CorsUpload script if you want to ‘genericise’ it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var key;
function uploadFile() {
 var file = document.getElementById('file').files[0];
 var fd = new FormData();
 key = folder + "/" + (new Date).getTime() + '-' + file.name;
 fd.append('key', key);
 fd.append('acl', 'public-read');
 fd.append('Content-Type', file.type);
 fd.append('AWSAccessKeyId', access_key);
 fd.append('policy', policy);
 fd.append('signature', signature);
 fd.append("file", file);
 
 var xhr = new XMLHttpRequest();
 xhr.upload.addEventListener("progress", uploadProgress, false);
 xhr.addEventListener("load", uploadComplete, false);
 xhr.addEventListener("error", uploadFailed, false);
 xhr.addEventListener("abort", uploadCanceled, false);
 
 //MUST BE LAST LINE BEFORE YOU SEND 
 xhr.open('POST', 'https://bucket-upload.s3.amazonaws.com/', true);
 xhr.send(fd);
}

And that’s it! Here are some of the helper methods referenced above. I have made the page just redirect to the uploaded image (will need to change the base URL if you aren’t using the Sydney region) but obviously change that to whatever you need. I ended up including it in a WYSIWYG editor and an email attachment so there isn’t any limit to it’s uses.

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function uploadProgress(evt) {
 if (evt.lengthComputable) {
  $("#progressbar").progressbar({
   value: Math.round(evt.loaded * 100 / evt.total)
  });
 } else {
  $("#progressbar").progressbar("option", "value", false);
 }
}
 
function uploadComplete(evt) {
 /* This event is raised when the server send back a response */
 if (evt.target.status == 204) {
  window.location = 'https://s3-ap-southeast-2.amazonaws.com/bucket-upload/' + key;
 } else {
  uploadFailed(evt);
 }
}
 
function uploadFailed(evt) {
 alert("There was an error attempting to upload the file." + evt);
}
 
function uploadCanceled(evt) {
 alert("The upload has been canceled by the user or the browser dropped the connection.");
}

That uses jQuery UI to create the pretty progress bar, but you can write it straight out as a % if you don’t have that library imported.

27
document.getElementById('progressbar').innerHTML = Math.round(evt.loaded*100 / evt.total) + '%';

Leave a Reply


%d bloggers like this: