nat-api
Version:
Port mapping with UPnP and NAT-PMP
231 lines (182 loc) • 6.25 kB
JavaScript
const async = require('async')
const Device = require('./device')
const Ssdp = require('./ssdp')
class Client {
constructor (opts) {
this.ssdp = new Ssdp()
this.timeout = 1800
this._destroyed = false
}
static createClient () {
return new Client()
}
portMapping (options, callback) {
const self = this
if (self._destroyed) throw new Error('client is destroyed')
if (!callback) callback = noop
this.findGateway(function (err, gateway, address) {
if (err) return callback(err)
const ports = self._normalizeOptions(options)
const description = options.description || 'node:nat:upnp'
const protocol = options.protocol ? options.protocol.toUpperCase() : 'TCP'
let ttl = 60 * 30
if (typeof options.ttl === 'number') ttl = options.ttl
if (typeof options.ttl === 'string' && !isNaN(options.ttl)) ttl = Number(options.ttl)
gateway.run('AddPortMapping', [
['NewRemoteHost', ports.remote.host],
['NewExternalPort', ports.remote.port],
['NewProtocol', protocol],
['NewInternalPort', ports.internal.port],
['NewInternalClient', ports.internal.host || address],
['NewEnabled', 1],
['NewPortMappingDescription', description],
['NewLeaseDuration', ttl]
], callback)
})
}
portUnmapping (options, callback) {
const self = this
if (self._destroyed) throw new Error('client is destroyed')
if (!callback) callback = noop
this.findGateway(function (err, gateway/* , address */) {
if (err) return callback(err)
const ports = self._normalizeOptions(options)
const protocol = options.protocol ? options.protocol.toUpperCase() : 'TCP'
gateway.run('DeletePortMapping', [
['NewRemoteHost', ports.remote.host],
['NewExternalPort', ports.remote.port],
['NewProtocol', protocol]
], callback)
})
}
getMappings (options, callback) {
const self = this
if (self._destroyed) throw new Error('client is destroyed')
if (typeof options === 'function') {
callback = options
options = null
}
if (!options) options = {}
if (!callback) callback = noop
this.findGateway(function (err, gateway, address) {
if (err) return callback(err)
let i = 0
let end = false
let results = []
async.whilst(function () {
return !end
}, function (callback) {
gateway.run('GetGenericPortMappingEntry', [
['NewPortMappingIndex', i++]
], (err, data) => {
if (err) {
// If we got an error on index 0, ignore it in case this router starts indicies on 1
if (i !== 1) end = true
return callback(null)
}
let key = null
Object.keys(data).some(function (k) {
if (!/:GetGenericPortMappingEntryResponse/.test(k)) return false
key = k
return true
})
if (!key) return callback(Error('Incorrect response'))
data = data[key]
const result = {
public: {
host: (typeof data.NewRemoteHost === 'string') && (data.NewRemoteHost || ''),
port: parseInt(data.NewExternalPort, 10)
},
private: {
host: data.NewInternalClient,
port: parseInt(data.NewInternalPort, 10)
},
protocol: data.NewProtocol.toLowerCase(),
enabled: data.NewEnabled === '1',
description: data.NewPortMappingDescription,
ttl: parseInt(data.NewLeaseDuration, 10)
}
result.local = (result.private.host === address)
results.push(result)
callback(null)
})
}, (err) => {
if (err) return callback(err)
if (options.local) {
results = results.filter((item) => {
return item.local
})
}
if (options.description) {
results = results.filter((item) => {
if (typeof item.description !== 'string') return false
if (options.description instanceof RegExp) {
return item.description.match(options.description) !== null
} else {
return item.description.indexOf(options.description) !== -1
}
})
}
callback(null, results)
})
})
}
externalIp (callback) {
const self = this
if (self._destroyed) throw new Error('client is destroyed')
if (!callback) callback = noop
this.findGateway(function (err, gateway/* , address */) {
if (err) return callback(err)
gateway.run('GetExternalIPAddress', [], function (err, data) {
if (err) return callback(err)
let key = null
Object.keys(data).some(function (k) {
if (!/:GetExternalIPAddressResponse$/.test(k)) return false
key = k
return true
})
if (!key) return callback(Error('Incorrect response'))
callback(null, data[key].NewExternalIPAddress)
})
})
}
findGateway (callback) {
const self = this
if (self._destroyed) throw new Error('client is destroyed')
if (!callback) callback = noop
let timeouted = false
const p = this.ssdp.search(
'urn:schemas-upnp-org:device:InternetGatewayDevice:1'
)
const timeout = setTimeout(function () {
timeouted = true
p.emit('end')
callback(new Error('timeout'))
}, this.timeout)
p.on('device', function (info, address) {
if (timeouted) return
clearTimeout(timeout)
p.emit('end')
// Create gateway
callback(null, new Device(info.location), address)
})
}
destroy () {
this._destroyed = true
this.ssdp.destroy()
}
_normalizeOptions (options) {
function toObject (addr) {
if (typeof addr === 'number') return { port: addr }
if (typeof addr === 'string' && !isNaN(addr)) return { port: Number(addr) }
if (typeof addr === 'object') return addr
return {}
}
return {
remote: toObject(options.public),
internal: toObject(options.private)
}
}
}
function noop () {}
module.exports = Client