From ceac90ec07d3c05784d9430589f5b4bdc3e36637 Mon Sep 17 00:00:00 2001 From: Serene Han Date: Wed, 30 Dec 2015 11:08:31 -0800 Subject: [PATCH] begin including flashproxy websocket stuff into the JS proxy --- README.md | 6 +- proxy/snowflake.js | 316 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ded99b..ab8c8cc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # snowflake-pt -WebRTC Pluggable Transport +A Pluggable Transport using WebRTC ### Usage -`go build webrtc-client.go` -`tor -f torrc` +- `go build webrtc-client.go` +- `tor -f torrc` More documentation on the way. diff --git a/proxy/snowflake.js b/proxy/snowflake.js index d6fc025..ae8cddc 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -27,8 +27,10 @@ var MODE = { CHAT: 2 } var currentMode = MODE.INIT; +var CONNECTIONS_PER_CLIENT = 2; // Signalling channel - just tells user to copy paste to the peer. +// Eventually this should go over the facilitator. var Signalling = { send: function(msg) { log("---- Please copy the below to peer ----\n"); @@ -237,3 +239,317 @@ var log = function(msg) { } window.onload = init; + +// +// some code sourced from flashproxy.js +// TODO: refactor / webrtc-afy it +// + +/* Does the WebSocket implementation in this browser support binary frames? (RFC + 6455 section 5.6.) If not, we have to use base64-encoded text frames. It is + assumed that the client and relay endpoints always support binary frames. */ +function have_websocket_binary_frames() { + var BROWSERS = [ + { idString: "Chrome", verString: "Chrome", version: 16 }, + { idString: "Safari", verString: "Version", version: 6 }, + { idString: "Firefox", verString: "Firefox", version: 11 } + ]; + var ua; + + ua = window.navigator.userAgent; + if (!ua) + return false; + + for (var i = 0; i < BROWSERS.length; i++) { + var matches, reg; + + reg = "\\b" + BROWSERS[i].idString + "\\b"; + if (!ua.match(new RegExp(reg, "i"))) + continue; + reg = "\\b" + BROWSERS[i].verString + "\\/(\\d+)"; + matches = ua.match(new RegExp(reg, "i")); + return matches !== null && Number(matches[1]) >= BROWSERS[i].version; + } + + return false; +} + +function make_websocket(addr) { + var url; + var ws; + + url = build_url("ws", addr.host, addr.port, "/"); + + if (have_websocket_binary_frames()) + ws = new WebSocket(url); + else + ws = new WebSocket(url, "base64"); + /* "User agents can use this as a hint for how to handle incoming binary + data: if the attribute is set to 'blob', it is safe to spool it to disk, + and if it is set to 'arraybuffer', it is likely more efficient to keep + the data in memory." */ + ws.binaryType = "arraybuffer"; + + return ws; +} + +function Snowflake() { + if (HEADLESS) { + /* No badge. */ + } else if (DEBUG) { + this.badge_elem = debug_div; + } else { + this.badge = new Badge(); + this.badge_elem = this.badge.elem; + } + if (this.badge_elem) + this.badge_elem.setAttribute("id", "flashproxy-badge"); + + this.proxy_pairs = []; + + this.start = function() { + var client_addr; + var relay_addr; + var rate_limit_bytes; + log("Snowflake starting.") + // TODO: Facilitator interaction + }; + + this.proxy_main = function() { + var params; + var base_url, url; + var xhr; + + if (this.proxy_pairs.length >= this.max_num_clients * CONNECTIONS_PER_CLIENT) { + setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval * 1000); + return; + } + + params = [["r", "1"]]; + params.push(["transport", "websocket"]); + params.push(["transport", "webrtc"]); + }; + + this.begin_proxy = function(client_addr, relay_addr) { + for (var i=0; i 0) + this.proxy_pairs.pop().close(); + }; + + this.disable = function() { + puts("Disabling."); + this.cease_operation(); + if (this.badge) + this.badge.disable(); + }; + + this.die = function() { + puts("Dying."); + this.cease_operation(); + if (this.badge) + this.badge.die(); + }; +} + +/* An instance of a client-relay connection. */ +function ProxyPair(client_addr, relay_addr, rate_limit) { + var MAX_BUFFER = 10 * 1024 * 1024; + + function log(s) { + if (!SAFE_LOGGING) { + s = format_addr(client_addr) + '|' + format_addr(relay_addr) + ' : ' + s + } + // puts(s) + window.log(s) + } + + this.client_addr = client_addr; + this.relay_addr = relay_addr; + this.rate_limit = rate_limit; + + this.c2r_schedule = []; + this.r2c_schedule = []; + + this.running = true; + this.flush_timeout_id = null; + + /* This callback function can be overridden by external callers. */ + this.cleanup_callback = function() { + }; + + this.connect = function() { + log("Client: connecting."); + this.client_s = make_websocket(this.client_addr); + + /* Try to connect to the client first (since that is more likely to + fail) and only after that try to connect to the relay. */ + this.client_s.label = "Client"; + this.client_s.onopen = this.client_onopen_callback; + this.client_s.onclose = this.onclose_callback; + this.client_s.onerror = this.onerror_callback; + this.client_s.onmessage = this.onmessage_client_to_relay; + }; + + this.client_onopen_callback = function(event) { + var ws = event.target; + + log(ws.label + ": connected."); + log("Relay: connecting."); + this.relay_s = make_websocket(this.relay_addr); + + this.relay_s.label = "Relay"; + this.relay_s.onopen = this.relay_onopen_callback; + this.relay_s.onclose = this.onclose_callback; + this.relay_s.onerror = this.onerror_callback; + this.relay_s.onmessage = this.onmessage_relay_to_client; + }.bind(this); + + this.relay_onopen_callback = function(event) { + var ws = event.target; + + log(ws.label + ": connected."); + }.bind(this); + + this.maybe_cleanup = function() { + if (this.running && is_closed(this.client_s) && is_closed(this.relay_s)) { + this.running = false; + this.cleanup_callback(); + return true; + } + return false; + } + + this.onclose_callback = function(event) { + var ws = event.target; + + log(ws.label + ": closed."); + this.flush(); + + if (this.maybe_cleanup()) { + puts("Complete."); + } + }.bind(this); + + this.onerror_callback = function(event) { + var ws = event.target; + + log(ws.label + ": error."); + this.close(); + + // we can't rely on onclose_callback to cleanup, since one common error + // case is when the client fails to connect and the relay never starts. + // in that case close() is a NOP and onclose_callback is never called. + this.maybe_cleanup(); + }.bind(this); + + this.onmessage_client_to_relay = function(event) { + this.c2r_schedule.push(event.data); + this.flush(); + }.bind(this); + + this.onmessage_relay_to_client = function(event) { + this.r2c_schedule.push(event.data); + this.flush(); + }.bind(this); + + function is_open(ws) { + return ws !== undefined && ws.readyState === WebSocket.OPEN; + } + + function is_closed(ws) { + return ws === undefined || ws.readyState === WebSocket.CLOSED; + } + + this.close = function() { + if (!is_closed(this.client_s)) + this.client_s.close(); + if (!is_closed(this.relay_s)) + this.relay_s.close(); + }; + + /* Send as much data as the rate limit currently allows. */ + this.flush = function() { + var busy; + + if (this.flush_timeout_id) + clearTimeout(this.flush_timeout_id); + this.flush_timeout_id = null; + + busy = true; + while (busy && !this.rate_limit.is_limited()) { + var chunk; + busy = false; + + if (is_open(this.client_s) && + this.client_s.bufferedAmount < MAX_BUFFER && + this.r2c_schedule.length > 0) { + chunk = this.r2c_schedule.shift(); + this.rate_limit.update(chunk.length); + this.client_s.send(chunk); + busy = true; + } + if (is_open(this.relay_s) && + this.relay_s.bufferedAmount < MAX_BUFFER && + this.c2r_schedule.length > 0) { + chunk = this.c2r_schedule.shift(); + this.rate_limit.update(chunk.length); + this.relay_s.send(chunk); + busy = true; + } + } + + if (is_closed(this.relay_s) && + !is_closed(this.client_s) && + this.client_s.bufferedAmount === 0 && + this.r2c_schedule.length === 0) { + log("Client: closing."); + this.client_s.close(); + } + if (is_closed(this.client_s) && + !is_closed(this.relay_s) && + this.relay_s.bufferedAmount === 0 && + this.c2r_schedule.length === 0) { + log("Relay: closing."); + this.relay_s.close(); + } + + if (this.r2c_schedule.length > 0 || + (is_open(this.client_s) && this.client_s.bufferedAmount > 0) || + this.c2r_schedule.length > 0 || + (is_open(this.relay_s) && this.relay_s.bufferedAmount > 0)) + this.flush_timeout_id = setTimeout( + this.flush.bind(this), this.rate_limit.when() * 1000); + }; +} +