|
2 | 2 | /// <reference path='../../../third_party/freedom-typings/udp-socket.d.ts' /> |
3 | 3 | /// <reference path='../../../third_party/sha1/sha1.d.ts' /> |
4 | 4 | /// <reference path='../../../third_party/typings/lodash/lodash.d.ts' /> |
| 5 | +/// <reference path='../../../third_party/typings/generic/url.d.ts' /> |
| 6 | +/// <reference path='../../../third_party/ipaddrjs/ipaddrjs.d.ts' /> |
| 7 | + |
5 | 8 |
|
6 | 9 | import arraybuffers = require('../../../third_party/uproxy-lib/arraybuffers/arraybuffers'); |
7 | 10 | import logging = require('../../../third_party/uproxy-lib/logging/logging'); |
8 | 11 | import _ = require('lodash'); |
9 | 12 | import globals = require('./globals'); |
10 | 13 | import sha1 = require('crypto/sha1'); |
| 14 | +import ipaddr = require('ipaddr.js'); |
11 | 15 |
|
12 | 16 | // Both Ping and NAT type detection need help from a server. The following |
13 | 17 | // ip/port is the instance we run on EC2. |
@@ -854,3 +858,332 @@ export function doNatProvoking() :Promise<string> { |
854 | 858 | }); |
855 | 859 | }); |
856 | 860 | } |
| 861 | + |
| 862 | +// Closes the OS-level sockets and discards its Freedom object |
| 863 | +function closeSocket(socket:freedom_UdpSocket.Socket) { |
| 864 | + socket.destroy().then(() => { |
| 865 | + freedom['core.udpsocket'].close(socket); |
| 866 | + }); |
| 867 | +} |
| 868 | + |
| 869 | +// Test if NAT-PMP is supported by the router, returns a boolean |
| 870 | +export function probePmpSupport(routerIp:string, privateIp:string) :Promise<boolean> { |
| 871 | + var socket :freedom_UdpSocket.Socket; |
| 872 | + var _probePmpSupport = new Promise((F, R) => { |
| 873 | + socket = freedom['core.udpsocket'](); |
| 874 | + |
| 875 | + // Fulfill when we get any reply (failure is on timeout in wrapper function) |
| 876 | + socket.on('onData', (pmpResponse:freedom_UdpSocket.RecvFromInfo) => { |
| 877 | + closeSocket(socket); |
| 878 | + F(true); |
| 879 | + }); |
| 880 | + |
| 881 | + // Bind a UDP port and send a NAT-PMP request |
| 882 | + socket.bind(privateIp, 0). |
| 883 | + then((result:number) => { |
| 884 | + if (result != 0) { |
| 885 | + R(new Error('Failed to bind to a port: Err= ' + result)); |
| 886 | + } |
| 887 | + |
| 888 | + // Construct the NAT-PMP map request as an ArrayBuffer |
| 889 | + // Map internal port 55555 to external port 55555 w/ 120 sec lifetime |
| 890 | + var pmpBuffer = new ArrayBuffer(12); |
| 891 | + var pmpView = new DataView(pmpBuffer); |
| 892 | + // Version and OP fields (1 byte each) |
| 893 | + pmpView.setInt8(0, 0); |
| 894 | + pmpView.setInt8(1, 1); |
| 895 | + // Reserved, internal port, external port fields (2 bytes each) |
| 896 | + pmpView.setInt16(2, 0, false); |
| 897 | + pmpView.setInt16(4, 55555, false); |
| 898 | + pmpView.setInt16(6, 55555, false); |
| 899 | + // Mapping lifetime field (4 bytes) |
| 900 | + pmpView.setInt32(8, 120, false); |
| 901 | + |
| 902 | + socket.sendTo(pmpBuffer, routerIp, 5351); |
| 903 | + }); |
| 904 | + }); |
| 905 | + |
| 906 | + // Give _probePmpSupport 2 seconds before timing out |
| 907 | + return Promise.race([ |
| 908 | + countdownFulfill(2000, false, () => { closeSocket(socket); }), |
| 909 | + _probePmpSupport |
| 910 | + ]); |
| 911 | +} |
| 912 | + |
| 913 | +// Test if PCP is supported by the router, returns a boolean |
| 914 | +export function probePcpSupport(routerIp:string, privateIp:string) :Promise<boolean> { |
| 915 | + var socket :freedom_UdpSocket.Socket; |
| 916 | + var _probePcpSupport = new Promise((F, R) => { |
| 917 | + socket = freedom['core.udpsocket'](); |
| 918 | + |
| 919 | + // Fulfill when we get any reply (failure is on timeout in wrapper function) |
| 920 | + socket.on('onData', (pcpResponse:freedom_UdpSocket.RecvFromInfo) => { |
| 921 | + closeSocket(socket); |
| 922 | + F(true); |
| 923 | + }); |
| 924 | + |
| 925 | + // Bind a UDP port and send a PCP request |
| 926 | + socket.bind(privateIp, 0). |
| 927 | + then((result:number) => { |
| 928 | + if (result != 0) { |
| 929 | + R(new Error('Failed to bind to a port: Err= ' + result)); |
| 930 | + } |
| 931 | + |
| 932 | + // Create the PCP MAP request as an ArrayBuffer |
| 933 | + // Map internal port 55556 to external port 55556 w/ 120 sec lifetime |
| 934 | + var pcpBuffer = new ArrayBuffer(60); |
| 935 | + var pcpView = new DataView(pcpBuffer); |
| 936 | + // Version field (1 byte) |
| 937 | + pcpView.setInt8(0, 0b00000010); |
| 938 | + // R and Opcode fields (1 bit + 7 bits) |
| 939 | + pcpView.setInt8(1, 0b00000001); |
| 940 | + // Reserved field (2 bytes) |
| 941 | + pcpView.setInt16(2, 0, false); |
| 942 | + // Requested lifetime (4 bytes) |
| 943 | + pcpView.setInt32(4, 120, false); |
| 944 | + // Client IP address (128 bytes; we use the IPv4 -> IPv6 mapping) |
| 945 | + pcpView.setInt32(8, 0, false); |
| 946 | + pcpView.setInt32(12, 0, false); |
| 947 | + pcpView.setInt16(16, 0, false); |
| 948 | + pcpView.setInt16(18, 0xffff, false); |
| 949 | + // Start of IPv4 octets of the client's private IP |
| 950 | + var ipOctets = ipaddr.IPv4.parse(privateIp).octets; |
| 951 | + pcpView.setInt8(20, ipOctets[0]); |
| 952 | + pcpView.setInt8(21, ipOctets[1]); |
| 953 | + pcpView.setInt8(22, ipOctets[2]); |
| 954 | + pcpView.setInt8(23, ipOctets[3]); |
| 955 | + // Mapping Nonce (12 bytes) |
| 956 | + pcpView.setInt32(24, randInt(0, 0xffffffff), false); |
| 957 | + pcpView.setInt32(28, randInt(0, 0xffffffff), false); |
| 958 | + pcpView.setInt32(32, randInt(0, 0xffffffff), false); |
| 959 | + // Protocol (1 byte) |
| 960 | + pcpView.setInt8(36, 17); |
| 961 | + // Reserved (3 bytes) |
| 962 | + pcpView.setInt16(37, 0, false); |
| 963 | + pcpView.setInt8(39, 0); |
| 964 | + // Internal and external ports |
| 965 | + pcpView.setInt16(40, 55556, false); |
| 966 | + pcpView.setInt16(42, 55556, false); |
| 967 | + // External IP address (128 bytes; we use the all-zero IPv4 -> IPv6 mapping) |
| 968 | + pcpView.setFloat64(44, 0, false); |
| 969 | + pcpView.setInt16(52, 0, false); |
| 970 | + pcpView.setInt16(54, 0xffff, false); |
| 971 | + pcpView.setInt32(56, 0, false); |
| 972 | + |
| 973 | + socket.sendTo(pcpBuffer, routerIp, 5351); |
| 974 | + }); |
| 975 | + }); |
| 976 | + |
| 977 | + // Give _probePcpSupport 2 seconds before timing out |
| 978 | + return Promise.race([ |
| 979 | + countdownFulfill(2000, false, () => { closeSocket(socket); }), |
| 980 | + _probePcpSupport |
| 981 | + ]); |
| 982 | +} |
| 983 | + |
| 984 | +// Send if UPnP AddPortMapping is supported by the router |
| 985 | +// Returns a 'true' boolean if UPnP is supported |
| 986 | +// Errors with a message if something breaks or times out (not supported) |
| 987 | +export function probeUpnpSupport(privateIp:string) :Promise<boolean> { |
| 988 | + return new Promise((F, R) => { |
| 989 | + sendSsdpRequest(privateIp). |
| 990 | + then(fetchControlUrl). |
| 991 | + then((controlUrl:string) => sendAddPortMapping(controlUrl, privateIp)). |
| 992 | + then((result:boolean) => F(result)). |
| 993 | + catch((err:Error) => R(err)); |
| 994 | + }); |
| 995 | +} |
| 996 | + |
| 997 | +// Send a UPnP SSDP request to discover UPnP devices on the network |
| 998 | +function sendSsdpRequest(privateIp:string) :Promise<ArrayBuffer> { |
| 999 | + var socket :freedom_UdpSocket.Socket; |
| 1000 | + var _sendSsdpRequest = new Promise((F, R) => { |
| 1001 | + socket = freedom['core.udpsocket'](); |
| 1002 | + |
| 1003 | + // Fulfill when we get any reply (failure is on timeout or invalid parsing) |
| 1004 | + socket.on('onData', (ssdpResponse:freedom_UdpSocket.RecvFromInfo) => { |
| 1005 | + closeSocket(socket); |
| 1006 | + F(ssdpResponse.data); |
| 1007 | + }); |
| 1008 | + |
| 1009 | + // Bind a socket and send the SSDP request |
| 1010 | + socket.bind(privateIp, 0). |
| 1011 | + then((result:number) => { |
| 1012 | + if (result != 0) { |
| 1013 | + R(new Error('Failed to bind to a port: Err= ' + result)); |
| 1014 | + } |
| 1015 | + |
| 1016 | + // Construct and send a UPnP SSDP message |
| 1017 | + var ssdpStr = 'M-SEARCH * HTTP/1.1\r\n' + |
| 1018 | + 'HOST: 239.255.255.250:1900\r\n' + |
| 1019 | + 'MAN: ssdp:discover\r\n' + |
| 1020 | + 'MX: 10\r\n' + |
| 1021 | + 'ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1'; |
| 1022 | + var ssdpBuffer = arraybuffers.stringToArrayBuffer(ssdpStr); |
| 1023 | + socket.sendTo(ssdpBuffer, '239.255.255.250', 1900); |
| 1024 | + }); |
| 1025 | + }); |
| 1026 | + |
| 1027 | + // Give _sendSsdpRequest 1 second before timing out |
| 1028 | + return Promise.race([ |
| 1029 | + countdownReject(1000, '(SSDP timed out)', () => { closeSocket(socket); }), |
| 1030 | + _sendSsdpRequest |
| 1031 | + ]); |
| 1032 | +} |
| 1033 | + |
| 1034 | +// Get the UPnP control URL from SSDP response |
| 1035 | +function fetchControlUrl(ssdpResponse:ArrayBuffer) :Promise<string> { |
| 1036 | + var _fetchControlUrl = new Promise((F, R) => { |
| 1037 | + // Get UPnP device profile URL from the LOCATION header |
| 1038 | + var ssdpStr = arraybuffers.arrayBufferToString(ssdpResponse); |
| 1039 | + var startIndex = ssdpStr.indexOf('LOCATION: ') + 10; |
| 1040 | + var endIndex = ssdpStr.indexOf('\n', startIndex); |
| 1041 | + var locationUrl = ssdpStr.substring(startIndex, endIndex); |
| 1042 | + |
| 1043 | + // Reject if there is no LOCATION header |
| 1044 | + if (startIndex === -1) { |
| 1045 | + R(new Error('(No LOCATION header for UPnP device)')); |
| 1046 | + } |
| 1047 | + |
| 1048 | + // Get the XML device description at location URL |
| 1049 | + var xhr = new XMLHttpRequest(); |
| 1050 | + xhr.open('GET', locationUrl, true); |
| 1051 | + xhr.onreadystatechange = () => { |
| 1052 | + if (xhr.readyState === 4) { |
| 1053 | + // Get control URL from XML file |
| 1054 | + // Ideally we would parse and traverse the XML tree for this, |
| 1055 | + // but DOMParser is not available here |
| 1056 | + var xmlDoc = xhr.responseText; |
| 1057 | + var preIndex = xmlDoc.indexOf('WANIPConnection'); |
| 1058 | + var startIndex = xmlDoc.indexOf('<controlUrl>', preIndex) + 13; |
| 1059 | + var endIndex = xmlDoc.indexOf('</controlUrl>', startIndex); |
| 1060 | + |
| 1061 | + // Reject if there is no controlUrl |
| 1062 | + if (preIndex === -1 || startIndex === -1) { |
| 1063 | + R(new Error('(Could not parse control URL)')); |
| 1064 | + } |
| 1065 | + |
| 1066 | + // Combine the controlUrl path with the locationUrl |
| 1067 | + var controlUrlPath = xmlDoc.substring(startIndex, endIndex); |
| 1068 | + var locationUrlParser = new URL(locationUrl); |
| 1069 | + var controlUrl = 'http://' + locationUrlParser.host + |
| 1070 | + '/' + controlUrlPath; |
| 1071 | + |
| 1072 | + F(controlUrl); |
| 1073 | + } |
| 1074 | + } |
| 1075 | + xhr.send(); |
| 1076 | + }); |
| 1077 | + |
| 1078 | + // Give _fetchControlUrl 1 second before timing out |
| 1079 | + return Promise.race([ |
| 1080 | + countdownReject(1000, '(Timed out when retrieving description XML)'), |
| 1081 | + _fetchControlUrl |
| 1082 | + ]); |
| 1083 | +} |
| 1084 | + |
| 1085 | +// Send a UPnP AddPortMapping request |
| 1086 | +function sendAddPortMapping(controlUrl:string, privateIp:string) :Promise<boolean> { |
| 1087 | + var _sendAddPortMapping = new Promise((F, R) => { |
| 1088 | + var internalPort = 55557; |
| 1089 | + var externalPort = 55557; |
| 1090 | + var leaseDuration = 120; // Note: Some routers may not support a non-zero duration |
| 1091 | + |
| 1092 | + // Create the AddPortMapping SOAP request string |
| 1093 | + var apm = '<?xml version="1.0"?>' + |
| 1094 | + '<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">' + |
| 1095 | + '<s:Body>' + |
| 1096 | + '<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">' + |
| 1097 | + '<NewExternalPort>' + externalPort + '</NewExternalPort>' + |
| 1098 | + '<NewProtocol>UDP</NewProtocol>' + |
| 1099 | + '<NewInternalPort>' + internalPort + '</NewInternalPort>' + |
| 1100 | + '<NewInternalClient>' + privateIp + '</NewInternalClient>' + |
| 1101 | + '<NewEnabled>1</NewEnabled>' + |
| 1102 | + '<NewPortMappingDescription>uProxy UPnP probe</NewPortMappingDescription>' + |
| 1103 | + '<NewLeaseDuration>' + leaseDuration + '</NewLeaseDuration>' + |
| 1104 | + '</u:AddPortMapping>' + |
| 1105 | + '</s:Body>' + |
| 1106 | + '</s:Envelope>'; |
| 1107 | + |
| 1108 | + // Create an XMLHttpRequest that encapsulates the SOAP string |
| 1109 | + var xhr = new XMLHttpRequest(); |
| 1110 | + xhr.open('POST', controlUrl, true); |
| 1111 | + xhr.setRequestHeader('Content-Type', 'text/xml'); |
| 1112 | + xhr.setRequestHeader('SOAPAction', '"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"'); |
| 1113 | + |
| 1114 | + // Send the AddPortMapping request |
| 1115 | + xhr.onreadystatechange = () => { |
| 1116 | + if (xhr.readyState === 4) { F(true); } |
| 1117 | + } |
| 1118 | + xhr.send(apm); |
| 1119 | + }); |
| 1120 | + |
| 1121 | + // Give _sendAddPortMapping 1 second to run before timing out |
| 1122 | + return Promise.race([ |
| 1123 | + countdownReject(1000, '(AddPortMapping timed out)'), |
| 1124 | + _sendAddPortMapping |
| 1125 | + ]); |
| 1126 | +} |
| 1127 | + |
| 1128 | +// Returns a promise for the internal IP address of the computer |
| 1129 | +export function getInternalIp() :Promise<string> { |
| 1130 | + var _getInternalIp = new Promise((F, R) => { |
| 1131 | + var pc = freedom['core.rtcpeerconnection']({ |
| 1132 | + iceServers: globals.settings.stunServers |
| 1133 | + }); |
| 1134 | + |
| 1135 | + // One of the ICE candidates is the internal host IP; return it |
| 1136 | + pc.on('onicecandidate', |
| 1137 | + (candidate?:freedom_RTCPeerConnection.OnIceCandidateEvent) => { |
| 1138 | + if (candidate.candidate) { |
| 1139 | + var cand = candidate.candidate.candidate.split(' '); |
| 1140 | + if (cand[7] === 'host') { |
| 1141 | + var internalIp = cand[4]; |
| 1142 | + if (ipaddr.IPv4.isValid(internalIp)) { |
| 1143 | + F(internalIp); |
| 1144 | + } |
| 1145 | + } |
| 1146 | + } |
| 1147 | + }); |
| 1148 | + |
| 1149 | + // Set up the PeerConnection to start generating ICE candidates |
| 1150 | + pc.createDataChannel('dummy data channel'). |
| 1151 | + then(pc.createOffer). |
| 1152 | + then(pc.setLocalDescription); |
| 1153 | + }); |
| 1154 | + |
| 1155 | + // Give _getInternalIp 2 seconds to run before timing out |
| 1156 | + return Promise.race([ |
| 1157 | + countdownReject(2000, 'getInternalIp() failed'), |
| 1158 | + _getInternalIp |
| 1159 | + ]); |
| 1160 | +} |
| 1161 | + |
| 1162 | +// Generate a random integer between min and max |
| 1163 | +function randInt(min:number, max:number) :number { |
| 1164 | + return Math.floor(Math.random() * (max - min + 1)) + min; |
| 1165 | +} |
| 1166 | + |
| 1167 | +// Return a promise that fulfills in a given time with a boolean |
| 1168 | +// Can run a callback function before fulfilling |
| 1169 | +function countdownFulfill(time:number, bool:boolean, |
| 1170 | + callback?:Function) :Promise<boolean> { |
| 1171 | + return new Promise<boolean>((F, R) => { |
| 1172 | + setTimeout(() => { |
| 1173 | + if (callback !== undefined) { callback(); } |
| 1174 | + F(bool); |
| 1175 | + }, time); |
| 1176 | + }); |
| 1177 | +} |
| 1178 | + |
| 1179 | +// Return a promise that rejects in a given time with an Error message |
| 1180 | +// Can call a callback function before rejecting |
| 1181 | +function countdownReject(time:number, msg:string, |
| 1182 | + callback?:Function) :Promise<any> { |
| 1183 | + return new Promise<any>((F, R) => { |
| 1184 | + setTimeout(() => { |
| 1185 | + if (callback !== undefined) { callback(); } |
| 1186 | + R(new Error(msg)); |
| 1187 | + }, time); |
| 1188 | + }); |
| 1189 | +} |
0 commit comments