Skip to main content
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Laura Springer
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Laura Springer ( @w3bchick )

ColdFusion 10 - Cross-Linking Standard Session Management To WebSocket Session Management

By on
Tags:

By now, we're all used to ColdFusion's standard session management - the one that's typically powered by session cookies that get posted back to the server with every request. ColdFusion 10's built-in WebSocket server does not operate on cookies; instead, the client (browser) establishes a constant(ish) link to the server that creates a handshake using a server-provided ClientID. Unlike standard sessions, which are unique to a given user, a WebSocket session is unique to a given JavaScript object instance. This means that a single user may have multiple WebSocket sessions associated with it. In this case, I use the term, "associated," loosely as there is no actual tie between the standard session and the WebSocket session. In this post, however, I'll demonstrate one way in which you can programmatically bind a user's WebSocket sessions to their standard ColdFusion session management.

NOTE: At the time of this writing, ColdFusion 10 was in public beta.

Both a user and a WebSocket connection have session management. Each of these sessions is made available across requests; and, any modifications made to these sessions are persisted across subsequent requests. These two types of sessions act very similar; however, they use different identifiers and they live in completely different memory spaces. As such, there's no inherent way for you to associate your user's standard session with your user's WebSocket connections. Instead, we have to create this link programmatically.

When a standard ColdFusion session is created, it is given session identifiers; typically, CFID and CFTOKEN. These identifiers are then passed back to the server with each HTTP request in order to maintain the server-side session association. Understanding this, I figured the easiest way to bind standard session management to WebSocket session managent would be to pass the CFID and CFTOKEN values with each WebSocket request. This is easy enough as the underlying ColdFusion WebSocket library allows for custom headers on both publish and subscribe requests.

Passing the session identifiers along with the WebSocket request is only half of the battle - ColdFusion still doesn't do anything with them implicitly. As such, we have to manually examine the WebSocket requests and then manually create an association based on the provided identifiers.

I would have liked to encapsulate this process as much as possible within my ColdFusion 10 WebSocket AMD JavaScript module; however, there are simply too many moving parts - too many assumptions that would have to be made. As such, I came up with what I think was a good compromise - I created a new event handler in the Application.cfc ColdFusion framework component:

  • onWSSessionStart( user )

Using my WebSocket proxy / channel listener - WSApplication.cfc - this new event handler is invoked whenever a new WebSocket connection is established (in a meaningful way). This event handlers does not implicitly link your WebSocket session to your standard ColdFusion session; rather, it simply provides you with an opportunity to explicitly link the given "user" (aka, WebSocket session) to a standard ColdFusion session scope.

In my demo, I am storing a collection of active Session scopes within my Application scope. Then, within this new onWSSessionStart() event, I can use the passed-in CFID and CFTOKEN values in order to bind the given user to the appropriate Session scope:

NOTE: My WSApplication.cfc WebSocket proxy populates the Form scope with any custom headers that get passed-through with the WebSocket request. The CFID and CFTOKEN Form values in the following code are being passed-through as custom headers (which you will see later in the post).

Application.cfc - Our ColdFusion Application Framework Component

<cfscript>
// NOTE: CFScript tags added for Gist color-coding. Remove.

