UNPKG

@superhero/websocket

Version:
207 lines (174 loc) 5.41 kB
const Debug = require('@superhero/debug'), Server = require('net').Server, Events = require('events'), Codec = require('./codec') module.exports = class { constructor(options) { this.config = Object.assign( { debug : true, onClose : false }, options) const debug = new Debug({ debug:this.config.debug, prefix:'ws server:' }) this.log = debug.log.bind(debug) this.events = new Events() this.server = new Server() this.sockets = [] // ping-pong to keep the connection alive // this is not part of the standard, but it's required becouse of // limitations in the browser api // https://tools.ietf.org/html/rfc6455#page-29 this.events.on('ping', (socket) => socket.emit('pong')) for(let event of ['close','connection','listening']) this.server.on(event, () => this.log(event)) for(let event of ['error']) this.server.on(event, (...a) => this.log(event, ...a)) this.server.on('connection', this.onConnection.bind(this)) } onConnection(socket) { this.sockets.push(socket) for(let event of ['close','connection','drain','end','lookup','timeout']) socket.on(event, () => this.log('socket:', event)) for(let event of ['error']) socket.on(event, (...a) => this.log('socket:', event, ...a)) const ctx = { socket } ctx.emit = this.emit.bind(this, socket) ctx.chunks = [] socket.on('data', this.onData .bind(this, ctx)) socket.on('close', this.onClose.bind(this, ctx)) } onData(ctx, data) { this.log('socket:', 'data') // messages can come in multiple chunks that needs to be glued together ctx.buffer = Buffer.concat([ctx.buffer, data].filter(_ => _)) if(!ctx.headers) this.handshake(ctx) if(ctx.headers) this.dispatch(ctx) } onClose(ctx) { this.sockets.splice(this.sockets.indexOf(ctx.socket), 1) this.config.onClose && this.config.onClose(ctx) } handshake(ctx) { this.log('socket:', 'handshake:', 'received') const s = ctx.buffer.toString() if(s.match(/\r\n\r\n/)) { // parse headers and store remaining buffer const parts = s.split('\r\n\r\n'), rows = parts.shift().split('\r\n'), buffer = parts.join(''), divided = rows.map((row) => row.split(':').map((item) => item.trim())), headers = divided.reduce((obj, header) => { obj[header[0].toLowerCase()] = header[1] return obj }, {}), key = headers['sec-websocket-key'], signature = Codec.signature(key) if(!key) { this.log('socket:', 'handshake:', 'sec-websocket-key missing') ctx.socket.destroy() return } if(key.length < 10) { this.log('socket:', 'handshake:', 'sec-websocket-key to short', key) socket.destroy() return } this.log('socket:', 'handshake:', 'received:', 'key:', key) ctx.headers = headers ctx.buffer = Buffer.from(buffer) // write headers back to the client and establish a handshake ctx.socket.write( [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + signature ].join('\r\n') + '\r\n\r\n', (error) => { error ? this.log('socket:', 'handshake:', 'error:', error) : this.log('socket:', 'handshake:', 'sent:', 'signature:', signature) }) } else { this.log('socket:', 'handshake:', 'header incomplete') } } dispatch(ctx) { // messages can also come in the same chunk that needs to be divided... for(const decoded of Codec.decode(ctx.buffer)) { ctx.buffer = decoded.buffer // destroys the socket if specific 2 bytes // well, this works.. but I have no clue why, the specifications // states [0xFF][0x00]. Or if older protocol: [0x00][message][0xFF] if(decoded.msg.length == 2 &&(decoded.msg.charCodeAt(0) == 3 && decoded.msg.charCodeAt(1) == 233) ||(decoded.msg.charCodeAt(0) == 3 && decoded.msg.charCodeAt(1) == 65533)) { ctx.socket.destroy() break } else { try { ctx.chunks.push(decoded.msg) const msg = ctx.chunks.join(), dto = JSON.parse(msg) ctx.chunks.length = 0 this.log('received message:', dto) this.events.emit(dto.event, ctx, dto.data) } catch(error) { this.log(error) this.log('a message could not be parsed:', ctx.chunks) } } } } emit(socket, event, data, toAll) { this.log('emitting, to everyone:', !!toAll, 'event:', event, 'data:', data) const dto = JSON.stringify({ event, data }), encoded = Codec.encode(dto) const promises = [] for(let soc of (toAll ? this.sockets : [socket])) promises.push( new Promise((fulfill, reject) => soc.write(encoded, (error) => { if(error) { this.log('error emitting:', event, data, error) reject(error) } else { this.log('emitted:', event, data) fulfill() } }))) return Promise.all(promises) } }