socketcluster-server
Version:
Server module for SocketCluster
1,475 lines (1,220 loc) • 127 kB
JavaScript
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;