component
	output="false"
	hint="I define the application settings and event handlers."
	{


	// Define the application settings.
	this.name = hash( getCurrentTemplatePath() );
	this.applicationTimeout = createTimeSpan( 0, 0, 1, 0 );

	// Enable session management.
	this.sessionManagement = true;
	this.sessionTimeout = createTimeSpan( 0, 0, 1, 0 );

	// Set up the WebSocket channels.
	this.wsChannels = [
		{
			name: "demo",
			cfcListener: "WSApplication"
		}
	];


	// I initialize the application.
	function onApplicationStart(){

		// This is a collection of active sessions in the application
		// as determined by a hash of the session cookies.
		application.sessions = {};

		// Return true the request can be processed.
		return( true );

	}


	// I initialize the session.
	function onSessionStart(){

		// Add this session reference to the active, cached sessions
		// so we can easily keep track of the session across
		// different request mediums. In this case, we're hashing the
		// look-up key for the session so that we can pass it publicly
		// without concern.
		application.sessions[ hash( session.sessionID ) ] = session;

		// Set up the initial session values.
		session.isLoggedIn = false;
		session.name = "";

		// Return out.
		return;

	}


	// I execute the request.
	function onRequest(){

		// Check to see if the use is "logged-in". If not, we'll
		// force them into the login form.
		if (session.isLoggedIn){

			// Show standard form.
			include "index.cfm";

		} else {

			// Force the login.
			include "login.cfm";

		}

		// Return out.
		return;

	}


	// I teardown the session.
	function onSessionEnd( sessionScope, applicationScope ){

		// Remove the active session from the application cache.
		structDelete(
			applicationScope.sessions,
			hash( sessionScope.sessionID )
		);

		// Return out.
		return;

	}


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //
	// -- WebSocket Event Handlers -------------------------- //
	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// I initialize the WebSocket session.
	function onWSSessionStart( user ){

		// Param the form values for CFID and CFTOKEN. NOTE: These
		// are getting passed from the client as custom header and
		// be injected into the FORM scope in WSApplication.cfc.
		param name="form.cfid" type="string" default="";
		param name="form.cftoken" type="string" default="";

		// Create our session ID key for active sessions using the
		// application name and the session tokens.
		var sessionID = "#this.name#_#form.cfid#_#form.cftoken#";

		// Hash the sessionID - this is how we are keying our cached
		// session references.
		var sessionHash = hash( sessionID );

		// Check to see if we have an active standard session
		// associated with these session tokens.
		if (structKeyExists( application.sessions, sessionHash )){

			// Bind the STANDARD session to the WEBSOCKET session!!!
			user.session = application.sessions[ sessionHash ];

		}

		// Return out.
		return;

	}


	// I initialize the incoming WebSocket request. In this case
	// we're just gonna run through a number of scopes and data
	// points to see if they exist during a WebSocket request.
	function onWSRequestStart( type, channel, user ){

		// Check to see if this is a subscribe request. If so, let's
		// we have to make sure that the user is logged-in.
		if (
					!user.session.isLoggedIn
				&&
					(
						(type == "subscribe") ||
						(type == "publish")
					)
			){

			// User must log-in first.
			return( false );

		}

		// If we made it this far, return true so that the request
		// may be fully processed.
		return( true );

	}


	// I execute the WebSocket request.
	function onWSRequest( channel, publisher, message ){

		// Check to see if this is a server-initiated response. If
		// so, then the publisher won't have a valid clientID or a
		// session.
		if (publisher.clientID){

			// Prepend the FROM user's name.
			return( "[FROM: #publisher.session.name#] #message#" );

		// This is a server-initiated event.
		} else {

			// Prepend the Server's name.
			return( "[FROM: Server] #message#" );

		}

	}


	// I execute the response to the WebSocket subscriber.
	function onWSResponse( channel, subscriber, message ){

		// Prepend the TO user's name.
		return( "[TO: #subscriber.session.name#] #message#" );

	}


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// I log the arguments to the text file for debugging.
	function logData( data ){

		// Create a log file path for debugging.
		var logFilePath = (
			getDirectoryFromPath( getCurrentTemplatePath() ) &
			"log.txt"
		);

		// Dump to TXT file.
		writeDump( var=data, output=logFilePath );

	}


}

// NOTE: CFScript tags added for Gist color-coding. Remove.
</cfscript>

NOTE: As you may have seen, I make reference to a "login.cfm" page. I am not showing the code for this page in the demo; so, if you are interested, just watch the video.

As you can see, when the new WebSocket session is created, I am manually looking up the relevant ColdFusion session and then storing a reference to it within my WebSocket session. Since the WebSocket session is persisted across WebSocket requests, once this reference is established, it can then be referenced in subsequent WebSocket event handlers.

ColdFusion doesn't provide any native WebSocket session management - it only provides hooks regarding publication and subscribe events. As such, I needed to listen to all WebSocket events and then manually manage the WebSocket session behind the scenes in my WSApplication.cfc channel listener. In the following ColdFusion component, you can see that I examine every incoming WebSocket request for the persistent key, "websocketSessionID". If doesn't exist, I consider the WebSocket session to be new and I explicitly invoke the aforementioned onWSSessionStart() event handler.

WSApplication.cfc - Our WebSocket Proxy / Channel Listener

<cfscript>
// NOTE: CFScript tags added for Gist color-coding. Remove.

