diff --git a/index.js b/index.js index 0dc2a00..2bac1fd 100644 --- a/index.js +++ b/index.js @@ -12,11 +12,9 @@ */ const EventEmitter = require('events') -const inherits = require('inherits') const stream = require('stream') -const deprecate = require('util').deprecate const timers = require('timers') -const Buffer = require('buffer').Buffer +const { Buffer } = require('buffer') // Track open servers and sockets to route incoming sockets (via onAccept and onReceive) // to the right handlers. @@ -24,35 +22,26 @@ const servers = {} const sockets = {} // Thorough check for Chrome App since both Edge and Chrome implement dummy chrome object -if ( - typeof chrome === 'object' && - typeof chrome.runtime === 'object' && - typeof chrome.runtime.id === 'string' && - typeof chrome.sockets === 'object' && - typeof chrome.sockets.tcpServer === 'object' && - typeof chrome.sockets.tcp === 'object' -) { - chrome.sockets.tcpServer.onAccept.addListener(onAccept) - chrome.sockets.tcpServer.onAcceptError.addListener(onAcceptError) +if (typeof chrome?.sockets?.tcp === 'object') { chrome.sockets.tcp.onReceive.addListener(onReceive) chrome.sockets.tcp.onReceiveError.addListener(onReceiveError) } -function onAccept (info) { - if (info.socketId in servers) { - servers[info.socketId]._onAccept(info.clientSocketId) - } else { - console.error('Unknown server socket id: ' + info.socketId) - } -} - -function onAcceptError (info) { - if (info.socketId in servers) { - servers[info.socketId]._onAcceptError(info.resultCode) - } else { - console.error('Unknown server socket id: ' + info.socketId) - } -} +// function onAccept (info) { +// if (info.socketId in servers) { +// servers[info.socketId]._onAccept(info.clientSocketId) +// } else { +// console.error('Unknown server socket id: ' + info.socketId) +// } +// } + +// function onAcceptError (info) { +// if (info.socketId in servers) { +// servers[info.socketId]._onAcceptError(info.resultCode) +// } else { +// console.error('Unknown server socket id: ' + info.socketId) +// } +// } function onReceive (info) { if (info.socketId in sockets) { @@ -109,17 +98,12 @@ exports.createServer = function (options, connectionListener) { * @param {function} listener * @return {Socket} */ -exports.connect = exports.createConnection = function () { - const argsLen = arguments.length - let args = new Array(argsLen) - for (let i = 0; i < argsLen; i++) args[i] = arguments[i] +exports.connect = exports.createConnection = function (...args) { args = normalizeConnectArgs(args) const s = new Socket(args[0]) return Socket.prototype.connect.apply(s, args) } -inherits(Server, EventEmitter) - /** * Class: net.Server * ================= @@ -142,275 +126,313 @@ inherits(Server, EventEmitter) * Emitted when an error occurs. The 'close' event will be called directly * following this event. See example in discussion of server.listen. */ -function Server (options, connectionListener) { - if (!(this instanceof Server)) return new Server(options, connectionListener) - EventEmitter.call(this) - - if (typeof options === 'function') { - connectionListener = options - options = {} - this.on('connection', connectionListener) - } else if (options == null || typeof options === 'object') { - options = options || {} - - if (typeof connectionListener === 'function') { +class Server extends EventEmitter { + constructor (options, connectionListener) { + super() + + if (typeof options === 'function') { + connectionListener = options + options = {} this.on('connection', connectionListener) - } - } else { - throw new TypeError('options must be an object') - } + } else if (options == null || typeof options === 'object') { + options = options || {} - this._connections = 0 + if (typeof connectionListener === 'function') { + this.on('connection', connectionListener) + } + } else { + throw new TypeError('options must be an object') + } - Object.defineProperty(this, 'connections', { - get: deprecate(() => this._connections, - 'Server.connections property is deprecated. ' + - 'Use Server.getConnections method instead.'), - set: deprecate((val) => (this._connections = val), - 'Server.connections property is deprecated.'), - configurable: true, - enumerable: false - }) + this._connections = 0 - this.id = null // a number > 0 - this.connecting = false + this.id = null // a number > 0 + this.connecting = false - this.allowHalfOpen = options.allowHalfOpen || false - this.pauseOnConnect = !!options.pauseOnConnect - this._address = null + this.allowHalfOpen = options.allowHalfOpen || false + this.pauseOnConnect = !!options.pauseOnConnect + this._address = null - this._host = null - this._port = null - this._backlog = null -} -exports.Server = Server + this._host = null + this._port = null + this._backlog = null + } -Server.prototype._usingSlaves = false // not used + get connections () { + return this._connections + } -/** - * server.listen(port, [host], [backlog], [callback]) - * - * Begin accepting connections on the specified port and host. If the host is - * omitted, the server will accept connections directed to any IPv4 address - * (INADDR_ANY). A port value of zero will assign a random port. - * - * Backlog is the maximum length of the queue of pending connections. The - * actual length will be determined by your OS through sysctl settings such as - * tcp_max_syn_backlog and somaxconn on linux. The default value of this - * parameter is 511 (not 512). - * - * This function is asynchronous. When the server has been bound, 'listening' - * event will be emitted. The last parameter callback will be added as an - * listener for the 'listening' event. - * - * @return {Socket} - */ -Server.prototype.listen = function (/* variable arguments... */) { - const lastArg = arguments[arguments.length - 1] - if (typeof lastArg === 'function') { - this.once('listening', lastArg) + set connections (val) { + this._connections = val } - let port = toNumber(arguments[0]) + _usingSlaves = false + + /** + * server.listen(port, [host], [backlog], [callback]) + * + * Begin accepting connections on the specified port and host. If the host is + * omitted, the server will accept connections directed to any IPv4 address + * (INADDR_ANY). A port value of zero will assign a random port. + * + * Backlog is the maximum length of the queue of pending connections. The + * actual length will be determined by your OS through sysctl settings such as + * tcp_max_syn_backlog and somaxconn on linux. The default value of this + * parameter is 511 (not 512). + * + * This function is asynchronous. When the server has been bound, 'listening' + * event will be emitted. The last parameter callback will be added as an + * listener for the 'listening' event. + * + * @return {Socket} + */ + listen (/* variable arguments... */) { + const lastArg = arguments[arguments.length - 1] + if (typeof lastArg === 'function') { + this.once('listening', lastArg) + } - let address + let port = toNumber(arguments[0]) - // The third optional argument is the backlog size. - // When the ip is omitted it can be the second argument. - let backlog = toNumber(arguments[1]) || toNumber(arguments[2]) || undefined + let address - if (arguments[0] !== null && typeof arguments[0] === 'object') { - const h = arguments[0] + // The third optional argument is the backlog size. + // When the ip is omitted it can be the second argument. + let backlog = toNumber(arguments[1]) || toNumber(arguments[2]) || undefined - if (h._handle || h.handle) { - throw new Error('handle is not supported in Chrome Apps.') - } - if (typeof h.fd === 'number' && h.fd >= 0) { - throw new Error('fd is not supported in Chrome Apps.') - } + if (arguments[0] !== null && typeof arguments[0] === 'object') { + const h = arguments[0] - // The first argument is a configuration object - if (h.backlog) { - backlog = h.backlog - } + if (h._handle || h.handle) { + throw new Error('handle is not supported in Chrome Apps.') + } + if (typeof h.fd === 'number' && h.fd >= 0) { + throw new Error('fd is not supported in Chrome Apps.') + } - if (typeof h.port === 'number' || typeof h.port === 'string' || - (typeof h.port === 'undefined' && 'port' in h)) { - // Undefined is interpreted as zero (random port) for consistency - // with net.connect(). - address = h.host || null - port = h.port - } else if (h.path && isPipeName(h.path)) { + // The first argument is a configuration object + if (h.backlog) { + backlog = h.backlog + } + + if (typeof h.port === 'number' || typeof h.port === 'string' || + (typeof h.port === 'undefined' && 'port' in h)) { + // Undefined is interpreted as zero (random port) for consistency + // with net.connect(). + address = h.host || null + port = h.port + } else if (h.path && isPipeName(h.path)) { + throw new Error('Pipes are not supported in Chrome Apps.') + } else { + throw new Error('Invalid listen argument: ' + h) + } + } else if (isPipeName(arguments[0])) { + // UNIX socket or Windows pipe. throw new Error('Pipes are not supported in Chrome Apps.') + } else if (arguments[1] === undefined || + typeof arguments[1] === 'function' || + typeof arguments[1] === 'number') { + // The first argument is the port, no IP given. + address = null } else { - throw new Error('Invalid listen argument: ' + h) + // The first argument is the port, the second an IP. + address = arguments[1] } - } else if (isPipeName(arguments[0])) { - // UNIX socket or Windows pipe. - throw new Error('Pipes are not supported in Chrome Apps.') - } else if (arguments[1] === undefined || - typeof arguments[1] === 'function' || - typeof arguments[1] === 'number') { - // The first argument is the port, no IP given. - address = null - } else { - // The first argument is the port, the second an IP. - address = arguments[1] - } - // now do something with port, address, backlog - - if (this.id) { - this.close() - } + // now do something with port, address, backlog + if (this.id) { + this.close() + } - // If port is invalid or undefined, bind to a random port. - assertPort(port) - this._port = port | 0 + // If port is invalid or undefined, bind to a random port. + assertPort(port) + this._port = port | 0 - this._host = address + this._host = address - let isAny6 = !this._host - if (isAny6) { - this._host = '::' - } + let isAny6 = !this._host + if (isAny6) { + this._host = '::' + } - this._backlog = typeof backlog === 'number' ? backlog : undefined + this._backlog = typeof backlog === 'number' ? backlog : undefined - this.connecting = true + this.connecting = true - chrome.sockets.tcpServer.create((createInfo) => { - if (!this.connecting || this.id) { - ignoreLastError() - chrome.sockets.tcpServer.close(createInfo.socketId) - return - } - if (chrome.runtime.lastError) { - this.emit('error', new Error(chrome.runtime.lastError.message)) - return + if (chrome.sockets?.tcpServer?.create) { + chrome.sockets.tcpServer.create((createInfo) => { + if (!this.connecting || this.id) { + ignoreLastError() + chrome.sockets.tcpServer.close(createInfo.socketId) + return + } + if (chrome.runtime.lastError) { + this.emit('error', new Error(chrome.runtime.lastError.message)) + return + } + const socketId = this.id = createInfo.socketId + servers[this.id] = this + const listen = () => chrome.sockets.tcpServer.listen(this.id, this._host, + this._port, this._backlog, + (result) => { + // callback may be after close + if (this.id !== socketId) { + ignoreLastError() + return + } + if (result !== 0 && isAny6) { + ignoreLastError() + this._host = '0.0.0.0' // try IPv4 + isAny6 = false + return listen() + } + this._onListen(result) + }) + listen() + }) + } else { + this._address = {} + this.emit('listening') } - const socketId = this.id = createInfo.socketId - servers[this.id] = this + return this + } + + _onListen (result) { + this.connecting = false - const listen = () => chrome.sockets.tcpServer.listen(this.id, this._host, - this._port, this._backlog, - (result) => { - // callback may be after close - if (this.id !== socketId) { + if (result === 0) { + const idBefore = this.id + chrome.sockets.tcpServer.getInfo(this.id, (info) => { + if (this.id !== idBefore) { ignoreLastError() return } - if (result !== 0 && isAny6) { - ignoreLastError() - this._host = '0.0.0.0' // try IPv4 - isAny6 = false - return listen() + if (chrome.runtime.lastError) { + this._onListen(-2) // net::ERR_FAILED + return } - this._onListen(result) + this._address = { + port: info.localPort, + family: info.localAddress && + info.localAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4', + address: info.localAddress + } + this.emit('listening') }) - listen() - }) + } else { + this.emit('error', exceptionWithHostPort(result, 'listen', this._host, this._port)) + if (this.id) { + chrome.sockets.tcpServer.close(this.id) + delete servers[this.id] + this.id = null + } + } + } - return this -} + _onAccept (clientSocketId) { + // Set the `maxConnections` property to reject connections when the server's + // connection count gets high. + if (this.maxConnections && this._connections >= this.maxConnections) { + chrome.sockets.tcp.close(clientSocketId) + console.warn('Rejected connection - hit `maxConnections` limit') + return + } -Server.prototype._onListen = function (result) { - this.connecting = false + this._connections += 1 - if (result === 0) { - const idBefore = this.id - chrome.sockets.tcpServer.getInfo(this.id, (info) => { - if (this.id !== idBefore) { - ignoreLastError() - return - } - if (chrome.runtime.lastError) { - this._onListen(-2) // net::ERR_FAILED - return - } + const acceptedSocket = new Socket({ + server: this, + id: clientSocketId, + allowHalfOpen: this.allowHalfOpen, + pauseOnCreate: this.pauseOnConnect + }) + acceptedSocket.on('connect', () => this.emit('connection', acceptedSocket)) + } - this._address = { - port: info.localPort, - family: info.localAddress && info.localAddress.indexOf(':') !== -1 - ? 'IPv6' - : 'IPv4', - address: info.localAddress + _onAcceptError (resultCode) { + this.emit('error', errnoException(resultCode, 'accept')) + this.close() + } + + /** + * Stops the server from accepting new connections and keeps existing + * connections. This function is asynchronous, the server is finally closed + * when all connections are ended and the server emits a 'close' event. + * Optionally, you can pass a callback to listen for the 'close' event. + * @param {function} callback + */ + close (callback) { + if (typeof callback === 'function') { + if (!this.id) { + this.once('close', () => callback(new Error('Not running'))) + } else { + this.once('close', callback) } - this.emit('listening') - }) - } else { - this.emit('error', exceptionWithHostPort(result, 'listen', this._host, this._port)) + } + if (this.id) { chrome.sockets.tcpServer.close(this.id) delete servers[this.id] this.id = null } - } -} - -Server.prototype._onAccept = function (clientSocketId) { - // Set the `maxConnections` property to reject connections when the server's - // connection count gets high. - if (this.maxConnections && this._connections >= this.maxConnections) { - chrome.sockets.tcp.close(clientSocketId) - console.warn('Rejected connection - hit `maxConnections` limit') - return - } - - this._connections += 1 + this._address = null + this.connecting = false - const acceptedSocket = new Socket({ - server: this, - id: clientSocketId, - allowHalfOpen: this.allowHalfOpen, - pauseOnCreate: this.pauseOnConnect - }) - acceptedSocket.on('connect', () => this.emit('connection', acceptedSocket)) -} + this._emitCloseIfDrained() -Server.prototype._onAcceptError = function (resultCode) { - this.emit('error', errnoException(resultCode, 'accept')) - this.close() -} + return this + } -/** - * Stops the server from accepting new connections and keeps existing - * connections. This function is asynchronous, the server is finally closed - * when all connections are ended and the server emits a 'close' event. - * Optionally, you can pass a callback to listen for the 'close' event. - * @param {function} callback - */ -Server.prototype.close = function (callback) { - if (typeof callback === 'function') { - if (!this.id) { - this.once('close', () => callback(new Error('Not running'))) - } else { - this.once('close', callback) + _emitCloseIfDrained () { + if (this.id || this.connecting || this._connections) { + return } + + process.nextTick(emitCloseNT, this) } - if (this.id) { - chrome.sockets.tcpServer.close(this.id) - delete servers[this.id] - this.id = null + get listening () { + return !!this._address } - this._address = null - this.connecting = false - this._emitCloseIfDrained() + /** + * Returns the bound address, the address family name and port of the socket + * as reported by the operating system. Returns an object with three + * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } + * + * @return {Object} information + */ + address () { + return this._address + } - return this -} + ref () { + // No chrome.socket equivalent + return this + } -Server.prototype._emitCloseIfDrained = function () { - if (this.id || this.connecting || this._connections) { - return + unref () { + // No chrome.socket equivalent + return this } - process.nextTick(emitCloseNT, this) + /** + * Asynchronously get the number of concurrent connections on the server. + * Works when sockets were sent to forks. + * + * Callback should take two arguments err and count. + * + * @param {function} callback + */ + getConnections (callback) { + process.nextTick(callback, null, this._connections) + } } +exports.Server = Server function emitCloseNT (self) { if (self.id || self.connecting || self._connections) { @@ -419,45 +441,6 @@ function emitCloseNT (self) { self.emit('close') } -Object.defineProperty(Server.prototype, 'listening', { - get: function () { - return !!this._address - }, - configurable: true, - enumerable: true -}) - -/** - * Returns the bound address, the address family name and port of the socket - * as reported by the operating system. Returns an object with three - * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } - * - * @return {Object} information - */ -Server.prototype.address = function () { - return this._address -} - -Server.prototype.unref = -Server.prototype.ref = function () { - // No chrome.socket equivalent - return this -} - -/** - * Asynchronously get the number of concurrent connections on the server. - * Works when sockets were sent to forks. - * - * Callback should take two arguments err and count. - * - * @param {function} callback - */ -Server.prototype.getConnections = function (callback) { - process.nextTick(callback, null, this._connections) -} - -inherits(Socket, stream.Duplex) - /** * Class: net.Socket * ================= @@ -522,524 +505,511 @@ inherits(Socket, stream.Duplex) * Emitted once the socket is fully closed. The argument had_error is a * boolean which says if the socket was closed due to a transmission error. */ -function Socket (options) { - if (!(this instanceof Socket)) return new Socket(options) +class Socket extends stream.Duplex { + constructor (options) { + if (typeof options === 'number') { + options = { fd: options } // Legacy interface. + } else if (options === undefined) { + options = {} + } - if (typeof options === 'number') { - options = { fd: options } // Legacy interface. - } else if (options === undefined) { - options = {} - } + if (options.handle) { + throw new Error('handle is not supported in Chrome Apps.') + } else if (options.fd !== undefined) { + throw new Error('fd is not supported in Chrome Apps.') + } - if (options.handle) { - throw new Error('handle is not supported in Chrome Apps.') - } else if (options.fd !== undefined) { - throw new Error('fd is not supported in Chrome Apps.') - } + options.decodeStrings = true + options.objectMode = false + super(options) - options.decodeStrings = true - options.objectMode = false - stream.Duplex.call(this, options) + this.destroyed = false + this._hadError = false // Used by _http_client.js + this.id = null // a number > 0 + this._parent = null + this._host = null + this._port = null + this._pendingData = null + + this.ondata = null + this.onend = null + + this._init() + this._reset() + + // default to *not* allowing half open sockets + // Note: this is not possible in Chrome Apps, see https://crbug.com/124952 + this.allowHalfOpen = options.allowHalfOpen || false + + // shut down the socket when we're finished with it. + this.on('finish', this.destroy) + + if (options.server) { + this.server = this._server = options.server + this.id = options.id + sockets[this.id] = this + + if (options.pauseOnCreate) { + // stop the handle from reading and pause the stream + // (Already paused in Chrome version) + this._readableState.flowing = false + } + + // For incoming sockets (from server), it's already connected. + this.connecting = true + this.writable = true + this._onConnect() + } + } - this.destroyed = false - this._hadError = false // Used by _http_client.js - this.id = null // a number > 0 - this._parent = null - this._host = null - this._port = null - this._pendingData = null + // called when creating new Socket, or when re-using a closed Socket + _init () { + // The amount of received bytes. + this.bytesRead = 0 - this.ondata = null - this.onend = null + this._bytesDispatched = 0 - this._init() - this._reset() + // Reserve properties + this.server = null + this._server = null + } - // default to *not* allowing half open sockets - // Note: this is not possible in Chrome Apps, see https://crbug.com/124952 - this.allowHalfOpen = options.allowHalfOpen || false + // called when creating new Socket, or when closing a Socket + _reset () { + this.remoteAddress = this.remotePort = + this.localAddress = this.localPort = null + this.remoteFamily = 'IPv4' + this.readable = this.writable = false + this.connecting = false + } - // shut down the socket when we're finished with it. - this.on('finish', this.destroy) + /** + * socket.connect(port, [host], [connectListener]) + * socket.connect(options, [connectListener]) + * + * Opens the connection for a given socket. If port and host are given, then + * the socket will be opened as a TCP socket, if host is omitted, localhost + * will be assumed. If a path is given, the socket will be opened as a unix + * socket to that path. + * + * Normally this method is not needed, as net.createConnection opens the + * socket. Use this only if you are implementing a custom Socket. + * + * This function is asynchronous. When the 'connect' event is emitted the + * socket is established. If there is a problem connecting, the 'connect' + * event will not be emitted, the 'error' event will be emitted with the + * exception. + * + * The connectListener parameter will be added as an listener for the + * 'connect' event. + * + * @param {Object} options + * @param {function} cb + * @return {Socket} this socket (for chaining) + */ + connect () { + const argsLen = arguments.length + let args = new Array(argsLen) + for (let i = 0; i < argsLen; i++) args[i] = arguments[i] + args = normalizeConnectArgs(args) + const options = args[0] + const cb = args[1] + + if (options.path) { + throw new Error('Pipes are not supported in Chrome Apps.') + } - if (options.server) { - this.server = this._server = options.server - this.id = options.id - sockets[this.id] = this + if (this.id) { + // already connected, destroy and connect again + this.destroy() + } - if (options.pauseOnCreate) { - // stop the handle from reading and pause the stream - // (Already paused in Chrome version) - this._readableState.flowing = false + if (this.destroyed) { + this._readableState.reading = false + this._readableState.ended = false + this._readableState.endEmitted = false + this._writableState.ended = false + this._writableState.ending = false + this._writableState.finished = false + this._writableState.errorEmitted = false + this._writableState.length = 0 + this.destroyed = false } - // For incoming sockets (from server), it's already connected. this.connecting = true this.writable = true - this._onConnect() - } -} -exports.Socket = Socket -// called when creating new Socket, or when re-using a closed Socket -Socket.prototype._init = function () { - // The amount of received bytes. - this.bytesRead = 0 + this._host = options.host || 'localhost' + this._port = options.port - this._bytesDispatched = 0 + if (typeof this._port !== 'undefined') { + if (typeof this._port !== 'number' && typeof this._port !== 'string') { + throw new TypeError('"port" option should be a number or string: ' + this._port) + } + if (!isLegalPort(this._port)) { + throw new RangeError('"port" option should be >= 0 and < 65536: ' + this._port) + } + } + this._port |= 0 - // Reserve properties - this.server = null - this._server = null -} + this._init() -// called when creating new Socket, or when closing a Socket -Socket.prototype._reset = function () { - this.remoteAddress = this.remotePort = - this.localAddress = this.localPort = null - this.remoteFamily = 'IPv4' - this.readable = this.writable = false - this.connecting = false -} + this._unrefTimer() -/** - * socket.connect(port, [host], [connectListener]) - * socket.connect(options, [connectListener]) - * - * Opens the connection for a given socket. If port and host are given, then - * the socket will be opened as a TCP socket, if host is omitted, localhost - * will be assumed. If a path is given, the socket will be opened as a unix - * socket to that path. - * - * Normally this method is not needed, as net.createConnection opens the - * socket. Use this only if you are implementing a custom Socket. - * - * This function is asynchronous. When the 'connect' event is emitted the - * socket is established. If there is a problem connecting, the 'connect' - * event will not be emitted, the 'error' event will be emitted with the - * exception. - * - * The connectListener parameter will be added as an listener for the - * 'connect' event. - * - * @param {Object} options - * @param {function} cb - * @return {Socket} this socket (for chaining) - */ -Socket.prototype.connect = function () { - const argsLen = arguments.length - let args = new Array(argsLen) - for (let i = 0; i < argsLen; i++) args[i] = arguments[i] - args = normalizeConnectArgs(args) - const options = args[0] - const cb = args[1] + if (typeof cb === 'function') { + this.once('connect', cb) + } - if (options.path) { - throw new Error('Pipes are not supported in Chrome Apps.') - } + chrome.sockets.tcp.create((createInfo) => { + if (!this.connecting || this.id) { + ignoreLastError() + chrome.sockets.tcp.close(createInfo.socketId) + return + } + if (chrome.runtime.lastError) { + this.destroy(new Error(chrome.runtime.lastError.message)) + return + } - if (this.id) { - // already connected, destroy and connect again - this.destroy() - } + this.id = createInfo.socketId + sockets[this.id] = this - if (this.destroyed) { - this._readableState.reading = false - this._readableState.ended = false - this._readableState.endEmitted = false - this._writableState.ended = false - this._writableState.ending = false - this._writableState.finished = false - this._writableState.errorEmitted = false - this._writableState.length = 0 - this.destroyed = false - } + chrome.sockets.tcp.setPaused(this.id, true) - this.connecting = true - this.writable = true + chrome.sockets.tcp.connect(this.id, this._host, this._port, (result) => { + // callback may come after call to destroy + if (this.id !== createInfo.socketId) { + ignoreLastError() + return + } + if (result !== 0) { + this.destroy(exceptionWithHostPort(result, 'connect', this._host, this._port)) + return + } - this._host = options.host || 'localhost' - this._port = options.port + this._unrefTimer() + this._onConnect() + }) + }) - if (typeof this._port !== 'undefined') { - if (typeof this._port !== 'number' && typeof this._port !== 'string') { - throw new TypeError('"port" option should be a number or string: ' + this._port) - } - if (!isLegalPort(this._port)) { - throw new RangeError('"port" option should be >= 0 and < 65536: ' + this._port) - } + return this } - this._port |= 0 - this._init() + _onConnect () { + const idBefore = this.id + chrome.sockets.tcp.getInfo(this.id, (result) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + if (chrome.runtime.lastError) { + this.destroy(new Error(chrome.runtime.lastError.message)) + return + } - this._unrefTimer() + this.remoteAddress = result.peerAddress + this.remoteFamily = result.peerAddress && + result.peerAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4' + this.remotePort = result.peerPort + this.localAddress = result.localAddress + this.localPort = result.localPort + + this.connecting = false + this.readable = true + + this.emit('connect') + // start the first read, or get an immediate EOF. + // this doesn't actually consume any bytes, because len=0 + if (!this.isPaused()) this.read(0) + }) + } + + /** + * The number of characters currently buffered to be written. + * @type {number} + */ + get bufferSize () { + if (this.id) { + let bytes = this._writableState.length + if (this._pendingData) bytes += this._pendingData.length + return bytes + } + } - if (typeof cb === 'function') { - this.once('connect', cb) + end (data, encoding) { + stream.Duplex.prototype.end.call(this, data, encoding) + this.writable = false } - chrome.sockets.tcp.create((createInfo) => { - if (!this.connecting || this.id) { - ignoreLastError() - chrome.sockets.tcp.close(createInfo.socketId) + _write (chunk, encoding, callback) { + if (!callback) callback = () => { } + + if (this.connecting) { + this._pendingData = chunk + this.once('connect', () => this._write(chunk, encoding, callback)) return } - if (chrome.runtime.lastError) { - this.destroy(new Error(chrome.runtime.lastError.message)) + this._pendingData = null + + if (!this.id) { + callback(new Error('This socket is closed')) return } - this.id = createInfo.socketId - sockets[this.id] = this - - chrome.sockets.tcp.setPaused(this.id, true) + // assuming buffer is browser implementation (`buffer` package on npm) + let buffer = chunk.buffer + if (chunk.byteLength !== buffer.byteLength) { + buffer = buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength) + } - chrome.sockets.tcp.connect(this.id, this._host, this._port, (result) => { - // callback may come after call to destroy - if (this.id !== createInfo.socketId) { + const idBefore = this.id + chrome.sockets.tcp.send(this.id, buffer, (sendInfo) => { + if (this.id !== idBefore) { ignoreLastError() return } - if (result !== 0) { - this.destroy(exceptionWithHostPort(result, 'connect', this._host, this._port)) - return - } - this._unrefTimer() - this._onConnect() + if (sendInfo.resultCode < 0) { + this._destroy(exceptionWithHostPort(sendInfo.resultCode, 'write', this.remoteAddress, this.remotePort), callback) + } else { + this._unrefTimer() + callback(null) + } }) - }) - return this -} + this._bytesDispatched += chunk.length + } -Socket.prototype._onConnect = function () { - const idBefore = this.id - chrome.sockets.tcp.getInfo(this.id, (result) => { - if (this.id !== idBefore) { - ignoreLastError() - return - } - if (chrome.runtime.lastError) { - this.destroy(new Error(chrome.runtime.lastError.message)) + _read (bufferSize) { + if (this.connecting || !this.id) { + this.once('connect', () => this._read(bufferSize)) return } - this.remoteAddress = result.peerAddress - this.remoteFamily = result.peerAddress && result.peerAddress.indexOf(':') !== -1 - ? 'IPv6' - : 'IPv4' - this.remotePort = result.peerPort - this.localAddress = result.localAddress - this.localPort = result.localPort + chrome.sockets.tcp.setPaused(this.id, false) - this.connecting = false - this.readable = true - - this.emit('connect') - // start the first read, or get an immediate EOF. - // this doesn't actually consume any bytes, because len=0 - if (!this.isPaused()) this.read(0) - }) -} - -/** - * The number of characters currently buffered to be written. - * @type {number} - */ -Object.defineProperty(Socket.prototype, 'bufferSize', { - get: function () { - if (this.id) { - let bytes = this._writableState.length - if (this._pendingData) bytes += this._pendingData.length - return bytes - } + const idBefore = this.id + chrome.sockets.tcp.getInfo(this.id, (result) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + if (chrome.runtime.lastError || !result.connected) { + this._onReceiveError(-15) // workaround for https://crbug.com/518161 + } + }) } -}) -Socket.prototype.end = function (data, encoding) { - stream.Duplex.prototype.end.call(this, data, encoding) - this.writable = false -} + _onReceive (data) { + const buffer = Buffer.from(data) + const offset = this.bytesRead -Socket.prototype._write = function (chunk, encoding, callback) { - if (!callback) callback = () => {} + this.bytesRead += buffer.length + this._unrefTimer() - if (this.connecting) { - this._pendingData = chunk - this.once('connect', () => this._write(chunk, encoding, callback)) - return + if (this.ondata) { + console.error('socket.ondata = func is non-standard, use socket.on(\'data\', func)') + this.ondata(buffer, offset, this.bytesRead) + } + if (!this.push(buffer)) { // if returns false, then apply backpressure + chrome.sockets.tcp.setPaused(this.id, true) + } } - this._pendingData = null - if (!this.id) { - callback(new Error('This socket is closed')) - return + _onReceiveError (resultCode) { + if (resultCode === -100) { // net::ERR_CONNECTION_CLOSED + if (this.onend) { + console.error('socket.onend = func is non-standard, use socket.on(\'end\', func)') + this.once('end', this.onend) + } + this.push(null) + this.destroy() + } else if (resultCode < 0) { + this.destroy(errnoException(resultCode, 'read')) + } } - // assuming buffer is browser implementation (`buffer` package on npm) - let buffer = chunk.buffer - if (chunk.byteLength !== buffer.byteLength) { - buffer = buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength) + /** + * The amount of bytes sent. + * @return {number} + */ + get bytesWritten () { + if (this.id) return this._bytesDispatched + this.bufferSize } - const idBefore = this.id - chrome.sockets.tcp.send(this.id, buffer, (sendInfo) => { - if (this.id !== idBefore) { - ignoreLastError() - return - } - - if (sendInfo.resultCode < 0) { - this._destroy(exceptionWithHostPort(sendInfo.resultCode, 'write', this.remoteAddress, this.remotePort), callback) - } else { - this._unrefTimer() - callback(null) - } - }) - - this._bytesDispatched += chunk.length -} - -Socket.prototype._read = function (bufferSize) { - if (this.connecting || !this.id) { - this.once('connect', () => this._read(bufferSize)) - return + destroy (exception) { + this._destroy(exception) } - chrome.sockets.tcp.setPaused(this.id, false) + _destroy (exception, cb) { + const fireErrorCallbacks = () => { + if (cb) cb(exception) + if (exception && !this._writableState.errorEmitted) { + process.nextTick(emitErrorNT, this, exception) + this._writableState.errorEmitted = true + } + } - const idBefore = this.id - chrome.sockets.tcp.getInfo(this.id, (result) => { - if (this.id !== idBefore) { - ignoreLastError() + if (this.destroyed) { + // already destroyed, fire error callbacks + fireErrorCallbacks() return } - if (chrome.runtime.lastError || !result.connected) { - this._onReceiveError(-15) // workaround for https://crbug.com/518161 - } - }) -} - -Socket.prototype._onReceive = function (data) { - const buffer = Buffer.from(data) - const offset = this.bytesRead - - this.bytesRead += buffer.length - this._unrefTimer() - if (this.ondata) { - console.error('socket.ondata = func is non-standard, use socket.on(\'data\', func)') - this.ondata(buffer, offset, this.bytesRead) - } - if (!this.push(buffer)) { // if returns false, then apply backpressure - chrome.sockets.tcp.setPaused(this.id, true) - } -} - -Socket.prototype._onReceiveError = function (resultCode) { - if (resultCode === -100) { // net::ERR_CONNECTION_CLOSED - if (this.onend) { - console.error('socket.onend = func is non-standard, use socket.on(\'end\', func)') - this.once('end', this.onend) + if (this._server) { + this._server._connections -= 1 + if (this._server._emitCloseIfDrained) this._server._emitCloseIfDrained() } - this.push(null) - this.destroy() - } else if (resultCode < 0) { - this.destroy(errnoException(resultCode, 'read')) - } -} -function protoGetter (name, callback) { - Object.defineProperty(Socket.prototype, name, { - configurable: false, - enumerable: true, - get: callback - }) -} + this._reset() -/** - * The amount of bytes sent. - * @return {number} - */ -protoGetter('bytesWritten', function bytesWritten () { - if (this.id) return this._bytesDispatched + this.bufferSize -}) + for (let s = this; s !== null; s = s._parent) timers.unenroll(s) // eslint-disable-line n/no-deprecated-api -Socket.prototype.destroy = function (exception) { - this._destroy(exception) -} + this.destroyed = true -Socket.prototype._destroy = function (exception, cb) { - const fireErrorCallbacks = () => { - if (cb) cb(exception) - if (exception && !this._writableState.errorEmitted) { - process.nextTick(emitErrorNT, this, exception) - this._writableState.errorEmitted = true + // If _destroy() has been called before chrome.sockets.tcp.create() + // callback, we don't have an id. Therefore we don't need to close + // or disconnect + if (this.id) { + delete sockets[this.id] + chrome.sockets.tcp.close(this.id, () => { + if (this.destroyed) { + this.emit('close', !!exception) + } + }) + this.id = null } - } - if (this.destroyed) { - // already destroyed, fire error callbacks fireErrorCallbacks() - return } - if (this._server) { - this._server._connections -= 1 - if (this._server._emitCloseIfDrained) this._server._emitCloseIfDrained() - } - - this._reset() - - for (let s = this; s !== null; s = s._parent) timers.unenroll(s) // eslint-disable-line node/no-deprecated-api + destroySoon () { + if (this.writable) this.end() - this.destroyed = true - - // If _destroy() has been called before chrome.sockets.tcp.create() - // callback, we don't have an id. Therefore we don't need to close - // or disconnect - if (this.id) { - delete sockets[this.id] - chrome.sockets.tcp.close(this.id, () => { - if (this.destroyed) { - this.emit('close', !!exception) - } - }) - this.id = null + if (this._writableState.finished) this.destroy() } - fireErrorCallbacks() -} + /** + * Sets the socket to timeout after timeout milliseconds of inactivity on the socket. + * By default net.Socket do not have a timeout. When an idle timeout is triggered the + * socket will receive a 'timeout' event but the connection will not be severed. The + * user must manually end() or destroy() the socket. + * + * If timeout is 0, then the existing idle timeout is disabled. + * + * The optional callback parameter will be added as a one time listener for the 'timeout' event. + * + * @param {number} timeout + * @param {function} callback + */ + setTimeout (timeout, callback) { + if (timeout === 0) { + timers.unenroll(this) // eslint-disable-line n/no-deprecated-api + if (callback) { + this.removeListener('timeout', callback) + } + } else { + timers.enroll(this, timeout) // eslint-disable-line n/no-deprecated-api + timers._unrefActive(this) + if (callback) { + this.once('timeout', callback) + } + } -Socket.prototype.destroySoon = function () { - if (this.writable) this.end() + return this + } - if (this._writableState.finished) this.destroy() -} + _onTimeout () { + this.emit('timeout') + } -/** - * Sets the socket to timeout after timeout milliseconds of inactivity on the socket. - * By default net.Socket do not have a timeout. When an idle timeout is triggered the - * socket will receive a 'timeout' event but the connection will not be severed. The - * user must manually end() or destroy() the socket. - * - * If timeout is 0, then the existing idle timeout is disabled. - * - * The optional callback parameter will be added as a one time listener for the 'timeout' event. - * - * @param {number} timeout - * @param {function} callback - */ -Socket.prototype.setTimeout = function (timeout, callback) { - if (timeout === 0) { - timers.unenroll(this) // eslint-disable-line node/no-deprecated-api - if (callback) { - this.removeListener('timeout', callback) - } - } else { - timers.enroll(this, timeout) // eslint-disable-line node/no-deprecated-api - timers._unrefActive(this) - if (callback) { - this.once('timeout', callback) + _unrefTimer () { + for (let s = this; s !== null; s = s._parent) { + timers._unrefActive(s) } } - return this -} - -Socket.prototype._onTimeout = function () { - this.emit('timeout') -} + /** + * Disables the Nagle algorithm. By default TCP connections use the Nagle + * algorithm, they buffer data before sending it off. Setting true for noDelay + * will immediately fire off data each time socket.write() is called. noDelay + * defaults to true. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {boolean} [noDelay] Optional + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ + setNoDelay (noDelay, callback) { + if (!this.id) { + this.once('connect', () => this.setNoDelay(noDelay, callback)) + return this + } -Socket.prototype._unrefTimer = function unrefTimer () { - for (let s = this; s !== null; s = s._parent) { - timers._unrefActive(s) - } -} + // backwards compatibility: assume true when `noDelay` is omitted + noDelay = noDelay === undefined ? true : !!noDelay + chrome.sockets.tcp.setNoDelay(this.id, noDelay, chromeCallbackWrap(callback)) -/** - * Disables the Nagle algorithm. By default TCP connections use the Nagle - * algorithm, they buffer data before sending it off. Setting true for noDelay - * will immediately fire off data each time socket.write() is called. noDelay - * defaults to true. - * - * NOTE: The Chrome version of this function is async, whereas the node - * version is sync. Keep this in mind. - * - * @param {boolean} [noDelay] Optional - * @param {function} callback CHROME-SPECIFIC: Called when the configuration - * operation is done. - */ -Socket.prototype.setNoDelay = function (noDelay, callback) { - if (!this.id) { - this.once('connect', () => this.setNoDelay(noDelay, callback)) return this } - // backwards compatibility: assume true when `noDelay` is omitted - noDelay = noDelay === undefined ? true : !!noDelay - chrome.sockets.tcp.setNoDelay(this.id, noDelay, chromeCallbackWrap(callback)) + /** + * Enable/disable keep-alive functionality, and optionally set the initial + * delay before the first keepalive probe is sent on an idle socket. enable + * defaults to false. + * + * Set initialDelay (in milliseconds) to set the delay between the last data + * packet received and the first keepalive probe. Setting 0 for initialDelay + * will leave the value unchanged from the default (or previous) setting. + * Defaults to 0. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {boolean} [enable] Optional + * @param {number} [initialDelay] + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ + setKeepAlive (enable, initialDelay, callback) { + if (!this.id) { + this.once('connect', () => this.setKeepAlive(enable, initialDelay, callback)) + return this + } - return this -} + chrome.sockets.tcp.setKeepAlive(this.id, !!enable, ~~(initialDelay / 1000), + chromeCallbackWrap(callback)) -/** - * Enable/disable keep-alive functionality, and optionally set the initial - * delay before the first keepalive probe is sent on an idle socket. enable - * defaults to false. - * - * Set initialDelay (in milliseconds) to set the delay between the last data - * packet received and the first keepalive probe. Setting 0 for initialDelay - * will leave the value unchanged from the default (or previous) setting. - * Defaults to 0. - * - * NOTE: The Chrome version of this function is async, whereas the node - * version is sync. Keep this in mind. - * - * @param {boolean} [enable] Optional - * @param {number} [initialDelay] - * @param {function} callback CHROME-SPECIFIC: Called when the configuration - * operation is done. - */ -Socket.prototype.setKeepAlive = function (enable, initialDelay, callback) { - if (!this.id) { - this.once('connect', () => this.setKeepAlive(enable, initialDelay, callback)) return this } - chrome.sockets.tcp.setKeepAlive(this.id, !!enable, ~~(initialDelay / 1000), - chromeCallbackWrap(callback)) - - return this -} - -/** - * Returns the bound address, the address family name and port of the socket - * as reported by the operating system. Returns an object with three - * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } - * - * @return {Object} information - */ -Socket.prototype.address = function () { - return { - address: this.localAddress, - port: this.localPort, - family: this.localAddress && this.localAddress.indexOf(':') !== -1 - ? 'IPv6' - : 'IPv4' + /** + * Returns the bound address, the address family name and port of the socket + * as reported by the operating system. Returns an object with three + * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } + * + * @return {Object} information + */ + address () { + return { + address: this.localAddress, + port: this.localPort, + family: this.localAddress && + this.localAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4' + } } -} -Object.defineProperty(Socket.prototype, '_connecting', { - get: function () { + get _connecting () { return this.connecting } -}) -Object.defineProperty(Socket.prototype, 'readyState', { - get: function () { + get readyState () { if (this.connecting) { return 'opening' } else if (this.readable && this.writable) { @@ -1048,13 +1018,18 @@ Object.defineProperty(Socket.prototype, 'readyState', { return 'closed' } } -}) -Socket.prototype.unref = -Socket.prototype.ref = function () { - // No chrome.socket equivalent - return this + ref () { + // No chrome.socket equivalent + return this + } + + unref () { + // No chrome.socket equivalent + return this + } } +exports.Socket = Socket // // EXPORTED HELPERS