forked from feross/chrome-dgram
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
504 lines (443 loc) · 15.2 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
/* 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)
} else {
console.error('Undefined chrome ref, could not register on-receive listener')
}
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: 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: 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
}