Using ColdFusion With Pusher - A Notification Service Powered By HTML5 WebSockets
I recently signed up for the Beta program of Pusher, a realtime push notification service powered by HTML5 WebSockets. Pusher provides a RESTful web service to which you can POST client notification messages. The Pusher web service engine then relays those notifications to all clients that have subscribed to your "pusher application" over HTML5 WebSockets. From what I have read, WebSockets allow the browser to open up a direct line of communication with the server outside of the standard request-response life cycle. The server can then "push" information through the WebSocket, directly to the browser, without the browser having to go out and request it (typically via polling).
In an effort to learn more about the world of HTML5, I thought I would create a simple "Hello World" type example powered by the Pusher service. As it turns out, though, even "simple" in this case is not all that simple. The complexity of the Pusher interaction lies in the POST to the RESTful web service. The POST can be performed by a straightforward CFHTTP call; but, the POST itself had to be digitally signed using a shared-key-encrypted version of the message. I don't know about you, but I find cryptography to be anything but straightforward. And, granted I'm no security expert, but the need to sign your POST just seems a bit excessive (not to mention it literally doubles the length of your code). Of course, more likely than not, that might just be my frustrated ignorance talking.
That said, for this example, I created a small ColdFusion form that accepts a message. Upon submit, ColdFusion posts that message via CFHTTP to my Pusher application. I then have another page that opens a WebSocket connection to the Pusher service for the given channel and binds to my "helloWorld" event. When that helloWorld event is triggered, the event handler simply adds the given message to the receiver's page.
Here is the ColdFusion page that takes the message and POSTs it to the Pusher application:
<!--- Param the form value. --->
<cfparam name="form.message" type="string" default="" />
<!---
Check to see if the message is available (if the user
submitted the form).
--->
<cfif len( form.message )>
<!---
Pusher information used to tie this push notification
to a given application and to a given client (listening
for a given channel).
--->
<cfset pusherAppID = "1306" />
<cfset pusherKey = "61b7e858d47e67683bd8" />
<cfset pusherSecret = "****************" />
<cfset pusherChannel = "helloWorld" />
<!---
Event we are triggering (this is what the client will
"bind" to on the given channel).
--->
<cfset pusherEvent = "hellowWorldEvent" />
<!---
Data we are actually "pushing". All data must be sent as
JSON (although it doesn't have to be simple string values).
--->
<cfset pusherMessage = serializeJSON( form.message ) />
<!--- Authentication information. --->
<cfset authVersion = "1.0" />
<cfset authMD5Body = lcase( hash( pusherMessage, "md5" ) ) />
<cfset authTimeStamp = fix( getTickCount() / 1000 ) />
<!--- Build the post resource (the RESTfule resource). --->
<cfset pusherResource = "/apps/#pusherAppID#/channels/#pusherChannel#/events" />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
The following is the digital signing of the HTTP request.
Frankly, this stuff is pretty far above my understanding
of cryptology. I have adapted code from the PusherApp
ColdFusion component written by Bradley Lambert:
http://github.com/blambert/pusher-cfc
To be 100% honest with you, I find this step to be very
frustrating. Do we really need so much security around this?
--->
<!---
Create the list of query string values (alpha-sorted with no
URL encoding).
--->
<cfsavecontent variable="queryStringData">
<cfoutput>
auth_key=#pusherKey#
auth_timestamp=#authTimeStamp#
auth_version=#authVersion#
body_md5=#authMD5Body#
name=#pusherEvent#
</cfoutput>
</cfsavecontent>
<!---
Create the raw signature data. This is the HTTP
method, the resource, and the alpha-ordered query
string (non-URL-encoded values).
--->
<cfset signatureData = (
("POST" & chr( 10 )) &
(pusherResource & chr( 10 )) &
reReplace(
trim( queryStringData ),
"\s+",
"&",
"all"
)
) />
<!---
Create our secret key generator. This can create a secret
key from a given byte array. Initialize it with the byte
array version of our PushApp secret key and the algorithm
we want to use to generate the secret key.
--->
<cfset secretKeySpec = createObject(
"java",
"javax.crypto.spec.SecretKeySpec"
).init(
toBinary( toBase64( pusherSecret ) ),
"HmacSHA256"
)
/>
<!---
Create our MAC (Message Authentication Code) generator
to encrypt the message data using the PusherApp shared
secret key.
--->
<cfset mac = createObject( "java", "javax.crypto.Mac" )
.getInstance( "HmacSHA256" )
/>
<!--- Initialize the MAC instance using our secret key. --->
<cfset mac.init( secretKeySpec ) />
<!---
Complete the mac operation, encrypting the given secret
key (that we created above).
--->
<cfset encryptedBytes = mac.doFinal( signatureData.getBytes() ) />
<!---
Now that we have the encrypted data, we have to convert
that data to a HEX-encoded string. We will use the big
integer for this.
--->
<cfset bigInt = createObject( "java", "java.math.BigInteger" )
.init( 1, encryptedBytes )
/>
<!--- Convert the encrypted bytes to the HEX string. --->
<cfset secureSignature = bigInt.toString(16) />
<!---
Apparently, we need to make sure the signature is at least
32 characters long. As such, let's just left-pad with spaces
and then replace with zeroes.
--->
<cfset secureSignature = replace(
lJustify( secureSignature, 32 ),
" ",
"0",
"all"
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now that we have all the values we want to post, including
our encrypted signature, we can post to the PusherApp REST
web service using CFHTTP.
--->
<cfhttp
result="post"
method="post"
url="http://api.pusherapp.com#pusherResource#">
<!---
Alert the post resource that the value is coming through
as JSON data.
--->
<cfhttpparam
type="header"
name="content-type"
value="application/json"
/>
<!--- Set the authorization parameters. --->
<cfhttpparam
type="url"
name="auth_version"
value="#authVersion#"
/>
<cfhttpparam
type="url"
name="auth_key"
value="#pusherKey#"
/>
<cfhttpparam
type="url"
name="auth_timestamp"
value="#authTimeStamp#"
/>
<cfhttpparam
type="url"
name="body_md5"
value="#authMD5Body#"
/>
<!--- Sent the name of the pusher event. --->
<cfhttpparam
type="url"
name="name"
value="#pusherEvent#"
/>
<!--- Send the actual message (JSON data). --->
<cfhttpparam
type="body"
value="#pusherMessage#"
/>
<!--- Digitally sign the HTTP request. --->
<cfhttpparam
type="url"
name="auth_signature"
value="#secureSignature#"
/>
</cfhttp>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<cfoutput>
<!DOCTYPE HTML>
<html>
<head>
<title>PusherApp Hello World - ColdFusion Pusher</title>
</head>
<body>
<h1>
PusherApp Hello World - ColdFusion Pusher
</h1>
<form action="#cgi.script_name#" method="post">
<p>
Enter your message to push:
</p>
<p>
<input type="text" name="message" size="40" />
<input type="submit" value="Push It, Push It Real Good!" />
</p>
</form>
</body>
</html>
</cfoutput>
As you can see, the part that encrypts the message signature literally doubles the amount of code needed to get this example up and running. As I don't know much about cryptography (probably where my frustration really comes from), I had to borrow somewhat from Bradley Lambert's Pusher.cfc. Other than the encryption, however, this code really isn't doing all that much.
Once the message gets POSTed to the Pusher web service, the Pusher notification engine will then send that message (event) to all clients that have opened up a WebSocket to the given application. The Pusher web service is designed to work with modern browsers, like Chrome, that have native support for WebSockets. For browsers that do not yet have native WebSocket support, however, Pusher provides a Flash-based fallback system as a stop-gap measure. As you can see in the code below, in order to take advantage of this Flash-fallback, all you have to do is save the Pusher App's WebSocket SWF file locally and tell the WebSocket Javascript class where it can be found.
<!DOCTYPE HTML>
<html>
<head>
<title>PushApp Hello World With ColdFusion</title>
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="http://js.pusherapp.com/1.4/pusher.min.js"></script>
<script type="text/javascript">
// This is for compatability with browsers that don't yet
// support Web Sockets, but DO support Flash.
//
// NOTE: This SWF can be downloaded from the PusherApp
// website. It is a Flash proxy to the standard Web
// Sockets interface.
WebSocket.__swfLocation = "./WebSocketMain.swf";
// When the DOM is ready, init the scripts. Technically, we
// can bind the PusherApp before DOM ready, but we can't
// respond to it, so no need to bind early.
$(function(){
// Get a reference to the message list.
var messages = $( "#messages" );
// Create a Pusher server object with your app's key and
// the channel you want to listen on. Channels are unique
// to your application.
var server = new Pusher(
"61b7e858d47e67683bd8",
"helloWorld"
);
// Now that we have the pusher connection for a given
// channel, we want to listen for certain events to come
// over that channel.
server.bind(
"hellowWorldEvent",
function( data ) {
// The data that comes through is the evaluated
// JSON data that was pushed to the client. In
// our case, it's just a simple string.
messages.prepend(
"<li>" + data + "</li>"
);
}
);
});
</script>
</head>
<body>
<h1>
PusherApp Hello World With ColdFusion
</h1>
<p>
This page will listen for PusherApp via HTML5 Socket connectsion
(or, if not supported, a Flash SWF fallback). Messages will be posted
below as they arrive.
</p>
<ol id="messages">
<!-- To be populated via PUSH notification. -->
</ol>
</body>
</html>
As you can see, the client code for interacting with the Pusher realtime notification service could not be any simpler; binding to events on the WebSocket is no different than binding to standard Javascript events as you might do with jQuery. The only thing of any real note on this end is that the native HTML5 WebSocket support is much faster than the Flash fallback approach. Native WebSockets seem to receive the message instantaneously, whereas the Flash SWF, on the other hand, has a noticeable delay in its performance.
The HTML5-WebSocket-powered Pusher realtime client notification system seems very cool! I am sure that anyone who has built an application that has to continually poll the server for information will instantly see the value provided by this service. The only thing that I'm a little bit weary of is the fact that a critical part of my application might require the availability of a 3rd party system. Of course, the idea of being able to offload the management of such things is also super appealing. Plus, I believe in their FAQs, they mention wanting to use EC2 in the future to provide massive scaling and availability. In any case, this is something that I will definitely be playing around with a bit more.
Want to use code from this post? Check out the license.
Reader Comments
Nice little example of Web Sockets Ben, I know a few months ago I was playing around with it but I was trying to get a local java version up and running on my machine with no luck in the end.
Interesting, will have to look into this. Yeah the crypto seems excessive, but for an app I am working on that will have a push component, that security actually will be useful.
I think too the notification service seems it might be meant to be small and compatible with iOS (based on what has been said about notification). So not a good way to send large messages.
@Josh,
Yeah, seems like it would be a pain to set up your own WebSocket server locally. It's funny, in the Pusher FAQs, they even address that question (as to why not just set something up locally):
You are very welcome to! From our experience it is a bit annoying to do this, and means you spend less time on making cool stuff.
@Kevin,
I am not sure how much information can be pushed. Apparently, only iOS4 supports web sockets. I'll be playing around more with this.
Ben you should check out the webkitNotifications api for browsers. That is something I think will be really handy in the future.
@Josh,
I'm not familiar with that. I'll definitely have to check it out, thanks!
Hi Ben
Thanks for the great post. Sorry the cryptography is a bit complex. We wanted to make sure you can trust that no-one other than you can send messages to browsers viewing your app. :)
There are several libraries available that hide this complexity. For example in the ruby library we maintain, after you set the app id, key and secret there is a single command to encrypt and post your message
Also, we are already set up and running fully on EC2, and are working on making our service reliable as good as possible.
Laurie
@Laurie,
No worries. As I mentioned above, I am sure that my frustrating stems more from the fact that I don't know much about cryptography than the fact that it is required. That said, what kind of security does signing add above simply requiring authentication with the HTTP request? I am not attacking - I literally don't know much about that theory.
Pusher app is very cool, if your doing any Ruby work, their gem does truly make it way to easy.
However for local home grown stuff, I have been looking at http://jwebsocket.org/. Being java, it is much more CF_friendly.
@Ben Nadel,
The main difference with HTTP Auth is how secure it actually is.
HTTP Basic is rather insecure, as it sends authentication in plain text.
HTTP Digest is more secure, but has some legacy issues. For example with some clients it is possible for a man in the middle to tell the client to downgrade to an older version of the auth protocol, where attacks have been discovered. This allows the attacker to get access to the user's credentials and effectively hack your account. I honestly can't speak to how many http libraries are vulnerable to this, as there are so many, and in so many different languages.
A third option would be HTTP Auth over SSL, which is secure, but adds quite a high percentage overhead when dealing with lots of small requests.
The ideal scenario (from our point of view) is to abstract the cryptography into libraries for each language, so that the application developer can ignore them.
Hope that helps :-)
@Laurie,
Yeah, I've heard SSL adds a lot of overhead. I guess this makes the most sense. I am sure if ColdFusion had native support for the desired encryption algorithms, I wouldn't give it a second thought. I assume since I have to bring in so many Java libraries, I simply become much more aware of the effort.
@Ben,
Looks as if you should modify your "A Little Bit About Me" blurb to end with "using ColdFusion, jQuery and HTML 5".
Speaking of which, you may want to check out my plugins.jquery.com contributions so far: attrNameBegins and defineDatasets. I wrote them based on an e-mail conversation I had with Ian Hickson about how they should behave. They allow you to code source-code-compatible JS for HTML 5 datasets (data-* attributes) in advance of browser support. Of course, the HTML 5 implementation requires a getter/setter implementation that allows even deletion, so there physically isn't any way to do that with current JS. (Though I can't think of a reason why I would ever want to do such a thing.)
I also have an HTML 5 Web Forms 2.0 support matrix that I haven't published yet, if you're interested in that. One of the interesting things about Web Forms 2.0 on iOS is that if you use input type="search", Safari for iOS will pop up a keyboard with a Search key. Or input type="email" pops up a keyboard for e-mail addresses. Or input type="number" pops up a numeric keypad. Context sensitive keyboards.
HTML 5 is such a treasure trove of new features.
@Steve,
Yeah, this HTML5 stuff is super cool so far. I'm really enjoying it. I don't even have a really good sense of what HTML5 truly encompass; so, I'm just checking it out a little bit at a time. I'm sure it will be totally diluted the way "DHTML" and then later "AJAX" became. But, very cool stuff so far.
Your test app should strip all tags from the output, not just the ones that seem dangerous at first glance. The security-minded part of my brain can't get past my revulsion for XSS exploits and appreciate how useful it actually is. :(
I know it's just a hello-world testing rig, but secure code starts in the home, as they say.
@Mysterion,
I appreciate your comment; I'll make the htmlEditFormat() updates for my Pusher.cfc project. However, for any given post, there's only so much you can add before a post would be overwhelming (I think). I mean, technically, there should probably also be a try/catch around my CFHTTP request and some sort of reasonable timeout on it as well; but, like you said, it's just a "hello world" example.
That said, I happen to think realtime notification is a very exciting thing. If you come up with any good use cases on your end, I'd love to hear about them.
@Mysterion,
I have added htmlEditFormat() to my Pusher.cfc's online demo:
www.bennadel.com/resources/projects/pusher/demo/index.cfm
Again, thanks for the feedback.
@Ben,
jQuery 1.4.3 core includes the functionality of my .defineDatasets() plugin!
This is an own-horn-tooting followup to my Jun 28, 2010 at 4:24 PM post in this thread. jQuery 1.4.3 was just released today:
http://blog.jquery.com/2010/10/16/jquery-143-released/
It contains a new behavior for the .data() method. If an HTML element specifies data-something="example" among its attributes, $(ref).data("something") will retrieve "example". In other words, they implemented it in the jQuery .data() way, not in the HTML5 way. But they recognized the need for it, and they implemented it! Yay!
I feel vindicated for recognizing that this HTML-to-JS communication feature was needed and writing the plugin to do it. So now if you want to do it the jQuery way, you can use the .data() method, or if you want to do it the HTML5 way, you can use my plugin. And my :attrNameBegins(prefix) selector can be used with either technique!
Woo and hoo!
@Steve,
Ha ha, awesome stuff :) Way to see the way of the future! The 1.4.3 release looks cool. At the jQuery Conference this last weekend, John Resig told us that they rebuild a lot of the CSS and attribute stuff to make it significantly faster. I love how they are constantly optimizing.
@Ben
I have not been able to find much on running a NodeJS server/app in production.. ( all my playing around has been to just run node in a command prompt/terminal)
I can't imagine that is the best practice for production. Do you know how one run's a node app on a private Windows machine ? could it even be the CF server?