CORS POST to AWS S3 using PHP and AJAX

I’m going to post this here in case it helps someone down the line.  I spent days trying to get this to work including countless hours with AWS (Amazon Web Services) support, who go above and beyond to try to help.  Note: I modified the code a bit from my working, live example, so the code below is untested.  There may be a few minor bugs that I will let you work out :-)  And, sorry for the lack of formatting on the code below.

The problem is to upload a file from an HTML form on a website and have it go directly to an AWS S3 Bucket.  Sounds easy, right?  Here are the issues:

  1. The POST upload will work directly, however when we put the request in AJAX we run into issues with the denial of Cross Origin Requests Sharing (CORS).  This means a request from one domain is not accepted by a different domain … unless it is approved on the receiving domain’s server configuration.
  2. The request requires a policy and a signature that need to be generated on the requesting server.  The signature includes information (filename) of the file being uploaded, so it needs to be generated AFTER the file is selected.  Unless the filename is set in advance.
  3. The secret access key needs to be secure so the public may not see it.
  4. The AJAX upload POST request needs to be in a particular format.

The solutions:

  1. A few years ago AWS began to allow CORS on the S3 Buckets.  THis needs to be configured.  I’ll show you how to do in in a bit.
  2. I found many examples how to do this with Rails, but not with PHP.  The policy and signature may be done with AJAX requests to PHP scripts.  I’ll show you how i did this in a bit.
  3. The secret access key may be hidden in the PHP scripts as mentioned above.
  4. I’ll show you this.  I found it in this StackOverflow question here: http://stackoverflow.com/questions/11240127/uploading-image-to-amazon-s3-with-html-javascript-jquery-with-ajax-request-n.  I modified it a bit to work with PHP.

Here is the code below:

Enabling CORS on your S3 Bucket

