UNPKG

@koush/chrome-net

Version:

Use the Node `net` API in Chrome Apps

1,202 lines (1,045 loc) 34.8 kB
/* global chrome */ 'use strict' /** * net * === * * The net module provides you with an asynchronous network wrapper. It * contains methods for creating both servers and clients (called streams). * You can include this module with require('chrome-net') */ var EventEmitter = require('events') var inherits = require('inherits') var stream = require('stream') var deprecate = require('util').deprecate var timers = require('timers') var Buffer = require('buffer').Buffer // Track open servers and sockets to route incoming sockets (via onAccept and onReceive) // to the right handlers. var servers = {} var 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) 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 onReceive (info) { if (info.socketId in sockets) { sockets[info.socketId]._onReceive(info.data) } else { // console.error('Unknown socket id: ' + info.socketId) } } function onReceiveError (info) { if (info.socketId in sockets) { sockets[info.socketId]._onReceiveError(info.resultCode) } else { if (info.resultCode === -100) return // net::ERR_CONNECTION_CLOSED // console.error('Unknown socket id: ' + info.socketId) } } /** * Creates a new TCP server. The connectionListener argument is automatically * set as a listener for the 'connection' event. * * @param {Object} options * @param {function} connectionListener * @return {Server} */ exports.createServer = function (options, connectionListener) { return new Server(options, connectionListener) } /** * net.connect(options, [connectionListener]) * net.createConnection(options, [connectionListener]) * * Constructs a new socket object and opens the socket to the given location. * When the socket is established, the 'connect' event will be emitted. * * For TCP sockets, options argument should be an object which specifies: * * port: Port the client should connect to (Required). * host: Host the client should connect to. Defaults to 'localhost'. * localAddress: Local interface to bind to for network connections. * * =============================================================== * * net.connect(port, [host], [connectListener]) * net.createConnection(port, [host], [connectListener]) * * Creates a TCP connection to port on host. If host is omitted, * 'localhost' will be assumed. The connectListener parameter will be * added as an listener for the 'connect' event. * * @param {Object} options * @param {function} listener * @return {Socket} */ exports.connect = exports.createConnection = function () { const argsLen = arguments.length var args = new Array(argsLen) for (var i = 0; i < argsLen; i++) args[i] = arguments[i] args = normalizeConnectArgs(args) var s = new Socket(args[0]) return Socket.prototype.connect.apply(s, args) } inherits(Server, EventEmitter) /** * Class: net.Server * ================= * * This class is used to create a TCP server. * * Event: 'listening' * Emitted when the server has been bound after calling server.listen. * * Event: 'connection' * - Socket object The connection object * Emitted when a new connection is made. socket is an instance of net.Socket. * * Event: 'close' * Emitted when the server closes. Note that if connections exist, this event * is not emitted until all connections are ended. * * Event: 'error' * - Error Object * 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') { this.on('connection', connectionListener) } } else { throw new TypeError('options must be an object') } this._connections = 0 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.id = null // a number > 0 this.connecting = false this.allowHalfOpen = options.allowHalfOpen || false this.pauseOnConnect = !!options.pauseOnConnect this._address = null this._host = null this._port = null this._backlog = null } exports.Server = Server Server.prototype._usingSlaves = false // not used /** * 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... */) { var lastArg = arguments[arguments.length - 1] if (typeof lastArg === 'function') { this.once('listening', lastArg) } var port = toNumber(arguments[0]) var address // The third optional argument is the backlog size. // When the ip is omitted it can be the second argument. var backlog = toNumber(arguments[1]) || toNumber(arguments[2]) || undefined if (arguments[0] !== null && typeof arguments[0] === 'object') { var h = arguments[0] 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.') } // 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 { // 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() } // If port is invalid or undefined, bind to a random port. assertPort(port) this._port = port | 0 this._host = address var isAny6 = !this._host if (isAny6) { this._host = '::' } this._backlog = typeof backlog === 'number' ? backlog : undefined 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 } var socketId = this.id = createInfo.socketId servers[this.id] = this var 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() }) return this } Server.prototype._onListen = function (result) { this.connecting = false if (result === 0) { var 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 } this._address = { port: info.localPort, family: info.localAddress && info.localAddress.indexOf(':') !== -1 ? 'IPv6' : 'IPv4', address: info.localAddress } 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 var acceptedSocket = new Socket({ server: this, id: clientSocketId, allowHalfOpen: this.allowHalfOpen, pauseOnCreate: this.pauseOnConnect }) acceptedSocket.on('connect', () => this.emit('connection', acceptedSocket)) } Server.prototype._onAcceptError = function (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 */ 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) } } if (this.id) { chrome.sockets.tcpServer.close(this.id) delete servers[this.id] this.id = null } this._address = null this.connecting = false this._emitCloseIfDrained() return this } Server.prototype._emitCloseIfDrained = function () { if (this.id || this.connecting || this._connections) { return } process.nextTick(emitCloseNT, this) } function emitCloseNT (self) { if (self.id || self.connecting || self._connections) { return } 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 * ================= * * This object is an abstraction of a TCP or UNIX socket. net.Socket instances * implement a duplex Stream interface. They can be created by the user and * used as a client (with connect()) or they can be created by Node and passed * to the user through the 'connection' event of a server. * * Construct a new socket object. * * options is an object with the following defaults: * * { fd: null // NO CHROME EQUIVALENT * type: null * allowHalfOpen: false // NO CHROME EQUIVALENT * } * * `type` can only be 'tcp4' (for now). * * Event: 'connect' * Emitted when a socket connection is successfully established. See * connect(). * * Event: 'data' * - Buffer object * Emitted when data is received. The argument data will be a Buffer or * String. Encoding of data is set by socket.setEncoding(). (See the Readable * Stream section for more information.) * * Note that the data will be lost if there is no listener when a Socket * emits a 'data' event. * * Event: 'end' * Emitted when the other end of the socket sends a FIN packet. * * By default (allowHalfOpen == false) the socket will destroy its file * descriptor once it has written out its pending write queue. However, * by setting allowHalfOpen == true the socket will not automatically * end() its side allowing the user to write arbitrary amounts of data, * with the caveat that the user is required to end() their side now. * * Event: 'timeout' * Emitted if the socket times out from inactivity. This is only to notify * that the socket has been idle. The user must manually close the connection. * * See also: socket.setTimeout() * * Event: 'drain' * Emitted when the write buffer becomes empty. Can be used to throttle * uploads. * * See also: the return values of socket.write() * * Event: 'error' * - Error object * Emitted when an error occurs. The 'close' event will be called directly * following this event. * * Event: 'close' * - had_error Boolean true if the socket had a transmission error * 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) 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.') } 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() } } 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._bytesDispatched = 0 // Reserve properties this.server = null this._server = null } // 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 } /** * 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 var args = new Array(argsLen) for (var i = 0; i < argsLen; i++) args[i] = arguments[i] args = normalizeConnectArgs(args) var options = args[0] var cb = args[1] if (options.path) { throw new Error('Pipes are not supported in Chrome Apps.') } if (this.id) { // already connected, destroy and connect again this.destroy() } 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 } this.connecting = true this.writable = true this._host = options.host || 'localhost' this._port = options.port 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 this._init() this._unrefTimer() if (typeof cb === 'function') { this.once('connect', cb) } 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 } this.id = createInfo.socketId sockets[this.id] = this chrome.sockets.tcp.setPaused(this.id, 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._unrefTimer() this._onConnect() }) }) return this } Socket.prototype._onConnect = function () { var 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.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} */ Object.defineProperty(Socket.prototype, 'bufferSize', { get: function () { if (this.id) { var bytes = this._writableState.length if (this._pendingData) bytes += this._pendingData.length return bytes } } }) Socket.prototype.end = function (data, encoding) { stream.Duplex.prototype.end.call(this, data, encoding) this.writable = false } Socket.prototype._write = function (chunk, encoding, callback) { if (!callback) callback = () => {} if (this.connecting) { this._pendingData = chunk this.once('connect', () => this._write(chunk, encoding, callback)) return } this._pendingData = null if (!this.id) { callback(new Error('This socket is closed')) return } // assuming buffer is browser implementation (`buffer` package on npm) var buffer = chunk.buffer if (chunk.byteLength !== buffer.byteLength) { buffer = buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength) } var 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 } chrome.sockets.tcp.setPaused(this.id, false) var 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._onReceive = function (data) { var buffer = Buffer.from(data) var 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) } 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 }) } /** * The amount of bytes sent. * @return {number} */ protoGetter('bytesWritten', function bytesWritten () { if (this.id) return this._bytesDispatched + this.bufferSize }) Socket.prototype.destroy = function (exception) { this._destroy(exception) } Socket.prototype._destroy = function (exception, cb) { var fireErrorCallbacks = () => { if (cb) cb(exception) if (exception && !this._writableState.errorEmitted) { process.nextTick(emitErrorNT, this, exception) this._writableState.errorEmitted = true } } 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 (var s = this; s !== null; s = s._parent) timers.unenroll(s) // eslint-disable-line node/no-deprecated-api 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 } fireErrorCallbacks() } Socket.prototype.destroySoon = function () { if (this.writable) this.end() if (this._writableState.finished) this.destroy() } /** * 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) } } return this } Socket.prototype._onTimeout = function () { this.emit('timeout') } Socket.prototype._unrefTimer = function unrefTimer () { for (var s = this; s !== null; s = s._parent) { timers._unrefActive(s) } } /** * 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)) return this } /** * 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' } } Object.defineProperty(Socket.prototype, '_connecting', { get: function () { return this.connecting } }) Object.defineProperty(Socket.prototype, 'readyState', { get: function () { if (this.connecting) { return 'opening' } else if (this.readable && this.writable) { return 'open' } else { return 'closed' } } }) Socket.prototype.unref = Socket.prototype.ref = function () { // No chrome.socket equivalent return this } // // EXPORTED HELPERS // // Source: https://developers.google.com/web/fundamentals/input/form/provide-real-time-validation#use-these-attributes-to-validate-input var IPv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ var IPv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/ exports.isIPv4 = IPv4Regex.test.bind(IPv4Regex) exports.isIPv6 = IPv6Regex.test.bind(IPv6Regex) exports.isIP = function (ip) { return exports.isIPv4(ip) ? 4 : exports.isIPv6(ip) ? 6 : 0 } // // HELPERS // /** * Returns an array [options] or [options, cb] * It is the same as the argument of Socket.prototype.connect(). */ function normalizeConnectArgs (args) { var options = {} if (args[0] !== null && typeof args[0] === 'object') { // connect(options, [cb]) options = args[0] } else if (isPipeName(args[0])) { // connect(path, [cb]) throw new Error('Pipes are not supported in Chrome Apps.') } else { // connect(port, [host], [cb]) options.port = args[0] if (typeof args[1] === 'string') { options.host = args[1] } } var cb = args[args.length - 1] return typeof cb === 'function' ? [options, cb] : [options] } function toNumber (x) { return (x = Number(x)) >= 0 ? x : false } function isPipeName (s) { return typeof s === 'string' && toNumber(s) === false } // Check that the port number is not NaN when coerced to a number, // is an integer and that it falls within the legal range of port numbers. function isLegalPort (port) { if ((typeof port !== 'number' && typeof port !== 'string') || (typeof port === 'string' && port.trim().length === 0)) { return false } return +port === (+port >>> 0) && port <= 0xFFFF } function assertPort (port) { if (typeof port !== 'undefined' && !isLegalPort(port)) { throw new RangeError('"port" argument must be >= 0 and < 65536') } } // Call the getter function to prevent "Unchecked runtime.lastError" errors function ignoreLastError () { void chrome.runtime.lastError // eslint-disable-line no-void } function chromeCallbackWrap (callback) { return () => { var error if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError.message) error = new Error(chrome.runtime.lastError.message) } if (callback) callback(error) } } function emitErrorNT (self, err) { self.emit('error', err) } // Full list of possible error codes: https://code.google.com/p/chrome-browser/source/browse/trunk/src/net/base/net_error_list.h // TODO: Try to reproduce errors in both node & Chrome Apps and extend this list // (what conditions lead to EPIPE?) var errorChromeToUv = { '-10': 'EACCES', '-22': 'EACCES', '-138': 'EACCES', '-147': 'EADDRINUSE', '-108': 'EADDRNOTAVAIL', '-103': 'ECONNABORTED', '-102': 'ECONNREFUSED', '-101': 'ECONNRESET', '-16': 'EEXIST', '-8': 'EFBIG', '-109': 'EHOSTUNREACH', '-4': 'EINVAL', '-23': 'EISCONN', '-6': 'ENOENT', '-13': 'ENOMEM', '-106': 'ENONET', '-18': 'ENOSPC', '-11': 'ENOSYS', '-15': 'ENOTCONN', '-105': 'ENOTFOUND', '-118': 'ETIMEDOUT', '-100': 'EOF' } function errnoException (err, syscall, details) { var uvCode = errorChromeToUv[err] || 'UNKNOWN' var message = syscall + ' ' + err + ' ' + details if (chrome.runtime.lastError) { message += ' ' + chrome.runtime.lastError.message } message += ' (mapped uv code: ' + uvCode + ')' var e = new Error(message) e.code = e.errno = uvCode // TODO: expose chrome error code; what property name? e.syscall = syscall return e } function exceptionWithHostPort (err, syscall, address, port, additional) { var details if (port && port > 0) { details = address + ':' + port } else { details = address } if (additional) { details += ' - Local (' + additional + ')' } var ex = errnoException(err, syscall, details) ex.address = address if (port) { ex.port = port } return ex }