component
	extends="CFIDE.websocket.ChannelListener"
	output="true"
	hint="I define the application settings and event handlers."
	{


	// Store an instance of the Application.cfc that we'll use to
	// process these WebSocket requests. This component
	// (WSApplication), gets cached. As such, we'll have to re-
	// instantiate the target Application component at key points
	// during the lifecycle.
	this.application = {};


	// I teardown a subscription, removing any necessary settings
	// for the given subscriber.
	function afterUnsubscribe( requestInfo ){

		// Initialize the WebSocket request.
		this.prepareWebSocketRequest( requestInfo );

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSRequestStart )){

			// Nothing to do.
			return;

		}

		// Pass this off to the application for processing. Since
		// this has no bearing on the request, we don't have to
		// capture the response. This is purely a utilitarian call.
		this.application.onWSRequestStart(
			"unsubscribe",
			requestInfo.channelName,
			this.normalizeConnection( requestInfo.connectionInfo )
		);

		// Return out.
		return;

	}


	// I determine if the given user can publish the given information.
	function allowPublish( requestInfo ){

		// Initialize the WebSocket request.
		this.prepareWebSocketRequest( requestInfo );

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSRequestStart )){

			// Nothing to do.
			return( true );

		}

		// Pass this off to the application for processing.
		var result = this.application.onWSRequestStart(
			"publish",
			requestInfo.channelName,
			this.normalizeConnection( requestInfo.connectionInfo )
		);

		// Check to see if the request should be processed.
		if (
			isNull( result ) ||
			!isBoolean( result ) ||
			result
			){

			return( true );

		}

		// If we made it this far, the request should not processed.
		return( false );

	}


	// I determine if the given user can subscribe to the given channel.
	function allowSubscribe( requestInfo ){

		// Initialize the WebSocket request.
		this.prepareWebSocketRequest( requestInfo );

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSRequestStart )){

			// Nothing to do.
			return( true );

		}

		// Pass this off to the application for processing.
		var result = this.application.onWSRequestStart(
			"subscribe",
			requestInfo.channelName,
			this.normalizeConnection( requestInfo.connectionInfo )
		);

		// Check to see if the request should be processed.
		if (
			isNull( result ) ||
			!isBoolean( result ) ||
			result
			){

			return( true );

		}

		// If we made it this far, the request should not processed.
		return( false );

	}


	// I initialize the message publication, allowing an opportunity
	// to format and manipulate the message.
	function beforePublish( message, requestInfo ){

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSRequest )){

			// Nothing to do.
			return( message );

		}

		// Pass this off to the application for processing.
		var result = this.application.onWSRequest(
			requestInfo.channelName,
			this.normalizeConnection( requestInfo.connectionInfo ),
			message
		);

		// Return the new message.
		return( result );

	}


	// I initialize the message sending, allowing an opportunity to
	// format and manipulate a message before it is sent to the
	// given user.
	function beforeSendMessage( message, requestInfo ){

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSResponse )){

			// Nothing to do.
			return( message );

		}

		// Pass this off to the application for processing.
		var result = this.application.onWSResponse(
			requestInfo.channelName,
			this.normalizeConnection( requestInfo.connectionInfo ),
			message
		);

		// Return the new message.
		return( result );

	}


	// I determine if the given message should be sent to the given
	// client. This is invoked for EVERY client that is subscribed to
	// to the given channel.
	function canSendMessage( message, subscriberInfo, publisherInfo ){

		// Check to see if the application will process this event.
		if (isNull( this.application.onWSResponseStart )){

			// Nothing to do.
			return( true );

		}

		// Pass this off to the application for processing.
		var result = this.application.onWSResponseStart(
			subscriberInfo.channelName,
			this.normalizeConnection( subscriberInfo.connectionInfo ),
			this.normalizeConnection( publisherInfo.connectionInfo ),
			message
		);

		// Check to see if the response should be processed.
		if (
			isNull( result ) ||
			!isBoolean( result ) ||
			result
			){

			return( true );

		}

		// If we made it this far, the response should not processed.
		return( false );

	}


	// I normalize the connection infor making sure that is has the
	// following fields:
	//
	// - authenticated
	// - clientID
	// - connectionTime
	//
	// If a channel has no subscribers or a message is published from
	// the server, this information will be missing. To make
	// processing easier, we're just gonna fill it in with defaults.
	function normalizeConnection( connection ){

		// Check to see if this connection is missing information.
		if (isNull( connection.clientid )){

			// Normalize. We're using quoted values to mimic the JSON
			// keys that would have come across the connection.
			connection[ "authenticated" ] = "NO";
			connection[ "clientid" ] = 0;
			connection[ "connectiontime" ] = now();

		}

		// Return the normalized connection.
		return( connection );

	}


	// I populate the FORM scope using the given request information.
	function populateFormScope( requestInfo ){

		// Move everything from the request info into the Form scope,
		// as long as the key is not black-listed.
		structAppend(
			form,
			structFilter(
				requestInfo,
				function( key, value ){

					return(
						(key != "channelName") &&
						(key != "connectionInfo")
					);

				}
			)
		);

		// Return out.
		return;

	}


	// I prepare the incoming WebSocket request and WebSocket session.
	function prepareWebSocketRequest( requestInfo ){

		// First, populate the FORM scope with all the custom headers
		// in the request.
		this.populateFormScope( requestInfo );

		// Now, let's update the cached application scope.
		this.application = new Application();

		// Get the user/connection associated with the given request.
		// This will be the object persisted across all requests from
		// the given client.
		var user = requestInfo.connectionInfo;

		// Let's check to see if the WebSocket connection associated
		// the current request has been initialized.
		if (isNull( user.websocketSessionID )){

			// The WebSocket connection needs to be initialized.
			// Give it a session ID.
			user.websocketSessionID = "WS-SESSION-#createUUID()#";

			// Check to see if the application has an event handler
			// of the WebSocket session start.
			if (!isNull( this.application.onWSSessionStart )){

				// Initialize the session.
				this.application.onWSSessionStart( user );

			}

		}

		// Return out.
		return;

	}


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// I log the arguments to the text file for debugging.
	function logData( data ){

		// Create a log file path for debugging.
		var logFilePath = (
			getDirectoryFromPath( getCurrentTemplatePath() ) &
			"log.txt"
		);

		// Dump to TXT file.
		writeDump( var=data, output=logFilePath );

	}


}