Log into your AWS account and browse over to S3.  Select the bucket that you wish you upload to and click on “Properties.”  Open the “Permissions” area.  Click on “Edit CORS Configuration.”  There you will see the CORS Configuration.  Change it to the code below substututing [YOUR_FULL_DOMAIN] for your full domain name.  This si the domain from where the POST request will originate.  You may use one wildcard (ex: http://*.yourdomain.com).  You may also just use a straight wildcard here * to open it up to all origins.  This is helpful for testing and debugging.

<?xml version=”1.0″ encoding=”UTF-8″?>
<CORSConfiguration xmlns=”http://s3.amazonaws.com/doc/2006-03-01/”>
<CORSRule>
<AllowedOrigin>[YOUR_FULL_DOMAIN]</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

PHP Scripts to Generate Policy and Signature

I use two different scripts.  Each will be called with an AJAX request.  These will be separate PHP files on your server:

PHP to generate the policy.  (I saved this as filename:  php/s3_upload_create_policy.php):

<?php

$key = $_GET[‘key’]; // This is the filename as it will be uploaded to S3
$type = $_GET[‘type’]; // TThis is the file type

date_default_timezone_set(‘America/Los_Angeles’); // Update to your time zone
$now = strtotime(date(“Y-m-d\TG:i:s”));
$expire = date(‘Y-m-d\TG:i:s\Z’, strtotime(‘+ 10 hours’, $now)); // This sets the policy expiration time. I select 10 hours from now, but it can be much less. Keep in mind time zone differences from your instance time to the time of the AWS region that your bucket is located.

$aws_access_key_id = ‘[YOUR_ACCESS_KEY_ID]’; // Substitute for your access key ID
$bucket = ‘[YOUR_S3_BUCKET]’; // Substiture for your S3 Bucket name (ex: my_aws_bucket)

$acl = ‘public-read’; // if you prefer you can use ‘private’
$url = ‘http://’.$bucket.’.s3.amazonaws.com’;

$policy_document=’
{“expiration”: “‘.$expire.'”,
“conditions”: [
{“bucket”: “‘.$bucket.'”},
[“starts-with”, “$key”, “‘.$key.'”],
{“acl”: “‘.$acl.'”},
[“starts-with”, “$Content-Type”, “”]
]
}’;

// create policy
$policy = base64_encode($policy_document);

echo $policy;

?>

PHP to generate the signature.  (I saved this as filename:  php/s3_upload_create_signature.php):

<?php
$policy = $_GET[‘policy’];
$aws_secret_key = ‘[AWS_SECRET_ACCESS_KEY]’; // Substitute for your secret access key

function hmacsha1($key,$data)
{
$blocksize=64;
$hashfunc=’sha1′;
if (strlen($key)>$blocksize)
$key=pack(‘H*’, $hashfunc($key));
$key=str_pad($key,$blocksize,chr(0x00));
$ipad=str_repeat(chr(0x36),$blocksize);
$opad=str_repeat(chr(0x5c),$blocksize);
$hmac = pack(
‘H*’,$hashfunc(
($key^$opad).pack(
‘H*’,$hashfunc(
($key^$ipad).$data
))));
return bin2hex($hmac);
}

function hex2b64($str)
{
$raw = ”;
for ($i=0; $i < strlen($str); $i+=2)
{
$raw .= chr(hexdec(substr($str, $i, 2)));
}
return base64_encode($raw);
}

$signature = hex2b64(hmacsha1($aws_secret_key, $policy));
echo $signature;

?>

Page Script

And finally we need the HTML page script.  This includes the javascript and the form.

Here is the javascript.  This is mainly copied from the StackOverflow question mentioned above, with a few minor tweaks including nested AJAX requests to the PHP documents above to generate policy and signature.  You will need JQuery as well.  I’m using 2.1.3:

<script type=”text/javascript” src=”/js/jquery-2.1.3.min.js”></script>
<script type=”text/javascript”>

function uploadFile() {

bucket_name = [YOUR_AWS_BUCKET_NAME]; // Substitue with your bucket name
access_key_id = [ACCESS_KEY_ID]; // Substitue with your Acess Key ID

var file = document.getElementById(‘file’).files[0];
var fd = new FormData();

var key = ‘uploaded/’+file.name;
var type = file.type;

theSource = ‘/php/s3_upload_create_policy.php?key=’+key+’&type=’+type;

var xmlhttp;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
} else {// code for IE6, IE5
xmlhttp=new ActiveXObject(“Microsoft.XMLHTTP”);
}
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
policy = xmlhttp.responseText;

theSource2 = ‘/php/s3_upload_create_signature.php?policy=’+policy;

var xmlhttp2;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp2=new XMLHttpRequest();
} else {// code for IE6, IE5
xmlhttp2=new ActiveXObject(“Microsoft.XMLHTTP”);
}
xmlhttp2.onreadystatechange=function() {
if (xmlhttp2.readyState==4 && xmlhttp2.status==200) {
signature = xmlhttp2.responseText;

fd.append(‘key’, key);
fd.append(‘acl’, ‘public-read’);
fd.append(‘Content-Type’, file.type);
fd.append(‘AWSAccessKeyId’, access_key_id);
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);

xhr.open(‘POST’, ‘https://’+bucket_name+’.s3.amazonaws.com/’, true); //MUST BE LAST LINE BEFORE YOU SEND

xhr.send(fd);

}
}
xmlhttp2.open(“GET”,theSource2,true);
xmlhttp2.send();

}
}
xmlhttp.open(“GET”,theSource,true);
xmlhttp.send();

}

function uploadProgress(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round(evt.loaded * 100 / evt.total);
document.getElementById(‘progressNumber’).innerHTML = percentComplete.toString() + ‘%’;
}
else {
document.getElementById(‘progressNumber’).innerHTML = ‘unable to compute’;
}
}

function uploadComplete(evt) {
/* This event is raised when the server send back a response */
alert(“Done – ” + evt.target.responseText );
}

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.”);
}

</script>

And finally here is the HTML.  This is mainly copied from the StackOverflow question mentioned above:

<form id=”form1″ enctype=”multipart/form-data” method=”post”>
<div class=”row”>
<label for=”file”>Select a File to Upload</label><br />
<input type=”file” name=”file” id=”file”/>
</div>
<div id=”fileName”></div>
<div id=”fileSize”></div>
<div id=”fileType”></div>
<div class=”row”>
<input type=”button” onClick=”uploadFile()” value=”Upload” />
</div>
<div id=”progressNumber”></div>
</form>

The last mystery will be how to get the Access Key ID and Secret Access Key.  This is done in AWS’s IAM service.  I will not go into detail.  You need to set up an IAM user that has access to the S3 Buckets.  I did this a long time ago and do not have the specific steps.

 

Leave a Reply