123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- 'use strict';
- const EventEmitter = require('events');
- const crypto = require('crypto');
- const https = require('https');
- const http = require('http');
- const url = require('url');
- const PerMessageDeflate = require('./permessage-deflate');
- const EventTarget = require('./event-target');
- const extension = require('./extension');
- const constants = require('./constants');
- const Receiver = require('./receiver');
- const Sender = require('./sender');
- const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
- const protocolVersions = [8, 13];
- const closeTimeout = 30 * 1000;
- class WebSocket extends EventEmitter {
-
- constructor (address, protocols, options) {
- super();
- this.readyState = WebSocket.CONNECTING;
- this.protocol = '';
- this._binaryType = constants.BINARY_TYPES[0];
- this._finalize = this.finalize.bind(this);
- this._closeFrameReceived = false;
- this._closeFrameSent = false;
- this._closeMessage = '';
- this._closeTimer = null;
- this._finalized = false;
- this._closeCode = 1006;
- this._extensions = {};
- this._isServer = true;
- this._receiver = null;
- this._sender = null;
- this._socket = null;
- this._error = null;
- if (address !== null) {
- if (!protocols) {
- protocols = [];
- } else if (typeof protocols === 'string') {
- protocols = [protocols];
- } else if (!Array.isArray(protocols)) {
- options = protocols;
- protocols = [];
- }
- initAsClient.call(this, address, protocols, options);
- }
- }
- get CONNECTING () { return WebSocket.CONNECTING; }
- get CLOSING () { return WebSocket.CLOSING; }
- get CLOSED () { return WebSocket.CLOSED; }
- get OPEN () { return WebSocket.OPEN; }
-
- get binaryType () {
- return this._binaryType;
- }
- set binaryType (type) {
- if (constants.BINARY_TYPES.indexOf(type) < 0) return;
- this._binaryType = type;
-
-
-
- if (this._receiver) this._receiver._binaryType = type;
- }
-
- get bufferedAmount () {
- if (!this._socket) return 0;
-
-
-
- return (this._socket.bufferSize || 0) + this._sender._bufferedBytes;
- }
-
- get extensions () {
- return Object.keys(this._extensions).join();
- }
-
- setSocket (socket, head, maxPayload) {
- socket.setTimeout(0);
- socket.setNoDelay();
- socket.on('close', this._finalize);
- socket.on('error', this._finalize);
- socket.on('end', this._finalize);
- this._receiver = new Receiver(this._extensions, maxPayload, this.binaryType);
- this._sender = new Sender(socket, this._extensions);
- this._socket = socket;
- if (head.length > 0) socket.unshift(head);
- socket.on('data', this._receiver.add);
- this._receiver.onmessage = (data) => this.emit('message', data);
- this._receiver.onping = (data) => {
- this.pong(data, !this._isServer, constants.NOOP);
- this.emit('ping', data);
- };
- this._receiver.onpong = (data) => this.emit('pong', data);
- this._receiver.onclose = (code, reason) => {
-
-
-
- this._socket.removeListener('data', this._receiver.add);
- this._closeFrameReceived = true;
- this._closeMessage = reason;
- this._closeCode = code;
- if (code === 1005) this.close();
- else this.close(code, reason);
- };
- this._receiver.onerror = (error, code) => {
- if (this._error) return;
- this._closeCode = code;
- if (!this._finalized) this.finalize(error);
- else this.emit('error', error);
- };
- this.readyState = WebSocket.OPEN;
- this.emit('open');
- }
-
- finalize (error) {
- if (this._finalized) return;
- this.readyState = WebSocket.CLOSING;
- this._finalized = true;
- if (!this._socket) {
-
-
-
- this.emit('error', error);
- this.readyState = WebSocket.CLOSED;
- this.emit('close', this._closeCode, this._closeMessage);
- return;
- }
- clearTimeout(this._closeTimer);
- this._socket.removeListener('data', this._receiver.add);
- this._socket.removeListener('close', this._finalize);
- this._socket.removeListener('error', this._finalize);
- this._socket.removeListener('end', this._finalize);
- this._socket.on('error', constants.NOOP);
- if (error) {
- if (error !== true) this._error = error;
- this._socket.destroy();
- } else {
- this._socket.end();
- }
- this._receiver.cleanup(() => {
- const err = this._error;
- if (err) {
- this._error = null;
- this.emit('error', err);
- }
- this.readyState = WebSocket.CLOSED;
- if (this._extensions[PerMessageDeflate.extensionName]) {
- this._extensions[PerMessageDeflate.extensionName].cleanup();
- }
- this.emit('close', this._closeCode, this._closeMessage);
- });
- }
-
- close (code, data) {
- if (this.readyState === WebSocket.CLOSED) return;
- if (this.readyState === WebSocket.CONNECTING) {
- this._req.abort();
- this.finalize(
- new Error('WebSocket was closed before the connection was established')
- );
- return;
- }
- if (this.readyState === WebSocket.CLOSING) {
- if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
- return;
- }
- this.readyState = WebSocket.CLOSING;
- this._sender.close(code, data, !this._isServer, (err) => {
-
-
-
-
- if (err) return;
- this._closeFrameSent = true;
- if (!this._finalized) {
- if (this._closeFrameReceived) this._socket.end();
-
-
-
-
- this._closeTimer = setTimeout(this._finalize, closeTimeout, true);
- }
- });
- }
-
- ping (data, mask, cb) {
- if (typeof data === 'function') {
- cb = data;
- data = mask = undefined;
- } else if (typeof mask === 'function') {
- cb = mask;
- mask = undefined;
- }
- if (this.readyState !== WebSocket.OPEN) {
- const err = new Error(
- `WebSocket is not open: readyState ${this.readyState} ` +
- `(${readyStates[this.readyState]})`
- );
- if (cb) return cb(err);
- throw err;
- }
- if (typeof data === 'number') data = data.toString();
- if (mask === undefined) mask = !this._isServer;
- this._sender.ping(data || constants.EMPTY_BUFFER, mask, cb);
- }
-
- pong (data, mask, cb) {
- if (typeof data === 'function') {
- cb = data;
- data = mask = undefined;
- } else if (typeof mask === 'function') {
- cb = mask;
- mask = undefined;
- }
- if (this.readyState !== WebSocket.OPEN) {
- const err = new Error(
- `WebSocket is not open: readyState ${this.readyState} ` +
- `(${readyStates[this.readyState]})`
- );
- if (cb) return cb(err);
- throw err;
- }
- if (typeof data === 'number') data = data.toString();
- if (mask === undefined) mask = !this._isServer;
- this._sender.pong(data || constants.EMPTY_BUFFER, mask, cb);
- }
-
- send (data, options, cb) {
- if (typeof options === 'function') {
- cb = options;
- options = {};
- }
- if (this.readyState !== WebSocket.OPEN) {
- const err = new Error(
- `WebSocket is not open: readyState ${this.readyState} ` +
- `(${readyStates[this.readyState]})`
- );
- if (cb) return cb(err);
- throw err;
- }
- if (typeof data === 'number') data = data.toString();
- const opts = Object.assign({
- binary: typeof data !== 'string',
- mask: !this._isServer,
- compress: true,
- fin: true
- }, options);
- if (!this._extensions[PerMessageDeflate.extensionName]) {
- opts.compress = false;
- }
- this._sender.send(data || constants.EMPTY_BUFFER, opts, cb);
- }
-
- terminate () {
- if (this.readyState === WebSocket.CLOSED) return;
- if (this.readyState === WebSocket.CONNECTING) {
- this._req.abort();
- this.finalize(
- new Error('WebSocket was closed before the connection was established')
- );
- return;
- }
- this.finalize(true);
- }
- }
- readyStates.forEach((readyState, i) => {
- WebSocket[readyStates[i]] = i;
- });
- ['open', 'error', 'close', 'message'].forEach((method) => {
- Object.defineProperty(WebSocket.prototype, `on${method}`, {
-
- get () {
- const listeners = this.listeners(method);
- for (var i = 0; i < listeners.length; i++) {
- if (listeners[i]._listener) return listeners[i]._listener;
- }
- },
-
- set (listener) {
- const listeners = this.listeners(method);
- for (var i = 0; i < listeners.length; i++) {
-
-
-
- if (listeners[i]._listener) this.removeListener(method, listeners[i]);
- }
- this.addEventListener(method, listener);
- }
- });
- });
- WebSocket.prototype.addEventListener = EventTarget.addEventListener;
- WebSocket.prototype.removeEventListener = EventTarget.removeEventListener;
- module.exports = WebSocket;
- function initAsClient (address, protocols, options) {
- options = Object.assign({
- protocolVersion: protocolVersions[1],
- protocol: protocols.join(','),
- perMessageDeflate: true,
- handshakeTimeout: null,
- localAddress: null,
- headers: null,
- family: null,
- origin: null,
- agent: null,
- host: null,
-
-
-
- checkServerIdentity: null,
- rejectUnauthorized: null,
- passphrase: null,
- ciphers: null,
- ecdhCurve: null,
- cert: null,
- key: null,
- pfx: null,
- ca: null
- }, options);
- if (protocolVersions.indexOf(options.protocolVersion) === -1) {
- throw new RangeError(
- `Unsupported protocol version: ${options.protocolVersion} ` +
- `(supported versions: ${protocolVersions.join(', ')})`
- );
- }
- this._isServer = false;
- this.url = address;
- const serverUrl = url.parse(address);
- const isUnixSocket = serverUrl.protocol === 'ws+unix:';
- if (!serverUrl.host && (!isUnixSocket || !serverUrl.path)) {
- throw new Error(`Invalid URL: ${address}`);
- }
- const isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:';
- const key = crypto.randomBytes(16).toString('base64');
- const httpObj = isSecure ? https : http;
- var perMessageDeflate;
- const requestOptions = {
- port: serverUrl.port || (isSecure ? 443 : 80),
- host: serverUrl.hostname,
- path: '/',
- headers: {
- 'Sec-WebSocket-Version': options.protocolVersion,
- 'Sec-WebSocket-Key': key,
- 'Connection': 'Upgrade',
- 'Upgrade': 'websocket'
- }
- };
- if (options.headers) Object.assign(requestOptions.headers, options.headers);
- if (options.perMessageDeflate) {
- perMessageDeflate = new PerMessageDeflate(
- options.perMessageDeflate !== true ? options.perMessageDeflate : {},
- false
- );
- requestOptions.headers['Sec-WebSocket-Extensions'] = extension.format({
- [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
- });
- }
- if (options.protocol) {
- requestOptions.headers['Sec-WebSocket-Protocol'] = options.protocol;
- }
- if (options.origin) {
- if (options.protocolVersion < 13) {
- requestOptions.headers['Sec-WebSocket-Origin'] = options.origin;
- } else {
- requestOptions.headers.Origin = options.origin;
- }
- }
- if (options.host) requestOptions.headers.Host = options.host;
- if (serverUrl.auth) requestOptions.auth = serverUrl.auth;
- if (options.localAddress) requestOptions.localAddress = options.localAddress;
- if (options.family) requestOptions.family = options.family;
- if (isUnixSocket) {
- const parts = serverUrl.path.split(':');
- requestOptions.socketPath = parts[0];
- requestOptions.path = parts[1];
- } else if (serverUrl.path) {
-
-
-
- if (serverUrl.path.charAt(0) !== '/') {
- requestOptions.path = `/${serverUrl.path}`;
- } else {
- requestOptions.path = serverUrl.path;
- }
- }
- var agent = options.agent;
-
-
-
- if (
- options.rejectUnauthorized != null ||
- options.checkServerIdentity ||
- options.passphrase ||
- options.ciphers ||
- options.ecdhCurve ||
- options.cert ||
- options.key ||
- options.pfx ||
- options.ca
- ) {
- if (options.passphrase) requestOptions.passphrase = options.passphrase;
- if (options.ciphers) requestOptions.ciphers = options.ciphers;
- if (options.ecdhCurve) requestOptions.ecdhCurve = options.ecdhCurve;
- if (options.cert) requestOptions.cert = options.cert;
- if (options.key) requestOptions.key = options.key;
- if (options.pfx) requestOptions.pfx = options.pfx;
- if (options.ca) requestOptions.ca = options.ca;
- if (options.checkServerIdentity) {
- requestOptions.checkServerIdentity = options.checkServerIdentity;
- }
- if (options.rejectUnauthorized != null) {
- requestOptions.rejectUnauthorized = options.rejectUnauthorized;
- }
- if (!agent) agent = new httpObj.Agent(requestOptions);
- }
- if (agent) requestOptions.agent = agent;
- this._req = httpObj.get(requestOptions);
- if (options.handshakeTimeout) {
- this._req.setTimeout(options.handshakeTimeout, () => {
- this._req.abort();
- this.finalize(new Error('Opening handshake has timed out'));
- });
- }
- this._req.on('error', (error) => {
- if (this._req.aborted) return;
- this._req = null;
- this.finalize(error);
- });
- this._req.on('response', (res) => {
- if (!this.emit('unexpected-response', this._req, res)) {
- this._req.abort();
- this.finalize(new Error(`Unexpected server response: ${res.statusCode}`));
- }
- });
- this._req.on('upgrade', (res, socket, head) => {
- this.emit('upgrade', res);
-
-
-
-
- if (this.readyState !== WebSocket.CONNECTING) return;
- this._req = null;
- const digest = crypto.createHash('sha1')
- .update(key + constants.GUID, 'binary')
- .digest('base64');
- if (res.headers['sec-websocket-accept'] !== digest) {
- socket.destroy();
- return this.finalize(new Error('Invalid Sec-WebSocket-Accept header'));
- }
- const serverProt = res.headers['sec-websocket-protocol'];
- const protList = (options.protocol || '').split(/, */);
- var protError;
- if (!options.protocol && serverProt) {
- protError = 'Server sent a subprotocol but none was requested';
- } else if (options.protocol && !serverProt) {
- protError = 'Server sent no subprotocol';
- } else if (serverProt && protList.indexOf(serverProt) === -1) {
- protError = 'Server sent an invalid subprotocol';
- }
- if (protError) {
- socket.destroy();
- return this.finalize(new Error(protError));
- }
- if (serverProt) this.protocol = serverProt;
- if (perMessageDeflate) {
- try {
- const extensions = extension.parse(
- res.headers['sec-websocket-extensions']
- );
- if (extensions[PerMessageDeflate.extensionName]) {
- perMessageDeflate.accept(
- extensions[PerMessageDeflate.extensionName]
- );
- this._extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
- }
- } catch (err) {
- socket.destroy();
- this.finalize(new Error('Invalid Sec-WebSocket-Extensions header'));
- return;
- }
- }
- this.setSocket(socket, head, 0);
- });
- }
|