| 'use strict'; |
| |
| const http = require('http'); |
| const EventEmitter = require('events'); |
| const EE_ERROR = 'Registering more than one listener to a WebSocket is not supported.'; |
| const DEFAULT_PAYLOAD_LIMIT = 16777216; |
| |
| function noop() {} |
| |
| function abortConnection(socket, code, name) { |
| socket.end('HTTP/1.1 ' + code + ' ' + name + '\r\n\r\n'); |
| } |
| |
| function emitConnection(ws) { |
| this.emit('connection', ws); |
| } |
| |
| function onServerMessage(message, webSocket) { |
| webSocket.internalOnMessage(message); |
| } |
| |
| const native = (() => { |
| try { |
| try { |
| return process.binding('uws_builtin'); |
| } catch (e) { |
| return require(`./uws_${process.platform}_${process.versions.modules}`); |
| } |
| } catch (e) { |
| const version = process.version.substring(1).split('.').map(function(n) { |
| return parseInt(n); |
| }); |
| const lessThanSixFour = version[0] < 6 || (version[0] === 6 && version[1] < 4); |
| |
| if (process.platform === 'win32' && lessThanSixFour) { |
| throw new Error('µWebSockets requires Node.js 6.4.0 or greater on Windows.'); |
| } else { |
| throw new Error('Compilation of µWebSockets has failed and there is no pre-compiled binary ' + |
| 'available for your system. Please install a supported C++11 compiler and reinstall the module \'uws\'.'); |
| } |
| } |
| })(); |
| |
| native.setNoop(noop); |
| |
| var _upgradeReq = null; |
| |
| const clientGroup = native.client.group.create(0, DEFAULT_PAYLOAD_LIMIT); |
| |
| native.client.group.onConnection(clientGroup, (external) => { |
| const webSocket = native.getUserData(external); |
| webSocket.external = external; |
| webSocket.internalOnOpen(); |
| }); |
| |
| native.client.group.onMessage(clientGroup, (message, webSocket) => { |
| webSocket.internalOnMessage(message); |
| }); |
| |
| native.client.group.onDisconnection(clientGroup, (external, code, message, webSocket) => { |
| webSocket.external = null; |
| |
| process.nextTick(() => { |
| webSocket.internalOnClose(code, message); |
| }); |
| |
| native.clearUserData(external); |
| }); |
| |
| native.client.group.onPing(clientGroup, (message, webSocket) => { |
| webSocket.onping(message); |
| }); |
| |
| native.client.group.onPong(clientGroup, (message, webSocket) => { |
| webSocket.onpong(message); |
| }); |
| |
| native.client.group.onError(clientGroup, (webSocket) => { |
| process.nextTick(() => { |
| webSocket.internalOnError({ |
| message: 'uWs client connection error', |
| stack: 'uWs client connection error' |
| }); |
| }); |
| }); |
| |
| class WebSocket { |
| constructor(external) { |
| this.external = external; |
| this.internalOnMessage = noop; |
| this.internalOnClose = noop; |
| this.onping = noop; |
| this.onpong = noop; |
| } |
| |
| get upgradeReq() { |
| return _upgradeReq; |
| } |
| |
| set onmessage(f) { |
| if (f) { |
| this.internalOnMessage = (message) => { |
| f({data: message}); |
| }; |
| } else { |
| this.internalOnMessage = noop; |
| } |
| } |
| |
| set onopen(f) { |
| if (f) { |
| this.internalOnOpen = f; |
| } else { |
| this.internalOnOpen = noop; |
| } |
| } |
| |
| set onclose(f) { |
| if (f) { |
| this.internalOnClose = (code, message) => { |
| f({code: code, reason: message}); |
| }; |
| } else { |
| this.internalOnClose = noop; |
| } |
| } |
| |
| set onerror(f) { |
| if (f && this instanceof WebSocketClient) { |
| this.internalOnError = f; |
| } else { |
| this.internalOnError = noop; |
| } |
| } |
| |
| emit(eventName, arg1, arg2) { |
| if (eventName === 'message') { |
| this.internalOnMessage(arg1); |
| } else if (eventName === 'close') { |
| this.internalOnClose(arg1, arg2); |
| } else if (eventName === 'ping') { |
| this.onping(arg1); |
| } else if (eventName === 'pong') { |
| this.onpong(arg1); |
| } |
| return this; |
| } |
| |
| on(eventName, f) { |
| if (eventName === 'message') { |
| if (this.internalOnMessage !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnMessage = f; |
| } else if (eventName === 'close') { |
| if (this.internalOnClose !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnClose = f; |
| } else if (eventName === 'ping') { |
| if (this.onping !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.onping = f; |
| } else if (eventName === 'pong') { |
| if (this.onpong !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.onpong = f; |
| } else if (eventName === 'open') { |
| if (this.internalOnOpen !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnOpen = f; |
| } else if (eventName === 'error' && this instanceof WebSocketClient) { |
| if (this.internalOnError !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnError = f; |
| } |
| return this; |
| } |
| |
| once(eventName, f) { |
| if (eventName === 'message') { |
| if (this.internalOnMessage !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnMessage = (message) => { |
| this.internalOnMessage = noop; |
| f(message); |
| }; |
| } else if (eventName === 'close') { |
| if (this.internalOnClose !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.internalOnClose = (code, message) => { |
| this.internalOnClose = noop; |
| f(code, message); |
| }; |
| } else if (eventName === 'ping') { |
| if (this.onping !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.onping = () => { |
| this.onping = noop; |
| f(); |
| }; |
| } else if (eventName === 'pong') { |
| if (this.onpong !== noop) { |
| throw Error(EE_ERROR); |
| } |
| this.onpong = () => { |
| this.onpong = noop; |
| f(); |
| }; |
| } |
| return this; |
| } |
| |
| removeAllListeners(eventName) { |
| if (!eventName || eventName === 'message') { |
| this.internalOnMessage = noop; |
| } |
| if (!eventName || eventName === 'close') { |
| this.internalOnClose = noop; |
| } |
| if (!eventName || eventName === 'ping') { |
| this.onping = noop; |
| } |
| if (!eventName || eventName === 'pong') { |
| this.onpong = noop; |
| } |
| return this; |
| } |
| |
| removeListener(eventName, cb) { |
| if (eventName === 'message' && this.internalOnMessage === cb) { |
| this.internalOnMessage = noop; |
| } else if (eventName === 'close' && this.internalOnClose === cb) { |
| this.internalOnClose = noop; |
| } else if (eventName === 'ping' && this.onping === cb) { |
| this.onping = noop; |
| } else if (eventName === 'pong' && this.onpong === cb) { |
| this.onpong = noop; |
| } |
| return this; |
| } |
| |
| get OPEN() { |
| return WebSocketClient.OPEN; |
| } |
| |
| get CLOSED() { |
| return WebSocketClient.CLOSED; |
| } |
| |
| get readyState() { |
| return this.external ? WebSocketClient.OPEN : WebSocketClient.CLOSED; |
| } |
| |
| get _socket() { |
| const address = this.external ? native.getAddress(this.external) : new Array(3); |
| return { |
| remotePort: address[0], |
| remoteAddress: address[1], |
| remoteFamily: address[2] |
| }; |
| } |
| |
| // from here down, functions are not common between client and server |
| |
| ping(message, options, dontFailWhenClosed) { |
| if (this.external) { |
| native.server.send(this.external, message, WebSocketClient.OPCODE_PING); |
| } |
| } |
| |
| terminate() { |
| if (this.external) { |
| native.server.terminate(this.external); |
| this.external = null; |
| } |
| } |
| |
| send(message, options, cb) { |
| if (this.external) { |
| if (typeof options === 'function') { |
| cb = options; |
| options = null; |
| } |
| |
| const binary = options && options.binary || typeof message !== 'string'; |
| |
| native.server.send(this.external, message, binary ? WebSocketClient.OPCODE_BINARY : WebSocketClient.OPCODE_TEXT, cb ? (() => { |
| process.nextTick(cb); |
| }) : undefined); |
| } else if (cb) { |
| cb(new Error('not opened')); |
| } |
| } |
| |
| close(code, data) { |
| if (this.external) { |
| native.server.close(this.external, code, data); |
| this.external = null; |
| } |
| } |
| } |
| |
| class WebSocketClient extends WebSocket { |
| constructor(uri) { |
| super(null); |
| this.internalOnOpen = noop; |
| this.internalOnError = noop; |
| native.connect(clientGroup, uri, this); |
| } |
| |
| ping(message, options, dontFailWhenClosed) { |
| if (this.external) { |
| native.client.send(this.external, message, WebSocketClient.OPCODE_PING); |
| } |
| } |
| |
| terminate() { |
| if (this.external) { |
| native.client.terminate(this.external); |
| this.external = null; |
| } |
| } |
| |
| send(message, options, cb) { |
| if (this.external) { |
| if (typeof options === 'function') { |
| cb = options; |
| options = null; |
| } |
| |
| const binary = options && options.binary || typeof message !== 'string'; |
| |
| native.client.send(this.external, message, binary ? WebSocketClient.OPCODE_BINARY : WebSocketClient.OPCODE_TEXT, cb ? (() => { |
| process.nextTick(cb); |
| }) : undefined); |
| } else if (cb) { |
| cb(new Error('not opened')); |
| } |
| } |
| |
| close(code, data) { |
| if (this.external) { |
| native.client.close(this.external, code, data); |
| this.external = null; |
| } |
| } |
| } |
| |
| class Server extends EventEmitter { |
| constructor(options, callback) { |
| super(); |
| |
| if (!options) { |
| throw new TypeError('missing options'); |
| } |
| |
| if (options.port === undefined && !options.server && !options.noServer) { |
| throw new TypeError('invalid options'); |
| } |
| |
| var nativeOptions = WebSocketClient.PERMESSAGE_DEFLATE; |
| |
| if (options.perMessageDeflate !== undefined) { |
| if (options.perMessageDeflate === false) { |
| nativeOptions = 0; |
| } |
| } |
| |
| this.serverGroup = native.server.group.create(nativeOptions, options.maxPayload === undefined ? DEFAULT_PAYLOAD_LIMIT : options.maxPayload); |
| |
| // can these be made private? |
| this._upgradeCallback = noop; |
| this._upgradeListener = null; |
| this._noDelay = options.noDelay === undefined ? true : options.noDelay; |
| this._lastUpgradeListener = true; |
| this._passedHttpServer = options.server; |
| |
| if (!options.noServer) { |
| this.httpServer = options.server ? options.server : http.createServer((request, response) => { |
| // todo: default HTTP response |
| response.end(); |
| }); |
| |
| if (options.path && (!options.path.length || options.path[0] !== '/')) { |
| options.path = '/' + options.path; |
| } |
| |
| this.httpServer.on('upgrade', this._upgradeListener = ((request, socket, head) => { |
| if (!options.path || options.path == request.url.split('?')[0].split('#')[0]) { |
| if (options.verifyClient) { |
| const info = { |
| origin: request.headers.origin, |
| secure: request.connection.authorized !== undefined || request.connection.encrypted !== undefined, |
| req: request |
| }; |
| |
| if (options.verifyClient.length === 2) { |
| options.verifyClient(info, (result, code, name) => { |
| if (result) { |
| this.handleUpgrade(request, socket, head, emitConnection); |
| } else { |
| abortConnection(socket, code, name); |
| } |
| }); |
| } else { |
| if (options.verifyClient(info)) { |
| this.handleUpgrade(request, socket, head, emitConnection); |
| } else { |
| abortConnection(socket, 400, 'Client verification failed'); |
| } |
| } |
| } else { |
| this.handleUpgrade(request, socket, head, emitConnection); |
| } |
| } else { |
| if (this._lastUpgradeListener) { |
| abortConnection(socket, 400, 'URL not supported'); |
| } |
| } |
| })); |
| |
| this.httpServer.on('newListener', (eventName, listener) => { |
| if (eventName === 'upgrade') { |
| this._lastUpgradeListener = false; |
| } |
| }); |
| |
| this.httpServer.on('error', (err) => { |
| this.emit('error', err); |
| }); |
| } |
| |
| native.server.group.onDisconnection(this.serverGroup, (external, code, message, webSocket) => { |
| webSocket.external = null; |
| |
| process.nextTick(() => { |
| webSocket.internalOnClose(code, message); |
| }); |
| |
| native.clearUserData(external); |
| }); |
| |
| native.server.group.onMessage(this.serverGroup, onServerMessage); |
| |
| native.server.group.onPing(this.serverGroup, (message, webSocket) => { |
| webSocket.onping(message); |
| }); |
| |
| native.server.group.onPong(this.serverGroup, (message, webSocket) => { |
| webSocket.onpong(message); |
| }); |
| |
| native.server.group.onConnection(this.serverGroup, (external) => { |
| const webSocket = new WebSocket(external); |
| |
| native.setUserData(external, webSocket); |
| this._upgradeCallback(webSocket); |
| _upgradeReq = null; |
| }); |
| |
| if (options.port !== undefined) { |
| if (options.host) { |
| this.httpServer.listen(options.port, options.host, () => { |
| this.emit('listening'); |
| callback && callback(); |
| }); |
| } else { |
| this.httpServer.listen(options.port, () => { |
| this.emit('listening'); |
| callback && callback(); |
| }); |
| } |
| } |
| } |
| |
| handleUpgrade(request, socket, upgradeHead, callback) { |
| if (socket._isNative) { |
| if (this.serverGroup) { |
| _upgradeReq = request; |
| this._upgradeCallback = callback ? callback : noop; |
| native.upgrade(this.serverGroup, socket.external, secKey, request.headers['sec-websocket-extensions'], request.headers['sec-websocket-protocol']); |
| } |
| } else { |
| const secKey = request.headers['sec-websocket-key']; |
| const socketHandle = socket.ssl ? socket._parent._handle : socket._handle; |
| const sslState = socket.ssl ? socket.ssl._external : null; |
| if (socketHandle && secKey && secKey.length == 24) { |
| socket.setNoDelay(this._noDelay); |
| const ticket = native.transfer(socketHandle.fd === -1 ? socketHandle : socketHandle.fd, sslState); |
| socket.on('close', (error) => { |
| if (this.serverGroup) { |
| _upgradeReq = request; |
| this._upgradeCallback = callback ? callback : noop; |
| native.upgrade(this.serverGroup, ticket, secKey, request.headers['sec-websocket-extensions'], request.headers['sec-websocket-protocol']); |
| } |
| }); |
| } |
| socket.destroy(); |
| } |
| } |
| |
| broadcast(message, options) { |
| if (this.serverGroup) { |
| native.server.group.broadcast(this.serverGroup, message, options && options.binary || false); |
| } |
| } |
| |
| startAutoPing(interval, userMessage) { |
| if (this.serverGroup) { |
| native.server.group.startAutoPing(this.serverGroup, interval, userMessage); |
| } |
| } |
| |
| close(cb) { |
| if (this._upgradeListener && this.httpServer) { |
| this.httpServer.removeListener('upgrade', this._upgradeListener); |
| |
| if (!this._passedHttpServer) { |
| this.httpServer.close(); |
| } |
| } |
| |
| if (this.serverGroup) { |
| native.server.group.close(this.serverGroup); |
| this.serverGroup = null; |
| } |
| |
| if (typeof cb === 'function') { |
| // compatibility hack, 15 seconds timeout |
| setTimeout(cb, 20000); |
| } |
| } |
| |
| get clients() { |
| if (this.serverGroup) { |
| return { |
| length: native.server.group.getSize(this.serverGroup), |
| forEach: ((cb) => {native.server.group.forEach(this.serverGroup, cb)}) |
| }; |
| } |
| } |
| } |
| |
| WebSocketClient.PERMESSAGE_DEFLATE = 1; |
| WebSocketClient.SERVER_NO_CONTEXT_TAKEOVER = 2; |
| WebSocketClient.CLIENT_NO_CONTEXT_TAKEOVER = 4; |
| WebSocketClient.OPCODE_TEXT = 1; |
| WebSocketClient.OPCODE_BINARY = 2; |
| WebSocketClient.OPCODE_PING = 9; |
| WebSocketClient.OPEN = 1; |
| WebSocketClient.CLOSED = 0; |
| WebSocketClient.Server = Server; |
| WebSocketClient.http = native.httpServer; |
| WebSocketClient.native = native; |
| module.exports = WebSocketClient; |