123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- /* global snowflake, log, dbg, Util, PeerConnection, Snowflake, Parse, WS */
- /*
- Represents a single:
- client <-- webrtc --> snowflake <-- websocket --> relay
- Every ProxyPair has a Snowflake ID, which is necessary when responding to the
- Broker with an WebRTC answer.
- */
- class ProxyPair {
- /*
- Constructs a ProxyPair where:
- - @relayAddr is the destination relay
- - @rateLimit specifies a rate limit on traffic
- */
- constructor(relayAddr, rateLimit, pcConfig) {
- // Given a WebRTC DataChannel, prepare callbacks.
- this.prepareDataChannel = this.prepareDataChannel.bind(this);
- // Assumes WebRTC datachannel is connected.
- this.connectRelay = this.connectRelay.bind(this);
- // WebRTC --> websocket
- this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
- // websocket --> WebRTC
- this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
- this.onError = this.onError.bind(this);
- // Send as much data in both directions as the rate limit currently allows.
- this.flush = this.flush.bind(this);
- this.relayAddr = relayAddr;
- this.rateLimit = rateLimit;
- this.pcConfig = pcConfig;
- this.id = Util.genSnowflakeID();
- this.c2rSchedule = [];
- this.r2cSchedule = [];
- }
- // Prepare a WebRTC PeerConnection and await for an SDP offer.
- begin() {
- this.pc = new PeerConnection(this.pcConfig, {
- optional: [
- {
- DtlsSrtpKeyAgreement: true
- },
- {
- RtpDataChannels: false
- }
- ]
- });
- this.pc.onicecandidate = (evt) => {
- // Browser sends a null candidate once the ICE gathering completes.
- if (null === evt.candidate) {
- // TODO: Use a promise.all to tell Snowflake about all offers at once,
- // once multiple proxypairs are supported.
- dbg('Finished gathering ICE candidates.');
- return snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
- }
- };
- // OnDataChannel triggered remotely from the client when connection succeeds.
- return this.pc.ondatachannel = (dc) => {
- var channel;
- channel = dc.channel;
- dbg('Data Channel established...');
- this.prepareDataChannel(channel);
- return this.client = channel;
- };
- }
- receiveWebRTCOffer(offer) {
- if ('offer' !== offer.type) {
- log('Invalid SDP received -- was not an offer.');
- return false;
- }
- try {
- this.pc.setRemoteDescription(offer);
- } catch (error) {
- log('Invalid SDP message.');
- return false;
- }
- dbg('SDP ' + offer.type + ' successfully received.');
- return true;
- }
- prepareDataChannel(channel) {
- channel.onopen = () => {
- log('WebRTC DataChannel opened!');
- this.running = true;
- snowflake.state = Snowflake.MODE.WEBRTC_READY;
- snowflake.ui.setActive(true);
- // This is the point when the WebRTC datachannel is done, so the next step
- // is to establish websocket to the server.
- return this.connectRelay();
- };
- channel.onclose = () => {
- log('WebRTC DataChannel closed.');
- snowflake.ui.setStatus('disconnected by webrtc.');
- snowflake.ui.setActive(false);
- snowflake.state = Snowflake.MODE.INIT;
- this.flush();
- return this.close();
- };
- channel.onerror = function() {
- return log('Data channel error!');
- };
- channel.binaryType = "arraybuffer";
- return channel.onmessage = this.onClientToRelayMessage;
- }
- connectRelay() {
- var params, peer_ip, ref;
- dbg('Connecting to relay...');
- // Get a remote IP address from the PeerConnection, if possible. Add it to
- // the WebSocket URL's query string if available.
- // MDN marks remoteDescription as "experimental". However the other two
- // options, currentRemoteDescription and pendingRemoteDescription, which
- // are not marked experimental, were undefined when I tried them in Firefox
- // 52.2.0.
- // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
- peer_ip = Parse.ipFromSDP((ref = this.pc.remoteDescription) != null ? ref.sdp : void 0);
- params = [];
- if (peer_ip != null) {
- params.push(["client_ip", peer_ip]);
- }
- var relay = this.relay = WS.makeWebsocket(this.relayAddr, params);
- this.relay.label = 'websocket-relay';
- this.relay.onopen = () => {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = 0;
- }
- log(relay.label + ' connected!');
- return snowflake.ui.setStatus('connected');
- };
- this.relay.onclose = () => {
- log(relay.label + ' closed.');
- snowflake.ui.setStatus('disconnected.');
- snowflake.ui.setActive(false);
- snowflake.state = Snowflake.MODE.INIT;
- this.flush();
- return this.close();
- };
- this.relay.onerror = this.onError;
- this.relay.onmessage = this.onRelayToClientMessage;
- // TODO: Better websocket timeout handling.
- return this.timer = setTimeout((() => {
- if (0 === this.timer) {
- return;
- }
- log(relay.label + ' timed out connecting.');
- return relay.onclose();
- }), 5000);
- }
- onClientToRelayMessage(msg) {
- dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes');
- this.c2rSchedule.push(msg.data);
- return this.flush();
- }
- onRelayToClientMessage(event) {
- dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes');
- this.r2cSchedule.push(event.data);
- return this.flush();
- }
- onError(event) {
- var ws;
- ws = event.target;
- log(ws.label + ' error.');
- return this.close();
- }
- // Close both WebRTC and websocket.
- close() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = 0;
- }
- if (this.webrtcIsReady()) {
- this.client.close();
- }
- this.client = null;
- if (this.relayIsReady()) {
- this.relay.close();
- }
- this.relay = null;
- this.onCleanup();
- this.active = false;
- this.running = false;
- }
- flush() {
- var busy, checkChunks;
- if (this.flush_timeout_id) {
- clearTimeout(this.flush_timeout_id);
- }
- this.flush_timeout_id = null;
- busy = true;
- checkChunks = () => {
- var chunk;
- busy = false;
- // WebRTC --> websocket
- if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) {
- chunk = this.c2rSchedule.shift();
- this.rateLimit.update(chunk.byteLength);
- this.relay.send(chunk);
- busy = true;
- }
- // websocket --> WebRTC
- if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
- chunk = this.r2cSchedule.shift();
- this.rateLimit.update(chunk.byteLength);
- this.client.send(chunk);
- return busy = true;
- }
- };
- while (busy && !this.rateLimit.isLimited()) {
- checkChunks();
- }
- if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) {
- return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
- }
- }
- webrtcIsReady() {
- return null !== this.client && 'open' === this.client.readyState;
- }
- relayIsReady() {
- return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState);
- }
- isClosed(ws) {
- return void 0 === ws || WebSocket.CLOSED === ws.readyState;
- }
- }
- ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024;
- ProxyPair.prototype.pc = null;
- ProxyPair.prototype.client = null; // WebRTC Data channel
- ProxyPair.prototype.relay = null; // websocket
- ProxyPair.prototype.timer = 0;
- ProxyPair.prototype.running = false; // Whether a datachannel is opened
- ProxyPair.prototype.active = false; // Whether serving a client.
- ProxyPair.prototype.flush_timeout_id = null;
- ProxyPair.prototype.onCleanup = null;
- ProxyPair.prototype.id = null;
|