@silentbot1/nat-api
Version:
Port mapping with UPnP and NAT-PMP
174 lines (133 loc) • 4.86 kB
JavaScript
import Ssdp from './ssdp.js'
export default class Client {
constructor (opts = {}) {
this.permanentFallback = opts.permanentFallback || false
this.ssdp = new Ssdp({ permanentFallback: this.permanentFallback })
this.timeout = 1800
this._destroyed = false
}
async portMapping (options) {
if (this._destroyed) throw new Error('client is destroyed')
const { gateway, address } = await this.findGateway()
const ports = this._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)
return 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]
])
}
async portUnmapping (options) {
if (this._destroyed) throw new Error('client is destroyed')
const { gateway } = await this.findGateway()
const ports = this._normalizeOptions(options)
const protocol = options.protocol ? options.protocol.toUpperCase() : 'TCP'
return gateway.run('DeletePortMapping', [
['NewRemoteHost', ports.remote.host],
['NewExternalPort', ports.remote.port],
['NewProtocol', protocol]
])
}
async getMappings (options) {
if (this._destroyed) throw new Error('client is destroyed')
if (!options) options = {}
const { gateway, address } = await this.findGateway()
let end = false
let i = 0
let results = []
do {
let data
try {
data = await gateway.run('GetGenericPortMappingEntry', [['NewPortMappingIndex', i++]])
} catch (e) {
if (i !== 1) end = true
break
}
let key = null
Object.keys(data).some((k) => {
if (!/:GetGenericPortMappingEntryResponse/.test(k)) return false
key = k
return true
})
if (!key) throw new 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)
} while (!end)
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
}
})
}
return results
}
async externalIp () {
if (this._destroyed) throw new Error('client is destroyed')
const { gateway } = await this.findGateway()
const data = await gateway.run('GetExternalIPAddress', [])
let key = null
Object.keys(data).some((k) => {
if (!/:GetExternalIPAddressResponse$/.test(k)) return false
key = k
return true
})
if (!key) throw new Error('Incorrect response')
return data[key].NewExternalIPAddress
}
async findGateway () {
if (this._destroyed) throw new Error('client is destroyed')
const { device, address } = await this.ssdp.search(
'urn:schemas-upnp-org:device:InternetGatewayDevice:1', this.timeout
)
return { gateway: device, address }
}
destroy () {
this._destroyed = true
return this.ssdp.destroy()
}
_normalizeOptions (options) {
const 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)
}
}
}