proxypair.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /* global snowflake, log, dbg, Util, PeerConnection, Snowflake, Parse, WS */
  2. /*
  3. Represents a single:
  4. client <-- webrtc --> snowflake <-- websocket --> relay
  5. Every ProxyPair has a Snowflake ID, which is necessary when responding to the
  6. Broker with an WebRTC answer.
  7. */
  8. class ProxyPair {
  9. /*
  10. Constructs a ProxyPair where:
  11. - @relayAddr is the destination relay
  12. - @rateLimit specifies a rate limit on traffic
  13. */
  14. constructor(relayAddr, rateLimit, pcConfig) {
  15. // Given a WebRTC DataChannel, prepare callbacks.
  16. this.prepareDataChannel = this.prepareDataChannel.bind(this);
  17. // Assumes WebRTC datachannel is connected.
  18. this.connectRelay = this.connectRelay.bind(this);
  19. // WebRTC --> websocket
  20. this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
  21. // websocket --> WebRTC
  22. this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
  23. this.onError = this.onError.bind(this);
  24. // Send as much data in both directions as the rate limit currently allows.
  25. this.flush = this.flush.bind(this);
  26. this.relayAddr = relayAddr;
  27. this.rateLimit = rateLimit;
  28. this.pcConfig = pcConfig;
  29. this.id = Util.genSnowflakeID();
  30. this.c2rSchedule = [];
  31. this.r2cSchedule = [];
  32. }
  33. // Prepare a WebRTC PeerConnection and await for an SDP offer.
  34. begin() {
  35. this.pc = new PeerConnection(this.pcConfig, {
  36. optional: [
  37. {
  38. DtlsSrtpKeyAgreement: true
  39. },
  40. {
  41. RtpDataChannels: false
  42. }
  43. ]
  44. });
  45. this.pc.onicecandidate = (evt) => {
  46. // Browser sends a null candidate once the ICE gathering completes.
  47. if (null === evt.candidate) {
  48. // TODO: Use a promise.all to tell Snowflake about all offers at once,
  49. // once multiple proxypairs are supported.
  50. dbg('Finished gathering ICE candidates.');
  51. return snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
  52. }
  53. };
  54. // OnDataChannel triggered remotely from the client when connection succeeds.
  55. return this.pc.ondatachannel = (dc) => {
  56. var channel;
  57. channel = dc.channel;
  58. dbg('Data Channel established...');
  59. this.prepareDataChannel(channel);
  60. return this.client = channel;
  61. };
  62. }
  63. receiveWebRTCOffer(offer) {
  64. if ('offer' !== offer.type) {
  65. log('Invalid SDP received -- was not an offer.');
  66. return false;
  67. }
  68. try {
  69. this.pc.setRemoteDescription(offer);
  70. } catch (error) {
  71. log('Invalid SDP message.');
  72. return false;
  73. }
  74. dbg('SDP ' + offer.type + ' successfully received.');
  75. return true;
  76. }
  77. prepareDataChannel(channel) {
  78. channel.onopen = () => {
  79. log('WebRTC DataChannel opened!');
  80. this.running = true;
  81. snowflake.state = Snowflake.MODE.WEBRTC_READY;
  82. snowflake.ui.setActive(true);
  83. // This is the point when the WebRTC datachannel is done, so the next step
  84. // is to establish websocket to the server.
  85. return this.connectRelay();
  86. };
  87. channel.onclose = () => {
  88. log('WebRTC DataChannel closed.');
  89. snowflake.ui.setStatus('disconnected by webrtc.');
  90. snowflake.ui.setActive(false);
  91. snowflake.state = Snowflake.MODE.INIT;
  92. this.flush();
  93. return this.close();
  94. };
  95. channel.onerror = function() {
  96. return log('Data channel error!');
  97. };
  98. channel.binaryType = "arraybuffer";
  99. return channel.onmessage = this.onClientToRelayMessage;
  100. }
  101. connectRelay() {
  102. var params, peer_ip, ref;
  103. dbg('Connecting to relay...');
  104. // Get a remote IP address from the PeerConnection, if possible. Add it to
  105. // the WebSocket URL's query string if available.
  106. // MDN marks remoteDescription as "experimental". However the other two
  107. // options, currentRemoteDescription and pendingRemoteDescription, which
  108. // are not marked experimental, were undefined when I tried them in Firefox
  109. // 52.2.0.
  110. // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
  111. peer_ip = Parse.ipFromSDP((ref = this.pc.remoteDescription) != null ? ref.sdp : void 0);
  112. params = [];
  113. if (peer_ip != null) {
  114. params.push(["client_ip", peer_ip]);
  115. }
  116. var relay = this.relay = WS.makeWebsocket(this.relayAddr, params);
  117. this.relay.label = 'websocket-relay';
  118. this.relay.onopen = () => {
  119. if (this.timer) {
  120. clearTimeout(this.timer);
  121. this.timer = 0;
  122. }
  123. log(relay.label + ' connected!');
  124. return snowflake.ui.setStatus('connected');
  125. };
  126. this.relay.onclose = () => {
  127. log(relay.label + ' closed.');
  128. snowflake.ui.setStatus('disconnected.');
  129. snowflake.ui.setActive(false);
  130. snowflake.state = Snowflake.MODE.INIT;
  131. this.flush();
  132. return this.close();
  133. };
  134. this.relay.onerror = this.onError;
  135. this.relay.onmessage = this.onRelayToClientMessage;
  136. // TODO: Better websocket timeout handling.
  137. return this.timer = setTimeout((() => {
  138. if (0 === this.timer) {
  139. return;
  140. }
  141. log(relay.label + ' timed out connecting.');
  142. return relay.onclose();
  143. }), 5000);
  144. }
  145. onClientToRelayMessage(msg) {
  146. dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes');
  147. this.c2rSchedule.push(msg.data);
  148. return this.flush();
  149. }
  150. onRelayToClientMessage(event) {
  151. dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes');
  152. this.r2cSchedule.push(event.data);
  153. return this.flush();
  154. }
  155. onError(event) {
  156. var ws;
  157. ws = event.target;
  158. log(ws.label + ' error.');
  159. return this.close();
  160. }
  161. // Close both WebRTC and websocket.
  162. close() {
  163. if (this.timer) {
  164. clearTimeout(this.timer);
  165. this.timer = 0;
  166. }
  167. if (this.webrtcIsReady()) {
  168. this.client.close();
  169. }
  170. this.client = null;
  171. if (this.relayIsReady()) {
  172. this.relay.close();
  173. }
  174. this.relay = null;
  175. this.onCleanup();
  176. this.active = false;
  177. this.running = false;
  178. }
  179. flush() {
  180. var busy, checkChunks;
  181. if (this.flush_timeout_id) {
  182. clearTimeout(this.flush_timeout_id);
  183. }
  184. this.flush_timeout_id = null;
  185. busy = true;
  186. checkChunks = () => {
  187. var chunk;
  188. busy = false;
  189. // WebRTC --> websocket
  190. if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) {
  191. chunk = this.c2rSchedule.shift();
  192. this.rateLimit.update(chunk.byteLength);
  193. this.relay.send(chunk);
  194. busy = true;
  195. }
  196. // websocket --> WebRTC
  197. if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
  198. chunk = this.r2cSchedule.shift();
  199. this.rateLimit.update(chunk.byteLength);
  200. this.client.send(chunk);
  201. return busy = true;
  202. }
  203. };
  204. while (busy && !this.rateLimit.isLimited()) {
  205. checkChunks();
  206. }
  207. if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) {
  208. return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
  209. }
  210. }
  211. webrtcIsReady() {
  212. return null !== this.client && 'open' === this.client.readyState;
  213. }
  214. relayIsReady() {
  215. return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState);
  216. }
  217. isClosed(ws) {
  218. return void 0 === ws || WebSocket.CLOSED === ws.readyState;
  219. }
  220. }
  221. ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024;
  222. ProxyPair.prototype.pc = null;
  223. ProxyPair.prototype.client = null; // WebRTC Data channel
  224. ProxyPair.prototype.relay = null; // websocket
  225. ProxyPair.prototype.timer = 0;
  226. ProxyPair.prototype.running = false; // Whether a datachannel is opened
  227. ProxyPair.prototype.active = false; // Whether serving a client.
  228. ProxyPair.prototype.flush_timeout_id = null;
  229. ProxyPair.prototype.onCleanup = null;
  230. ProxyPair.prototype.id = null;