Skip to main content
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Ben Margolis
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Ben Margolis ( @tangon )

Pushing Base64-Encoded Images Over HTML5 WebSockets With Pusher And ColdFusion

By on

Yesterday, I started playing around with Pusher, a realtime push notification web service that allows your server to "push" updates to the client using native HTML5 or Flash-based WebSockets. If you don't know what WebSockets are (neither did I), they provide a way for the client to open up a direct line of communication with the server. The server can then send information back to the client at any time without the client having to constantly check for it. In my first demo, all I did was push a short, user-entered text message to the client. In this demo, however, I wanted to further test the bandwidth supplied by Pusher's WebSockets by pushing sizable Base64-encoded images to client.

Pushing Base64-encoded images from the server to the client (via Pusher) is not really a great use case. Typically, you'd probably just push an image URL to client and have the client make a subsequent request for the full image binary. The only real reason you might want to push an image is if you needed to supply an image that was not web-accessible and you also didn't want to stream the image up using something like CFContent. That said, we're not here to debate use cases, we're here to push the limits of Pusher's HTML5 WebSockets.

In the following code, I am allowing the user to select a photograph. The selected photograph is then read into memory by ColdFusion and written back to the server as Base64 data (there's no way to currently encode Base64 images on the fly). This Base64 image data is then read back into memory and further serves as the text data being posted to the Pusher web service via ColdFusion's CFHTTP tag. Beyond the actual image encoding, this Pusher demo is almost exactly the same as my first one.

<!--- Param the url value. --->
<cfparam name="url.id" type="numeric" default="0" />


<!--- Check to see if the image was selected by the user. --->
<cfif url.id>


	<!---
		Pusher information used to tie this push notification
		to a given application and to a given client (listening
		for a given channel).

		NOTE: Your Pusher account can have multiple applications
		associated with it (each of which can have on-the-fly
		channel creation).
	--->
	<cfset pusherAppID = "1346" />
	<cfset pusherKey = "6a4bebacf22ae8081689" />
	<cfset pusherSecret = "****************" />
	<cfset pusherChannel = "imagePush" />

	<!---
		Event we are triggering (this is what the client will
		"bind" to on the given channel).
	--->
	<cfset pusherEvent = "imageEvent" />

	<!---
		At this point, we need to assemble the data we are actually
		going to Push using Pusher. In this case, we want to send
		the Base64-encoded data of the image to Push. In ColdFusion,
		there is no on-the-fly Base64 encoding. As such, we need
		read the image in and then write it to a temp file.
	--->

	<!--- Read the image in. --->
	<cfset image = imageNew( "./images/image#url.id#.jpg" ) />

	<!---
		Write it to disk as Base64 encoding. By adding the fourth,
		option argument (inHTMLFormat), ColdFusion will automatically
		prepend the base64 data with, "data:image/jpg;base64,", such
		that it can easily be used as a Data Url for image source.
	--->
	<cfset imageWriteBase64(
		image,
		"./base64.txt",
		"jpg",
		true
		) />

	<!--- Read in the Base64 data. --->
	<cfset base64Data = fileRead( expandPath( "./base64.txt" ) ) />

	<!---
		Now that we have the image in Base64 data, we have to encode
		that value in JSON to be passed to the Pusher app. All values
		"pushed" must be in JSON format.
	--->
	<cfset pusherData = serializeJSON( base64Data ) />

	<!--- Authentication information. --->
	<cfset authVersion = "1.0" />
	<cfset authMD5Body = lcase( hash( pusherData, "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
	--->


	<!---
		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 data (JSON data). --->
		<cfhttpparam
			type="body"
			value="#pusherData#"
			/>

		<!--- Digitally sign the HTTP request. --->
		<cfhttpparam
			type="url"
			name="auth_signature"
			value="#secureSignature#"
			/>

	</cfhttp>


</cfif>


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<cfoutput>

	<!DOCTYPE HTML>
	<html>
	<head>
		<title>Pushing Base64 Image Data With ColdFusion</title>
		<style type="text/css">

			a {
				margin-right: 10px ;
				}

			img {
				border: 3px solid ##333333 ;
				height: 100px ;
				}

		</style>
	</head>
	<body>

		<h1>
			Pushing Base64 Image Data With ColdFusion
		</h1>

		<p>
			<!---
				Loop over the images to provide pushing options
				(images will be converted to Base64 for "pushing").
			--->
			<cfloop
				index="imageIndex"
				from="1"
				to="4"
				step="1">

				<a href="#cgi.script_name#?id=#imageIndex#">
					<img src="./images/image#imageIndex#.jpg" />
				</a>

			</cfloop>

		</p>

		<p>
			Images Credit:<br />
			http://www.pbase.com/gilp/stephanie_richard
		</p>

	</body>
	</html>

</cfoutput>

On the other end of Pusher is our client. And just as before, the client opens up an HTML5 WebSocket connection to the Pusher server to listen for push events. In our case, the client is connecting to the "imagePush" channel of our application and is binding to "imageEvent"-type events. When one of these push events is detected, the client takes the Base64-encoded image data associated with the push event and uses it as the SRC attribute value of an IMG tag on the page.

NOTE: Not all browsers support Base64-encoded data URLs for images.

<!DOCTYPE HTML>
<html>
<head>
	<title>Pushing Base64 Image Data 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 image placeholder.
			var image = $( "#image" );

			// 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(
				"6a4bebacf22ae8081689",
				"imagePush"
			);

			// 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(
				"imageEvent",
				function( data ) {

					// The data that comes through is the evaluated
					// JSON data that was pushed to the client. In
					// our case, it's the Base64 encoded image data
					// formatted to be used as a data URL.
					image.attr( "src", data );
				}
			);

		});

	</script>
