chrome-dgram
Version:
Use the Node `dgram` API in Chrome Apps
504 lines (442 loc) • 15.5 kB
JavaScript
/*! chrome-dgram. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/* global chrome */
/**
* UDP / Datagram Sockets
* ======================
*
* Datagram sockets are available through require('chrome-dgram').
*/
exports.Socket = Socket
var EventEmitter = require('events').EventEmitter
var inherits = require('inherits')
var series = require('run-series')
var BIND_STATE_UNBOUND = 0
var BIND_STATE_BINDING = 1
var BIND_STATE_BOUND = 2
// Track open sockets to route incoming data (via onReceive) to the right handlers.
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.udp === 'object'
) {
chrome.sockets.udp.onReceive.addListener(onReceive)
chrome.sockets.udp.onReceiveError.addListener(onReceiveError)
}
function onReceive (info) {
if (info.socketId in sockets) {
sockets[info.socketId]._onReceive(info)
} else {
console.error('Unknown socket id: ' + info.socketId)
}
}
function onReceiveError (info) {
if (info.socketId in sockets) {
sockets[info.socketId]._onReceiveError(info.resultCode)
} else {
console.error('Unknown socket id: ' + info.socketId)
}
}
/**
* dgram.createSocket(type, [callback])
*
* Creates a datagram Socket of the specified types. Valid types are `udp4`
* and `udp6`.
*
* Takes an optional callback which is added as a listener for message events.
*
* Call socket.bind if you want to receive datagrams. socket.bind() will bind
* to the "all interfaces" address on a random port (it does the right thing
* for both udp4 and udp6 sockets). You can then retrieve the address and port
* with socket.address().address and socket.address().port.
*
* @param {string} type Either 'udp4' or 'udp6'
* @param {function} listener Attached as a listener to message events.
* Optional
* @return {Socket} Socket object
*/
exports.createSocket = function (type, listener) {
return new Socket(type, listener)
}
inherits(Socket, EventEmitter)
/**
* Class: dgram.Socket
*
* The dgram Socket class encapsulates the datagram functionality. It should
* be created via `dgram.createSocket(type, [callback])`.
*
* Event: 'message'
* - msg Buffer object. The message
* - rinfo Object. Remote address information
* Emitted when a new datagram is available on a socket. msg is a Buffer and
* rinfo is an object with the sender's address information and the number
* of bytes in the datagram.
*
* Event: 'listening'
* Emitted when a socket starts listening for datagrams. This happens as soon
* as UDP sockets are created.
*
* Event: 'close'
* Emitted when a socket is closed with close(). No new message events will
* be emitted on this socket.
*
* Event: 'error'
* - exception Error object
* Emitted when an error occurs.
*/
function Socket (options, listener) {
var self = this
EventEmitter.call(self)
if (typeof options === 'string') options = { type: options }
if (options.type !== 'udp4') throw new Error('Bad socket type specified. Valid types are: udp4')
if (typeof listener === 'function') self.on('message', listener)
self._destroyed = false
self._bindState = BIND_STATE_UNBOUND
self._bindTasks = []
}
/**
* socket.bind(port, [address], [callback])
*
* For UDP sockets, listen for datagrams on a named port and optional address.
* If address is not specified, the OS will try to listen on all addresses.
* After binding is done, a "listening" event is emitted and the callback(if
* specified) is called. Specifying both a "listening" event listener and
* callback is not harmful but not very useful.
*
* A bound datagram socket keeps the node process running to receive
* datagrams.
*
* If binding fails, an "error" event is generated. In rare case (e.g. binding
* a closed socket), an Error may be thrown by this method.
*
* @param {number} port
* @param {string} address Optional
* @param {function} callback Function with no parameters, Optional. Callback
* when binding is done.
*/
Socket.prototype.bind = function (port, address, callback) {
var self = this
if (typeof address === 'function') {
callback = address
address = undefined
}
if (!address) address = '0.0.0.0'
if (!port) port = 0
if (self._bindState !== BIND_STATE_UNBOUND) throw new Error('Socket is already bound')
self._bindState = BIND_STATE_BINDING
if (typeof callback === 'function') self.once('listening', callback)
chrome.sockets.udp.create(function (createInfo) {
self.id = createInfo.socketId
sockets[self.id] = self
var bindFns = self._bindTasks.map(function (t) { return t.fn })
series(bindFns, function (err) {
if (err) return self.emit('error', err)
chrome.sockets.udp.bind(self.id, address, port, function (result) {
if (result < 0) {
self.emit('error', new Error('Socket ' + self.id + ' failed to bind. ' +
chrome.runtime.lastError.message))
return
}
chrome.sockets.udp.getInfo(self.id, function (socketInfo) {
if (!socketInfo.localPort || !socketInfo.localAddress) {
self.emit('error', new Error('Cannot get local port/address for Socket ' + self.id))
return
}
self._port = socketInfo.localPort
self._address = socketInfo.localAddress
self._bindState = BIND_STATE_BOUND
self.emit('listening')
self._bindTasks.map(function (t) {
t.callback()
})
})
})
})
})
}
/**
* Internal function to receive new messages and emit `message` events.
*/
Socket.prototype._onReceive = function (info) {
var self = this
var buf = Buffer.from(new Uint8Array(info.data))
var rinfo = {
address: info.remoteAddress,
family: 'IPv4',
port: info.remotePort,
size: buf.length
}
self.emit('message', buf, rinfo)
}
Socket.prototype._onReceiveError = function (resultCode) {
var self = this
self.emit('error', new Error('Socket ' + self.id + ' receive error ' + resultCode))
}
/**
* socket.send(buf, offset, length, port, address, [callback])
*
* For UDP sockets, the destination port and IP address must be
* specified. A string may be supplied for the address parameter, and it will
* be resolved with DNS. An optional callback may be specified to detect any
* DNS errors and when buf may be re-used. Note that DNS lookups will delay
* the time that a send takes place, at least until the next tick. The only
* way to know for sure that a send has taken place is to use the callback.
*
* If the socket has not been previously bound with a call to bind, it's
* assigned a random port number and bound to the "all interfaces" address
* (0.0.0.0 for udp4 sockets, ::0 for udp6 sockets).
*
* @param {Buffer|Arrayish|string} buf Message to be sent
* @param {number} offset Offset in the buffer where the message starts. Optional.
* @param {number} length Number of bytes in the message. Optional.
* @param {number} port destination port
* @param {string} address destination IP
* @param {function} callback Callback when message is done being delivered.
* Optional.
*
* Valid combinations:
* send(buffer, offset, length, port, address, callback)
* send(buffer, offset, length, port, address)
* send(buffer, offset, length, port)
* send(bufferOrList, port, address, callback)
* send(bufferOrList, port, address)
* send(bufferOrList, port)
*
*/
Socket.prototype.send = function (buffer, offset, length, port, address, callback) {
var self = this
var list
if (address || (port && typeof port !== 'function')) {
buffer = sliceBuffer(buffer, offset, length)
} else {
callback = port
port = offset
address = length
}
if (!Array.isArray(buffer)) {
if (typeof buffer === 'string') {
list = [Buffer.from(buffer)]
} else if (!(buffer instanceof Buffer)) {
throw new TypeError('First argument must be a buffer or a string')
} else {
list = [buffer]
}
} else if (!(list = fixBufferList(buffer))) {
throw new TypeError('Buffer list arguments must be buffers or strings')
}
port = port >>> 0
if (port === 0 || port > 65535) {
throw new RangeError('Port should be > 0 and < 65536')
}
// Normalize callback so it's always a function
if (typeof callback !== 'function') {
callback = function () {}
}
if (self._bindState === BIND_STATE_UNBOUND) self.bind(0)
// If the socket hasn't been bound yet, push the outbound packet onto the
// send queue and send after binding is complete.
if (self._bindState !== BIND_STATE_BOUND) {
// If the send queue hasn't been initialized yet, do it, and install an
// event handler that flishes the send queue after binding is done.
if (!self._sendQueue) {
self._sendQueue = []
self.once('listening', function () {
// Flush the send queue.
for (var i = 0; i < self._sendQueue.length; i++) {
self.send.apply(self, self._sendQueue[i])
}
self._sendQueue = undefined
})
}
self._sendQueue.push([buffer, offset, length, port, address, callback])
return
}
var ab = Buffer.concat(list).buffer
chrome.sockets.udp.send(self.id, ab, address, port, function (sendInfo) {
if (sendInfo.resultCode < 0) {
var err = new Error('Socket ' + self.id + ' send error ' + sendInfo.resultCode)
callback(err)
self.emit('error', err)
} else {
callback(null)
}
})
}
function sliceBuffer (buffer, offset, length) {
if (typeof buffer === 'string') {
buffer = Buffer.from(buffer)
} else if (!(buffer instanceof Buffer)) {
throw new TypeError('First argument must be a buffer or string')
}
offset = offset >>> 0
length = length >>> 0
// assuming buffer is browser implementation (`buffer` package on npm)
var buf = buffer.buffer
if (buffer.byteOffset || buffer.byteLength !== buf.byteLength) {
buf = buf.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
}
if (offset || length !== buffer.length) {
buf = buf.slice(offset, length)
}
return Buffer.from(buf)
}
function fixBufferList (list) {
var newlist = new Array(list.length)
for (var i = 0, l = list.length; i < l; i++) {
var buf = list[i]
if (typeof buf === 'string') {
newlist[i] = Buffer.from(buf)
} else if (!(buf instanceof Buffer)) {
return null
} else {
newlist[i] = buf
}
}
return newlist
}
/**
* Close the underlying socket and stop listening for data on it.
*/
Socket.prototype.close = function () {
var self = this
if (self._destroyed) return
delete sockets[self.id]
chrome.sockets.udp.close(self.id)
self._destroyed = true
self.emit('close')
}
/**
* Returns an object containing the address information for a socket. For UDP
* sockets, this object will contain address, family and port.
*
* @return {Object} information
*/
Socket.prototype.address = function () {
var self = this
return {
address: self._address,
port: self._port,
family: 'IPv4'
}
}
Socket.prototype.setBroadcast = function (flag) {
// No chrome.sockets equivalent
}
Socket.prototype.setTTL = function (ttl) {
// No chrome.sockets equivalent
}
// NOTE: Multicast code is untested. Pull requests accepted for bug fixes and to
// add tests!
/**
* Sets the IP_MULTICAST_TTL socket option. TTL stands for "Time to Live," but
* in this context it specifies the number of IP hops that a packet is allowed
* to go through, specifically for multicast traffic. Each router or gateway
* that forwards a packet decrements the TTL. If the TTL is decremented to 0
* by a router, it will not be forwarded.
*
* The argument to setMulticastTTL() is a number of hops between 0 and 255.
* The default on most systems is 1.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {number} ttl
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.setMulticastTTL = function (ttl, callback) {
var self = this
if (!callback) callback = function () {}
if (self._bindState === BIND_STATE_BOUND) {
setMulticastTTL(callback)
} else {
self._bindTasks.push({
fn: setMulticastTTL,
callback
})
}
function setMulticastTTL (callback) {
chrome.sockets.udp.setMulticastTimeToLive(self.id, ttl, callback)
}
}
/**
* Sets or clears the IP_MULTICAST_LOOP socket option. When this option is
* set, multicast packets will also be received on the local interface.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {boolean} flag
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.setMulticastLoopback = function (flag, callback) {
var self = this
if (!callback) callback = function () {}
if (self._bindState === BIND_STATE_BOUND) {
setMulticastLoopback(callback)
} else {
self._bindTasks.push({
fn: setMulticastLoopback,
callback
})
}
function setMulticastLoopback (callback) {
chrome.sockets.udp.setMulticastLoopbackMode(self.id, flag, callback)
}
}
/**
* Tells the kernel to join a multicast group with IP_ADD_MEMBERSHIP socket
* option.
*
* If multicastInterface is not specified, the OS will try to add membership
* to all valid interfaces.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {string} multicastAddress
* @param {string} [multicastInterface] Optional
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.addMembership = function (multicastAddress,
multicastInterface,
callback) {
var self = this
if (!callback) callback = function () {}
chrome.sockets.udp.joinGroup(self.id, multicastAddress, callback)
}
/**
* Opposite of addMembership - tells the kernel to leave a multicast group
* with IP_DROP_MEMBERSHIP socket option. This is automatically called by the
* kernel when the socket is closed or process terminates, so most apps will
* never need to call this.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* If multicastInterface is not specified, the OS will try to drop membership
* to all valid interfaces.
*
* @param {[type]} multicastAddress
* @param {[type]} multicastInterface Optional
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.dropMembership = function (multicastAddress,
multicastInterface,
callback) {
var self = this
if (!callback) callback = function () {}
chrome.sockets.udp.leaveGroup(self.id, multicastAddress, callback)
}
Socket.prototype.unref = function () {
// No chrome.sockets equivalent
}
Socket.prototype.ref = function () {
// No chrome.sockets equivalent
}