UNPKG

@achingbrain/nat-port-mapper

Version:
122 lines 4.83 kB
import { isIPv6, isIPv4 } from '@chainsafe/is-ip'; import { logger } from '@libp2p/logger'; import { isPrivateIp } from '../utils.js'; import { DEFAULT_REFRESH_THRESHOLD, DEFAULT_REFRESH_TIMEOUT, DEVICE_WAN_IP_CONNECTION_2 } from './constants.js'; import { Device } from './device.js'; import { discoverGateways } from './discovery.js'; import { findLocalAddresses, findNamespacedKey, stripHostBrackets } from './utils.js'; export class InternetGatewayService { id; host; port; family; gateway; log; mappings; options; constructor(gateway, options = {}) { this.gateway = gateway; this.mappings = new Map(); this.id = gateway.service.uniqueServiceName; this.options = options; this.host = ''; this.port = 0; this.setGateway(gateway); this.family = isIPv6(this.host) ? 'IPv6' : 'IPv4'; this.log = logger(`nat-port-mapper:upnp:internetgatewaydevice2:${this.family.toLowerCase()}`); } async getGateway(options) { if (this.gateway.service.expires > Date.now()) { return this.gateway; } for await (const service of discoverGateways({ ...this.options, ...options })) { if (service.uniqueServiceName !== this.id) { continue; } const host = stripHostBrackets(service.location.hostname); if (isIPv4(host) && this.family === 'IPv6') { continue; } if (isIPv6(host) && this.family === 'IPv4') { continue; } this.setGateway(new Device(service)); return this.gateway; } throw new Error(`Could not resolve gateway with USN ${this.id}`); } setGateway(gateway) { this.gateway = gateway; this.host = stripHostBrackets(this.gateway.service.location.hostname); this.port = Number(this.gateway.service.location.port ?? (this.gateway.service.location.protocol === 'http:' ? 80 : 443)); } async externalIp(options) { this.log.trace('discover external IP address'); const gateway = await this.getGateway(options); const response = await gateway.run(DEVICE_WAN_IP_CONNECTION_2, 'GetExternalIPAddress', [], options); const key = findNamespacedKey('GetExternalIPAddressResponse', response); this.log.trace('discovered external IP address %s', response[key].NewExternalIPAddress); return response[key].NewExternalIPAddress; } async *mapAll(localPort, options = {}) { let mapped = false; for (const host of findLocalAddresses(this.family)) { try { const mapping = await this.map(localPort, host, options); mapped = true; yield mapping; } catch (err) { this.log.error('error mapping %s:%d - %e', host, localPort, err); } } if (!mapped) { throw new Error(`All attempts to map port ${localPort} failed`); } } async map(localPort, localHost, options) { const port = await this.mapPort(localPort, localHost, options); return { externalHost: isPrivateIp(localHost) === true ? await this.externalIp(options) : localHost, externalPort: port, internalHost: localHost, internalPort: localPort, protocol: options?.protocol?.toUpperCase() === 'UDP' ? 'UDP' : 'TCP' }; } addMapping(localPort, mapping) { const mappings = this.mappings.get(localPort) ?? []; mappings.push(mapping); this.mappings.set(localPort, mappings); } configureRefresh(localPort, mapping, options = {}) { if (options.autoRefresh === false || this.options.autoRefresh === false) { return; } const refresh = ((localPort, options = {}) => { this.refreshPort(localPort, { ...options, signal: AbortSignal.timeout(options.refreshTimeout ?? this.options.refreshTimeout ?? DEFAULT_REFRESH_TIMEOUT) }) .catch(err => { this.log.error('could not refresh port mapping - %e', err); }); }).bind(this, localPort, { ...options, signal: undefined }); const ms = (mapping.ttl * 1000) - (options.refreshThreshold ?? this.options.refreshThreshold ?? DEFAULT_REFRESH_THRESHOLD); mapping.refreshTimeout = setTimeout(refresh, ms); } async stop(options) { for (const port of this.mappings.keys()) { await this.unmap(port, options); } this.mappings.clear(); this.gateway.close(); } } //# sourceMappingURL=internet-gateway-service.js.map