nat-upnp-rejetto
Version:
Port mapping via UPnP APIs
114 lines (113 loc) • 5.57 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Device = void 0;
const node_http_1 = __importDefault(require("node:http"));
const node_https_1 = __importDefault(require("node:https"));
const consumers_1 = require("node:stream/consumers");
const url_1 = require("url");
const fast_xml_parser_1 = require("fast-xml-parser");
class Device {
constructor(url) {
this.description = url;
this.services = [
"urn:schemas-upnp-org:service:WANIPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:2",
"urn:schemas-upnp-org:service:WANPPPConnection:1",
];
}
getXML(url) {
return __awaiter(this, void 0, void 0, function* () {
return httpRequest(url).then(consumers_1.text).then(data => new fast_xml_parser_1.XMLParser().parse(data))
.catch(() => new Error("Failed to lookup device description"));
});
}
getService(types) {
return __awaiter(this, void 0, void 0, function* () {
return this.getXML(this.description).then(({ root: xml }) => {
const services = this.parseDescription(xml).services.filter(({ serviceType }) => types.includes(serviceType));
if (services.length === 0 ||
!services[0].controlURL ||
!services[0].SCPDURL) {
throw new Error("Service not found");
}
const baseUrl = new url_1.URL(xml.baseURL, this.description);
const prefix = (url) => new url_1.URL(url, baseUrl.toString()).toString();
return {
service: services[0].serviceType,
SCPDURL: prefix(services[0].SCPDURL),
controlURL: prefix(services[0].controlURL),
};
});
});
}
run(action, args) {
return __awaiter(this, void 0, void 0, function* () {
const info = yield this.getService(this.services);
const body = `<?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=${JSON.stringify(info.service)}>${args.reduce((p, [a, b]) => p + `<${a !== null && a !== void 0 ? a : ''}>${b !== null && b !== void 0 ? b : ''}</${a !== null && a !== void 0 ? a : ''}>`, '')}</u:${action}>
</s:Body></s:Envelope>`;
return httpRequest(info.controlURL, {
method: 'post',
headers: {
"Content-Type": 'text/xml; charset="utf-8"',
"Content-Length": "" + Buffer.byteLength(body),
Connection: "close",
SOAPAction: JSON.stringify(info.service + "#" + action),
},
}, body)
.then(consumers_1.text, consumers_1.text).then(data => {
var _a;
const res = new fast_xml_parser_1.XMLParser({ removeNSPrefix: true }).parse(data).Envelope.Body;
if (res.Fault)
throw ((_a = res.Fault.detail) === null || _a === void 0 ? void 0 : _a.UPnPError) || res.Fault;
return res;
});
});
}
parseDescription(info) {
const services = [];
const devices = [];
function traverseDevices(device) {
var _a, _b, _c, _d;
if (!device)
return;
const serviceList = (_b = (_a = device.serviceList) === null || _a === void 0 ? void 0 : _a.service) !== null && _b !== void 0 ? _b : [];
const deviceList = (_d = (_c = device.deviceList) === null || _c === void 0 ? void 0 : _c.device) !== null && _d !== void 0 ? _d : [];
devices.push(device);
if (Array.isArray(serviceList)) {
services.push(...serviceList);
}
else {
services.push(serviceList);
}
if (Array.isArray(deviceList)) {
deviceList.forEach(traverseDevices);
}
else {
traverseDevices(deviceList);
}
}
traverseDevices(info.device);
return {
services,
devices,
};
}
}
exports.Device = Device;
exports.default = Device;
function httpRequest(url, options = {}, body = '') {
return new Promise((resolve, reject) => (url.startsWith('https:') ? node_https_1.default : node_http_1.default).request(url, options, (res) => __awaiter(this, void 0, void 0, function* () { return !res.statusCode || res.statusCode >= 400 ? reject(res) : resolve(res); })).on('error', reject).end(body));
}