@achingbrain/nat-port-mapper
Version:
Port mapping with UPnP and NAT-PMP
98 lines • 3.41 kB
JavaScript
import { setMaxListeners } from 'node:events';
import { NS_SOAP } from './constants.js';
import { fetchXML } from './fetch.js';
import { getNamespace } from './utils.js';
export class Device {
service;
shutdownController;
constructor(service) {
this.service = service;
// used to terminate network operations on shutdown
this.shutdownController = new AbortController();
setMaxListeners(Infinity, this.shutdownController.signal);
}
async run(device, action, args, options) {
const info = this.getService(device);
const requestBody = `<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:${action} xmlns:u="${info.service}">${args.map((args) => `
<${args[0]}>${args[1] ?? ''}</${args[0]}>`).join('')}
</u:${action}>
</s:Body>
</s:Envelope>`;
const responseBody = await fetchXML(new URL(info.controlURL), {
...options,
method: 'POST',
headers: {
'Content-Type': 'text/xml',
'Content-Length': requestBody.length.toString(),
SOAPAction: JSON.stringify(info.service + '#' + action)
},
body: requestBody
});
const soapns = getNamespace(responseBody, NS_SOAP);
return responseBody[soapns + 'Body'];
}
getService(type) {
const [service] = this.parseDescription(this.service.details).services
.filter(device => device.serviceType === type);
// Use the first available service
if (service?.controlURL == null || service.SCPDURL == null) {
throw new Error('Service not found');
}
const base = new URL(this.service.location);
function addPrefix(u) {
let uri;
try {
uri = new URL(u);
}
catch (err) {
// Is only the path of the URL
uri = new URL(u, base.href);
}
uri.host = uri.host ?? base.host;
uri.protocol = uri.protocol ?? base.protocol;
return uri.toString();
}
return {
service: service.serviceType,
SCPDURL: addPrefix(service.SCPDURL),
controlURL: addPrefix(service.controlURL)
};
}
parseDescription(info) {
const services = [];
const devices = [];
function toArray(item) {
return Array.isArray(item) ? item : [item];
}
function traverseServices(service) {
if (service == null) {
return;
}
services.push(service);
}
function traverseDevices(device) {
if (device == null) {
return;
}
devices.push(device);
if (device.deviceList?.device != null) {
toArray(device.deviceList.device).forEach(traverseDevices);
}
if (device.serviceList?.service != null) {
toArray(device.serviceList.service).forEach(traverseServices);
}
}
traverseDevices(info.device);
return {
services,
devices
};
}
close() {
this.shutdownController.abort();
}
}
//# sourceMappingURL=device.js.map