diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..dbaae20 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,46 @@ +{ + "disallowKeywordsOnNewLine": [ "else" ], + "disallowMixedSpacesAndTabs": true, + "disallowMultipleLineStrings": true, + "disallowMultipleVarDecl": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowQuotedKeysInObjects": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpaceAfterPrefixUnaryOperators": true, + "disallowSpaceBeforePostfixUnaryOperators": true, + "disallowSpacesInCallExpression": true, + "disallowTrailingComma": true, + "disallowTrailingWhitespace": true, + "disallowYodaConditions": true, + + "requireCommaBeforeLineBreak": true, + "requireOperatorBeforeLineBreak": true, + "requireSpaceAfterBinaryOperators": true, + "requireSpaceAfterKeywords": [ "if", "for", "while", "else", "try", "catch" ], + "requireSpaceAfterLineComment": true, + "requireSpaceBeforeBinaryOperators": true, + "requireSpaceBeforeBlockStatements": true, + "requireSpaceBeforeKeywords": [ "else", "catch" ], + "requireSpaceBeforeObjectValues": true, + "requireSpaceBetweenArguments": true, + "requireSpacesInAnonymousFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInFunctionDeclaration": { + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInConditionalExpression": true, + "requireSpacesInForStatement": true, + "requireSpacesInsideArrayBrackets": "all", + "requireSpacesInsideObjectBrackets": "all", + "requireDotNotation": true, + + "maximumLineLength": 80, + "validateIndentation": 2, + "validateLineBreaks": "LF", + "validateParameterSeparator": ", ", + "validateQuoteMarks": "'" +} diff --git a/lib/nat-upnp/client.js b/lib/nat-upnp/client.js index a391501..9627dc2 100644 --- a/lib/nat-upnp/client.js +++ b/lib/nat-upnp/client.js @@ -1,12 +1,12 @@ -var nat = require('../nat-upnp'), - async = require('async'); +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(); @@ -35,16 +35,16 @@ Client.prototype.portMapping = function portMapping(options, callback) { var ports = normalizeOptions(options); 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', typeof options.ttl === 'number' ? - options.ttl : 60 * 30] + [ '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', typeof options.ttl === 'number' ? + options.ttl : 60 * 30 ] ], callback); }); }; @@ -52,16 +52,16 @@ Client.prototype.portMapping = function portMapping(options, callback) { Client.prototype.portUnmapping = function portMapping(options, callback) { if (!callback) callback = function() {}; - this.findGateway(function(err, gateway, address) { + 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'] + [ 'NewRemoteHost', ports.remote.host ], + [ 'NewExternalPort', ports.remote.port ], + [ 'NewProtocol', options.protocol ? + options.protocol.toUpperCase() : 'TCP' ] ], callback); }); }; @@ -76,15 +76,15 @@ Client.prototype.getMappings = function getMappings(options, callback) { this.findGateway(function(err, gateway, address) { if (err) return callback(err); - var i = 0, - end = false, - results = []; + var i = 0; + var end = false; + var results = []; async.whilst(function() { return !end; }, function(callback) { gateway.run('GetGenericPortMappingEntry', [ - ['NewPortMappingIndex', ++i] + [ 'NewPortMappingIndex', ++i ] ], function(err, data) { if (err) { end = true; @@ -111,7 +111,7 @@ Client.prototype.getMappings = function getMappings(options, callback) { port: parseInt(data.NewInternalPort, 10) }, protocol: data.NewProtocol.toLowerCase(), - enabled: data.NewEnabled == 1, + enabled: data.NewEnabled === 1, description: data.NewPortMappingDescription, ttl: parseInt(data.NewLeaseDuration, 10) }; @@ -141,12 +141,12 @@ Client.prototype.getMappings = function getMappings(options, callback) { } callback(null, results); - }) + }); }); }; Client.prototype.externalIp = function externalIp(callback) { - this.findGateway(function(err, gateway, address) { + this.findGateway(function(err, gateway/*, address*/) { if (err) return callback(err); gateway.run('GetExternalIPAddress', [], function(err, data) { if (err) return callback(err); @@ -166,9 +166,9 @@ Client.prototype.externalIp = function externalIp(callback) { }; Client.prototype.findGateway = function findGateway(callback) { - var timeout, - timeouted = false, - p = this.ssdp.search( + var timeout; + var timeouted = false; + var p = this.ssdp.search( 'urn:schemas-upnp-org:device:InternetGatewayDevice:1' ); @@ -178,7 +178,7 @@ Client.prototype.findGateway = function findGateway(callback) { callback(new Error('timeout')); }, this.timeout); - p.on('device', function(info, address) { + p.on('device', function (info, address) { if (timeouted) return; p.emit('end'); clearTimeout(timeout); diff --git a/lib/nat-upnp/ssdp.js b/lib/nat-upnp/ssdp.js index 493ba05..d5d200b 100644 --- a/lib/nat-upnp/ssdp.js +++ b/lib/nat-upnp/ssdp.js @@ -1,16 +1,14 @@ -var dgram = require('dgram'), - util = require('util'), - os = require('os'), - net = require('net'), - netroute = require('netroute'), - ip = require('ip'), - EventEmitter = require('events').EventEmitter; - +var dgram = require('dgram'); +var util = require('util'); +var os = require('os'); +var EventEmitter = require('events').EventEmitter; var ssdp = exports; -function Ssdp() { +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; @@ -20,16 +18,28 @@ function Ssdp() { // 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, - interfaces = os.networkInterfaces(); + var self = this; + var interfaces = os.networkInterfaces(); this.sockets = Object.keys(interfaces).reduce(function(a, key) { return a.concat(interfaces[key].filter(function(item) { @@ -57,8 +67,8 @@ Ssdp.prototype.search = function search(device, promise) { // If promise was ended before binding - do not send queries if (promise._ended) return; - var self = this, - query = new Buffer('M-SEARCH * HTTP/1.1\r\n' + + 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' + @@ -87,8 +97,8 @@ Ssdp.prototype.search = function search(device, promise) { }; Ssdp.prototype.createSocket = function createSocket(interface) { - var self = this, - socket = dgram.createSocket(interface.family === 'IPv4' ? + var self = this; + var socket = dgram.createSocket(interface.family === 'IPv4' ? 'udp4' : 'udp6'); socket.on('message', function (message, info) { @@ -103,18 +113,16 @@ Ssdp.prototype.createSocket = function createSocket(interface) { process.nextTick(function() { // Unqueue this._queue once all sockets are ready function onready() { - if (self._boundCount === self.sockets.length) return; - self._bound = true; + if (self._boundCount < self.sockets.length) return; + self._bound = true; self._queue.forEach(function(item) { - if (item.action === 'search') { - return self.search(item.device, item.promise); - } + return self[item.action](item.device, item.promise); }); } socket.on('listening', function() { - self._boundCount++; + self._boundCount += 1; onready(); }); @@ -125,76 +133,25 @@ Ssdp.prototype.createSocket = function createSocket(interface) { }); socket.address = interface.address; - socket.bind(self.port, 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) { - response = response.split(/\r\n/g); - - var status = response[0], - headers = response.slice(1); - // Ignore incorrect packets - if (!/^(HTTP|NOTIFY)/.test(status)) return; + if (!/^(HTTP|NOTIFY)/m.test(response)) return; - // Parse headers from lines to hashmap - headers = headers.reduce(function(headers, line) { - line.replace(/^([^:]*)\s*:\s*(.*)$/, function(a, key, value) { - headers[key.toLowerCase()] = value; - }); - return headers; - }, {}); - - // We do not have interest in headers without location - if (!headers.location) return; + var headers = Ssdp.parseMimeHeader(response); - var interfaces = os.networkInterfaces(), - routes = netroute.getInfo(), - local; - - local = routes.IPv4.concat(routes.IPv6).filter(function(route) { - if (route.gateway) { - if (ip.isEqual(remote.address, route.gateway)) return true; - } - - // Remove /24 part from address - if (route.destination) { - route.destination = route.destination.replace(/\/\d+$/, ''); - } - - if (net.isIP(route.destination) && route.netmask) { - return ip.isEqual(ip.mask(remote.address, route.netmask), - ip.mask(route.destination, route.netmask)); - } - if (net.isIP(route.destination)) { - return ip.isEqual(remote.address, route.destination) || - ip.isEqual(route.destination, '::'); - } - }).map(function(route) { - return interfaces[route.interface].filter(function(addr) { - return net.isIP(addr.address) === net.isIP(route.gateway) || - net.isIP(addr.address) === net.isIP(route.destination) || - net.isIP(addr.address) && net.isIP(route.netmask) && - net.isIP(route.gateway) && - ip.isEqual(ip.mask(addr.address, route.netmask), - ip.mask(route.gateway, route.netmask)); - }).map(function(addr) { - return addr.address; - }); - }).reduce(function(prev, next) { - return prev.concat(next); - }, []); - - if (local.length === 0) { - local = addr; - } else { - local = local[0]; - } + // We are only interested in messages that can be matched against the original + // search target + if (!headers.st) return; - this.emit('_device', headers, local); + this.emit('_device', headers, remote.address); }; Ssdp.prototype.close = function close() {