UNPKG

socketcluster-server

Version:
1,475 lines (1,220 loc) 127 kB
const assert = require('assert'); const socketClusterServer = require('../'); const AGAction = require('../action'); const socketClusterClient = require('socketcluster-client'); const localStorage = require('localStorage'); const AGSimpleBroker = require('ag-simple-broker'); // Add to the global scope like in browser. global.localStorage = localStorage; let clientOptions; let serverOptions; let allowedUsers = { bob: true, alice: true }; const PORT_NUMBER = 8008; const WS_ENGINE = 'ws'; const LOG_WARNINGS = false; const LOG_ERRORS = false; const TEN_DAYS_IN_SECONDS = 60 * 60 * 24 * 10; let validSignedAuthTokenBob = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYiIsImV4cCI6MzE2Mzc1ODk3OTA4MDMxMCwiaWF0IjoxNTAyNzQ3NzQ2fQ.dSZOfsImq4AvCu-Or3Fcmo7JNv1hrV3WqxaiSKkTtAo'; let validSignedAuthTokenAlice = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWNlIiwiaWF0IjoxNTE4NzI4MjU5LCJleHAiOjMxNjM3NTg5NzkwODAzMTB9.XxbzPPnnXrJfZrS0FJwb_EAhIu2VY5i7rGyUThtNLh4'; let invalidSignedAuthToken = 'fakebGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fakec2VybmFtZSI6ImJvYiIsImlhdCI6MTUwMjYyNTIxMywiZXhwIjoxNTAyNzExNjEzfQ.fakemYcOOjM9bzmS4UYRvlWSk_lm3WGHvclmFjLbyOk'; let server, client; function wait(duration) { return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); }); } async function resolveAfterTimeout(duration, value) { await wait(duration); return value; }; function connectionHandler(socket) { (async () => { for await (let rpc of socket.procedure('login')) { if (rpc.data && allowedUsers[rpc.data.username]) { socket.setAuthToken(rpc.data); rpc.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; rpc.error(err); } } })(); (async () => { for await (let rpc of socket.procedure('loginWithTenDayExpiry')) { if (allowedUsers[rpc.data.username]) { socket.setAuthToken(rpc.data, { expiresIn: TEN_DAYS_IN_SECONDS }); rpc.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; rpc.error(err); } } })(); (async () => { for await (let rpc of socket.procedure('loginWithTenDayExp')) { if (allowedUsers[rpc.data.username]) { rpc.data.exp = Math.round(Date.now() / 1000) + TEN_DAYS_IN_SECONDS; socket.setAuthToken(rpc.data); rpc.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; rpc.error(err); } } })(); (async () => { for await (let rpc of socket.procedure('loginWithTenDayExpAndExpiry')) { if (allowedUsers[rpc.data.username]) { rpc.data.exp = Math.round(Date.now() / 1000) + TEN_DAYS_IN_SECONDS; socket.setAuthToken(rpc.data, { expiresIn: TEN_DAYS_IN_SECONDS * 100 // 1000 days }); rpc.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; rpc.error(err); } } })(); (async () => { for await (let rpc of socket.procedure('loginWithIssAndIssuer')) { if (allowedUsers[rpc.data.username]) { rpc.data.iss = 'foo'; try { await socket.setAuthToken(rpc.data, { issuer: 'bar' }); } catch (err) {} rpc.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; rpc.error(err); } } })(); (async () => { for await (let rpc of socket.procedure('setAuthKey')) { server.signatureKey = rpc.data; server.verificationKey = rpc.data; rpc.end(); } })(); (async () => { for await (let rpc of socket.procedure('proc')) { rpc.end('success ' + rpc.data); } })(); }; function bindFailureHandlers(server) { if (LOG_ERRORS) { (async () => { for await (let {error} of server.listener('error')) { console.error('ERROR', error); } })(); } if (LOG_WARNINGS) { (async () => { for await (let {warning} of server.listener('warning')) { console.warn('WARNING', warning); } })(); } } describe('Integration tests', function () { beforeEach('Prepare options', async function () { clientOptions = { hostname: '127.0.0.1', port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }; serverOptions = { authKey: 'testkey', wsEngine: WS_ENGINE }; }); afterEach('Close server and client after each test', async function () { if (client) { client.closeAllListeners(); client.disconnect(); } if (server) { server.closeAllListeners(); server.httpServer.close(); await server.close(); } global.localStorage.removeItem('socketcluster.authToken'); }); describe('Client authentication', function () { beforeEach('Run the server before start', async function () { server = socketClusterServer.listen(PORT_NUMBER, serverOptions); bindFailureHandlers(server); server.setMiddleware(server.MIDDLEWARE_INBOUND, async (middlewareStream) => { for await (let action of middlewareStream) { if ( action.type === AGAction.AUTHENTICATE && (!action.authToken || action.authToken.username === 'alice') ) { let err = new Error('Blocked by MIDDLEWARE_INBOUND'); err.name = 'AuthenticateMiddlewareError'; action.block(err); continue; } action.allow(); } }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); }); it('Should not send back error if JWT is not provided in handshake', async function () { client = socketClusterClient.create(clientOptions); let event = await client.listener('connect').once(); assert.equal(event.authError === undefined, true); }); it('Should be authenticated on connect if previous JWT token is present', async function () { client = socketClusterClient.create(clientOptions); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); await client.listener('authenticate').once(); assert.equal(client.authState, 'authenticated'); client.disconnect(); client.connect(); let event = await client.listener('connect').once(); assert.equal(event.isAuthenticated, true); assert.equal(event.authError === undefined, true); }); it('Should send back error if JWT is invalid during handshake', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); client = socketClusterClient.create(clientOptions); await client.listener('connect').once(); // Change the setAuthKey to invalidate the current token. await client.invoke('setAuthKey', 'differentAuthKey'); client.disconnect(); client.connect(); let event = await client.listener('connect').once(); assert.equal(event.isAuthenticated, false); assert.notEqual(event.authError, null); assert.equal(event.authError.name, 'AuthTokenInvalidError'); }); it('Should allow switching between users', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); let authenticateEvents = []; let deauthenticateEvents = []; let authenticationStateChangeEvents = []; let authStateChangeEvents = []; (async () => { for await (let stateChangePacket of server.listener('authenticationStateChange')) { authenticationStateChangeEvents.push(stateChangePacket); } })(); (async () => { for await (let {socket} of server.listener('connection')) { (async () => { for await (let {authToken} of socket.listener('authenticate')) { authenticateEvents.push(authToken); } })(); (async () => { for await (let {oldAuthToken} of socket.listener('deauthenticate')) { deauthenticateEvents.push(oldAuthToken); } })(); (async () => { for await (let stateChangeData of socket.listener('authStateChange')) { authStateChangeEvents.push(stateChangeData); } })(); } })(); let clientSocketId; client = socketClusterClient.create(clientOptions); await client.listener('connect').once(); clientSocketId = client.id; client.invoke('login', {username: 'alice'}); await wait(100); assert.equal(deauthenticateEvents.length, 0); assert.equal(authenticateEvents.length, 2); assert.equal(authenticateEvents[0].username, 'bob'); assert.equal(authenticateEvents[1].username, 'alice'); assert.equal(authenticationStateChangeEvents.length, 1); assert.notEqual(authenticationStateChangeEvents[0].socket, null); assert.equal(authenticationStateChangeEvents[0].socket.id, clientSocketId); assert.equal(authenticationStateChangeEvents[0].oldAuthState, 'unauthenticated'); assert.equal(authenticationStateChangeEvents[0].newAuthState, 'authenticated'); assert.notEqual(authenticationStateChangeEvents[0].authToken, null); assert.equal(authenticationStateChangeEvents[0].authToken.username, 'bob'); assert.equal(authStateChangeEvents.length, 1); assert.equal(authStateChangeEvents[0].oldAuthState, 'unauthenticated'); assert.equal(authStateChangeEvents[0].newAuthState, 'authenticated'); assert.notEqual(authStateChangeEvents[0].authToken, null); assert.equal(authStateChangeEvents[0].authToken.username, 'bob'); }); it('Should emit correct events/data when socket is deauthenticated', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); let authenticationStateChangeEvents = []; let authStateChangeEvents = []; (async () => { for await (let stateChangePacket of server.listener('authenticationStateChange')) { authenticationStateChangeEvents.push(stateChangePacket); } })(); client = socketClusterClient.create(clientOptions); (async () => { for await (let event of client.listener('connect')) { client.deauthenticate(); } })(); let {socket} = await server.listener('connection').once(); let initialAuthToken = socket.authToken; (async () => { for await (let stateChangeData of socket.listener('authStateChange')) { authStateChangeEvents.push(stateChangeData); } })(); let {oldAuthToken} = await socket.listener('deauthenticate').once(); assert.equal(oldAuthToken, initialAuthToken); assert.equal(authStateChangeEvents.length, 2); assert.equal(authStateChangeEvents[0].oldAuthState, 'unauthenticated'); assert.equal(authStateChangeEvents[0].newAuthState, 'authenticated'); assert.notEqual(authStateChangeEvents[0].authToken, null); assert.equal(authStateChangeEvents[0].authToken.username, 'bob'); assert.equal(authStateChangeEvents[1].oldAuthState, 'authenticated'); assert.equal(authStateChangeEvents[1].newAuthState, 'unauthenticated'); assert.equal(authStateChangeEvents[1].authToken, null); assert.equal(authenticationStateChangeEvents.length, 2); assert.notEqual(authenticationStateChangeEvents[0], null); assert.equal(authenticationStateChangeEvents[0].oldAuthState, 'unauthenticated'); assert.equal(authenticationStateChangeEvents[0].newAuthState, 'authenticated'); assert.notEqual(authenticationStateChangeEvents[0].authToken, null); assert.equal(authenticationStateChangeEvents[0].authToken.username, 'bob'); assert.notEqual(authenticationStateChangeEvents[1], null); assert.equal(authenticationStateChangeEvents[1].oldAuthState, 'authenticated'); assert.equal(authenticationStateChangeEvents[1].newAuthState, 'unauthenticated'); assert.equal(authenticationStateChangeEvents[1].authToken, null); }); it('Should throw error if server socket deauthenticate is called after client disconnected and rejectOnFailedDelivery is true', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); client = socketClusterClient.create(clientOptions); let {socket} = await server.listener('connection').once(); client.disconnect(); let error; try { await socket.deauthenticate({rejectOnFailedDelivery: true}); } catch (err) { error = err; } assert.notEqual(error, null); assert.equal(error.name, 'BadConnectionError'); }); it('Should not throw error if server socket deauthenticate is called after client disconnected and rejectOnFailedDelivery is not true', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); client = socketClusterClient.create(clientOptions); let {socket} = await server.listener('connection').once(); client.disconnect(); socket.deauthenticate(); }); it('Should not authenticate the client if MIDDLEWARE_INBOUND blocks the authentication', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenAlice); client = socketClusterClient.create(clientOptions); // The previous test authenticated us as 'alice', so that token will be passed to the server as // part of the handshake. let event = await client.listener('connect').once(); // Any token containing the username 'alice' should be blocked by the MIDDLEWARE_INBOUND middleware. // This will only affects token-based authentication, not the credentials-based login event. assert.equal(event.isAuthenticated, false); assert.notEqual(event.authError, null); assert.equal(event.authError.name, 'AuthenticateMiddlewareError'); }); }); describe('Server authentication', function () { it('Token should be available after the authenticate listener resolves', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); await client.listener('authenticate').once(); assert.equal(client.authState, 'authenticated'); assert.notEqual(client.authToken, null); assert.equal(client.authToken.username, 'bob'); }); it('Authentication can be captured using the authenticate listener', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE, authTokenName: 'socketcluster.authToken' }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); await client.listener('authenticate').once(); assert.equal(client.authState, 'authenticated'); assert.notEqual(client.authToken, null); assert.equal(client.authToken.username, 'bob'); }); it('Previously authenticated client should still be authenticated after reconnecting', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); await client.listener('authenticate').once(); client.disconnect(); client.connect(); let event = await client.listener('connect').once(); assert.equal(event.isAuthenticated, true); assert.notEqual(client.authToken, null); assert.equal(client.authToken.username, 'bob'); }); it('Should set the correct expiry when using expiresIn option when creating a JWT with socket.setAuthToken', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('loginWithTenDayExpiry', {username: 'bob'}); await client.listener('authenticate').once(); assert.notEqual(client.authToken, null); assert.notEqual(client.authToken.exp, null); let dateMillisecondsInTenDays = Date.now() + TEN_DAYS_IN_SECONDS * 1000; let dateDifference = Math.abs(dateMillisecondsInTenDays - client.authToken.exp * 1000); // Expiry must be accurate within 1000 milliseconds. assert.equal(dateDifference < 1000, true); }); it('Should set the correct expiry when adding exp claim when creating a JWT with socket.setAuthToken', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('loginWithTenDayExp', {username: 'bob'}); await client.listener('authenticate').once(); assert.notEqual(client.authToken, null); assert.notEqual(client.authToken.exp, null); let dateMillisecondsInTenDays = Date.now() + TEN_DAYS_IN_SECONDS * 1000; let dateDifference = Math.abs(dateMillisecondsInTenDays - client.authToken.exp * 1000); // Expiry must be accurate within 1000 milliseconds. assert.equal(dateDifference < 1000, true); }); it('The exp claim should have priority over expiresIn option when using socket.setAuthToken', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('loginWithTenDayExpAndExpiry', {username: 'bob'}); await client.listener('authenticate').once(); assert.notEqual(client.authToken, null); assert.notEqual(client.authToken.exp, null); let dateMillisecondsInTenDays = Date.now() + TEN_DAYS_IN_SECONDS * 1000; let dateDifference = Math.abs(dateMillisecondsInTenDays - client.authToken.exp * 1000); // Expiry must be accurate within 1000 milliseconds. assert.equal(dateDifference < 1000, true); }); it('Should send back error if socket.setAuthToken tries to set both iss claim and issuer option', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); let warningMap = {}; (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); (async () => { await client.listener('authenticate').once(); throw new Error('Should not pass authentication because the signature should fail'); })(); (async () => { for await (let {warning} of server.listener('warning')) { assert.notEqual(warning, null); warningMap[warning.name] = warning; } })(); (async () => { for await (let {error} of server.listener('error')) { assert.notEqual(error, null); assert.equal(error.name, 'SocketProtocolError'); } })(); let closePackets = []; (async () => { let event = await client.listener('close').once(); closePackets.push(event); })(); let error; try { await client.invoke('loginWithIssAndIssuer', {username: 'bob'}); } catch (err) { error = err; } assert.notEqual(error, null); assert.equal(error.name, 'BadConnectionError'); await wait(1000); assert.equal(closePackets.length, 1); assert.equal(closePackets[0].code, 4002); server.closeListener('warning'); assert.notEqual(warningMap['SocketProtocolError'], null); }); it('Should trigger an authTokenSigned event and socket.signedAuthToken should be set after calling the socket.setAuthToken method', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); let authTokenSignedEventEmitted = false; (async () => { for await (let {socket} of server.listener('connection')) { (async () => { for await (let {signedAuthToken} of socket.listener('authTokenSigned')) { authTokenSignedEventEmitted = true; assert.notEqual(signedAuthToken, null); assert.equal(signedAuthToken, socket.signedAuthToken); } })(); (async () => { for await (let req of socket.procedure('login')) { if (allowedUsers[req.data.username]) { socket.setAuthToken(req.data); req.end(); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; req.error(err); } } })(); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); await Promise.all([ client.invoke('login', {username: 'bob'}), client.listener('authenticate').once() ]); assert.equal(authTokenSignedEventEmitted, true); }); it('The socket.setAuthToken call should reject if token delivery fails and rejectOnFailedDelivery option is true', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE, ackTimeout: 1000 }); bindFailureHandlers(server); let serverWarnings = []; (async () => { await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); })(); let {socket} = await server.listener('connection').once(); (async () => { for await (let {warning} of server.listener('warning')) { serverWarnings.push(warning); } })(); let req = await socket.procedure('login').once(); if (allowedUsers[req.data.username]) { req.end(); socket.disconnect(); let error; try { await socket.setAuthToken(req.data, {rejectOnFailedDelivery: true}); } catch (err) { error = err; } assert.notEqual(error, null); assert.equal(error.name, 'AuthError'); await wait(0); assert.notEqual(serverWarnings[0], null); assert.equal(serverWarnings[0].name, 'BadConnectionError'); assert.notEqual(serverWarnings[1], null); assert.equal(serverWarnings[1].name, 'AuthError'); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; req.error(err); } }); it('The socket.setAuthToken call should not reject if token delivery fails and rejectOnFailedDelivery option is not true', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE, ackTimeout: 1000 }); bindFailureHandlers(server); let serverWarnings = []; (async () => { await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); await client.listener('connect').once(); client.invoke('login', {username: 'bob'}); })(); let {socket} = await server.listener('connection').once(); (async () => { for await (let {warning} of server.listener('warning')) { serverWarnings.push(warning); } })(); let req = await socket.procedure('login').once(); if (allowedUsers[req.data.username]) { req.end(); socket.disconnect(); let error; try { await socket.setAuthToken(req.data); } catch (err) { error = err; } assert.equal(error, null); await wait(0); assert.notEqual(serverWarnings[0], null); assert.equal(serverWarnings[0].name, 'BadConnectionError'); } else { let err = new Error('Failed to login'); err.name = 'FailedLoginError'; req.error(err); } }); it('The verifyToken method of the authEngine receives correct params', async function () { global.localStorage.setItem('socketcluster.authToken', validSignedAuthTokenBob); server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); (async () => { await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); })(); return new Promise((resolve) => { server.setAuthEngine({ verifyToken: async (signedAuthToken, verificationKey, verificationOptions) => { await wait(500); assert.equal(signedAuthToken, validSignedAuthTokenBob); assert.equal(verificationKey, serverOptions.authKey); assert.notEqual(verificationOptions, null); assert.notEqual(verificationOptions.socket, null); resolve(); return Promise.resolve({}); } }); }); }); it('Should remove client data from the server when client disconnects before authentication process finished', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); server.setAuthEngine({ verifyToken: function (signedAuthToken, verificationKey, verificationOptions) { return resolveAfterTimeout(500, {}); } }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let serverSocket; (async () => { for await (let {socket} of server.listener('handshake')) { serverSocket = socket; } })(); await wait(100); assert.equal(server.clientsCount, 0); assert.equal(server.pendingClientsCount, 1); assert.notEqual(serverSocket, null); assert.equal(Object.keys(server.pendingClients)[0], serverSocket.id); client.disconnect(); await wait(1000); assert.equal(Object.keys(server.clients).length, 0); assert.equal(server.clientsCount, 0); assert.equal(server.pendingClientsCount, 0); assert.equal(JSON.stringify(server.pendingClients), '{}'); }); it('Should close the connection if the client tries to send a malformatted authenticate packet', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let originalInvoke = client.invoke; client.invoke = async function (...args) { if (args[0] === '#authenticate') { client.transmit(args[0], args[1]); return; } return originalInvoke.apply(this, args); }; client.authenticate(validSignedAuthTokenBob) let results = await Promise.all([ server.listener('closure').once(500), client.listener('close').once(100) ]); assert.equal(results[0].code, 4008); assert.equal(results[0].reason, 'Server rejected handshake from client'); assert.equal(results[1].code, 4008); assert.equal(results[1].reason, 'Server rejected handshake from client'); }); }); describe('Socket handshake', function () { it('Exchange is attached to socket before the handshake event is triggered', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let {socket} = await server.listener('handshake').once(); assert.notEqual(socket.exchange, null); }); it('Should close the connection if the client tries to send a message before the handshake', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); client.transport.socket.onopen = function () { client.transport.socket.send(Buffer.alloc(0)); }; let results = await Promise.all([ server.listener('closure').once(200), client.listener('close').once(200) ]); assert.equal(results[0].code, 4009); assert.equal(results[0].reason, 'Server received a message before the client handshake'); assert.equal(results[1].code, 4009); assert.equal(results[1].reason, 'Server received a message before the client handshake'); }); it('Should close the connection if the client tries to send a ping before the handshake', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); client.transport.socket.onopen = function () { client.transport.socket.send(''); }; let {code: closeCode} = await client.listener('close').once(200); assert.equal(closeCode, 4009); }); it('Should close the connection if the client tries to send a malformatted handshake', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); client.transport._handshake = async function () { this.transmit('#handshake', {}, {force: true}); }; let results = await Promise.all([ server.listener('closure').once(200), client.listener('close').once(200) ]); assert.equal(results[0].code, 4008); assert.equal(results[0].reason, 'Server rejected handshake from client'); assert.equal(results[1].code, 4008); assert.equal(results[1].reason, 'Server rejected handshake from client'); }); it('Should not close the connection if the client tries to send a message before the handshake and strictHandshake is false', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE, strictHandshake: false }); (async () => { for await (let {socket} of server.listener('connection')) { connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let realOnOpenFunction = client.transport.socket.onopen; client.transport.socket.onopen = function () { client.transport.socket.send(Buffer.alloc(0)); return realOnOpenFunction.apply(this, arguments); }; let packet = await client.listener('connect').once(200); assert.notEqual(packet, null); assert.notEqual(packet.id, null); }); }); describe('Socket connection', function () { it('Server-side socket connect event and server connection event should trigger', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); let connectionEmitted = false; let connectionEvent; (async () => { for await (let event of server.listener('connection')) { connectionEvent = event; connectionHandler(event.socket); connectionEmitted = true; } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let connectEmitted = false; let connectStatus; let socketId; (async () => { for await (let {socket} of server.listener('handshake')) { (async () => { for await (let serverSocketStatus of socket.listener('connect')) { socketId = socket.id; connectEmitted = true; connectStatus = serverSocketStatus; // This is to check that mutating the status on the server // doesn't affect the status sent to the client. serverSocketStatus.foo = 123; } })(); } })(); let clientConnectEmitted = false; let clientConnectStatus = false; (async () => { for await (let event of client.listener('connect')) { clientConnectEmitted = true; clientConnectStatus = event; } })(); await wait(300); assert.equal(connectEmitted, true); assert.equal(connectionEmitted, true); assert.equal(clientConnectEmitted, true); assert.notEqual(connectionEvent, null); assert.equal(connectionEvent.id, socketId); assert.equal(connectionEvent.pingTimeout, server.pingTimeout); assert.equal(connectionEvent.authError, null); assert.equal(connectionEvent.isAuthenticated, false); assert.notEqual(connectStatus, null); assert.equal(connectStatus.id, socketId); assert.equal(connectStatus.pingTimeout, server.pingTimeout); assert.equal(connectStatus.authError, null); assert.equal(connectStatus.isAuthenticated, false); assert.notEqual(clientConnectStatus, null); assert.equal(clientConnectStatus.id, socketId); assert.equal(clientConnectStatus.pingTimeout, server.pingTimeout); assert.equal(clientConnectStatus.authError, null); assert.equal(clientConnectStatus.isAuthenticated, false); assert.equal(clientConnectStatus.foo, null); // Client socket status should be a clone of server socket status; not // a reference to the same object. assert.notEqual(clientConnectStatus.foo, connectStatus.foo); }); it('Server-side connection event should trigger with large number of concurrent connections', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); let connectionList = []; (async () => { for await (let event of server.listener('connection')) { connectionList.push(event); } })(); await server.listener('ready').once(); let clientList = []; for (let i = 0; i < 100; i++) { client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); clientList.push(client); } await wait(2000); assert.equal(connectionList.length, 100); for (let client of clientList) { client.disconnect(); } await wait(1000); }); it('Server should support large a number of connections invoking procedures concurrently immediately upon connect', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); let connectionCount = 0; let requestCount = 0; (async () => { for await (let { socket } of server.listener('connection')) { connectionCount++; (async () => { for await (let request of socket.procedure('greeting')) { requestCount++; await wait(20); request.end('hello'); } })(); } })(); await server.listener('ready').once(); let clientList = []; for (let i = 0; i < 100; i++) { client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, autoConnect: true, authTokenName: 'socketcluster.authToken' }); clientList.push(client); await client.invoke('greeting'); } await wait(2500); assert.equal(requestCount, 100); assert.equal(connectionCount, 100); for (let client of clientList) { client.disconnect(); } await wait(1000); }); }); describe('Socket disconnection', function () { it('Server-side socket disconnect event should not trigger if the socket did not complete the handshake; instead, it should trigger connectAbort', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); server.setAuthEngine({ verifyToken: function (signedAuthToken, verificationKey, verificationOptions) { return resolveAfterTimeout(500, {}); } }); let connectionOnServer = false; (async () => { for await (let {socket} of server.listener('connection')) { connectionOnServer = true; connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let socketDisconnected = false; let socketDisconnectedBeforeConnect = false; let clientSocketAborted = false; (async () => { let {socket} = await server.listener('handshake').once(); assert.equal(server.pendingClientsCount, 1); assert.notEqual(server.pendingClients[socket.id], null); (async () => { await socket.listener('disconnect').once(); if (!connectionOnServer) { socketDisconnectedBeforeConnect = true; } socketDisconnected = true; })(); (async () => { let event = await socket.listener('connectAbort').once(); clientSocketAborted = true; assert.equal(event.code, 4444); assert.equal(event.reason, 'Disconnect before handshake'); })(); })(); let serverDisconnected = false; let serverSocketAborted = false; (async () => { await server.listener('disconnection').once(); serverDisconnected = true; })(); (async () => { await server.listener('connectionAbort').once(); serverSocketAborted = true; })(); await wait(100); client.disconnect(4444, 'Disconnect before handshake'); await wait(1000); assert.equal(socketDisconnected, false); assert.equal(socketDisconnectedBeforeConnect, false); assert.equal(clientSocketAborted, true); assert.equal(serverSocketAborted, true); assert.equal(serverDisconnected, false); }); it('Server-side socket disconnect event should trigger if the socket completed the handshake (not connectAbort)', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); server.setAuthEngine({ verifyToken: function (signedAuthToken, verificationKey, verificationOptions) { return resolveAfterTimeout(10, {}); } }); let connectionOnServer = false; (async () => { for await (let {socket} of server.listener('connection')) { connectionOnServer = true; connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let socketDisconnected = false; let socketDisconnectedBeforeConnect = false; let clientSocketAborted = false; (async () => { let {socket} = await server.listener('handshake').once(); assert.equal(server.pendingClientsCount, 1); assert.notEqual(server.pendingClients[socket.id], null); (async () => { let event = await socket.listener('disconnect').once(); if (!connectionOnServer) { socketDisconnectedBeforeConnect = true; } socketDisconnected = true; assert.equal(event.code, 4445); assert.equal(event.reason, 'Disconnect after handshake'); })(); (async () => { let event = await socket.listener('connectAbort').once(); clientSocketAborted = true; })(); })(); let serverDisconnected = false; let serverSocketAborted = false; (async () => { await server.listener('disconnection').once(); serverDisconnected = true; })(); (async () => { await server.listener('connectionAbort').once(); serverSocketAborted = true; })(); await wait(200); client.disconnect(4445, 'Disconnect after handshake'); await wait(1000); assert.equal(socketDisconnectedBeforeConnect, false); assert.equal(socketDisconnected, true); assert.equal(clientSocketAborted, false); assert.equal(serverDisconnected, true); assert.equal(serverSocketAborted, false); }); it('The close event should trigger when the socket loses the connection before the handshake', async function () { server = socketClusterServer.listen(PORT_NUMBER, { authKey: serverOptions.authKey, wsEngine: WS_ENGINE }); bindFailureHandlers(server); server.setAuthEngine({ verifyToken: function (signedAuthToken, verificationKey, verificationOptions) { return resolveAfterTimeout(500, {}); } }); (async () => { for await (let {socket} of server.listener('connection')) { connectionOnServer = true; connectionHandler(socket); } })(); await server.listener('ready').once(); client = socketClusterClient.create({ hostname: clientOptions.hostname, port: PORT_NUMBER, authTokenName: 'socketcluster.authToken' }); let serverSocketClosed = false; let serverSocketAborted = false;