</head>
<body>

	<h1>
		Pushing Base64 Image Data With ColdFusion
	</h1>

	<p>
		This page will listen for PusherApp via HTML5 Socket
		connection (or, if not supported, a Flash SWF fallback).
		The messages (image data) will be displayed below.
	</p>

	<!--- The placeholder image. --->
	<p>
		<img id="image" width="450" />
	</p>

</body>
</html>

Again, pushing images over WebSockets is not the most choice use case. But, to my surprise and delight, this worked really well. Not only did the Pusher service successfully handle the 80K of Base64-encoded image data, it did so was somewhat shocking speed. The Pusher application seems very powerful; and, the more I play around with it, the more I am excited to start using it in my applications.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

In your video you say there is no way to base 64 encode image data without coldfusion.

Why not load the original images in canavas then its arbitrary to get the base64 data without a server request. you could send the direct base64 data from canvas directly to the pusher service.

just some thoughts!

15,674 Comments

@Steve,

That's awesome! Great to hear it. The native support seems to be way faster than the Flash support.

@Josh,

I may have misspoke on the video. I did not mean to say there was no way to Base64 encode the image without ColdFusion. I mean to say there is no way to Base64 on the fly IN ColdFusion (hence the intermediary file write).

That said, you still need to post to the ColdFusion server for all of the other stuff, such as the cryptography libraries, not to mention that you probably want to keep your secret key hidden so that others cannot just look in your Javascript and find it.

9 Comments

Ben,

I did try pusherapp to send messages and it works perfectly fine. A problem what I'm trying to solve is to get notified whenever I receive an e-mail. Say I receive an email in my gmail inbox and I want pusher to send me a notification for this. Are you aware of any gmail apis that I could use.

15,674 Comments

@Sagar,

You could use IMAP or POP to monitor your GMail account and, if you get a message, it you can push an alert via Pusher, I suppose. I've used ColdFusion 8 with CFPOP and ColdFusion 9 with CFIMAP both to check GMail inboxes, so I know that's possible. I don't know what language you are using, but I would assume they have something similar.

9 Comments

Ben,

Yes, I do use ColdFusion as my primary language for development. CFPOP and CFIMAP can be used to retrieve the message from my gmail inbox, however the client (browser) should be notified (using pusher) of the new message in inbox and then I could use my jQuery to invoke the cfc which in turn would make a CFIMAP request to gmail.

The alternative to this is to keep polling for new messages i.e. make CFIMAP request every 1 - 3 mins , but I would like to get a hook on the gmail inbox (using any of the available APIs) and ask pusher to send the notification to the client (browser).

15,674 Comments

@Sagar,

Yeah, sounds like you get the right idea - use something like CFPOP to look for new messages on the server-side; then, if you see one (or more), you can POST a message to Pusher, which can then, in realtime, notify your client application.

1 Comments

i am a student of engineering.
i am developing a web application for paint and want to save the client image using php into mysql database and want to show the same image to the other client to do a collaborative drawing is it possible through php and mysql and html5 websockets or ajax?

15,674 Comments

@Nitesh,

That is a very cool idea. While I don't know all that much about the Canvas tag for drawing, the concept would probably be something like pushing canvas API commands from browser to browser as they are happening. So, for example, as you programmatically do moveTo() and lineTo() commands on one browser, you can simultaneously post those commands to the server, which pushes them to the other browser, which implements the same commands.

That would be a really fun little project to play with.

13 Comments

@Ben,

Just wanted to let you know that pusher are soon to start limiting sizes on messages. This was the detail that came through in the Feb dispatch:

"However, we will soon be limiting the size of messages we will accept on our REST API. More information will be available soon, follow us on Twitter (@pusher) for the latest news."

I asked them about it, and they said they "may" offer larger message sizes for a fee, or you could send through the file in parts.

I have just got into Pusher (and push services) and I am REALLY enjoying it, it just feels so powerful. A reason why I am liking it so much, is that although I love node.js (from the small portions that I have seen), its alot faster to get applications up and running.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel