On March 29, 2010 Amazon announced public availability of CloudFront’s private content streaming feature. When I first looked at Amazon’s CloudFront offering a few weeks ago, this feature was one I knew I needed and it wasn’t available so I didn’t look much further. With this announcement, I decided to look into things a little deeper.
According to the Amazon CloudFront Developer Guide, there are two parts to serving private content:
- Securing the content in your bucket so that end users only have access to the content through CloudFront;
- Restricting end user access to cached content.
To cover #1, you change the ACL for your S3 bucket so that there is no public access. You then have to add a CloudFront origin access identity and give it permission to access your content. Refer to the developer’s guide for more details.
To cover #2, you have to create urls containing a signed policy for your content and then use these urls when linking to the content. Below, you will find the c# extension methods I came up with to create my urls:
Code Snippet
- public static string CloudFrontStreamingUrl(this string path)
- {
- return CloudFrontStreamingUrl(path, null);
- }
-
- public static string CloudFrontStreamingUrl(this string path, int? secondsUntilExpiration)
- {
- const string keyPairId = "YOUR-KEY-PAIR";
- var pathWithoutExtension = path.Replace(Path.GetExtension(path), string.Empty);
-
- //Unix date plus seconds until expiration or 1 hour if null
- var expirationDate = GetUnixTimestamp() + (secondsUntilExpiration ?? 3600);
-
- //Policy as required by AWS, ready for signing. Path in this case should have extension
- var policy = "{\"Statement\":[{\"Resource\":\"" + path +
- "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":" + expirationDate + "}}}]}";
-
- //Signed policy
- var signature = Sign(policy);
-
- //Url encoded querystring to pass with request for file
- var queryString = HttpUtility.UrlEncode(string.Format("?Expires={0}&Signature={1}&Key-Pair-Id={2}",
- expirationDate, signature, keyPairId));
-
- //Path to be used for streaming content. Path to stream in this case does not have extension
- return string.Format("{0}{1}", pathWithoutExtension, queryString);
- }
And the two private methods referred to:
Code Snippet
- private static int GetUnixTimestamp()
- {
- //http://snippets.dzone.com/posts/show/3236
- return (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
- }
-
- private static string Sign(string policy)
- {
- //http://stackoverflow.com/questions/2284206
-
- // Amazon PEM converted to XML using OpenSslKey
- // http://www.jensign.com/opensslkey/index.html
- const string providerXmlString = "<RSAKeyValue><Modulus>........ YOUR-KEY-CONVERTED-TO-XML";
-
- using (var sha1 = new SHA1Managed())
- {
- var provider = new RSACryptoServiceProvider();
- RSACryptoServiceProvider.UseMachineKeyStore = false;
- provider.FromXmlString(providerXmlString);
-
- var plainbytes = Encoding.UTF8.GetBytes(policy);
-
- var hash = sha1.ComputeHash(plainbytes);
- var sig = provider.SignHash(hash, "SHA1");
-
- //Replace (+=/) with (-_~) per AWS developer guide
- return Convert.ToBase64String(sig).Replace("+", "-").Replace("=", "_").Replace("/", "~");
- }
- }
A few important points:
- To be able to sign your urls, the key pair needs to be converted to an xml string. This can be done using the converter from http://www.jensign.com/opensslkey/index.html.
- When signing a stream url, the full path is not signed like an http url. Only the stream identifier is signed. Be sure to read the documentation.
- Understand when you include the steam’s extension. It seems to be included when signing the policy, but not when referring to the stream by actual url. Check the documentation.
I hope this helps you, it took me a good half day to figure it all out…