// Generated by CoffeeScript 2.4.1 /* 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. */ var ProxyPair; ProxyPair = (function() { 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) { var e, err; if ('offer' !== offer.type) { log('Invalid SDP received -- was not an offer.'); return false; } try { err = this.pc.setRemoteDescription(offer); } catch (error) { e = error; log('Invalid SDP message.'); return false; } dbg('SDP ' + offer.type + ' successfully received.'); return true; } prepareDataChannel(channel) { channel.onopen = () => { log('WebRTC DataChannel opened!'); 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]); } 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(this.relay.label + ' connected!'); return snowflake.ui.setStatus('connected'); }; this.relay.onclose = () => { log(this.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(this.relay.label + ' timed out connecting.'); return this.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() { var relay; if (this.timer) { clearTimeout(this.timer); this.timer = 0; } this.running = false; if (this.webrtcIsReady()) { this.client.close(); } if (this.relayIsReady()) { this.relay.close(); } relay = null; return this.onCleanup(); } 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 = true; ProxyPair.prototype.active = false; // Whether serving a client. ProxyPair.prototype.flush_timeout_id = null; ProxyPair.prototype.onCleanup = null; ProxyPair.prototype.id = null; return ProxyPair; }).call(this);