Amazon CloudFront Invalidation with Coldfusion

Amazon’s CloudFront is great to reduce load on local servers if you are serving a lot of static files or files that can have a long cache timer set.

Sometimes you need to remove files from the CloudFront caches and this is where the Invalidation feature comes in.

An invalidation batch-request invalidates one or more files (paths) from all CloudFront edge location.

Currently it will take about 10-15 minutes for an avarage invalidation batch to run thru. But hey, better than nothing.

A while ago i integrated the SQS and S3 parts i found floating around into one single CFC. Today i added a part for CloudFront invalidation. So here it is:

<!---
Parts of this are based on Tim Dawe's 
http://amazonsig.riaforge.org

and 

Joe Danziger's Amazon S3 REST Wrapper
http://amazons3.riaforge.org/

Written by Patrick Liess
Twitter: @smrchy
http://www.tcs.de
--->

<cfcomponent>
	<cffunction name="init" output="false" hint="Returns an instance of the CFC initialized.">
		<cfargument name="awsAccessKeyId" type="string" required="true" />
		<cfargument name="secretAccessKey" type="string" required="true" />
		<cfscript>
			This.awsAccessKeyId = Arguments.awsAccessKeyId;
			This.secretAccessKey = Arguments.secretAccessKey;
			This.SQSVersion = "2009-02-01";
			This.SQSserviceUrl = "http://queue.amazonaws.com/";
			This.CFVersion = "2010-11-01";
			return This;
		</cfscript>
	</cffunction>

	<!--- CloudFront --->
	<cffunction name="CFInvalidation" output="false" returntype="any">
		<cfargument name="distributionid" type="string" required="yes">
		<cfargument name="patharray" type="array" required="yes">
		<cfset var i = 0>
		<cfset var dateTimeString = GetHTTPTimeString(Now())>
		<cfset var cs = "#dateTimeString#"> 
		<cfset var signature = createSignature(cs)>
		<cfset var thexml = "<InvalidationBatch>">
		<cfloop index="i" array="#Arguments.patharray#">
			<cfset thexml = thexml & "<Path>#i#</Path>">
		</cfloop>
		<cfset thexml = thexml & "<CallerReference>#CreateUUID()#</CallerReference></InvalidationBatch>">
		<cfhttp method="POST" url="https://cloudfront.amazonaws.com/#This.CFVersion#/distribution/#Arguments.distributionid#/invalidation">
			<cfhttpparam type="header" name="Date" value="#dateTimeString#">
			<cfhttpparam type="header" name="Content-Type" value="text/xml">
			<cfhttpparam type="header" name="Authorization" value="AWS #This.awsAccessKeyId#:#signature#">
			<cfhttpparam type="body" value="#thexml#">
		</cfhttp>
		<cfreturn cfhttp>
	</cffunction>


	<!--- S3 --->
	<cffunction name="S3createSignedURL" output="false" returntype="string">
		<cfargument name="bucketName" type="string" required="yes">
		<cfargument name="fileKey" type="string" required="yes">
		<cfargument name="minutesValid" type="string" required="false" default="60">
		<cfargument name="secure" type="boolean" required="false" default="false">
		<cfscript>
			var epochTime = DateDiff("s", DateConvert("utc2Local", "January 1 1970 00:00"), now()) + (arguments.minutesValid * 60);
			// Create a canonical string to send
			var cs = "GET\n\n\n#epochTime#\n/#arguments.bucketName#/#arguments.fileKey#";
			// Create a proper signature
			var signature = createSignature(cs);
			// Create the timed link for the image
			var protocol = 'http://';
			if (arguments.secure) {
				protocol = 'https://';	
			}
			var timedAmazonLink = protocol & arguments.bucketName&'.s3.amazonaws.com/' & arguments.fileKey & '?AWSAccessKeyId=' & URLEncodedFormat(This.awsAccessKeyId) & '&Expires=' & epochTime & '&Signature=' & URLEncodedFormat(signature);
			return timedAmazonLink;
		</cfscript>
	</cffunction>

	<cffunction name="S3deleteObject" output="false" returntype="struct">
		<cfargument name="bucketName" type="string" required="yes">
		<cfargument name="fileKey" type="string" required="yes">
		<cfset var dateTimeString = GetHTTPTimeString(Now())>
		<cfset var cs = "DELETE\n\n\n#dateTimeString#\n/#arguments.bucketName#/#arguments.fileKey#"> 
		<cfset var signature = createSignature(cs)>
		<cfhttp method="DELETE" url="http://s3.amazonaws.com/#arguments.bucketName#/#arguments.fileKey#">
			<cfhttpparam type="header" name="Date" value="#dateTimeString#">
			<cfhttpparam type="header" name="Authorization" value="AWS #This.awsAccessKeyId#:#signature#">
		</cfhttp>
		<!--- We return the full struct so we can check the cfhttp.statuscode for errors --->
		<cfreturn cfhttp>
	</cffunction>

	<cffunction name="S3putObject" access="public" output="false" returntype="string">
		<cfargument name="bucketName" type="string" required="yes">
		<cfargument name="filekey" type="string" required="yes">
		<cfargument name="filebinary" type="binary" required="yes">
		<cfargument name="contentType" type="string" required="yes">
		<cfargument name="ACL" type="string" required="no" default="public-read">
		<cfargument name="HTTPtimeout" type="numeric" required="no" default="300">
		<cfargument name="cacheControl" type="boolean" required="no" default="true">
		<cfargument name="cacheDays" type="numeric" required="no" default="300">
		<cfset var dateTimeString = GetHTTPTimeString(Now())>
		<cfset var cacheSeconds = Arguments.cacheDays * 86400>
		<cfset var cs = "PUT\n\n#arguments.contentType#\n#dateTimeString#\nx-amz-acl:#arguments.ACL#\n/#arguments.bucketName#/#arguments.filekey#">

		<!--- Never allow cacheControl for secure images --->
		<cfif arguments.ACL NEQ "public-read">
			<cfset arguments.cacheControl = false>
		</cfif>
		<cfset var signature = createSignature(cs)>

		<!--- Send the file to amazon. The "x-amz-acl" controls the access properties of the file --->
		<cfhttp method="PUT" url="http://s3.amazonaws.com/#arguments.bucketName#/#arguments.fileKey#" timeout="#arguments.HTTPtimeout#">
			<cfhttpparam type="header" name="Authorization" value="AWS #This.awsAccessKeyId#:#signature#">
			<cfhttpparam type="header" name="Content-Type" value="#arguments.contentType#">
			<cfhttpparam type="header" name="Date" value="#dateTimeString#">
			<cfhttpparam type="header" name="x-amz-acl" value="#arguments.ACL#">
			<cfhttpparam type="body" value="#Arguments.filebinary#">
			<cfif arguments.cacheControl>
				<cfhttpparam type="header" name="Cache-Control" value="max-age=#cacheSeconds#">
			</cfif>
		</cfhttp> 		
		<cfreturn cfhttp.FileContent>
	</cffunction>

	<!--- SQS --->
	<cffunction name="SQSgetQueueAttributes" output="false" returntype="string">
		<cfargument name="queue" type="string" required="true"/>
		<cfargument name="name" type="string" required="true"/>
		<cfset var requrl=	This.SQSserviceUrl & Arguments.queue &
							"?Action=GetQueueAttributes" &
							"&AttributeName=" & Arguments.name &
							"&Version=" & This.SQSVersion>
		<cfset requrl = signRequest(requrl,"GET")>
		<cfhttp method="GET" url="#requrl#" charset="UTF-8"></cfhttp>
		<cfreturn cfhttp.FileContent>
	</cffunction>

	<cffunction name="SQSdeleteMessage" output="false" returntype="struct">
		<cfargument name="queue" type="string" required="true"/>
		<cfargument name="receipthandle" type="string" required="true"/>
		<cfset var requrl=	This.SQSserviceUrl & Arguments.queue &
							"?Action=DeleteMessage" &
							"&ReceiptHandle=" & URLEncodedFormat(Arguments.receipthandle,"utf-8") &
							"&Version=" & This.SQSVersion>
		<cfset requrl = signRequest(requrl,"GET")>
		<cfhttp method="GET" url="#requrl#" charset="UTF-8"></cfhttp>
		<!--- We return the full struct so we can check the cfhttp.statuscode for errors --->
		<cfreturn cfhttp>
	</cffunction>


	<cffunction name="SQSreceiveMessage" output="false" returntype="string">
		<cfargument name="queue" type="string" required="true"/>
		<cfargument name="amount" type="numeric" required="true"/>
		<cfset var requrl=	This.SQSserviceUrl & Arguments.queue &
							"?Action=ReceiveMessage" &
							"&AttributeName=None" &
							"&MaxNumberOfMessages=" & Arguments.amount &
							"&Version=" & This.SQSVersion>
		<cfset requrl = signRequest(requrl,"GET")>
		<cfhttp method="GET" url="#requrl#" charset="UTF-8"></cfhttp>
		<cfreturn cfhttp.FileContent>
	</cffunction>

	<cffunction name="SQSsendMessage" output="false" returntype="string">
		<cfargument name="queue" type="string" required="true"/>
		<cfargument name="msg" type="string" required="true"/>
		<cfset var requrl=	This.SQSserviceUrl & Arguments.queue &
							"?Action=SendMessage" &
							"&MessageBody=" & URLEncodedFormat(Arguments.msg,"utf-8") &
							"&Version=" & This.SQSVersion>
		<cfset requrl = signRequest(requrl,"GET")>
		<cfhttp method="GET" url="#requrl#" charset="UTF-8"></cfhttp>
		<cfreturn cfhttp.FileContent>
	</cffunction>

	<cffunction name="SQSsetQueueAttributes" output="false" returntype="string">
		<cfargument name="queue" type="string" required="true"/>
		<cfargument name="name" type="string" required="true"/>
		<cfargument name="value" type="string" required="true"/>
		<cfset var requrl=	This.SQSserviceUrl & Arguments.queue &
							"?Action=SetQueueAttributes" &
							"&Attribute.Name=" & Arguments.name &
							"&Attribute.Value=" & Arguments.value &
							"&Version=" & This.SQSVersion>	
		<cfset requrl = signRequest(requrl,"GET")>
		<cfhttp method="GET" url="#requrl#" charset="UTF-8"></cfhttp>
		<cfreturn cfhttp.FileContent>
	</cffunction>	

	<!---- Util functions --->
	<cffunction name="signRequest" returntype="string" output="false">
		<cfargument name="request" required="yes" type="string">
		<cfargument name="method" required="no" default="GET" type="string">
		<cfscript>
			var lc = structnew();
			// Extract the URL part of the request and strip the protocol
			lc.requesturl = listfirst(arguments.request, "?");
			lc.requesturl = replacenocase(lc.requesturl, "http://", "");
			// Split into host and path
			lc.host = listfirst(lc.requesturl, "/");
			lc.path = right(lc.requesturl, len(lc.requesturl) - len(lc.host));
			// Process the query string parameters into a structure
			lc.querystring = listlast(arguments.request, "?");
			lc.strParams = structnew();
		</cfscript>

		<cfloop list="#lc.querystring#" index="i" delimiters="&">
			<cfset lc.strParams[listfirst(i, "=")] = urldecode(listlast(i, "="))>
		</cfloop>

		<cfscript>
			// Add the timestamp
			if (not StructKeyExists(lc.strParams, "Timestamp")) {
				lc.utcdate = dateconvert("local2Utc", now());
				lc.strParams["Timestamp"] = dateformat(lc.utcdate, 'yyyy-mm-dd') & "T" & timeformat(lc.utcdate, 'HH:mm:ss') & "Z";
			}
			// Add the standard parameters
			lc.strParams["AWSAccessKeyId"] = This.awsAccessKeyId;
			lc.strParams["SignatureVersion"] = 2;
			lc.strParams["SignatureMethod"] = "HmacSHA1";
			// Sort the parameters
			lc.keys = listsort(structkeylist(lc.strParams), "text");
			// Generate a new query string including timestamp, with parameters in the correct order, encoding as we go
			lc.qs = "";
		</cfscript>

		<cfloop list="#lc.keys#" index="i">
			<cfset lc.qs = lc.qs & rfc3986EncodedFormat(i) & "=" & rfc3986EncodedFormat(lc.strParams[i]) & "&">
		</cfloop>

		<cfscript>
			// Strip off the last & 
			lc.qs = left(lc.qs, len(lc.qs)-1);
			// Build the string to sign
			lc.stringToSign = arguments.method & chr(10);
			lc.stringToSign = lc.stringToSign & lc.host & chr(10);
			lc.stringToSign = lc.stringToSign & lc.path & chr(10);
			lc.stringToSign = lc.stringToSign & lc.qs;
			lc.signature = HMAC_SHA1(lc.stringToSign);
			// Return the new request URL
			return "http://" & lc.host & lc.path & "?" & lc.qs & "&Signature=" & urlencodedformat(tobase64(lc.signature));
		</cfscript>
	</cffunction>

	<cffunction name="HMAC_SHA1" returntype="binary" access="private" output="false">
		<cfargument name="signMessage" type="string" required="true" />
		<cfscript>
			var jMsg = JavaCast("string",arguments.signMessage).getBytes("iso-8859-1");
			var jKey = JavaCast("string",This.secretAccessKey).getBytes("iso-8859-1");
			var key = createObject("java","javax.crypto.spec.SecretKeySpec");
			var mac = createObject("java","javax.crypto.Mac");
			key = key.init(jKey,"HmacSHA1");
			mac = mac.getInstance(key.getAlgorithm());
			mac.init(key);
			mac.update(jMsg);
			return mac.doFinal();
		</cfscript>
	</cffunction>

	<cffunction name="createSignature" returntype="string" access="public" output="false">
		<cfargument name="stringIn" type="string" required="true" />
		<cfset var fixedData = replace(arguments.stringIn,"\n","#chr(10)#","all")>
		<cfset var digest = HMAC_SHA1(fixedData)>
		<cfset var signature = ToBase64("#digest#")>
		<cfreturn signature>
	</cffunction>

	<cffunction name="rfc3986EncodedFormat" returntype="string" output="false">
		<cfargument name="text" required="yes" type="string">
		<cfset var lc = structnew()>
		<cfset lc.objNet = createObject("java","java.net.URLEncoder")>
		<cfset lc.encodedText = lc.objNet.encode(arguments.text, 'utf-8').replace("+", "%20").replace("*", "%2A").replace("%7E", "~")>
		<cfreturn lc.encodedText>
	</cffunction>
</cfcomponent>
share