All files index.js

78.46% Statements 51/65
44.18% Branches 19/43
78.94% Functions 15/19
83.63% Lines 46/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122  1x 1x 1x 1x 1x   1x   1x     1x         1x 1x 1x 1x 1x 1x 1x 1x       1x     1x 1x 1x       1x         1x   1x 1x               1x 1x 1x         1x     1x 1x 1x 1x 1x     1x       1x                         1x         1x 1x 1x     1x           1x 1x 1x 1x                   1x       1x  
'use strict';
const ws = require('ws');
const Router = require('@hapi/call').Router;
const EventEmitter = require('events');
const helpers = require('./helpers');
const INTERPOLATION_REGEX = /\{([^}]*)\}/g;
async function sendMessage(socket, message) {
    Eif (socket.readyState === ws.OPEN) {
        // socket.auth.exp must be EPOCH in seconds but not milliseconds
        Iif (typeof socket.auth.exp === 'number' && socket.auth.exp * 1000 < Date.now()) {
            return socket.close(4403, 'Session expired!');
        }
        return socket.send(message);
    }
}
class SocketServer extends EventEmitter {
    constructor(utHttpServer = {}, config = {}) {
        super();
        this.router = new Router();
        this.rooms = {};
        this.wss = null;
        this.utHttpServer = utHttpServer;
        this.utHttpServerConfig = config;
        this.disableXsrf = (config.disableXsrf && config.disableXsrf.ws);
        this.disablePermissionVerify = (config.disablePermissionVerify && config.disablePermissionVerify.ws);
    }
 
    start(httpServerListener) {
        this.wss = new ws.Server({
            server: httpServerListener
        });
        this.wss.on('connection', (socket, req) => {
            const url = req.url.split('?').shift();
            try {
                // socket.upgradeReq not supported anymore:
                // https://github.com/websockets/ws/pull/1099
                // persist only fingerprint (for verification)
                socket.fingerprint = this.router.analyze(url).fingerprint;
            } catch (e) {
                return socket.close(4404, 'Wrong url:' + req.url);
            }
 
            Promise.resolve()
                .then(() => {
                    Iif (this.disableXsrf) return {};
                    const params = {
                        query: helpers.getTokens([req.url.replace(/[^?]+\?/ig, '')], ['&', '=']),
                        hashKey: this.utHttpServerConfig.jwt.key,
                        verifyOptions: {
                            ...this.utHttpServerConfig.jwt.verifyOptions,
                            ignoreExpiration: false
                        }
                    };
                    Eif (req.headers.authorization) {
                        const [scheme, token] = req.headers.authorization.split(' ');
                        Eif (scheme === 'Bearer') params.cookie = token;
                    } else if (req.headers.cookie) {
                        params.cookie = helpers.getTokens([req.headers.cookie], [';', '='])[this.utHttpServerConfig.jwt.cookieKey];
                    }
 
                    return helpers.jwtXsrfCheck(params);
                })
                .then(auth => {
                    socket.auth = auth;
                    const context = this.router.route(req.method.toLowerCase(), url);
                    Iif (context.isBoom) throw context;
                    Eif (!this.disablePermissionVerify) helpers.permissionVerify(socket, this.utHttpServerConfig.appId);
                    return context.route.verifyClient(socket);
                })
                .then(() => {
                    return this.router
                        .route(req.method.toLowerCase(), url).route
                        .handler(socket.fingerprint, socket);
                })
                .then(() => (this.emit('connection')))
                .catch((err) => {
                    this.utHttpServer.log && this.utHttpServer.log.error && this.utHttpServer.log.error(err);
                    if (!err.isBoom) return socket.close(4500, '4500');
                    socket.close(
                        4000 + parseInt(err.output.payload.statusCode), // based on https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
                        (4000 + parseInt(err.output.payload.statusCode)).toString() // Send status code as reason because Firefox/Edge show 1005 only as code
                    );
                });
        });
    }
 
    registerPath(path, verifyClient) {
        this.router.add({
            method: 'get',
            path
        }, {
            handler: (roomId, socket) => {
                Eif (!this.rooms[roomId]) this.rooms[roomId] = new Set();
                this.rooms[roomId].add(socket);
                socket.on('close', () => this.rooms[roomId].delete(socket));
            },
            verifyClient: socket => {
                return typeof verifyClient === 'function' ? verifyClient(socket, socket.fingerprint) : false;
            }
        });
    }
 
    publish({path = '', params = {}}, message) {
        const room = this.rooms[path.replace(INTERPOLATION_REGEX, (placeholder, label) => (params[label] || placeholder))];
        Eif (room && room.size) {
            const formattedMessage = helpers.formatMessage(message);
            room.forEach(socket => sendMessage(socket, formattedMessage));
        }
    }
 
    broadcast(message) {
        const formattedMessage = helpers.formatMessage(message);
        this.wss.clients.forEach(socket => sendMessage(socket, formattedMessage));
    }
 
    stop() {
        this.wss && this.wss.close();
    }
}
 
module.exports = SocketServer;