├── .gitignore ├── .npmignore ├── Gruntfile.js ├── README.md ├── package.json └── src ├── demo_chrome_app ├── background.js ├── manifest.json ├── page.js └── window.html ├── nat-pmp.js ├── pcp.js ├── port-control.js ├── port-control.json ├── upnp.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | build/ 3 | dist/ 4 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | build/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile for freedom-port-control 3 | **/ 4 | 5 | var path = require('path'); 6 | var freedomChromePath = path.dirname(require.resolve( 7 | 'freedom-for-chrome/package.json')); 8 | 9 | module.exports = function(grunt) { 10 | grunt.initConfig({ 11 | pkg: grunt.file.readJSON('package.json'), 12 | 13 | browserify: { 14 | main: { 15 | src: 'src/port-control.js', 16 | dest: 'build/port-control.js' 17 | }, 18 | options: { 19 | browserifyOptions: { 20 | debug: true, 21 | } 22 | } 23 | }, 24 | 25 | copy: { 26 | chromeDemo: { 27 | src: ['src/port-control.json', 28 | 'src/demo_chrome_app/*', 29 | 'build/port-control.js', 30 | freedomChromePath + '/freedom-for-chrome.js*'], 31 | dest: 'build/demo_chrome_app/', 32 | flatten: true, 33 | filter: 'isFile', 34 | expand: true, 35 | onlyIf: 'modified' 36 | }, 37 | dist: { 38 | src: ['build/port-control.js', 'src/port-control.json'], 39 | dest: 'dist/', 40 | flatten: true, 41 | filter: 'isFile', 42 | expand: true, 43 | } 44 | }, 45 | 46 | jshint: { 47 | all: ['src/**/*.js'], 48 | options: { 49 | jshintrc: true 50 | } 51 | }, 52 | 53 | clean: ['build/', 'dist/'] 54 | }); 55 | 56 | grunt.loadNpmTasks('grunt-browserify'); 57 | grunt.loadNpmTasks('grunt-contrib-clean'); 58 | grunt.loadNpmTasks('grunt-contrib-copy'); 59 | grunt.loadNpmTasks('grunt-contrib-jshint'); 60 | 61 | grunt.registerTask('build', [ 62 | 'jshint', 63 | 'browserify', 64 | 'copy', 65 | ]); 66 | grunt.registerTask('default', [ 67 | 'build' 68 | ]); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freedom.js Port Control 2 | 3 | Opens ports through a NAT with NAT-PMP, PCP, and UPnP. 4 | 5 | ## Build 6 | 7 | ``` 8 | npm install 9 | grunt build 10 | ``` 11 | 12 | This will build the module file at `build/port-control.js` and a demo Chrome app in `build/demo_chrome_app/`. 13 | 14 | ## Usage 15 | 16 | This module will allow you to control port mappings in a NAT and probe it for various settings. 17 | 18 | ### Probing methods 19 | 20 | To run all the NAT probing tests, 21 | 22 | ``` 23 | portControl.probeProtocolSupport(); 24 | ``` 25 | 26 | This method resolves to an object of the form `{"natPmp": true, "pcp": false, "upnp": true}`. 27 | 28 | You can also probe for a specific protocol: 29 | 30 | ``` 31 | portControl.probePmpSupport(); 32 | portControl.probePcpSupport(); 33 | portControl.probeUpnpSupport(); 34 | ``` 35 | All of these methods return a promise that will resolve to a boolean value. 36 | 37 | ### Add a port mapping 38 | 39 | To add a NAT port mapping with any protocol available, 40 | 41 | ``` 42 | // Map internal port 50000 to external port 50000 with a 2 hr lifetime 43 | portControl.addMapping(50000, 50000, 7200); 44 | ``` 45 | Passing in a lifetime of `0` seconds will keep the port mapping open indefinitely. If the actual lifetime of the mapping is less than the requested lifetime, the module will automatically handle refreshing the mapping to meet the requested lifetime. 46 | 47 | This method returns a promise that will resolve to a `Mapping` object of the form, 48 | ``` 49 | { 50 | "internalIp": "192.168.1.50", 51 | "internalPort": 50000, 52 | "externalIp": "104.132.34.50", 53 | "externalPort": 50000, 54 | "lifetime": 120, 55 | "protocol": "natPmp", 56 | ... 57 | } 58 | ``` 59 | 60 | _An important optimization note: by default, `addMapping()` will try all the protocols sequentially (in order of NAT-PMP, PCP, UPnP). If we're waiting for timeouts, then this method can take up to ~10 seconds to run. This may be too slow for practical purposes. Instead, run `probeProtocolSupport()` at some point before (also can take up to ~10 seconds), which will cache the results, so that `addMapping()` will only try one protocol that has worked before (this will take <2 seconds)._ 61 | 62 | You can also create a port mapping with a specific protocol: 63 | 64 | ``` 65 | portControl.addMappingPmp(55555, 55555, 7200); 66 | portControl.addMappingPcp(55556, 55556, 7200); 67 | portControl.addMappingUpnp(55557, 55557, 7200); 68 | ``` 69 | 70 | All of these methods return the same promise as `addMapping()` and refresh similarly. 71 | 72 | ### Delete port mapping 73 | 74 | To delete a NAT port mapping, 75 | 76 | ``` 77 | portControl.deleteMapping(55555); // 55555 is the external port of the mapping 78 | ``` 79 | 80 | This will delete the module's record of this mapping and also attempt to delete it from the NAT's routing tables. The method will resolve to a boolean, which is `true` if it succeeded and `false` otherwise. 81 | 82 | There are also methods for specific protocols, 83 | 84 | ``` 85 | portControl.deleteMappingPmp(55555); 86 | portControl.deleteMappingPcp(55556); 87 | portControl.deleteMappingUpnp(55557); 88 | ``` 89 | 90 | Note: all the deletion methods only work if we're tracking the port mapping in PortControl.activeMappings (see below). 91 | 92 | ### Get active port mappings 93 | 94 | To get the module's local record of the active port mappings, 95 | 96 | ``` 97 | portControl.getActiveMappings(); 98 | ``` 99 | 100 | This method will return a promise that resolves to an object containing `Mapping` objects, where the keys are the external ports of each mapping. `Mapping` objects are removed from this list when they expire or when they are explicitly deleted. 101 | 102 | ### IP address 103 | 104 | The module can also determine the user's private IP addresses (more than one if there are multiple active network interfaces), 105 | 106 | ``` 107 | portControl.getPrivateIps(); 108 | ``` 109 | 110 | This returns a promise that will resolve to an array of IP address strings, or reject with an error. 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedom-port-control", 3 | "description": "Opens ports through a NAT with NAT-PMP, PCP, and UPnP", 4 | "version": "0.9.11", 5 | "author": "Kenny Song ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/freedomjs/freedom-port-control" 9 | }, 10 | "devDependencies": { 11 | "freedom": "^0.6.21", 12 | "freedom-for-chrome": "^0.4.15", 13 | "freedom-for-firefox": "^0.6.9", 14 | "grunt": "^0.4.5", 15 | "grunt-browserify": "^3.7.0", 16 | "grunt-contrib-clean": "^0.6.0", 17 | "grunt-contrib-copy": "^0.8.0", 18 | "grunt-contrib-jshint": "^0.11.2", 19 | "ipaddr.js": "^0.1.3" 20 | }, 21 | "keywords": [ 22 | "freedom.js", 23 | "port", 24 | "control" 25 | ], 26 | "scripts": { 27 | "test": "echo \"Error: no test specified\" && exit 1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/demo_chrome_app/background.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('window.html', { 3 | 'bounds': { 4 | 'width': 400, 5 | 'height': 700 6 | } 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/demo_chrome_app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedom-port-control-demo", 3 | "description": "Opens ports through a NAT with NAT-PMP, PCP, and UPnP", 4 | "version": "0.1", 5 | "manifest_version": 2, 6 | "sockets": { 7 | "udp": { 8 | "send": "*", 9 | "bind": "*" 10 | } 11 | }, 12 | "app": { 13 | "background": { 14 | "scripts": ["background.js"] 15 | } 16 | }, 17 | "permissions": [ 18 | "http://*/", 19 | "system.network" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/demo_chrome_app/page.js: -------------------------------------------------------------------------------- 1 | var portControl; 2 | 3 | function start(instance) { 4 | console.log('Freedom and port-control loaded. In start().'); 5 | portControl = instance(); 6 | 7 | document.getElementById('test-IP').addEventListener('click', function () { 8 | portControl.getPrivateIps().then(function (privateIps) { 9 | document.getElementById('result-IP').innerText = "Your private IP addresses are: " + 10 | JSON.stringify(privateIps, null, 2); 11 | }).catch(function (err) { 12 | document.getElementById('result-IP').innerText = err.message; 13 | }); 14 | }); 15 | 16 | document.getElementById('test-router-cache').addEventListener('click', function () { 17 | portControl.getRouterIpCache().then(function (routerIps) { 18 | document.getElementById('result-router-cache').innerText = "Your cached router IPs are: " + 19 | JSON.stringify(routerIps, null, 2); 20 | }).catch(function (err) { 21 | document.getElementById('result-router-cache').innerText = err.message; 22 | }); 23 | }); 24 | 25 | document.getElementById('test-protocols').addEventListener('click', function () { 26 | portControl.probeProtocolSupport().then(function (protocolSupport) { 27 | document.getElementById('result-protocols').innerText = 28 | JSON.stringify(protocolSupport, null, 2); 29 | }); 30 | }); 31 | 32 | document.getElementById('mappings').addEventListener('click', function () { 33 | portControl.getActiveMappings().then(function (activeMappings) { 34 | document.getElementById('result-mappings').innerText = 35 | JSON.stringify(activeMappings, null, 2); 36 | }); 37 | }); 38 | 39 | document.getElementById('protocol-support').addEventListener('click', function () { 40 | portControl.getProtocolSupportCache().then(function (support) { 41 | document.getElementById('result-protocol-support').innerText = 42 | JSON.stringify(support, null, 2); 43 | }); 44 | }); 45 | 46 | document.getElementById('add-PMP').addEventListener('click', function () { 47 | var intPort = document.getElementById('internal-port-PMP').value; 48 | var extPort = document.getElementById('external-port-PMP').value; 49 | var lifetime = document.getElementById('lifetime-PMP').value; 50 | portControl.addMappingPmp(intPort, extPort, lifetime).then(function (mappingObj) { 51 | if (mappingObj.externalPort !== -1) { 52 | document.getElementById('result-PMP').innerText = 53 | "NAT-PMP mapping object: " + JSON.stringify(mappingObj, null, 2); 54 | } else { 55 | document.getElementById('result-PMP').innerText = "NAT-PMP failure."; 56 | } 57 | }); 58 | }); 59 | 60 | document.getElementById('delete-PMP').addEventListener('click', function () { 61 | var extPort = document.getElementById('external-port-PMP').value; 62 | portControl.deleteMappingPmp(extPort).then(function (deleteResult) { 63 | if (deleteResult) { 64 | document.getElementById('result-PMP').innerText = "Mapping deleted."; 65 | } else { 66 | document.getElementById('result-PMP').innerText = "Mapping could not be deleted."; 67 | } 68 | }); 69 | }); 70 | 71 | document.getElementById('add-PCP').addEventListener('click', function () { 72 | var intPort = document.getElementById('internal-port-PCP').value; 73 | var extPort = document.getElementById('external-port-PCP').value; 74 | var lifetime = document.getElementById('lifetime-PCP').value; 75 | portControl.addMappingPcp(intPort, extPort, lifetime).then(function (mappingObj) { 76 | if (mappingObj.externalPort !== -1) { 77 | document.getElementById('result-PCP').innerText = 78 | "PCP mapping object: " + JSON.stringify(mappingObj, null, 2); 79 | } else { 80 | document.getElementById('result-PCP').innerText = "PCP failure."; 81 | } 82 | }); 83 | }); 84 | 85 | document.getElementById('delete-PCP').addEventListener('click', function () { 86 | var extPort = document.getElementById('external-port-PCP').value; 87 | portControl.deleteMappingPcp(extPort).then(function (deleteResult) { 88 | if (deleteResult) { 89 | document.getElementById('result-PCP').innerText = "Mapping deleted."; 90 | } else { 91 | document.getElementById('result-PCP').innerText = "Mapping could not be deleted."; 92 | } 93 | }); 94 | }); 95 | 96 | document.getElementById('add-UPnP').addEventListener('click', function () { 97 | var intPort = document.getElementById('internal-port-UPnP').value; 98 | var extPort = document.getElementById('external-port-UPnP').value; 99 | var lifetime = document.getElementById('lifetime-UPnP').value; 100 | portControl.addMappingUpnp(intPort, extPort, lifetime).then(function (mappingObj) { 101 | if (mappingObj.externalPort !== -1) { 102 | document.getElementById('result-UPnP').innerText = 103 | "UPnP mapping object: " + JSON.stringify(mappingObj, null, 2); 104 | } else { 105 | document.getElementById('result-UPnP').innerText = "UPnP failure. (Check console for details)"; 106 | } 107 | }); 108 | }); 109 | 110 | document.getElementById('delete-UPnP').addEventListener('click', function () { 111 | var extPort = document.getElementById('external-port-UPnP').value; 112 | portControl.deleteMappingUpnp(extPort).then(function (deleteResult) { 113 | if (deleteResult) { 114 | document.getElementById('result-UPnP').innerText = "Mapping deleted."; 115 | } else { 116 | document.getElementById('result-UPnP').innerText = "Mapping could not be deleted."; 117 | } 118 | }); 119 | }); 120 | 121 | document.getElementById('add-all').addEventListener('click', function () { 122 | var intPort = document.getElementById('internal-port-all').value; 123 | var extPort = document.getElementById('external-port-all').value; 124 | var lifetime = document.getElementById('lifetime-all').value; 125 | portControl.addMapping(intPort, extPort, lifetime).then(function (mappingObj) { 126 | if (mappingObj.externalPort !== -1) { 127 | document.getElementById('result-all').innerText = 128 | JSON.stringify(mappingObj, null, 2); 129 | } else { 130 | document.getElementById('result-all').innerText = "All protocols failed."; 131 | } 132 | }); 133 | }); 134 | 135 | document.getElementById('delete-all').addEventListener('click', function () { 136 | var extPort = document.getElementById('external-port-all').value; 137 | portControl.deleteMapping(extPort).then(function (deleteResult) { 138 | if (deleteResult) { 139 | document.getElementById('result-all').innerText = "Mapping deleted."; 140 | } else { 141 | document.getElementById('result-all').innerText = "Mapping could not be deleted."; 142 | } 143 | }); 144 | }); 145 | } 146 | 147 | window.onload = function (port) { 148 | if (typeof freedom !== 'undefined') { 149 | freedom('port-control.json').then(start); 150 | } 151 | }.bind({}, self.port); 152 | -------------------------------------------------------------------------------- /src/demo_chrome_app/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 27 | 28 | 29 | 30 |

 31 | 
 32 |     
33 | 34 | 35 |

 36 | 
 37 |     
38 | 39 | 40 |

 41 | 
 42 |     
43 | 44 | 45 |

 46 | 
 47 |     
48 | 49 | 50 |

 51 | 
 52 |     
53 | 54 |

NAT-PMP

55 |
56 | Internal Port 57 | External Port 58 | Lifetime 59 |
60 | 61 | 62 |

 63 | 
 64 |     
65 | 66 |

PCP

67 |
68 | Internal Port 69 | External Port 70 | Lifetime 71 |
72 | 73 | 74 |

 75 | 
 76 |     
77 | 78 |

UPnP

79 |
80 | Internal Port 81 | External Port 82 | Lifetime 83 |
84 | 85 | 86 |

 87 | 
 88 |     
89 | 90 |

All protocols

91 |
92 | Internal Port 93 | External Port 94 | Lifetime 95 |
96 | 97 | 98 |

 99 |   
100 | 
101 | 


--------------------------------------------------------------------------------
/src/nat-pmp.js:
--------------------------------------------------------------------------------
  1 | var utils = require('./utils');
  2 | var ipaddr = require('ipaddr.js');
  3 | 
  4 | /**
  5 | * Probe if NAT-PMP is supported by the router
  6 | * @public
  7 | * @method probeSupport
  8 | * @param {object} activeMappings Table of active Mappings
  9 | * @param {Array} routerIpCache Router IPs that have previously worked
 10 | * @return {Promise} A promise for a boolean
 11 | */
 12 | var probeSupport = function (activeMappings, routerIpCache) {
 13 |   return addMapping(utils.NAT_PMP_PROBE_PORT, utils.NAT_PMP_PROBE_PORT, 120,
 14 |                        activeMappings, routerIpCache).
 15 |       then(function (mapping) { return mapping.externalPort !== -1; });
 16 | };
 17 | 
 18 | /**
 19 | * Makes a port mapping in the NAT with NAT-PMP,
 20 | * and automatically refresh the mapping every two minutes
 21 | * @public
 22 | * @method addMapping
 23 | * @param {number} intPort The internal port on the computer to map to
 24 | * @param {number} extPort The external port on the router to map to
 25 | * @param {number} lifetime Seconds that the mapping will last
 26 | *                          0 is infinity, i.e. a refresh every 24 hours
 27 | * @param {object} activeMappings Table of active Mappings
 28 | * @param {Array} routerIpCache Router IPs that have previously worked
 29 | * @return {Promise} A promise for the port mapping object
 30 | *                            Mapping.externalPort === -1 on failure
 31 | */
 32 | var addMapping = function (intPort, extPort, lifetime, activeMappings, routerIpCache) {
 33 |   var mapping = new utils.Mapping();
 34 |   mapping.internalPort = intPort;
 35 |   mapping.protocol = 'natPmp';
 36 | 
 37 |   // If lifetime is zero, we want to refresh every 24 hours
 38 |   var reqLifetime = (lifetime === 0) ? 24*60*60 : lifetime;
 39 | 
 40 |   // Send NAT-PMP requests to a list of router IPs and parse the first response
 41 |   function _sendPmpRequests(routerIps) {
 42 |     // Construct an array of ArrayBuffers, which are the responses of
 43 |     // sendPmpRequest() calls on all the router IPs. An error result
 44 |     // is caught and re-passed as null.
 45 |     return Promise.all(routerIps.map(function (routerIp) {
 46 |         return sendPmpRequest(routerIp, intPort, extPort, reqLifetime).
 47 |             then(function (pmpResponse) { return pmpResponse; }).
 48 |             catch(function (err) { return null; });
 49 |     // Check if any of the responses are successful (not null)
 50 |     // and parse the external port, router IP, and lifetime in the response
 51 |     })).then(function (responses) {
 52 |       for (var i = 0; i < responses.length; i++) {
 53 |         if (responses[i] !== null) {
 54 |           var responseView = new DataView(responses[i]);
 55 |           mapping.externalPort = responseView.getUint16(10);
 56 |           mapping.lifetime = responseView.getUint32(12);
 57 | 
 58 |           var routerIntIp = routerIps[i];
 59 |           if (routerIpCache.indexOf(routerIntIp) === -1) {
 60 |             routerIpCache.push(routerIntIp);
 61 |           }
 62 |           return routerIntIp;
 63 |         }
 64 |       }
 65 |     // Find the longest prefix match for all the client's internal IPs with
 66 |     // the router IP. This was the internal IP for the new mapping. (We want
 67 |     // to identify which network interface the socket bound to, since NAT-PMP
 68 |     // uses the request's source IP, not a specified one, for the mapping.)
 69 |     }).then(function (routerIntIp) {
 70 |       if (routerIntIp !== undefined) {
 71 |         return utils.getPrivateIps().then(function (privateIps) {
 72 |           mapping.internalIp = utils.longestPrefixMatch(privateIps, routerIntIp);
 73 |           return mapping;
 74 |         });
 75 |       }
 76 |       return mapping;
 77 |     }).catch(function (err) {
 78 |       return mapping;
 79 |     });
 80 |   }
 81 | 
 82 |   // Basically calls _sendPcpRequests on matchedRouterIps first, and if that 
 83 |   // doesn't work, calls it on otherRouterIps
 84 |   function _sendPmpRequestsInWaves() {
 85 |     return utils.getPrivateIps().then(function (privateIps) {
 86 |       // Try matchedRouterIps first (routerIpCache + router IPs that match the 
 87 |       // user's IPs), then otherRouterIps if it doesn't work. This avoids flooding
 88 |       // the local network with NAT-PMP requests
 89 |       var matchedRouterIps = utils.arrAdd(routerIpCache, utils.filterRouterIps(privateIps));
 90 |       var otherRouterIps = utils.arrDiff(utils.ROUTER_IPS, matchedRouterIps);
 91 |       return _sendPmpRequests(matchedRouterIps).then(function (mapping) {
 92 |         if (mapping.externalPort !== -1) { return mapping; }
 93 |         return _sendPmpRequests(otherRouterIps);
 94 |       });
 95 |     });
 96 |   }
 97 | 
 98 |   // Compare our requested parameters for the mapping with the response,
 99 |   // setting a refresh if necessary, and a timeout for deletion, and saving the 
100 |   // mapping object to activeMappings if the mapping succeeded
101 |   function _saveAndRefreshMapping(mapping) {
102 |     // If the actual lifetime is less than the requested lifetime,
103 |     // setTimeout to refresh the mapping when it expires
104 |     var dLifetime = reqLifetime - mapping.lifetime;
105 |     if (mapping.externalPort !== -1 && dLifetime > 0) {
106 |       mapping.timeoutId = setTimeout(addMapping.bind({}, intPort,
107 |         mapping.externalPort, dLifetime, activeMappings), mapping.lifetime*1000);
108 |     }
109 |     // If the original lifetime is 0, refresh every 24 hrs indefinitely
110 |     else if (mapping.externalPort !== -1 && lifetime === 0) {
111 |       mapping.timeoutId = setTimeout(addMapping.bind({}, intPort, 
112 |                        mapping.externalPort, 0, activeMappings), 24*60*60*1000);
113 |     }
114 |     // If we're not refreshing, delete the entry from activeMapping at expiration
115 |     else if (mapping.externalPort !== -1) {
116 |       setTimeout(function () { delete activeMappings[mapping.externalPort]; }, 
117 |                  mapping.lifetime*1000);
118 |     }
119 | 
120 |     // If mapping succeeded, attach a deleter function and add to activeMappings
121 |     if (mapping.externalPort !== -1) {
122 |       mapping.deleter = deleteMapping.bind({}, mapping.externalPort, 
123 |                                            activeMappings, routerIpCache);
124 |       activeMappings[mapping.externalPort] = mapping;
125 |     }
126 |     return mapping;
127 |   }
128 | 
129 |   // Try NAT-PMP requests to matchedRouterIps, then otherRouterIps. 
130 |   // After receiving a NAT-PMP response, set timeouts to delete/refresh the 
131 |   // mapping, add it to activeMappings, and return the mapping object
132 |   return _sendPmpRequestsInWaves().then(_saveAndRefreshMapping);
133 | };
134 | 
135 | /**
136 | * Deletes a port mapping in the NAT with NAT-PMP
137 | * @public
138 | * @method deleteMapping
139 | * @param {number} extPort The external port of the mapping to delete
140 | * @param {object} activeMappings Table of active Mappings
141 | * @param {Array} routerIpCache Router IPs that have previously worked
142 | * @return {Promise} True on success, false on failure
143 | */
144 | var deleteMapping = function (extPort, activeMappings, routerIpCache) {
145 |   // Send NAT-PMP requests to a list of router IPs and parse the first response
146 |   function _sendDeletionRequests(routerIps) {
147 |     return new Promise(function (F, R) {
148 |       // Retrieve internal port of this mapping; this may error
149 |       F(activeMappings[extPort].internalPort);
150 |     }).then(function (intPort) {
151 |       // Construct an array of ArrayBuffers, which are the responses of
152 |       // sendPmpRequest() calls on all the router IPs. An error result
153 |       // is caught and re-passed as null.
154 |       return Promise.all(routerIps.map(function (routerIp) {
155 |         return sendPmpRequest(routerIp, intPort, 0, 0).
156 |             then(function (pmpResponse) { return pmpResponse; }).
157 |             catch(function (err) { return null; });
158 |       }));
159 |     });
160 |   }
161 | 
162 |   // Basically calls _sendDeletionRequests on matchedRouterIps first, and if that 
163 |   // doesn't work, calls it on otherRouterIps
164 |   function _sendDeletionRequestsInWaves() {
165 |     return utils.getPrivateIps().then(function (privateIps) {
166 |       // Try matchedRouterIps first (routerIpCache + router IPs that match the 
167 |       // user's IPs), then otherRouterIps if it doesn't work. This avoids flooding
168 |       // the local network with PCP requests
169 |       var matchedRouterIps = utils.arrAdd(routerIpCache, utils.filterRouterIps(privateIps));
170 |       var otherRouterIps = utils.arrDiff(utils.ROUTER_IPS, matchedRouterIps);
171 |       return _sendDeletionRequests(matchedRouterIps).then(function (mapping) {
172 |         if (mapping.externalPort !== -1) { return mapping; }
173 |         return _sendDeletionRequests(otherRouterIps);
174 |       });
175 |     });
176 |   }
177 | 
178 |   // If any of the NAT-PMP responses were successful, delete the entry from 
179 |   // activeMappings and return true
180 |   function _deleteFromActiveMappings(responses) {
181 |     for (var i = 0; i < responses.length; i++) {
182 |       if (responses[i] !== null) {
183 |         var responseView = new DataView(responses[i]);
184 |         var successCode = responseView.getUint16(2);
185 |         if (successCode === 0) {
186 |           clearTimeout(activeMappings[extPort].timeoutId);
187 |           delete activeMappings[extPort];
188 |           return true;
189 |         }
190 |       }
191 |     }
192 |     return false;
193 |   }
194 | 
195 |   // Send NAT-PMP deletion requests to matchedRouterIps, then otherRouterIps;
196 |   // if that succeeds, delete the corresponding Mapping from activeMappings
197 |   return _sendDeletionRequestsInWaves().
198 |       then(_deleteFromActiveMappings).
199 |       catch(function (err) { return false; });
200 | };
201 | 
202 | /**
203 | * Send a NAT-PMP request to the router to add or delete a port mapping
204 | * @private
205 | * @method sendPmpRequest
206 | * @param {string} routerIp The IP address that the router can be reached at
207 | * @param {number} intPort The internal port on the computer to map to
208 | * @param {number} extPort The external port on the router to map to
209 | * @param {number} lifetime Seconds that the mapping will last
210 | * @return {Promise<{"resultCode": number, "address": string, "port": number, "data": ArrayBuffer}>}
211 | *         A promise that fulfills with the full NAT-PMP response object, or rejects on timeout
212 | */
213 | var sendPmpRequest = function (routerIp, intPort, extPort, lifetime) {
214 |   var socket;
215 | 
216 |   // Binds a socket and sends the NAT-PMP request from that socket to routerIp
217 |   var _sendPmpRequest = new Promise(function (F, R) {
218 |     socket = freedom['core.udpsocket']();
219 | 
220 |     // Fulfill when we get any reply (failure is on timeout in wrapper function)
221 |     socket.on('onData', function (pmpResponse) {
222 |       utils.closeSocket(socket);
223 |       F(pmpResponse.data);
224 |     });
225 | 
226 |     // TODO(kennysong): Handle an error case for all socket.bind() when this issue is fixed:
227 |     // https://github.com/uProxy/uproxy/issues/1687
228 | 
229 |     // Bind a UDP port and send a NAT-PMP request
230 |     socket.bind('0.0.0.0', 0).then(function (result) {
231 |       // NAT-PMP packet structure: https://tools.ietf.org/html/rfc6886#section-3.3
232 |       var pmpBuffer = utils.createArrayBuffer(12, [
233 |         [8, 1, 1],
234 |         [16, 4, intPort],
235 |         [16, 6, extPort],
236 |         [32, 8, lifetime]
237 |       ]);
238 |       socket.sendTo(pmpBuffer, routerIp, 5351);
239 |     });
240 |   });
241 | 
242 |   // Give _sendPmpRequest 2 seconds before timing out
243 |   return Promise.race([
244 |     utils.countdownReject(2000, 'No NAT-PMP response', function () {
245 |       utils.closeSocket(socket);
246 |     }),
247 |     _sendPmpRequest
248 |   ]);
249 | };
250 | 
251 | module.exports = {
252 |   probeSupport: probeSupport,
253 |   addMapping: addMapping,
254 |   deleteMapping: deleteMapping
255 | };
256 | 


--------------------------------------------------------------------------------
/src/pcp.js:
--------------------------------------------------------------------------------
  1 | var utils = require('./utils');
  2 | var ipaddr = require('ipaddr.js');
  3 | 
  4 | /**
  5 | * Probe if PCP is supported by the router
  6 | * @public
  7 | * @method probeSupport
  8 | * @param {object} activeMappings Table of active Mappings
  9 | * @param {Array} routerIpCache Router IPs that have previously worked
 10 | * @return {Promise} A promise for a boolean
 11 | */
 12 | var probeSupport = function (activeMappings, routerIpCache) {
 13 |   return addMapping(utils.PCP_PROBE_PORT, utils.PCP_PROBE_PORT, 120,
 14 |                        activeMappings, routerIpCache).
 15 |       then(function (mapping) { return mapping.externalPort !== -1; });
 16 | };
 17 | 
 18 | /**
 19 | * Makes a port mapping in the NAT with PCP,
 20 | * and automatically refresh the mapping every two minutes
 21 | * @public
 22 | * @method addMapping
 23 | * @param {number} intPort The internal port on the computer to map to
 24 | * @param {number} extPort The external port on the router to map to
 25 | * @param {number} lifetime Seconds that the mapping will last
 26 | *                          0 is infinity, i.e. a refresh every 24 hours
 27 | * @param {object} activeMappings Table of active Mappings
 28 | * @param {Array} routerIpCache Router IPs that have previously worked
 29 | * @return {Promise} A promise for the port mapping object 
 30 | *                            mapping.externalPort is -1 on failure
 31 | */
 32 | var addMapping = function (intPort, extPort, lifetime, activeMappings, routerIpCache) {
 33 |   var mapping = new utils.Mapping();
 34 |   mapping.internalPort = intPort;
 35 |   mapping.protocol = 'pcp';
 36 | 
 37 |   // If lifetime is zero, we want to refresh every 24 hours
 38 |   var reqLifetime = (lifetime === 0) ? 24*60*60 : lifetime;
 39 | 
 40 |   // Send PCP requests to a list of router IPs and parse the first response
 41 |   function _sendPcpRequests(routerIps) {
 42 |     return utils.getPrivateIps().then(function (privateIps) {
 43 |       // Construct an array of ArrayBuffers, which are the responses of
 44 |       // sendPcpRequest() calls on all the router IPs. An error result
 45 |       // is caught and re-passed as null.
 46 |       return Promise.all(routerIps.map(function (routerIp) {
 47 |         // Choose a privateIp based on the currently selected routerIp,
 48 |         // using a longest prefix match, and send a PCP request with that IP
 49 |         var privateIp = utils.longestPrefixMatch(privateIps, routerIp);
 50 |         return sendPcpRequest(routerIp, privateIp, intPort, extPort, 
 51 |                                     reqLifetime).
 52 |             then(function (pcpResponse) {
 53 |               return {"pcpResponse": pcpResponse, "privateIp": privateIp};
 54 |             }).
 55 |             catch(function (err) {
 56 |               return null;
 57 |             });
 58 |       }));
 59 |     }).then(function (responses) {
 60 |       // Check if any of the responses are successful (not null), and return
 61 |       // it as a Mapping object
 62 |       for (var i = 0; i < responses.length; i++) {
 63 |         if (responses[i] !== null) {
 64 |           var responseView = new DataView(responses[i].pcpResponse);
 65 |           var ipOctets = [responseView.getUint8(56), responseView.getUint8(57),
 66 |                           responseView.getUint8(58), responseView.getUint8(59)];
 67 |           var extIp = ipOctets.join('.');
 68 | 
 69 |           mapping.externalPort = responseView.getUint16(42);
 70 |           mapping.externalIp = extIp;
 71 |           mapping.internalIp = responses[i].privateIp;
 72 |           mapping.lifetime = responseView.getUint32(4);
 73 |           mapping.nonce = [responseView.getUint32(24), 
 74 |                            responseView.getUint32(28),
 75 |                            responseView.getUint32(32)];
 76 | 
 77 |           if (routerIpCache.indexOf(routerIps[i]) === -1) {
 78 |             routerIpCache.push(routerIps[i]);
 79 |           }
 80 |         }
 81 |       }
 82 |       return mapping;
 83 |     }).catch(function (err) {
 84 |       return mapping;
 85 |     });
 86 |   }
 87 | 
 88 |   // Basically calls _sendPcpRequests on matchedRouterIps first, and if that 
 89 |   // doesn't work, calls it on otherRouterIps
 90 |   function _sendPcpRequestsInWaves() {
 91 |     return utils.getPrivateIps().then(function (privateIps) {
 92 |       // Try matchedRouterIps first (routerIpCache + router IPs that match the 
 93 |       // user's IPs), then otherRouterIps if it doesn't work. This avoids flooding
 94 |       // the local network with PCP requests
 95 |       var matchedRouterIps = utils.arrAdd(routerIpCache, utils.filterRouterIps(privateIps));
 96 |       var otherRouterIps = utils.arrDiff(utils.ROUTER_IPS, matchedRouterIps);
 97 |       return _sendPcpRequests(matchedRouterIps).then(function (mapping) {
 98 |         if (mapping.externalPort !== -1) { return mapping; }
 99 |         return _sendPcpRequests(otherRouterIps);
100 |       });
101 |     });
102 |   }
103 | 
104 |   // Compare our requested parameters for the mapping with the response,
105 |   // setting a refresh if necessary, and a timeout for deletion, and saving the 
106 |   // mapping object to activeMappings if the mapping succeeded
107 |   function _saveAndRefreshMapping(mapping) {
108 |     // If the actual lifetime is less than the requested lifetime,
109 |     // setTimeout to refresh the mapping when it expires
110 |     var dLifetime = reqLifetime - mapping.lifetime;
111 |     if (mapping.externalPort !== -1 && dLifetime > 0) {
112 |       mapping.timeoutId = setTimeout(addMapping.bind({}, intPort,
113 |         mapping.externalPort, dLifetime, activeMappings), mapping.lifetime*1000);
114 |     }
115 |     // If the original lifetime is 0, refresh every 24 hrs indefinitely
116 |     else if (mapping.externalPort !== -1 && lifetime === 0) {
117 |       mapping.timeoutId = setTimeout(addMapping.bind({}, intPort,
118 |                        mapping.externalPort, 0, activeMappings), 24*60*60*1000);
119 |     }
120 |     // If we're not refreshing, delete the entry in activeMapping at expiration
121 |     else if (mapping.externalPort !== -1) {
122 |       setTimeout(function () { delete activeMappings[mapping.externalPort]; },
123 |                  mapping.lifetime*1000);
124 |     }
125 | 
126 |     // If mapping succeeded, attach a deleter function and add to activeMappings
127 |     if (mapping.externalPort !== -1) {
128 |       mapping.deleter = deleteMapping.bind({}, mapping.externalPort,
129 |                                            activeMappings, routerIpCache);
130 |       activeMappings[mapping.externalPort] = mapping;
131 |     }
132 |     return mapping;
133 |   }
134 | 
135 |   // Try PCP requests to matchedRouterIps, then otherRouterIps. 
136 |   // After receiving a PCP response, set timeouts to delete/refresh the 
137 |   // mapping, add it to activeMappings, and return the mapping object
138 |   return _sendPcpRequestsInWaves().then(_saveAndRefreshMapping);
139 | };
140 | 
141 | /**
142 | * Deletes a port mapping in the NAT with PCP
143 | * @public
144 | * @method deleteMapping
145 | * @param {number} extPort The external port of the mapping to delete
146 | * @param {object} activeMappings Table of active Mappings
147 | * @param {Array} routerIpCache Router IPs that have previously worked
148 | * @return {Promise} True on success, false on failure
149 | */
150 | var deleteMapping = function (extPort, activeMappings, routerIpCache) {
151 |   // Send PCP requests to a list of router IPs and parse the first response
152 |   function _sendDeletionRequests(routerIps) {
153 |     return utils.getPrivateIps().then(function (privateIps) {
154 |       // Get the internal port and nonce for this mapping; this may error
155 |       var intPort = activeMappings[extPort].internalPort;
156 |       var nonce = activeMappings[extPort].nonce;
157 | 
158 |       // Construct an array of ArrayBuffers, which are the responses of
159 |       // sendPmpRequest() calls on all the router IPs. An error result
160 |       // is caught and re-passed as null.
161 |       return Promise.all(routerIps.map(function (routerIp) {
162 |           // Choose a privateIp based on the currently selected routerIp,
163 |           // using a longest prefix match, and send a PCP request with that IP
164 |           var privateIp = utils.longestPrefixMatch(privateIps, routerIp);
165 |           return sendPcpRequest(routerIp, privateIp, intPort, 0, 0, nonce).
166 |               then(function (pcpResponse) { return pcpResponse; }).
167 |               catch(function (err) { return null; });
168 |         }));
169 |    });
170 |  }
171 | 
172 |   // Basically calls _sendDeletionRequests on matchedRouterIps first, and if that 
173 |   // doesn't work, calls it on otherRouterIps
174 |   function _sendDeletionRequestsInWaves() {
175 |     return utils.getPrivateIps().then(function (privateIps) {
176 |       // Try matchedRouterIps first (routerIpCache + router IPs that match the 
177 |       // user's IPs), then otherRouterIps if it doesn't work. This avoids flooding
178 |       // the local network with PCP requests
179 |       var matchedRouterIps = utils.arrAdd(routerIpCache, utils.filterRouterIps(privateIps));
180 |       var otherRouterIps = utils.arrDiff(utils.ROUTER_IPS, matchedRouterIps);
181 |       return _sendDeletionRequests(matchedRouterIps).then(function (mapping) {
182 |         if (mapping.externalPort !== -1) { return mapping; }
183 |         return _sendDeletionRequests(otherRouterIps);
184 |       });
185 |     });
186 |   }
187 | 
188 |   // If any of the PCP responses were successful, delete the entry from 
189 |   // activeMappings and return true
190 |   function _deleteFromActiveMappings(responses) {
191 |     for (var i = 0; i < responses.length; i++) {
192 |       if (responses[i] !== null) {
193 |         // Success code 8 (NO_RESOURCES) may denote that the mapping does not
194 |         // exist on the router, so we accept it as well
195 |         var responseView = new DataView(responses[i]);
196 |         var successCode = responseView.getUint8(3);
197 |         if (successCode === 0 || successCode === 8) {
198 |           clearTimeout(activeMappings[extPort].timeoutId);
199 |           delete activeMappings[extPort];
200 |           return true;
201 |         } 
202 |       }
203 |     }
204 |     return false;
205 |   }
206 | 
207 |   // Send PCP deletion requests to matchedRouterIps, then otherRouterIps;
208 |   // if that succeeds, delete the corresponding Mapping from activeMappings
209 |   return _sendDeletionRequestsInWaves().
210 |       then(_deleteFromActiveMappings).
211 |       catch(function (err) { return false; });
212 | };
213 | 
214 | /**
215 | * Send a PCP request to the router to map a port
216 | * @private
217 | * @method sendPcpRequest
218 | * @param {string} routerIp The IP address that the router can be reached at
219 | * @param {string} privateIp The private IP address of the user's computer
220 | * @param {number} intPort The internal port on the computer to map to
221 | * @param {number} extPort The external port on the router to map to
222 | * @param {number} lifetime Seconds that the mapping will last
223 | * @param {array} nonce (Optional) A specified nonce for the PCP request
224 | * @return {Promise} A promise that fulfills with the PCP response
225 | *                                or rejects on timeout
226 | */
227 | var sendPcpRequest = function (routerIp, privateIp, intPort, extPort, lifetime, 
228 |                                nonce) {
229 |   var socket;
230 | 
231 |   // Pre-process nonce and privateIp arguments
232 |   if (nonce === undefined) {
233 |     nonce = [utils.randInt(0, 0xffffffff), 
234 |              utils.randInt(0, 0xffffffff), 
235 |              utils.randInt(0, 0xffffffff)];
236 |   }
237 |   var ipOctets = ipaddr.IPv4.parse(privateIp).octets;
238 | 
239 |   // Bind a socket and send the PCP request from that socket to routerIp
240 |   var _sendPcpRequest = new Promise(function (F, R) {
241 |     socket = freedom['core.udpsocket']();
242 | 
243 |     // Fulfill when we get any reply (failure is on timeout in wrapper function)
244 |     socket.on('onData', function (pcpResponse) {
245 |       utils.closeSocket(socket);
246 |       F(pcpResponse.data);
247 |     });
248 | 
249 |     // Bind a UDP port and send a PCP request
250 |     socket.bind('0.0.0.0', 0).then(function (result) {
251 |       // PCP packet structure: https://tools.ietf.org/html/rfc6887#section-11.1
252 |       var pcpBuffer = utils.createArrayBuffer(60, [
253 |         [32, 0, 0x2010000],
254 |         [32, 4, lifetime],
255 |         [16, 18, 0xffff],
256 |         [8, 20, ipOctets[0]],
257 |         [8, 21, ipOctets[1]],
258 |         [8, 22, ipOctets[2]],
259 |         [8, 23, ipOctets[3]],
260 |         [32, 24, nonce[0]],
261 |         [32, 28, nonce[1]],
262 |         [32, 32, nonce[2]],
263 |         [8, 36, 17],
264 |         [16, 40, intPort],
265 |         [16, 42, extPort],
266 |         [16, 54, 0xffff],
267 |       ]);
268 |       socket.sendTo(pcpBuffer, routerIp, 5351);
269 |     });
270 |   });
271 | 
272 |   // Give _sendPcpRequest 2 seconds before timing out
273 |   return Promise.race([
274 |     utils.countdownReject(2000, 'No PCP response', function () {
275 |       utils.closeSocket(socket);
276 |     }),
277 |     _sendPcpRequest
278 |   ]);
279 | };
280 | 
281 | module.exports = {
282 |   probeSupport: probeSupport,
283 |   addMapping: addMapping,
284 |   deleteMapping: deleteMapping
285 | };
286 | 


--------------------------------------------------------------------------------
/src/port-control.js:
--------------------------------------------------------------------------------
  1 | var ipaddr = require('ipaddr.js');
  2 | var utils = require('./utils');
  3 | var natPmp = require('./nat-pmp');
  4 | var pcp = require('./pcp');
  5 | var upnp = require('./upnp');
  6 | 
  7 | var PortControl = function (dispatchEvent) {
  8 |   this.dispatchEvent = dispatchEvent;
  9 | };
 10 | 
 11 | /**
 12 | * A table that keeps track of information about active Mappings
 13 | * The Mapping type is defined in utils.js
 14 | * { externalPortNumber1: Mapping1, 
 15 | *   externalPortNumber2: Mapping2,
 16 | *   ...
 17 | * }
 18 | */
 19 | PortControl.prototype.activeMappings = {};
 20 | 
 21 | /**
 22 |  * An array of previous router IPs that have worked; we try these first when 
 23 |  * sending NAT-PMP and PCP requests
 24 |  */
 25 | PortControl.prototype.routerIpCache = [];
 26 | 
 27 | /**
 28 |  * An object that keeps track of which protocols are supported
 29 |  * This is updated every time this.probeProtocolSupport() is called
 30 |  * @property {boolean} natPmp A boolean stating if NAT-PMP is supported
 31 |  * @property {boolean} pcp A boolean stating if PCP is supported
 32 |  * @property {boolean} upnp A boolean stating if UPnP is supported
 33 |   * @property {string} [upnpControlUrl] The UPnP router's control URL
 34 |  */
 35 | PortControl.prototype.protocolSupportCache = {
 36 |   natPmp: undefined,
 37 |   pcp: undefined,
 38 |   upnp: undefined,
 39 |   upnpControlUrl: undefined
 40 | };
 41 | 
 42 | /**
 43 | * Add a port mapping through the NAT, using a protocol that probeProtocolSupport()
 44 | * found. If probeProtocolSupport() has not been previously called, i.e. 
 45 | * protocolSupportCache is empty, then we try each protocol until one works
 46 | * @public
 47 | * @method addMapping
 48 | * @param {number} intPort The internal port on the computer to map to
 49 | * @param {number} extPort The external port on the router to map to
 50 | * @param {number} lifetime Seconds that the mapping will last
 51 | *                          0 is infinity; handled differently per protocol
 52 | * @return {Promise} A promise for the port mapping object
 53 | *                            Mapping.externalPort === -1 on failure
 54 | **/
 55 | PortControl.prototype.addMapping = function (intPort, extPort, lifetime) {
 56 |   var _this = this;
 57 | 
 58 |   if (_this.protocolSupportCache.natPmp === undefined) {
 59 |     // We have no data in the protocolSupportCache,
 60 |     // so try to open a port with NAT-PMP, then PCP, then UPnP in that order
 61 |     return _this.addMappingPmp(intPort, extPort, lifetime).
 62 |       then(function (mapping) {
 63 |         if (mapping.externalPort !== -1) { 
 64 |           return mapping; 
 65 |         }
 66 |         return _this.addMappingPcp(intPort, extPort, lifetime);
 67 |       }).
 68 |       then(function (mapping) {
 69 |         if (mapping.externalPort !== -1) { 
 70 |           return mapping; 
 71 |         }
 72 |         return _this.addMappingUpnp(intPort, extPort, lifetime);
 73 |       });
 74 |   } else {
 75 |     // We have data from probing the router for protocol support,
 76 |     // so we can directly try one protocol, or return a failure Mapping
 77 |     if (_this.protocolSupportCache.natPmp) {
 78 |       return _this.addMappingPmp(intPort, extPort, lifetime);
 79 |     } else if (_this.protocolSupportCache.pcp) {
 80 |       return _this.addMappingPcp(intPort, extPort, lifetime);
 81 |     } else if (_this.protocolSupportCache.upnp) {
 82 |       return _this.addMappingUpnp(intPort, extPort, lifetime,
 83 |                                   _this.protocolSupportCache.upnpControlUrl);
 84 |     } else {
 85 |       var failureMapping = new utils.Mapping();
 86 |       failureMapping.errInfo = "No protocols are supported from last probe";
 87 |       return failureMapping;
 88 |     }
 89 |   }
 90 | };
 91 | 
 92 | /**
 93 | * Delete the port mapping locally and from the router (and stop refreshes)
 94 | * The port mapping must have a Mapping object in this.activeMappings
 95 | * @public
 96 | * @method deleteMapping
 97 | * @param {number} extPort The external port of the mapping to delete
 98 | * @return {Promise} True on success, false on failure
 99 | **/
100 | PortControl.prototype.deleteMapping = function (extPort) {
101 |   var mapping = this.activeMappings[extPort];
102 |   if (mapping === undefined) { 
103 |     return Promise.resolve(false); 
104 |   }
105 |   return mapping.deleter();
106 | };
107 | 
108 | /**
109 | * Probes the NAT for NAT-PMP, PCP, and UPnP support,
110 | * and returns an object representing the NAT configuration
111 | * Don't run probe before trying to map a port; instead, just try to map the port
112 | * @public
113 | * @method probeProtocolSupport
114 | * @return {Promise<{"natPmp": boolean, "pcp": boolean, "upnp": boolean}>}
115 | */
116 | PortControl.prototype.probeProtocolSupport = function () {
117 |   var _this = this;
118 | 
119 |   return Promise.all([this.probePmpSupport(), this.probePcpSupport(),
120 |     this.probeUpnpSupport(), this.getUpnpControlUrl()]).then(function (support) {
121 |       _this.protocolSupportCache.natPmp = support[0];
122 |       _this.protocolSupportCache.pcp = support[1];
123 |       _this.protocolSupportCache.upnp = support[2];
124 |       _this.protocolSupportCache.upnpControlUrl = support[3];
125 | 
126 |       return {
127 |         natPmp: support[0],
128 |         pcp: support[1],
129 |         upnp: support[2]
130 |       };
131 |     });
132 | };
133 | 
134 | /**
135 | * Probe if NAT-PMP is supported by the router
136 | * @public
137 | * @method probePmpSupport
138 | * @return {Promise} A promise for a boolean
139 | */
140 | PortControl.prototype.probePmpSupport = function () {
141 |   return natPmp.probeSupport(this.activeMappings, this.routerIpCache);
142 | };
143 | 
144 | /**
145 | * Makes a port mapping in the NAT with NAT-PMP,
146 | * and automatically refresh the mapping every two minutes
147 | * @public
148 | * @method addMappingPmp
149 | * @param {number} intPort The internal port on the computer to map to
150 | * @param {number} extPort The external port on the router to map to
151 | * @param {number} lifetime Seconds that the mapping will last
152 | *                          0 is infinity, i.e. a refresh every 24 hours
153 | * @return {Promise} A promise for the port mapping object
154 | *                            Mapping.externalPort === -1 on failure
155 | */
156 | PortControl.prototype.addMappingPmp = function (intPort, extPort, lifetime) {
157 |   return natPmp.addMapping(intPort, extPort, lifetime, this.activeMappings,
158 |                            this.routerIpCache);
159 | };
160 | 
161 | /**
162 | * Deletes a port mapping in the NAT with NAT-PMP
163 | * The port mapping must have a Mapping object in this.activeMappings
164 | * @public
165 | * @method deleteMappingPmp
166 | * @param {number} extPort The external port of the mapping to delete
167 | * @return {Promise} True on success, false on failure
168 | */
169 | PortControl.prototype.deleteMappingPmp = function (extPort) {
170 |   var mapping = this.activeMappings[extPort];
171 |   if (mapping === undefined || mapping.protocol !== 'natPmp') { 
172 |     return Promise.resolve(false); 
173 |   }
174 |   return mapping.deleter();
175 | };
176 | 
177 | /**
178 | * Probe if PCP is supported by the router
179 | * @public
180 | * @method probePcpSupport
181 | * @return {Promise} A promise for a boolean
182 | */
183 | PortControl.prototype.probePcpSupport = function () {
184 |   return pcp.probeSupport(this.activeMappings, this.routerIpCache);
185 | };
186 | 
187 | /**
188 | * Makes a port mapping in the NAT with PCP,
189 | * and automatically refresh the mapping every two minutes
190 | * @public
191 | * @method addMappingPcp
192 | * @param {number} intPort The internal port on the computer to map to
193 | * @param {number} extPort The external port on the router to map to
194 | * @param {number} lifetime Seconds that the mapping will last
195 | *                          0 is infinity, i.e. a refresh every 24 hours
196 | * @return {Promise} A promise for the port mapping object 
197 | *                            mapping.externalPort is -1 on failure
198 | */
199 | PortControl.prototype.addMappingPcp = function (intPort, extPort, lifetime) {
200 |   return pcp.addMapping(intPort, extPort, lifetime, this.activeMappings,
201 |                         this.routerIpCache);
202 | };
203 | 
204 | /**
205 | * Deletes a port mapping in the NAT with PCP
206 | * The port mapping must have a Mapping object in this.activeMappings
207 | * @public
208 | * @method deleteMappingPcp
209 | * @param {number} extPort The external port of the mapping to delete
210 | * @return {Promise} True on success, false on failure
211 | */
212 | PortControl.prototype.deleteMappingPcp = function (extPort) {
213 |   var mapping = this.activeMappings[extPort];
214 |   if (mapping === undefined || mapping.protocol !== 'pcp') { 
215 |     return Promise.resolve(false); 
216 |   }
217 |   return mapping.deleter();
218 | };
219 | 
220 | /**
221 | * Probe if UPnP AddPortMapping is supported by the router
222 | * @public
223 | * @method probeUpnpSupport
224 | * @return {Promise} A promise for a boolean
225 | */
226 | PortControl.prototype.probeUpnpSupport = function () {
227 |   return upnp.probeSupport(this.activeMappings);
228 | };
229 | 
230 | /**
231 | * Makes a port mapping in the NAT with UPnP AddPortMapping
232 | * @public
233 | * @method addMappingUpnp
234 | * @param {number} intPort The internal port on the computer to map to
235 | * @param {number} extPort The external port on the router to map to
236 | * @param {number} lifetime Seconds that the mapping will last
237 | *                          0 is infinity; a static AddPortMapping request
238 | * @param {string=} controlUrl Optional: a control URL for the router
239 | * @return {Promise} A promise for the port mapping object 
240 | *                               mapping.externalPort is -1 on failure
241 | */
242 | PortControl.prototype.addMappingUpnp = function (intPort, extPort, lifetime,
243 |                                                  controlUrl) {
244 |   return upnp.addMapping(intPort, extPort, lifetime, this.activeMappings,
245 |                          controlUrl);
246 | };
247 | 
248 | /**
249 | * Deletes a port mapping in the NAT with UPnP DeletePortMapping
250 | * The port mapping must have a Mapping object in this.activeMappings
251 | * @public
252 | * @method deleteMappingUpnp
253 | * @param {number} extPort The external port of the mapping to delete
254 | * @return {Promise} True on success, false on failure
255 | */
256 | PortControl.prototype.deleteMappingUpnp = function (extPort) {
257 |   var mapping = this.activeMappings[extPort];
258 |   if (mapping === undefined || mapping.protocol !== 'upnp') { 
259 |     return Promise.resolve(false); 
260 |   }
261 |   return mapping.deleter();
262 | };
263 | 
264 | /**
265 |  * Return the UPnP control URL of a router on the network that supports UPnP IGD
266 |  * @public
267 |  * @method getUpnpControlUrl
268 |  * @return {Promise} A promise for the URL, empty string if not supported
269 |  */
270 | PortControl.prototype.getUpnpControlUrl = function () {
271 |   return upnp.getUpnpControlUrl();
272 | };
273 | 
274 | /**
275 | * Returns the current value of activeMappings
276 | * @public
277 | * @method getActiveMappings
278 | * @return {Promise} A promise that resolves to activeMappings
279 | */
280 | PortControl.prototype.getActiveMappings = function () {
281 |   return Promise.resolve(this.activeMappings);
282 | };
283 | 
284 | /**
285 | * Return the router IP cache
286 | * @public
287 | * @method getRouterIpCache
288 | * @return {Promise>} A promise that resolves to routerIpCache
289 | */
290 | PortControl.prototype.getRouterIpCache = function () {
291 |   return Promise.resolve(this.routerIpCache);
292 | };
293 | 
294 | /**
295 |  * Return the protocol support cache
296 |  * @public
297 |  * @method getProtocolSupportCache
298 |  * @return {Promise} A promise that resolves to protocolSupportCache
299 |  */
300 | PortControl.prototype.getProtocolSupportCache = function () {
301 |   return Promise.resolve(this.protocolSupportCache);
302 | };
303 | 
304 | /**
305 | * Return the private IP addresses of the computer
306 | * @public
307 | * @method getPrivateIps
308 | * @return {Promise>} A promise that fulfills with a list of IPs,
309 | *                                  or rejects on timeout
310 | */
311 | PortControl.prototype.getPrivateIps = function () {
312 |   return utils.getPrivateIps();
313 | };
314 | 
315 | /**
316 | * Deletes all the currently active port mappings
317 | * @public
318 | * @method close
319 | */
320 | PortControl.prototype.close = function () {
321 |   var _this = this;
322 | 
323 |   return new Promise(function (F, R) {
324 |     // Get all the keys (extPorts) of activeMappings
325 |     var extPorts = [];
326 |     for (var extPort in _this.activeMappings) {
327 |       if (_this.activeMappings.hasOwnProperty(extPort)) {
328 |         extPorts.push(extPort);
329 |       }
330 |     }
331 | 
332 |     // Delete them all
333 |     Promise.all(extPorts.map(_this.deleteMapping.bind(_this))).then(function () {
334 |       F();
335 |     });
336 |   });
337 | };
338 | 
339 | if (typeof freedom !== 'undefined') {
340 |   freedom().providePromises(PortControl);
341 | }
342 | 


--------------------------------------------------------------------------------
/src/port-control.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "Port Control",
  3 |   "description": "Freedom port control manifest",
  4 |   "app": {
  5 |     "script": "port-control.js"
  6 |   },
  7 |   "constraints": {
  8 |     "isolation": "never"
  9 |   },
 10 |   "provides" : [
 11 |     "portControl"
 12 |   ],
 13 |   "default": "portControl",
 14 |   "api": {
 15 |     "portControl": {
 16 |       "addMapping": {
 17 |         "type": "method",
 18 |         "value": ["number", "number", "number"],
 19 |         "ret": {"internalIp": "string", "internalPort": "number",
 20 |                 "externalIp": "string", "externalPort": "number",
 21 |                 "lifetime": "number", "protocol": "string",
 22 |                 "timeoutId": "number", "nonce": ["array", "number"],
 23 |                 "errInfo": "string"}
 24 |       },
 25 | 
 26 |       "deleteMapping": {
 27 |         "type": "method",
 28 |         "value": ["number"],
 29 |         "ret": "boolean"
 30 |       },
 31 | 
 32 |       "probeProtocolSupport": {
 33 |         "type": "method",
 34 |         "value": [],
 35 |         "ret": {"natPmp": "boolean", "pcp": "boolean", "upnp": "boolean"}
 36 |       },
 37 | 
 38 |       "probePmpSupport": {
 39 |         "type": "method",
 40 |         "value": [],
 41 |         "ret": "boolean"
 42 |       },
 43 | 
 44 |       "addMappingPmp": {
 45 |         "type": "method",
 46 |         "value": ["number", "number", "number"],
 47 |         "ret": {"internalIp": "string", "internalPort": "number",
 48 |                 "externalIp": "string", "externalPort": "number",
 49 |                 "lifetime": "number", "protocol": "string",
 50 |                 "timeoutId": "number", "nonce": ["array", "number"],
 51 |                 "errInfo": "string"}
 52 |       },
 53 | 
 54 |       "deleteMappingPmp": {
 55 |         "type": "method",
 56 |         "value": ["number"],
 57 |         "ret": "boolean"
 58 |       },
 59 | 
 60 |       "probePcpSupport": {
 61 |         "type": "method",
 62 |         "value": [],
 63 |         "ret": "boolean"
 64 |       },
 65 | 
 66 |       "addMappingPcp": {
 67 |         "type": "method",
 68 |         "value": ["number", "number", "number"],
 69 |         "ret": {"internalIp": "string", "internalPort": "number",
 70 |                 "externalIp": "string", "externalPort": "number",
 71 |                 "lifetime": "number", "protocol": "string",
 72 |                 "timeoutId": "number", "nonce": ["array", "number"],
 73 |                 "errInfo": "string"}
 74 |       },
 75 | 
 76 |       "deleteMappingPcp": {
 77 |         "type": "method",
 78 |         "value": ["number"],
 79 |         "ret": "boolean"
 80 |       },
 81 | 
 82 |       "probeUpnpSupport": {
 83 |         "type": "method",
 84 |         "value": [],
 85 |         "ret": "boolean"
 86 |       },
 87 | 
 88 |       "addMappingUpnp": {
 89 |         "type": "method",
 90 |         "value": ["number", "number", "number", "string"],
 91 |         "ret": {"internalIp": "string", "internalPort": "number",
 92 |                 "externalIp": "string", "externalPort": "number",
 93 |                 "lifetime": "number", "protocol": "string",
 94 |                 "timeoutId": "number", "nonce": ["array", "number"],
 95 |                 "errInfo": "string"}
 96 |       },
 97 | 
 98 |       "deleteMappingUpnp": {
 99 |         "type": "method",
100 |         "value": ["number"],
101 |         "ret": "boolean"
102 |       },
103 | 
104 |       "getUpnpControlUrl": {
105 |         "type": "method",
106 |         "value": [],
107 |         "ret": "string"
108 |       },
109 | 
110 |       "getActiveMappings": {
111 |         "type": "method",
112 |         "value": [],
113 |         "ret": "object"
114 |       },
115 | 
116 |       "getRouterIpCache": {
117 |         "type": "method",
118 |         "value": [],
119 |         "ret": ["array", "string"]
120 |       },
121 | 
122 |       "getProtocolSupportCache": {
123 |         "type": "method",
124 |         "value": [],
125 |         "ret": {"natPmp": "boolean", "pcp": "boolean",
126 |                 "upnp": "boolean", "upnpControlUrl": "string"}
127 |       },
128 | 
129 |       "getPrivateIps": {
130 |         "type": "method",
131 |         "value": [],
132 |         "ret": ["array", "string"]
133 |       },
134 | 
135 |       "close": {
136 |         "type": "method",
137 |         "value": []
138 |       }
139 |     }
140 |   },
141 |   "permissions": [
142 |     "core.udpsocket",
143 |     "core.rtcpeerconnection"
144 |   ]
145 | }
146 | 


