I just updated Chrome to the newest version since there was one to do so. So the version I am on atm is: Version 152.0.7977.65 (Official Build) (64-bit)
When the new page load there is no lingering socket. It shows only 1 socket connection going out.
As for the code, it is pretty simple now as I spent some time tearing it apart to figure out what was going wrong. The Websocket.cfc is almost a duplicate of what is posted in the documentation.
Websocket.cfc
component hint="websockets"
{
Application.datasourceName = 'mytable';
Application.setKeyDataPath = "/home/test/domains/dev.test.com/keyData";
Application.usePBKDF = true;
Application.password = toBinary( SecretProviderGet("appPassword") );
Application.salt = toBinary( SecretProviderGet("appSalt") );
Application.keyRingFilename = toBinary( SecretProviderGet("appKeyRingFilename") );
Application.keyRingPath = SecretProviderGet("appKeyRingPath") & hash( toString( application.keyRingFilename, 'UTF-8' ), 'MD5', 'UTF-8', 142 ) & '.bin';
Application.engine = server.coldfusion.productname;
Application.masterKey = generatePBKDFKey( 'PBKDF2WithHmacSHA1', toString( Application.password, 'UTF-8' ), toString( Application.salt, 'UTF-8' ), 2048, 128 );
Application.developmentHmacKey = SecretProviderGet("appDevelopmentHmacKey");
Application.securityService = new cfc.model.services.EncDecService(
keyRingPath = application.keyRingPath,
masterKey = application.masterKey
);
Application.keyRing = Application.securityService.readKeyRingFromDisk();
Application.securityService = Application.securityService.init(
encryptionKey1 = Application.keyRing[1].key,
encryptionAlgorithm1 = Application.keyRing[1].alg,
encryptionEncoding1 = Application.keyRing[1].enc,
encryptionIV1 = binaryDecode( Application.keyRing[1].iv, 'BASE64' ),
encryptionKey2 = Application.keyRing[2].key,
encryptionAlgorithm2 = Application.keyRing[2].alg,
encryptionEncoding2 = Application.keyRing[2].enc,
encryptionIV2 = binaryDecode( Application.keyRing[2].iv, 'BASE64' ),
encryptionKey3 = Application.keyRing[3].key,
encryptionAlgorithm3 = Application.keyRing[3].alg,
encryptionEncoding3 = Application.keyRing[3].enc,
encryptionIV3 = binaryDecode( Application.keyRing[3].iv, 'BASE64' ),
hmacKey = ( ( Application.securityService.getEnvironment() eq 'prod' ) ? generateSecretKey( 'HMACSHA512' ) : application.developmentHmacKey ),
hmacAlgorithm = 'HMACSHA512',
hmacEncoding = 'UTF-8',
scN = 16384,
scR = 16,
scP = 1
);
// clear out temp keys from the application scope
for( item in [ 'password', 'salt', 'keyRingFilename', 'keyRingPath', 'keyRing', 'masterKey', 'developmentHmacKey', 'usePBKDF' ] ) {
structDelete( application, item );
}
static {
clientsByUser = {}; // userId -> wsClient
rolesByUser = {}; // userId -> array of roles
dsName = 'mytable'
}
public static function onFirstOpen( wsclients )
{
}
function onOpen( wsclient )
{
//as of snapshot 3.0.0.20 there is no access to client Session Scope or Server Application Scope.
//Session in websockets is only the session for that websocket
//static.wsclients.broadcast("There are now #static.wsclients.size()# connections");
//onOpen(), we have no way of identifying who just made contact, need to wait for the 1st message with information about user to be sent
//arguments.wsclient.send( 'incoming' );
lock name="socketOpen" timeout="30" throwontimeout="no" type="exclusive"
{
var userId = getUserIdFromRequest();//really is the socketID
dt = {}
dt.type = 0;
dt.message = '';
if( userId <= 0)
{
dt.message = 'Denied';
dt.userId = userId;
arguments.wsClient.send(serializeJSON(dt));
arguments.wsClient.close();
return;
}
static.clientsByUser[ userId ] = arguments.wsClient;
//static.rolesByUser[ userId ] = getRolesForUser( userId );
arguments.wsClient.send( serializeJSON(wsclient) );
arguments.wsClient.send( serializeJSON(session) );
arguments.wsClient.send( serializeJSON(cgi) );
arguments.wsClient.send( serializeJSON(userId) );
}
}
function onOpenAsync( wsclient )
{
}
function onMessage( wsclient, message )
{
if( isJSON(arguments.message))
{
local.msg = deserializeJSON(arguments.message);
//receiving a non init message from client, this should contain no data, just used to keepAlive
//update users activeCon section
if( structKeyExists(msg, "heartbeat") )
{
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
local.found = 0;
for ( var wsI in wsInstances )
{
if( structKeyExists(wsI.session.requestParameter, 'uuid'))
{
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'SELECT ID, userID
FROM websocket
WHERE token = :token AND userID = :userID AND websocketID = :websocketID';
queryService.setSQL(sql);
queryService.addParam( name='token', value='#wsI.session.requestParameter.uuid[1]#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
queryService.addParam( name='websocketID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
local.qFind = queryService.execute().getResult();
if( qFind.recordcount > 0)
{
//found the userID who sent the message
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE users
SET activeCon = :activeCon
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
//update the websocket time too
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE websocket
SET lastModified = :webService
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
local.dt = {};
dt.type = 0;
dt.heartbeat = 1;
dt.socketID = wsI.session.id;
dt.uuid = wsI.session.requestParameter.uuid[1];
arguments.wsclient.send( serializeJSON(dt) );
found++;
break;
}
}
catch(any e)
{
//close connection, there is an error
arguments.wsclient.send( e.Message );
arguments.wsclient.close();
}
}
else if(structKeyExists(wsI.session.requestParameter, 'ses'))
{
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'SELECT ID, userID
FROM websocket
WHERE token = :token AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='token', value='#wsI.session.requestParameter.ses[1]#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
local.qFind = queryService.execute().getResult();
if( qFind.recordcount > 0)
{
//found the userID who sent the message
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE users
SET activeCon = :activeCon
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
//update the websocket time too
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE websocket
SET lastModified = :webService
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
local.dt = {};
dt.type = 0;
dt.heartbeat = 1;
arguments.wsclient.send( serializeJSON(dt) );
found++;
break;
}
}
catch(any e)
{
//close connection, there is an error
arguments.wsclient.send( e.Message );
arguments.wsclient.close();
}
}
else
{
//close connection, something is wrong
arguments.wsclient.close();
}
}
if( found == 0)
{
arguments.wsclient.close();
}
}
}
<!------>
local.dt = {};
return serializeJSON(dt);
}
function onClose( wsclient, reasonPhrase )
{
var userId = getUserIdFromRequest();
structDelete( static.clientsByUser, userId );
structDelete( static.rolesByUser, userId );
//delete specfic websocket from db because a new one will be started on refresh/close
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
for ( var wsI in wsInstances )
{
//wsI.session has all info
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'DELETE FROM websocket
WHERE websocketID = :webID AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
local.qClose = queryService.execute().getResult();
}
catch(any e)
{
//failed
arguments.wsclient.send( e.Message );
}
}
}
function onError( wsclient, cfcatch )
{
}
public static function onLastClose()
{
}
// --- static helpers callable from anywhere in the app ---
public static boolean function sendToUser( required string websocketID, required any message )
{
if ( !structKeyExists( static.clientsByUser, arguments.websocketID ) )
{
try
{
//not here, remove from DB
local.queryService = new query(datasource = static.dsName);
local.sql = 'DELETE FROM websocket
WHERE websocketID = :websocketID';
queryService.setSQL(sql);
queryService.addParam( name='websocketID', value='#arguments.websocketID#', cfsqltype='cf_sql_longvarchar');
local.qDel = queryService.execute();
}
catch(any e)
{
//fail silently
}
}
else
{
var cl = static.clientsByUser[ arguments.websocketID ];
if ( !cl.isOpen() )
{
try
{
structDelete( static.clientsByUser, arguments.websocketID );
local.queryService = new query(datasource = static.dsName);
local.sql = 'DELETE FROM websocket
WHERE websocketID = :websocketID';
queryService.setSQL(sql);
queryService.addParam( name='websocketID', value='#arguments.websocketID#', cfsqltype='cf_sql_longvarchar');
local.qDel = queryService.execute();
}
catch(any e)
{
//fail silently
}
}
else
{
cl.send( arguments.message );
}
}
//collect all the socketIDs to send a message too
/*
local.queryService = new query(datasource = static.dsName);
local.sql = 'SELECT ID, websocketID
FROM websocket
WHERE userID = :userID AND websocketService = :socketType';
queryService.setSQL(sql);
queryService.addParam( name='userID', value='#arguments.userID#', cfsqltype='cf_sql_bigint');
queryService.addParam( name='socketType', value='#arguments.socketType#', cfsqltype='cf_sql_longvarchar');
local.qSockets = queryService.execute().getResult();
cfloop( query="qSockets" )
{
if ( !structKeyExists( static.clientsByUser, websocketID ) )
{
try
{
//not here, remove from DB
local.queryService = new query(datasource = static.dsName);
local.sql = 'DELETE FROM websocket
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='ID', value='#ID#', cfsqltype='cf_sql_bigint');
local.qDel = queryService.execute();
}
catch(any e)
{
//fail silently
}
}
else
{
var cl = static.clientsByUser[ websocketID ];
if ( !cl.isOpen() )
{
try
{
structDelete( static.clientsByUser, websocketID );
//not here, remove from DB
local.queryService = new query(datasource = static.dsName);
local.sql = 'DELETE FROM websocket
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='ID', value='#ID#', cfsqltype='cf_sql_bigint');
local.qDel = queryService.execute();
}
catch(any e)
{
//fail silently
}
}
else
{
cl.send( arguments.message );
}
}
}
*/
return true;
}
public static void function sendToRole( required string role, required any message )
{
for ( var userId in static.clientsByUser ) {
if ( arrayFind( static.rolesByUser[ userId ], arguments.role ) && static.clientsByUser[ userId ].isOpen() )
static.clientsByUser[ userId ].send( arguments.message );
}
}
// --- your auth integration ---
private string function getUserIdFromRequest()
{
//get the cgi request to see who is requesting a socket connection
local.cgiAr = ListToArray(cgi.QUERY_STRING,'&');
if( len(local.cgiAr) != 2)
{
return 0;
}
local.uuid = cgiAr[1].ListLast( "=" );
local.userID = cgiAr[2].ListLast("=");
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
for ( var wsI in wsInstances )
{
if( !structKeyExists(wsI.session.requestParameter, "uuid") )
{
return 0;
}
if( !structKeyExists(wsI.session.requestParameter, "userID") )
{
return 0;
}
local.socketUUID = wsI.session.requestParameter.uuid[1];
local.socketUserID = wsI.session.requestParameter.userID[1];
if( local.socketUUID == local.uuid && local.socketUserID == local.userID )
{
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'SELECT ID, userID
FROM websocket
WHERE token = :token AND websocketID = 0 AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='token', value='#local.uuid#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#local.userID#', cfsqltype='cf_sql_bigint');
local.qFind = queryService.execute().getResult();
if( qFind.recordcount > 0 )
{
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE websocket
SET websocketID = :webID
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_longvarchar');
local.qUpdate = queryService.execute();
return wsI.session.id;
}
}
catch(any e)
{
//failed
return 0;
}
}
}
return 0;
}
private array function getRolesForUser( required string userId )
{
// add your business logic here — look up roles for this user from your
// database, auth provider, etc.
return [ "user" ];
}
}
And the socket connector .cfm
<cfset sesID = "" />
<cfset thisUUID = CreateUUID() />
<cfif structKeyExists(Session, "readingObj")>
<cfset sesID = Session.readingObj.getReadingSessionId() />
</cfif>
<cfdump var="#sesID#" />
<cfif sesID NEQ "">
<cfset thisUUID = sesID />
</cfif>
<cfscript>
queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
sql = 'INSERT INTO websocket
( userID, token, websocketService, websocketID )
VALUE
( :userID, :token, :msg, 0)';
queryService.setSQL(sql);
queryService.addParam( name='userID', value='#Session.sessionObj.getUserId()#', cfsqltype='cf_sql_bigint');
queryService.addParam( name='token', value='#thisUUID#', cfsqltype='cf_sql_varchar');
queryService.addParam( name='msg', value='GENERAL', cfsqltype='cf_sql_varchar');
qWeb = queryService.execute();
</cfscript>
<cfdump var="#thisUUID#" />
<script type="text/javascript">
console.log('open socket');
_socket = new WebSocket("wss://<cfoutput>#Application.websocket#/ws/websocket?uuid=#thisUUID#&userID=#Session.sessionObj.getUserId()#</cfoutput>");
var _runTimer;
_socket.onopen = function(evt)
{
console.log("Connected");
stayActive();
//start an interal to send a ping every 1 minute to maintain heartbeat
};
function stayActive()
{
_runTimer = setInterval(stayConnected, 5000);// was 60000
}
function stayConnected()
{
console.log('stayConnected Called from somewhere!');
if(_socket)
{
let o = {}
o.heartbeat = 1;
o.userID = _socketUserID;
if( o.userID != 0)
{
_socket.send(JSON.stringify(o));
}
}
}
if(typeof _socket !== 'undefined')
{
_awaitingAnswer = 0;
_socket.onmessage = function(event)
{
console.log("Received onMessage:", event.data);
//console.log( JSON.parse(event.data) );
let resp = JSON.parse(event.data);
if( resp.type == 0)
{
//heartbeat response
}
};
_socket.onclose = function(evt)
{
console.log('closed me off now');
console.log(evt);
if(typeof(runTimer) !== 'undefined')
{
console.log('inverval should be cleared')
clearInterval(runTimer);
}
console.log("Connection closed");
};
_socket.onerror = function(error)
{
console.error("WebSocket error:", error);
};
}
</script>
The above code is used as an include on all pages, so it is the same code that runs everytime a page loads.
The reason the heartbeat is there is to keep the connection active, otherwise the browsers disconnect after a few minutes. Disabling it only causes the connection to close at some point. If I turn it off and use the form on the page to send a message to a user gives the same problem, the old socket is used, but eventually it does become the right socket.