This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

6
resources/app/node_modules/nat-upnp/lib/nat-upnp.js generated vendored Normal file
View File

@@ -0,0 +1,6 @@
var nat = exports;
nat.utils = require('./nat-upnp/utils');
nat.ssdp = require('./nat-upnp/ssdp');
nat.device = require('./nat-upnp/device');
nat.createClient = require('./nat-upnp/client').create;

View File

@@ -0,0 +1,202 @@
var nat = require('../nat-upnp');
var async = require('async');
var client = exports;
function Client() {
this.ssdp = nat.ssdp.create();
this.timeout = 1800;
}
client.create = function create() {
return new Client();
};
function 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)
};
}
Client.prototype.portMapping = function portMapping(options, callback) {
if (!callback) callback = function() {};
this.findGateway(function(err, gateway, address) {
if (err) return callback(err);
var ports = normalizeOptions(options);
var 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', options.protocol ?
options.protocol.toUpperCase() : 'TCP' ],
[ 'NewInternalPort', ports.internal.port ],
[ 'NewInternalClient', ports.internal.host || address ],
[ 'NewEnabled', 1 ],
[ 'NewPortMappingDescription', options.description || 'node:nat:upnp' ],
[ 'NewLeaseDuration', ttl ]
], callback);
});
};
Client.prototype.portUnmapping = function portMapping(options, callback) {
if (!callback) callback = function() {};
this.findGateway(function(err, gateway/*, address*/) {
if (err) return callback(err);
var ports = normalizeOptions(options);
gateway.run('DeletePortMapping', [
[ 'NewRemoteHost', ports.remote.host ],
[ 'NewExternalPort', ports.remote.port ],
[ 'NewProtocol', options.protocol ?
options.protocol.toUpperCase() : 'TCP' ]
], callback);
});
};
Client.prototype.getMappings = function getMappings(options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!options) options = {};
this.findGateway(function(err, gateway, address) {
if (err) return callback(err);
var i = 0;
var end = false;
var results = [];
async.whilst(function() {
return !end;
}, function(callback) {
gateway.run('GetGenericPortMappingEntry', [
[ 'NewPortMappingIndex', i++ ]
], function(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);
}
var key;
Object.keys(data).some(function(k) {
if (!/:GetGenericPortMappingEntryResponse/.test(k)) return false;
key = k;
return true;
});
data = data[key];
var 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);
});
}, function(err) {
if (err) return callback(err);
if (options.local) {
results = results.filter(function(item) {
return item.local;
});
}
if (options.description) {
results = results.filter(function(item) {
if (typeof item.description !== 'string')
return;
if (options.description instanceof RegExp) {
return item.description.match(options.description) !== null;
} else {
return item.description.indexOf(options.description) !== -1;
}
});
}
callback(null, results);
});
});
};
Client.prototype.externalIp = function externalIp(callback) {
this.findGateway(function(err, gateway/*, address*/) {
if (err) return callback(err);
gateway.run('GetExternalIPAddress', [], function(err, data) {
if (err) return callback(err);
var key;
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);
});
});
};
Client.prototype.findGateway = function findGateway(callback) {
var timeout;
var timeouted = false;
var p = this.ssdp.search(
'urn:schemas-upnp-org:device:InternetGatewayDevice:1'
);
timeout = setTimeout(function() {
timeouted = true;
p.emit('end');
callback(new Error('timeout'));
}, this.timeout);
p.on('device', function (info, address) {
if (timeouted) return;
p.emit('end');
clearTimeout(timeout);
// Create gateway
callback(null, nat.device.create(info.location), address);
});
};
Client.prototype.close = function close() {
this.ssdp.close();
};

View File

@@ -0,0 +1,162 @@
var nat = require('../nat-upnp'),
request = require('request'),
url = require('url'),
xml2js = require('xml2js'),
Buffer = require('buffer').Buffer;
var device = exports;
function Device(url) {
this.description = url;
this.services = [
'urn:schemas-upnp-org:service:WANIPConnection:1',
'urn:schemas-upnp-org:service:WANPPPConnection:1'
];
};
device.create = function create(url) {
return new Device(url);
};
Device.prototype._getXml = function _getXml(url, callback) {
var once = false;
function respond(err, body) {
if (once) return;
once = true;
callback(err, body);
}
request(url, function(err, res, body) {
if (err) return callback(err);
if (res.statusCode !== 200) {
respond(Error('Failed to lookup device description'));
return;
}
var parser = new xml2js.Parser();
parser.parseString(body, function(err, body) {
if (err) return respond(err);
respond(null, body);
});
});
};
Device.prototype.getService= function getService(types, callback) {
var self = this;
this._getXml(this.description, function(err, info) {
if (err) return callback(err);
var s = self.parseDescription(info).services.filter(function(service) {
return types.indexOf(service.serviceType) !== -1;
});
if (s.length === 0 || !s[0].controlURL || !s[0].SCPDURL) {
return callback(Error('Service not found'));
}
var base = url.parse(info.baseURL || self.description);
function prefix(u) {
var uri = url.parse(u);
uri.host = uri.host || base.host;
uri.protocol = uri.protocol || base.protocol;
return url.format(uri);
}
callback(null,{
service: s[0].serviceType,
SCPDURL: prefix(s[0].SCPDURL),
controlURL: prefix(s[0].controlURL)
});
});
};
Device.prototype.parseDescription = function parseDescription(info) {
var services = [],
devices = [];
function toArray(item) {
return Array.isArray(item) ? item : [ item ];
};
function traverseServices(service) {
if (!service) return;
services.push(service);
}
function traverseDevices(device) {
if (!device) return;
devices.push(device);
if (device.deviceList && device.deviceList.device) {
toArray(device.deviceList.device).forEach(traverseDevices);
}
if (device.serviceList && device.serviceList.service) {
toArray(device.serviceList.service).forEach(traverseServices);
}
}
traverseDevices(info.device);
return {
services: services,
devices: devices
};
};
Device.prototype.run = function run(action, args, callback) {
var self = this;
this.getService(this.services, function(err, info) {
if (err) return callback(err);
var 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.map(function(args) {
return '<' + args[0]+ '>' +
(args[1] === undefined ? '' : args[1]) +
'</' + args[0] + '>';
}).join('') +
'</u:' + action + '>' +
'</s:Body>' +
'</s:Envelope>';
request({
method: 'POST',
url: info.controlURL,
headers: {
'Content-Type': 'text/xml; charset="utf-8"',
'Content-Length': Buffer.byteLength(body),
'Connection': 'close',
'SOAPAction': JSON.stringify(info.service + '#' + action)
},
body: body
}, function(err, res, body) {
if (err) return callback(err);
var parser = new xml2js.Parser();
parser.parseString(body, function(err, body) {
if (res.statusCode !== 200) {
return callback(Error('Request failed: ' + res.statusCode));
}
var soapns = nat.utils.getNamespace(
body,
'http://schemas.xmlsoap.org/soap/envelope/');
callback(null, body[soapns + 'Body']);
});
});
});
};

