@koush/chrome-net
Version:
Use the Node `net` API in Chrome Apps
1,202 lines (1,045 loc) • 34.8 kB
JavaScript
/* 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
}