--------------------------------------------------------------------------------
/src/upnp.js:
--------------------------------------------------------------------------------
  1 | var utils = require('./utils');
  2 | 
  3 | /**
  4 | * Probe if UPnP AddPortMapping is supported by the router
  5 | * @public
  6 | * @method probeSupport
  7 | * @param {object} activeMappings Table of active Mappings
  8 | * @param {Array} routerIpCache Router IPs that have previously worked
  9 | * @return {Promise} A promise for a boolean
 10 | */
 11 | var probeSupport = function (activeMappings) {
 12 |   return addMapping(utils.UPNP_PROBE_PORT, utils.UPNP_PROBE_PORT, 120,
 13 |                     activeMappings).then(function (mapping) { 
 14 |         if (mapping.errInfo && 
 15 |             mapping.errInfo.indexOf('ConflictInMappingEntry') !== -1) {
 16 |           // This error response suggests that UPnP is enabled
 17 |           return true;
 18 |         }
 19 |         return mapping.externalPort !== -1; 
 20 |       });
 21 | };
 22 | 
 23 | /**
 24 | * Makes a port mapping in the NAT with UPnP AddPortMapping
 25 | * @public
 26 | * @method addMapping
 27 | * @param {number} intPort The internal port on the computer to map to
 28 | * @param {number} extPort The external port on the router to map to
 29 | * @param {number} lifetime Seconds that the mapping will last
 30 | *                          0 is infinity; a static AddPortMapping request
 31 | * @param {object} activeMappings Table of active Mappings
 32 | * @param {string=} controlUrl Optional: a control URL for the router
 33 | * @return {Promise} A promise for the port mapping object 
 34 | *                               mapping.externalPort is -1 on failure
 35 | */
 36 | var addMapping = function (intPort, extPort, lifetime, activeMappings,
 37 |                            controlUrl) {
 38 |   var internalIp;  // Internal IP of the user's computer
 39 |   var mapping = new utils.Mapping();
 40 |   mapping.internalPort = intPort;
 41 |   mapping.protocol = 'upnp';
 42 | 
 43 |   // Does the UPnP flow to send a AddPortMapping request
 44 |   // (1. SSDP, 2. GET location URL, 3. POST to control URL)
 45 |   // If we pass in a control URL, we don't need to do the SSDP step
 46 |   function _handleUpnpFlow() {
 47 |     if (controlUrl !== undefined) { return _handleControlUrl(controlUrl); }
 48 |     return _getUpnpControlUrl().then(function (url) {
 49 |       controlUrl = url;
 50 |       return _handleControlUrl(url);
 51 |     }).catch(_handleError);
 52 |   }
 53 | 
 54 |   // Process and send an AddPortMapping request to the control URL
 55 |   function _handleControlUrl(controlUrl) {
 56 |     return new Promise(function (F, R) {
 57 |       // Get the correct internal IP (if there are multiple network interfaces)
 58 |       // for this UPnP router, by doing a longest prefix match, and use it to
 59 |       // send an AddPortMapping request
 60 |       var routerIp = (new URL(controlUrl)).hostname;
 61 |       utils.getPrivateIps().then(function(privateIps) {
 62 |         internalIp = utils.longestPrefixMatch(privateIps, routerIp);
 63 |         sendAddPortMapping(controlUrl, internalIp, intPort, extPort, lifetime).
 64 |             then(function (response) { F(response); }).
 65 |             catch(function (err) { R(err); });
 66 |       });
 67 |     }).then(function (response) {
 68 |       // Success response to AddPortMapping (the internal IP of the mapping)
 69 |       // The requested external port will always be mapped on success, and the
 70 |       // lifetime will always be the requested lifetime; errors otherwise
 71 |       mapping.externalPort = extPort;
 72 |       mapping.internalIp = internalIp;
 73 |       mapping.lifetime = lifetime;
 74 |       return mapping;
 75 |     }).catch(_handleError);
 76 |   }
 77 | 
 78 |   // Save the Mapping object in activeMappings on success, and set a timeout 
 79 |   // to delete the mapping on expiration
 80 |   // Note: We never refresh for UPnP since 0 is infinity per the protocol and 
 81 |   // there is no maximum lifetime
 82 |   function _saveMapping(mapping) {
 83 |     // Delete the entry from activeMapping at expiration
 84 |     if (mapping.externalPort !== -1 && lifetime !== 0) {
 85 |       setTimeout(function () { delete activeMappings[mapping.externalPort]; },
 86 |                  mapping.lifetime*1000);
 87 |     }
 88 | 
 89 |     // If mapping succeeded, attach a deleter function and add to activeMappings
 90 |     if (mapping.externalPort !== -1) {
 91 |       mapping.deleter = deleteMapping.bind({}, mapping.externalPort, 
 92 |                                            activeMappings, controlUrl);
 93 |       activeMappings[mapping.externalPort] = mapping;
 94 |     }
 95 |     return mapping;
 96 |   }
 97 | 
 98 |   // If we catch an error, add it to the mapping object and console.log()
 99 |   function _handleError(err) {
100 |     console.log("UPnP failed at: " + err.message);
101 |     mapping.errInfo = err.message;
102 |     return mapping;
103 |   }
104 | 
105 |   // After receiving an AddPortMapping response, set a timeout to delete the 
106 |   // mapping, and add it to activeMappings
107 |   return _handleUpnpFlow().then(_saveMapping);
108 | };
109 | 
110 | /**
111 | * Deletes a port mapping in the NAT with UPnP DeletePortMapping
112 | * @public
113 | * @method deleteMapping
114 | * @param {number} extPort The external port of the mapping to delete
115 | * @param {object} activeMappings Table of active Mappings
116 | * @param {string} controlUrl A control URL for the router (not optional!)
117 | * @return {Promise} True on success, false on failure
118 | */
119 | var deleteMapping = function (extPort, activeMappings, controlUrl) {
120 |   // Do the UPnP flow to delete a mapping, and if successful, remove it from
121 |   // activeMappings and return true 
122 |   return sendDeletePortMapping(controlUrl, extPort).then(function() {
123 |     delete activeMappings[extPort];
124 |     return true;
125 |   }).catch(function (err) {
126 |     return false;
127 |   });
128 | };
129 | 
130 | /**
131 |  * Return the UPnP control URL of a router on the network that supports UPnP IGD
132 |  * This wraps sendSsdpRequest() and fetchControlUrl() together
133 |  * @private
134 |  * @method _getUpnpControlUrl
135 |  * @return {Promise} A promise for the URL, rejects if not supported
136 |  */
137 | var _getUpnpControlUrl = function () {
138 |   // After collecting all the SSDP responses, try to get the
139 |   // control URL field for each response, and return an array
140 |   return sendSsdpRequest().then(function (ssdpResponses) {
141 |     return Promise.all(ssdpResponses.map(function (ssdpResponse) {
142 |       return fetchControlUrl(ssdpResponse).
143 |           then(function (controlUrl) { return controlUrl; }).
144 |           catch(function (err) { return null; });
145 |     }));
146 |   }).then(function (controlUrls) {
147 |     // We return the first control URL we found; 
148 |     // there should always be at least one if we reached this block
149 |     for (var i = 0; i < controlUrls.length; i++) {
150 |       if (controlUrls[i] !== null) { return controlUrls[i]; }
151 |     }
152 |   }).catch(function (err) { return Promise.reject(err); });
153 | };
154 | 
155 | /**
156 |  * A public version of _getUpnpControlUrl that suppresses the Promise rejection,
157 |  * and replaces it with undefined. This is useful outside this module in a 
158 |  * Promise.all(), while inside we want to propagate the errors upwards
159 |  * @public
160 |  * @method getUpnpControlUrl
161 |  * @return {Promise} A promise for the URL, undefined if not supported
162 |  */
163 | var getUpnpControlUrl = function () {
164 |   return _getUpnpControlUrl().catch(function (err) {});
165 | };
166 | 
167 | /**
168 | * Send a UPnP SSDP request on the network and collects responses
169 | * @private
170 | * @method sendSsdpRequest
171 | * @return {Promise} A promise that fulfills with an array of SSDP response,
172 | *                          or rejects on timeout
173 | */
174 | var sendSsdpRequest = function () {
175 |   var ssdpResponses = [];
176 |   var socket = freedom['core.udpsocket']();
177 | 
178 |   // Fulfill when we get any reply (failure is on timeout or invalid parsing)
179 |   socket.on('onData', function (ssdpResponse) {
180 |     ssdpResponses.push(ssdpResponse.data);
181 |   });
182 | 
183 |   // Bind a socket and send the SSDP request
184 |   socket.bind('0.0.0.0', 0).then(function (result) {
185 |     // Construct and send a UPnP SSDP message
186 |     var ssdpStr = 'M-SEARCH * HTTP/1.1\r\n' +
187 |                   'HOST: 239.255.255.250:1900\r\n' +
188 |                   'MAN: "ssdp:discover"\r\n' +
189 |                   'MX: 3\r\n' +
190 |                   'ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n';
191 |     var ssdpBuffer = utils.stringToArrayBuffer(ssdpStr);
192 |     socket.sendTo(ssdpBuffer, '239.255.255.250', 1900);
193 |   });
194 | 
195 |   // Collect SSDP responses for 3 seconds before timing out
196 |   return new Promise(function (F, R) {
197 |     setTimeout(function () {
198 |       if (ssdpResponses.length > 0) { F(ssdpResponses); }
199 |       else { R(new Error("SSDP timeout")); }
200 |     }, 3000);
201 |   });
202 | };
203 | 
204 | /**
205 | * Fetch the control URL from the information provided in the SSDP response
206 | * @private
207 | * @method fetchControlUrl
208 | * @param {ArrayBuffer} ssdpResponse The ArrayBuffer response to the SSDP message
209 | * @return {string} The string of the control URL for the router
210 | */
211 | var fetchControlUrl = function (ssdpResponse) {
212 |   // Promise to parse the location URL from the SSDP response, then send a POST 
213 |   // xhr to the location URL to find the router's UPNP control URL
214 |   var _fetchControlUrl = new Promise(function (F, R) {
215 |     var ssdpStr = utils.arrayBufferToString(ssdpResponse);
216 |     var startIndex = ssdpStr.indexOf('LOCATION:') + 9;
217 |     var endIndex = ssdpStr.indexOf('\n', startIndex);
218 |     var locationUrl = ssdpStr.substring(startIndex, endIndex).trim();
219 | 
220 |     // Reject if there is no LOCATION header
221 |     if (startIndex === 8) {
222 |       R(new Error('No LOCATION header for UPnP device'));
223 |       return;
224 |     }
225 | 
226 |     // Get the XML device description at location URL
227 |     var xhr = new XMLHttpRequest();
228 |     xhr.open('GET', locationUrl, true);
229 |     xhr.onreadystatechange = function () {
230 |       if (xhr.readyState === 4) {
231 |         // Get control URL from XML file
232 |         // (Ideally we would parse and traverse the XML tree,
233 |         // but DOMParser is not available here)
234 |         var xmlDoc = xhr.responseText;
235 |         var preIndex = xmlDoc.indexOf('WANIPConnection');
236 |         var startIndex = xmlDoc.indexOf('', preIndex) + 13;
237 |         var endIndex = xmlDoc.indexOf('', startIndex);
238 | 
239 |         // Reject if there is no controlUrl
240 |         if (preIndex === -1 || startIndex === 12) {
241 |           R(new Error('Could not parse control URL'));
242 |           return;
243 |         }
244 | 
245 |         // Combine the controlUrl path with the locationUrl
246 |         var controlUrlPath = xmlDoc.substring(startIndex, endIndex);
247 |         var locationUrlParser = new URL(locationUrl);
248 |         var controlUrl = 'http://' + locationUrlParser.host +
249 |                          '/' + controlUrlPath;
250 | 
251 |         F(controlUrl);
252 |       }
253 |     };
254 |     xhr.send();
255 |   });
256 | 
257 |   // Give _fetchControlUrl 1 second before timing out
258 |   return Promise.race([
259 |     utils.countdownReject(1000, 'Time out when retrieving description XML'),
260 |     _fetchControlUrl
261 |   ]);
262 | };
263 | 
264 | /**
265 | * Send an AddPortMapping request to the router's control URL
266 | * @private
267 | * @method sendAddPortMapping
268 | * @param {string} controlUrl The control URL of the router
269 | * @param {string} privateIp The private IP address of the user's computer
270 | * @param {number} intPort The internal port on the computer to map to
271 | * @param {number} extPort The external port on the router to map to
272 | * @param {number} lifetime Seconds that the mapping will last
273 | * @return {string} The response string to the AddPortMapping request
274 | */
275 | var sendAddPortMapping = function (controlUrl, privateIp, intPort, extPort, lifetime) {
276 |   // Promise to send an AddPortMapping request to the control URL of the router
277 |   var _sendAddPortMapping = new Promise(function (F, R) {
278 |     // The AddPortMapping SOAP request string
279 |     var apm = '' +
280 |               '' +
281 |                '' +
282 |                   '' +
283 |                      '' + extPort + '' +
284 |                      'UDP' +
285 |                      '' + intPort + '' +
286 |                      '' + privateIp + '' +
287 |                      '1' +
288 |                      'uProxy UPnP' +
289 |                      '' + lifetime + '' +
290 |                   '' +
291 |                 '' +
292 |               '';
293 | 
294 |     // Create an XMLHttpRequest that encapsulates the SOAP string
295 |     var xhr = new XMLHttpRequest();
296 |     xhr.open('POST', controlUrl, true);
297 |     xhr.setRequestHeader('Content-Type', 'text/xml');
298 |     xhr.setRequestHeader('SOAPAction', '"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"');
299 | 
300 |     // Send the AddPortMapping request
301 |     xhr.onreadystatechange = function () {
302 |       if (xhr.readyState === 4 && xhr.status === 200) {
303 |         // Success response to AddPortMapping
304 |         F(xhr.responseText);
305 |       } else if (xhr.readyState === 4 && xhr.status === 500) {
306 |         // Error response to AddPortMapping
307 |         var responseText = xhr.responseText;
308 |         var startIndex = responseText.indexOf('') + 18;
309 |         var endIndex = responseText.indexOf('', startIndex);
310 |         var errorDescription = responseText.substring(startIndex, endIndex);
311 |         R(new Error('AddPortMapping Error: ' + errorDescription));
312 |       }
313 |     };
314 |     xhr.send(apm);
315 |   });
316 | 
317 |   // Give _sendAddPortMapping 1 second to run before timing out
318 |   return Promise.race([
319 |     utils.countdownReject(1000, 'AddPortMapping time out'),
320 |     _sendAddPortMapping
321 |   ]);
322 | };
323 | 
324 | /**
325 | * Send a DeletePortMapping request to the router's control URL
326 | * @private
327 | * @method sendDeletePortMapping
328 | * @param {string} controlUrl The control URL of the router
329 | * @param {number} extPort The external port of the mapping to delete
330 | * @return {string} The response string to the AddPortMapping request
331 | */
332 | var sendDeletePortMapping = function (controlUrl, extPort) {
333 |   // Promise to send an AddPortMapping request to the control URL of the router
334 |   var _sendDeletePortMapping = new Promise(function (F, R) {
335 |     // The DeletePortMapping SOAP request string
336 |     var apm = '' +
337 |               '' +
338 |                '' +
339 |                   '' +
340 |                      '' +
341 |                      '' + extPort + '' +
342 |                      'UDP' +
343 |                   '' +
344 |                 '' +
345 |               '';
346 | 
347 |     // Create an XMLHttpRequest that encapsulates the SOAP string
348 |     var xhr = new XMLHttpRequest();
349 |     xhr.open('POST', controlUrl, true);
350 |     xhr.setRequestHeader('Content-Type', 'text/xml');
351 |     xhr.setRequestHeader('SOAPAction', '"urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"');
352 | 
353 |     // Send the DeletePortMapping request
354 |     xhr.onreadystatechange = function () {
355 |       if (xhr.readyState === 4 && xhr.status === 200) {
356 |         // Success response to DeletePortMapping
357 |         F(xhr.responseText);
358 |       } else if (xhr.readyState === 4 && xhr.status === 500) {
359 |         // Error response to DeletePortMapping
360 |         // It seems that this almost never errors, even with invalid port numbers
361 |         var responseText = xhr.responseText;
362 |         var startIndex = responseText.indexOf('') + 18;
363 |         var endIndex = responseText.indexOf('', startIndex);
364 |         var errorDescription = responseText.substring(startIndex, endIndex);
365 |         R(new Error('DeletePortMapping Error: ' + errorDescription));
366 |       }
367 |     };
368 |     xhr.send(apm);
369 |   });
370 | 
371 |   // Give _sendDeletePortMapping 1 second to run before timing out
372 |   return Promise.race([
373 |     utils.countdownReject(1000, 'DeletePortMapping time out'),
374 |     _sendDeletePortMapping
375 |   ]);
376 | };
377 | 
378 | module.exports = {
379 |   probeSupport: probeSupport,
380 |   addMapping: addMapping,
381 |   deleteMapping: deleteMapping,
382 |   getUpnpControlUrl: getUpnpControlUrl
383 | };
384 | 