View File

@@ -0,0 +1,162 @@
var dgram = require('dgram');
var util = require('util');
var os = require('os');
var EventEmitter = require('events').EventEmitter;
var ssdp = exports;
function Ssdp(opts) {
EventEmitter.call(this);
this._opts = opts || {};
this._sourcePort = this._opts.sourcePort || 0;
this.multicast = '239.255.255.250';
this.port = 1900;
this._bound = false;
this._boundCount = 0;
this._closed = false;
this._queue = [];
// Create sockets on all external interfaces
this.createSockets();
}
util.inherits(Ssdp, EventEmitter);
ssdp.create = function create() {
return new Ssdp();
};
Ssdp.parseMimeHeader = function (headerStr) {
var lines = headerStr.split(/\r\n/g);
// Parse headers from lines to hashmap
return lines.reduce(function(headers, line) {
line.replace(/^([^:]*)\s*:\s*(.*)$/, function (a, key, value) {
headers[key.toLowerCase()] = value;
});
return headers;
}, {});
};
Ssdp.prototype.createSockets = function createSockets() {
var self = this;
var interfaces = os.networkInterfaces();
this.sockets = Object.keys(interfaces).reduce(function(a, key) {
return a.concat(interfaces[key].filter(function(item) {
return !item.internal;
}).map(function(item) {
return self.createSocket(item);
}));
}, []);
};
Ssdp.prototype.search = function search(device, promise) {
if (!promise) {
promise = new EventEmitter();
promise._ended = false;
promise.once('end', function() {
promise._ended = true;
});
}
if (!this._bound) {
this._queue.push({ action: 'search', device: device, promise: promise });
return promise;
}
// If promise was ended before binding - do not send queries
if (promise._ended) return;
var self = this;
var query = new Buffer('M-SEARCH * HTTP/1.1\r\n' +
'HOST: ' + this.multicast + ':' + this.port + '\r\n' +
'MAN: "ssdp:discover"\r\n' +
'MX: 1\r\n' +
'ST: ' + device + '\r\n' +
'\r\n');
// Send query on each socket
this.sockets.forEach(function(socket) {
socket.send(query, 0, query.length, this.port, this.multicast);
}, this);
function ondevice(info, address) {
if (promise._ended) return;
if (info.st !== device) return;
promise.emit('device', info, address);
}
this.on('_device', ondevice);
// Detach listener after receiving 'end' event
promise.once('end', function() {
self.removeListener('_device', ondevice);
});
return promise;
};
Ssdp.prototype.createSocket = function createSocket(interface) {
var self = this;
var socket = dgram.createSocket(interface.family === 'IPv4' ?
'udp4' : 'udp6');
socket.on('message', function (message, info) {
// Ignore messages after closing sockets
if (self._closed) return;
// Parse response
self.parseResponse(message.toString(), socket.address, info);
});
// Bind in next tick (sockets should be me in this.sockets array)
process.nextTick(function() {
// Unqueue this._queue once all sockets are ready
function onready() {
if (self._boundCount < self.sockets.length) return;
self._bound = true;
self._queue.forEach(function(item) {
return self[item.action](item.device, item.promise);
});
}
socket.on('listening', function() {
self._boundCount += 1;
onready();
});
// On error - remove socket from list and execute items from queue
socket.once('error', function() {
self.sockets.splice(self.sockets.indexOf(socket), 1);
onready();
});
socket.address = interface.address;
socket.bind(self._sourcePort, interface.address);
});
return socket;
};
// TODO create separate logic for parsing unsolicited upnp broadcasts,
// if and when that need arises
Ssdp.prototype.parseResponse = function parseResponse(response, addr, remote) {
// Ignore incorrect packets
if (!/^(HTTP|NOTIFY)/m.test(response)) return;
var headers = Ssdp.parseMimeHeader(response);
// We are only interested in messages that can be matched against the original
// search target
if (!headers.st) return;
this.emit('_device', headers, addr);
};
Ssdp.prototype.close = function close() {
this.sockets.forEach(function(socket) {
socket.close();
});
this._closed = true;
};

View File

@@ -0,0 +1,19 @@
var utils = exports;
utils.getNamespace = function getNamespace(data, uri) {
var ns;
if (data['@']) {
Object.keys(data['@']).some(function(key) {
if (!/^xmlns:/.test(key)) return;
if (data['@'][key] !== uri) {
return;
}
ns = key.replace(/^xmlns:/, '');
return true;
});
}
return ns ? ns + ':' : '';
};