// NOTE: CFScript tags added for Gist color-coding. Remove.
</cfscript>

Here, you can see how I am translating all the native WebSocket events into ColdFusion Application framework event handlers.

Now that you have a high-level understanding of how we're manually linking ColdFusion sessions to WebSocket sessions, let's take a look at the client-side code. For this to work, the client and the server have to be on the same page (no pun intended); that is, the client has to make sure to pass the CFID and CFTOKEN values along with the appropriate WebSocket requests.

In order to do this, I had to modify my ColdFusionWebSocket() AMD module to accept custom headers as part of its initialization. These headers are then appended to subsequent publish() and subscribe() requests. I wanted to try and pass the CFID and CFTOKEN values implicitly; however, since ColdFusion session cookies are now HTTP-only by default, they cannot be accessed via JavaScript (ie. document.cookie). As such, the developer has to explicitly provide the CFID and CFTOKEN values as custom headers when creating the ColdFusionWebSocket() instance.

In order to make the ColdFusion application name and the CFID / CFTOKEN values available to the client-side controller, I am creating intermediary, global JavaScript variables at the top of my demo. I'm not thrilled with this approach; but, it seems like the only way to get it done.

index.cfm - Our Client Side User Interface (UI)

<!--- Turn off debugging output. It can't help us in WebSockets. --->
<cfsetting showdebugoutput="false" />

<!--- Reset the output buffer. --->
<cfcontent type="text/html; charset=utf-8" />

<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>Using ColdFusion 10 WebSocket Sessions</title>

	<script type="text/javascript">
		<cfoutput>

			// We need to pass the Application name through with the
			// WebSocket connection so ColdFusion knows which memory
			// space to access.
			var coldfusionAppName = "#getApplicationMetaData().name#";

			// In order to bind the WebSocket "session" to the stanard
			// "user" session, we're gonna pass through the session
			// identifiers (ID and Token). These are the SAME thing
			// that would be in the user's cookies.
			var coldfusionSession = {
				cfid: "#session.cfid#",
				cftoken: "#session.cftoken#"
			};

		</cfoutput>
	</script>

	<!--
		Load the script loader and boot-strapping code. In this
		demo, the "main" JavaScript file acts as a Controller for
		the following Demo interface.
	-->
	<script
		type="text/javascript"
		src="./js/lib/require/require.js"
		data-main="./js/main">
	</script>
