An issue came up at work the other day that had us looking into possible issues with Flash/Flex communicating and transmitting data to Amazon’s S3 service. While some people started digging into the Flex code, I simply wanted to build a proof of concept that would eliminate the problem having anything to do with Flash.
So I did. And I chose to use the freely available SWFUpload library. Being lazy a developer, I set off to google to find some solutions for me. Really, the only well put together article on this topic that I could locate was here: http://shout.setfive.com/2011/04/21/upload-directly-to-s3-with-swfupload/.
This article was a great jumping off point. But it did not do all the work for me because it was written in PHP (I want this in ColdFusion) and, by all accounts, it’s largely inaccurate. Of course, it’s also likely I’m missing something here (not being a PHP developer). For instance, there’s a block of code on that page that reads:
5 => array( "Filename" => "The original filename of the file. THIS IS IMPORTANT." )
This, I found out, is important. Just like it said. Except that it’ll never work like this. Flash sends a variable in it’s HTTP POST of “Filename” which is suppose to include the original filename of the file chosen from the user’s computer which is now being uploaded. That’s great, except how is PHP (or CF) suppose to know the name of that file, server side, prior to it being uploaded? You can’t. Frankly, that alone, boggles my mind on how anyone has success with that article.
Instead, your policy should NOT include “Filename”. It should include a policy rule of an array that looks like:
["starts-with", "$Filename", ""]
That will tell the Amazon S3 service that the Filename variable being passed in the POST is OKAY to be named anything. And it is. Because, well, we don’t care about the original filename.
So that’s the down and dirty. That article linked above would have been perfect had it noted that and not suggested that the orignal filename need to be generated server side to create the policy prior to a file even being chosen. I went ahead and created a struct in CF that contained everything I needed for the S3 communication:
You will need this function:
(Though there might be a way to use the built in encrypt() function in CF if your server has SHA-1 available to you via that function. Haven’t even looked. Lazy)
<cffunction name="HMAC_SHA1" returntype="binary" access="private" output="false" hint="NSA SHA-1 Algorithm">
<!--- used from Joe Danziger (joe@ajaxcf.com) "Amazon S3 REST Wrapper v1.8" --->
<cfargument name="signKey" type="string" required="true" />
<cfargument name="signMessage" type="string" required="true" />
<cfset var jMsg = JavaCast("string",arguments.signMessage).getBytes("iso-8859-1") />
<cfset var jKey = JavaCast("string",arguments.signKey).getBytes("iso-8859-1") />
<cfset var key = createObject("java","javax.crypto.spec.SecretKeySpec") />
<cfset var mac = createObject("java","javax.crypto.Mac") />
<cfset key = key.init(jKey,"HmacSHA1") />
<cfset mac = mac.getInstance(key.getAlgorithm()) />
<cfset mac.init(key) />
<cfset mac.update(jMsg) />
<cfreturn mac.doFinal() />
</cffunction>
My S3 struct which contains all the data you need to start communication:
s3 = {
config = {
accessKey = "YOUR ACCESS KEY (ID)",
secretAccessKey = "YOUR SECRET ACCESS KEY",
expiration = dateAdd("d", 5, now()), // THIS SHOULD BE UTC BUT I'M BEING LAZY, AGAIN
bucket = "name-of-your-bucket"
},
policy = {},
policyJSON = "",
policyEncoded = "",
signature = ""
};
// maintain case of keys
s3.policy["expiration"] = "#dateFormat(s3.config.expiration, "yyyy-mm-dd")#T12:00:00.000Z"; // being lazy here, should be UTC
s3.policy["conditions"] = [];
s3.policy["conditions"][1]["bucket"] = s3.config.bucket;
s3.policy["conditions"][2]["redirect"] = "http://www.letskillowen.com/";
s3.policy["conditions"][3]["acl"] = "public-read";
s3.policy["conditions"][4]["x-amz-meta-uuid"] = createUUID();
s3.policy["conditions"][5] = ["starts-with", "$key", "path/to/key/"];
s3.policy["conditions"][6] = ["starts-with", "$Filename", ""];
// convert policy to JSON
s3.policyJSON = serializeJSON(s3.policy);
// created encoded policy
s3.policyEncoded = toBase64(s3.policyJSON);
// create signature
s3.signature = toBase64(HMAC_SHA1(s3.config.secretAccessKey, replace(s3.policyEncoded, "\n", chr(10), "all")));
Now all you gotta do it get all this to work with SWFUpload. Easy enough. You can use this as a basis for your SWFUpload config/settings:
var settings = {
upload_url: "http://#s3.config.bucket#.s3.amazonaws.com",
http_success : [201, 303, 200], /* Amazon returns a 303 on success because of the REDIRECT policy */
file_post_name: "file", /* Amazon expects the file data to be in a input named "file" */
post_params: {
'key': 'path/to/key/${filename}',
'acl': '#s3.policy["conditions"][3]["acl"]#',
'x-amz-meta-uuid': '#s3.policy["conditions"][4]["x-amz-meta-uuid"]#',
'redirect': '#jsStringFormat(s3.policy["conditions"][2]["redirect"])#',
'AWSAccessKeyId': '#s3.config.accessKey#',
'Policy': '#s3.policyEncoded#',
'Signature': '#s3.signature#'
},
// ADD THE REMAINDER OF YOUR SWFUPLOAD SETTINGS/CONFIG HERE
};
swfu = new SWFUpload(settings);
That’s it. Took me a few hours to figure out and test via trial and error, but enjoy. Should have you up and running in a few minutes. The CF code will create everything you need to communicate with the Amazon S3 service and the Javascript code will init all the needed settings into SWFUpload. Since we added a redirect policy (something I recommend as it seems the most accurate with S3 responses), we just make sure we are handling 303 responses as successful. They should be.
I guess now I just need to find something else to occupy my time. Ohhh… lava lamp on my desk…