UNPKG

nat-api

Version:

Port mapping with UPnP and NAT-PMP

196 lines (151 loc) 4.71 kB
const dgram = require('dgram') const os = require('os') const EventEmitter = require('events').EventEmitter const MULTICAST_IP_ADDRESS = '239.255.255.250' const MULTICAST_PORT = 1900 class Ssdp extends EventEmitter { constructor (opts) { super() opts = opts || {} this.multicast = MULTICAST_IP_ADDRESS this.port = MULTICAST_PORT this._destroyed = false this._sourcePort = opts.sourcePort || 0 this._bound = false this._boundCount = 0 this._destroyed = false this._queue = [] // Create sockets on all external interfaces this.createSockets() } createSockets () { if (this._destroyed) throw new Error('client is destroyed') const self = this const interfaces = os.networkInterfaces() this.sockets = [] for (const key in interfaces) { interfaces[key].filter(function (item) { return !item.internal }).forEach(function (item) { self.createSocket(item) }) } } search (device, promise) { if (this._destroyed) throw new Error('client is destroyed') if (!promise) { promise = new EventEmitter() promise._ended = false promise.once('end', function () { promise._ended = true }) } if (!this._bound) { this._queue.push({ action: 'search', device: device, promise: promise }) return promise } // If promise was ended before binding - do not send queries if (promise._ended) return const self = this const query = Buffer.from( 'M-SEARCH * HTTP/1.1\r\n' + 'HOST: ' + this.multicast + ':' + this.port + '\r\n' + 'MAN: "ssdp:discover"\r\n' + 'MX: 1\r\n' + 'ST: ' + device + '\r\n' + '\r\n' ) // Send query on each socket this.sockets.forEach(function (socket) { socket.send(query, 0, query.length, self.port, self.multicast) }) function onDevice (info, address) { if (promise._ended) return if (info.st !== device) return promise.emit('device', info, address) } this.on('_device', onDevice) // Detach listener after receiving 'end' event promise.once('end', function () { self.removeListener('_device', onDevice) }) return promise } createSocket (interf) { if (this._destroyed) throw new Error('client is destroyed') const self = this let socket = dgram.createSocket(interf.family === 'IPv4' ? 'udp4' : 'udp6') socket.on('message', function (message, info) { // Ignore messages after closing sockets if (self._destroyed) return // Parse response self._parseResponse(message.toString(), socket.address, info) }) // Unqueue this._queue once all sockets are ready function onReady () { if (self._boundCount < self.sockets.length) return self._bound = true self._queue.forEach(function (item) { return self[item.action](item.device, item.promise) }) } socket.on('listening', function () { self._boundCount += 1 onReady() }) function onClose () { if (socket) { const index = self.sockets.indexOf(socket) self.sockets.splice(index, 1) socket = null } } // On error - remove socket from list and execute items from queue socket.on('close', () => { onClose() }) socket.on('error', () => { // Ignore errors if (socket) { socket.close() // Force trigger onClose() - 'close()' does not guarantee to emit 'close' onClose() } onReady() }) socket.address = interf.address socket.bind(self._sourcePort, interf.address) this.sockets.push(socket) } // TODO create separate logic for parsing unsolicited upnp broadcasts, // if and when that need arises _parseResponse (response, addr, remote) { if (this._destroyed) return const self = this // Ignore incorrect packets if (!/^(HTTP|NOTIFY)/m.test(response)) return const headers = self._parseMimeHeader(response) // Messages that match the original search target if (!headers.st) return this.emit('_device', headers, addr) } _parseMimeHeader (headerStr) { if (this._destroyed) return const lines = headerStr.split(/\r\n/g) // Parse headers from lines to hashmap return lines.reduce(function (headers, line) { line.replace(/^([^:]*)\s*:\s*(.*)$/, function (a, key, value) { headers[key.toLowerCase()] = value }) return headers }, {}) } destroy () { this._destroyed = true while (this.sockets.length > 0) { const socket = this.sockets.shift() socket.close() } } } module.exports = Ssdp