</head>
<body>

	<h1>
		Publish A Message
	</h1>

	<p>
		<a href="#" class="publish">Publish something</a>
	</p>

	<p>
		<em>Messages show up in console-log</em>.
	</p>

</body>
</html>


<!---
	In a brief moment, publish something from the SERVER to the
	new user.

	NOTE: This will actually go to ALL users who are subscribed to
	the demo channel - for the demo, it's just me, though.
--->
<cfthread
	name="publishFromServer"
	action="run">

	<!--- Give the client-side WebSocket time to connect. --->
	<cfset sleep( 1500 ) />

	<!--- Publish welcome message. --->
	<cfset wsPublish( "demo", "Welcome to my App!" ) />

</cfthread>

In this demo, not only am I providing a way for the client to publish a message, I'm also explicitly pushing a message from the server down to the client. As you saw in the Application.cfc WS-based event handlers, the WebSocket messages are augmented with [TO] and [FROM] data based on the associated ColdFusion session.

Now, let's look at our JavaScript page controller; this instantiates the ColdFusionWebSocket() AMD module and makes sure that the given CFID and CFTOKEN values are defined as custom request headers:

main.js - Our JavaScript Page Controller

// Define the paths to be used in the script mappings. Also, define
// the named module for certain libraries that are AMD compliant.
require.config({
	baseUrl: "js/",
	paths: {
		"domReady": "lib/require/domReady",
		"jquery": "lib/jquery/jquery-1.7.1",
		"order": "lib/require/order",
		"text": "lib/require/text",
	}
});


// Load the application. In order for the demo controller to
// run, we need to wait for jQuery and the CFWebSocket module to
// become available.
require(
	[
		"jquery",
		"../../../cfwebsocket",
		"domReady"
	],
	function( $, ColdFusionWebSocket ){


		// Cache the DOM elements that we'll need in this demo.
		var dom = {};
		dom.publish = $( "a.publish" );


		// Create an instance of our ColdFusion WebSocket module
		// and subscribe to the "Demo" channel. We are setting the
		// CFID and CFTOKEN values as custom headers that will be
		// passed-through with each socket request. This way, we
		// can bind the WebSocket session to the native ColdFusion
		// session.
		var socket = new ColdFusionWebSocket(
			coldfusionAppName,
			"demo",
			{
				cfid: coldfusionSession.cfid,
				cftoken: coldfusionSession.cftoken
			}
		);


		// Listen for published messages on the "Demo" channel.
		socket.on(
			"message",
			"demo",
			function( event, data ){

				console.log( "Published:", data );

			}
		);


		// Listen for publish errors.
		socket.on(
			"error",
			function( event, message ){

				console.log( "Error:", message );

			}
		);


		// Bind to the publish link so this client can broadcast a
		// message to all subscribed users.
		dom.publish.click(
			function( event ){

				// Kill the default click behavior - this is not a
				// real link.
				event.preventDefault();

				// Publish something!
				socket.publish( "demo", "This is a test message." );

			}
		);


	}
);

As you can see, the only thing special about this controller is the way in which the ColdFusionWebSocket() module is instantiated; other than that, it's nothing more than event bindings on the UI and the WebSocket connection.

In order for this to work, we have to perform coordinated efforts on both the client and the server. As you can see, however, the bulk of the effort is definitely offloaded to the server. As long as the client passes the session identifiers through with the WebSocket requests, the client should be good to go; it's the server that has to worry about how to keep track of ColdFusion sessions and then how to link them to WebSocket sessions. But, when all is said and done, it's really not a huge effort at all.

It's probably worth mentioning that ColdFusion 10 does have some interesting new session management features: sessionRotate() and sessionInvalidate(). I'm not yet sure how direct references to the Session scope will behave in the long-term when used in conjunction with these two methods. I'll have to do more research into that.

This demo and the ColdFusionWebSocket() module are available on my GitHub account.

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

Reader Comments

1 Comments

Hi I have a python server on my machine binding a socket to its IP ADDRESS and to a port that I chose(50001) I would like to build a simple web application that allows me only to send a string from a websocket to my raw TCP socket what should I do?

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