--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
  1 | var ipaddr = require('ipaddr.js');
  2 | 
  3 | /**
  4 | * List of popular router default IPs
  5 | * Used as destination addresses for NAT-PMP and PCP requests
  6 | * http://www.techspot.com/guides/287-default-router-ip-addresses/
  7 | */
  8 | var ROUTER_IPS = ['192.168.1.1', '192.168.2.1', '192.168.11.1',
  9 |   '192.168.0.1', '192.168.0.30', '192.168.0.50', '192.168.20.1',
 10 |   '192.168.30.1', '192.168.62.1', '192.168.100.1', '192.168.102.1',
 11 |   '192.168.1.254', '192.168.10.1', '192.168.123.254', '192.168.4.1',
 12 |   '10.0.1.1', '10.1.1.1', '10.0.0.13', '10.0.0.2', '10.0.0.138'];
 13 | 
 14 | /**
 15 | * Port numbers used to probe NAT-PMP, PCP, and UPnP, which don't overlap to
 16 | * avoid port conflicts, which can have strange and inconsistent behaviors
 17 | * For the same reason, don't reuse for normal mappings after a probe (or ever)
 18 | */
 19 | var NAT_PMP_PROBE_PORT = 55555;
 20 | var PCP_PROBE_PORT = 55556;
 21 | var UPNP_PROBE_PORT = 55557;
 22 | 
 23 | /**
 24 | * An object representing a port mapping returned by mapping methods
 25 | * @typedef {Object} Mapping
 26 | * @property {string} internalIp
 27 | * @property {number} internalPort
 28 | * @property {string} externalIp Only provided by PCP, undefined for other protocols
 29 | * @property {number} externalPort The actual external port of the mapping, -1 on failure
 30 | * @property {number} lifetime The actual (response) lifetime of the mapping
 31 | * @property {string} protocol The protocol used to make the mapping ('natPmp', 'pcp', 'upnp')
 32 | * @property {number} timeoutId The timeout ID if the mapping is refreshed
 33 | * @property {array} nonce Only for PCP; the nonce field for deletion
 34 | * @property {function} deleter Deletes the mapping from activeMappings and router
 35 | * @property {string} errInfo Error message if failure; currently used only for UPnP 
 36 | */
 37 | var Mapping = function () {
 38 |    this.internalIp = undefined;
 39 |    this.internalPort = undefined;
 40 |    this.externalIp = undefined;
 41 |    this.externalPort = -1;
 42 |    this.lifetime = undefined;
 43 |    this.protocol = undefined;
 44 |    this.timeoutId = undefined;
 45 |    this.nonce = undefined;
 46 |    this.deleter = undefined;
 47 |    this.errInfo = undefined;
 48 | };
 49 | 
 50 | /**
 51 | * Return the private IP addresses of the computer
 52 | * @public
 53 | * @method getPrivateIps
 54 | * @return {Promise} A promise that fulfills with a list of IP address, 
 55 | *                           or rejects on timeout
 56 | */
 57 | var getPrivateIps = function () {
 58 |   var privateIps = [];
 59 |   var pc = freedom['core.rtcpeerconnection']({iceServers: []});
 60 | 
 61 |   // Find all the ICE candidates that are "host" candidates
 62 |   pc.on('onicecandidate', function (candidate) {
 63 |     if (candidate.candidate) {
 64 |       var cand = candidate.candidate.candidate.split(' ');
 65 |       if (cand[7] === 'host') {
 66 |         var privateIp = cand[4];
 67 |         if (ipaddr.IPv4.isValid(privateIp)) {
 68 |           if (privateIps.indexOf(privateIp) === -1) {
 69 |             privateIps.push(privateIp);
 70 |           }
 71 |         }
 72 |       }
 73 |     }
 74 |   });
 75 | 
 76 |   // Set up the PeerConnection to start generating ICE candidates
 77 |   pc.createDataChannel('dummy data channel').
 78 |       then(pc.createOffer).
 79 |       then(pc.setLocalDescription);
 80 | 
 81 |   // Gather candidates for 2 seconds before returning privateIps or timing out
 82 |   return new Promise(function (F, R) {
 83 |     setTimeout(function () {
 84 |       var cleanup = function() {
 85 |         freedom['core.rtcpeerconnection'].close(pc);
 86 |       };
 87 |       pc.close().then(cleanup, cleanup);
 88 |       if (privateIps.length > 0) { F(privateIps); }
 89 |       else { R(new Error("getPrivateIps() failed")); }
 90 |     }, 2000);
 91 |   });
 92 | };
 93 | 
 94 | /**
 95 | * Filters routerIps for only those that match any of the user's IPs in privateIps
 96 | * i.e. The longest prefix matches of the router IPs with each user IP* @public
 97 | * @method filterRouterIps 
 98 | * @param  {Array} privateIps Private IPs to match router IPs to 
 99 | * @return {Array} Router IPs that matched (one per private IP)
100 | */
101 | var filterRouterIps = function (privateIps) {
102 |   routerIps = [];
103 |   privateIps.forEach(function (privateIp) {
104 |     routerIps.push(longestPrefixMatch(ROUTER_IPS, privateIp));
105 |   });
106 |   return routerIps;
107 | };
108 | 
109 | /**
110 |  * Creates an ArrayBuffer with a compact matrix notation, i.e.
111 |  * [[bits, byteOffset, value], 
112 |  *  [8, 0, 1], //=> DataView.setInt8(0, 1)
113 |  *  ... ]
114 |  * @public
115 |  * @method createArrayBuffer 
116 |  * @param  {number} bytes Size of the ArrayBuffer in bytes
117 |  * @param  {Array>} matrix Matrix of values for the ArrayBuffer
118 |  * @return {ArrayBuffer} An ArrayBuffer constructed from matrix
119 |  */
120 | var createArrayBuffer = function (bytes, matrix) {
121 |   var buffer = new ArrayBuffer(bytes);
122 |   var view = new DataView(buffer);
123 |   for (var i = 0; i < matrix.length; i++) {
124 |     var row = matrix[i];
125 |     if (row[0] === 8) { view.setInt8(row[1], row[2]); } 
126 |     else if (row[0] === 16) { view.setInt16(row[1], row[2], false); } 
127 |     else if (row[0] === 32) { view.setInt32(row[1], row[2], false); }
128 |     else { console.error("Invalid parameters to createArrayBuffer"); }
129 |   }
130 |   return buffer;
131 | };
132 | 
133 | /**
134 | * Return a promise that rejects in a given time with an Error message,
135 | * and can call a callback function before rejecting
136 | * @public
137 | * @method countdownReject
138 | * @param {number} time Time in seconds
139 | * @param {number} msg Message to encapsulate in the rejected Error
140 | * @param {function} callback Function to call before rejecting
141 | * @return {Promise} A promise that will reject in the given time
142 | */
143 | var countdownReject = function (time, msg, callback) {
144 |   return new Promise(function (F, R) {
145 |     setTimeout(function () {
146 |       if (callback !== undefined) { callback(); }
147 |       R(new Error(msg));
148 |     }, time);
149 |   });
150 | };
151 | 
152 | /**
153 | * Close the OS-level sockets and discard its Freedom object
154 | * @public
155 | * @method closeSocket
156 | * @param {freedom_UdpSocket.Socket} socket The socket object to close
157 | */
158 | var closeSocket = function (socket) {
159 |   socket.destroy().then(function () {
160 |     freedom['core.udpsocket'].close(socket);
161 |   });
162 | };
163 | 
164 | /**
165 | * Takes a list of IP addresses and an IP address, and returns the longest prefix
166 | * match in the IP list with the IP
167 | * @public
168 | * @method longestPrefixMatch
169 | * @param {Array} ipList List of IP addresses to find the longest prefix match in
170 | * @param {string} matchIp The router's IP address as a string
171 | * @return {string} The IP from the given list with the longest prefix match
172 | */
173 | var longestPrefixMatch = function (ipList, matchIp) {
174 |   var prefixMatches = [];
175 |   matchIp = ipaddr.IPv4.parse(matchIp);
176 |   for (var i = 0; i < ipList.length; i++) {
177 |     var ip = ipaddr.IPv4.parse(ipList[i]);
178 |     // Use ipaddr.js to find the longest prefix length (mask length)
179 |     for (var mask = 1; mask < 32; mask++) {
180 |       if (!ip.match(matchIp, mask)) {
181 |         prefixMatches.push(mask - 1);
182 |         break;
183 |       }
184 |     }
185 |   }
186 | 
187 |   // Find the argmax for prefixMatches, i.e. the index of the correct private IP
188 |   var maxIndex = prefixMatches.indexOf(Math.max.apply(null, prefixMatches));
189 |   var correctIp = ipList[maxIndex];
190 |   return correctIp;
191 | };
192 | 
193 | /**
194 | * Return a random integer in a specified range
195 | * @public
196 | * @method randInt
197 | * @param {number} min Lower bound for the random integer
198 | * @param {number} max Upper bound for the random integer
199 | * @return {number} A random number between min and max
200 | */
201 | var randInt = function (min, max) {
202 |   return Math.floor(Math.random()* (max - min + 1)) + min;
203 | };
204 | 
205 | /**
206 | * Convert an ArrayBuffer to a UTF-8 string
207 | * @public
208 | * @method arrayBufferToString
209 | * @param {ArrayBuffer} buffer ArrayBuffer to convert
210 | * @return {string} A string converted from the ArrayBuffer
211 | */
212 | var arrayBufferToString = function (buffer) {
213 |     var bytes = new Uint8Array(buffer);
214 |     var a = [];
215 |     for (var i = 0; i < bytes.length; ++i) {
216 |         a.push(String.fromCharCode(bytes[i]));
217 |     }
218 |     return a.join('');
219 | };
220 | 
221 | /**
222 | * Convert a UTF-8 string to an ArrayBuffer
223 | * @public
224 | * @method stringToArrayBuffer
225 | * @param {string} s String to convert
226 | * @return {ArrayBuffer} An ArrayBuffer containing the string data
227 | */
228 | var stringToArrayBuffer = function (s) {
229 |     var buffer = new ArrayBuffer(s.length);
230 |     var bytes = new Uint8Array(buffer);
231 |     for (var i = 0; i < s.length; ++i) {
232 |         bytes[i] = s.charCodeAt(i);
233 |     }
234 |     return buffer;
235 | };
236 | 
237 | /**
238 |  * Returns the difference between two arrays
239 |  * @param  {Array} listA 
240 |  * @param  {Array} listB 
241 |  * @return {Array} The difference array
242 |  */
243 | var arrDiff = function (listA, listB) {
244 |   var diff = [];
245 |   listA.forEach(function (a) {
246 |     if (listB.indexOf(a) === -1) { diff.push(a); }
247 |   });
248 |   return diff;
249 | };
250 | 
251 | /**
252 |  * Adds two arrays, but doesn't include repeated elements
253 |  * @param  {Array} listA 
254 |  * @param  {Array} listB 
255 |  * @return {Array} The sum of the two arrays with no duplicates
256 |  */
257 | var arrAdd = function (listA, listB) {
258 |   var sum = [];
259 |   listA.forEach(function (a) {
260 |     if (sum.indexOf(a) === -1) { sum.push(a); }
261 |   });
262 |   listB.forEach(function (b) {
263 |     if (sum.indexOf(b) === -1) { sum.push(b); }
264 |   });
265 |   return sum;
266 | };
267 | 
268 | module.exports = {
269 |   ROUTER_IPS: ROUTER_IPS,
270 |   NAT_PMP_PROBE_PORT: NAT_PMP_PROBE_PORT,
271 |   PCP_PROBE_PORT: PCP_PROBE_PORT,
272 |   UPNP_PROBE_PORT: UPNP_PROBE_PORT,
273 |   Mapping: Mapping,
274 |   getPrivateIps: getPrivateIps,
275 |   createArrayBuffer: createArrayBuffer,
276 |   countdownReject: countdownReject,
277 |   closeSocket: closeSocket,
278 |   filterRouterIps: filterRouterIps,
279 |   longestPrefixMatch: longestPrefixMatch,
280 |   randInt: randInt,
281 |   arrayBufferToString: arrayBufferToString,
282 |   stringToArrayBuffer: stringToArrayBuffer,
283 |   arrAdd: arrAdd,
284 |   arrDiff: arrDiff
285 | };
286 | 


--------------------------------------------------------------------------------