commit 2d8a1690ba833829f086e0bfe9fc450513682d44 Author: Arlo Breault arlolra@gmail.com Date: Wed May 8 16:13:22 2019 -0400
Initialize snowflake instance with a config --- proxy/Cakefile | 2 ++ proxy/broker.coffee | 24 +++++++++-------- proxy/config.coffee | 26 ++++++++++++++++++ proxy/init.coffee | 57 ++++++++++------------------------------ proxy/proxypair.coffee | 10 +++---- proxy/snowflake.coffee | 25 +++++++++++------- proxy/spec/broker.spec.coffee | 12 ++++----- proxy/spec/init.spec.coffee | 28 ++++++++++++++++++++ proxy/spec/proxypair.spec.coffee | 5 ++-- proxy/spec/snowflake.spec.coffee | 43 ++++++++---------------------- proxy/util.coffee | 4 +-- 11 files changed, 124 insertions(+), 112 deletions(-)
diff --git a/proxy/Cakefile b/proxy/Cakefile index 8013dac..e0d610c 100644 --- a/proxy/Cakefile +++ b/proxy/Cakefile @@ -11,6 +11,7 @@ FILES = [ 'broker.coffee' 'ui.coffee' 'snowflake.coffee' + 'config.coffee' ] FILES_SPEC = [ 'spec/util.spec.coffee' @@ -19,6 +20,7 @@ FILES_SPEC = [ 'spec/proxypair.spec.coffee' 'spec/snowflake.spec.coffee' 'spec/websocket.spec.coffee' + 'spec/init.spec.coffee' ] FILES_ALL = FILES.concat FILES_SPEC OUTFILE = 'snowflake.js' diff --git a/proxy/broker.coffee b/proxy/broker.coffee index 981a761..ca8a2e5 100644 --- a/proxy/broker.coffee +++ b/proxy/broker.coffee @@ -7,12 +7,14 @@ to get assigned to clients.
# Represents a broker running remotely. class Broker - @STATUS_OK = 200 - @STATUS_GONE = 410 - @STATUS_GATEWAY_TIMEOUT = 504 + @STATUS: + OK: 200 + GONE: 410 + GATEWAY_TIMEOUT: 504
- @MESSAGE_TIMEOUT = 'Timed out waiting for a client offer.' - @MESSAGE_UNEXPECTED = 'Unexpected status.' + @MESSAGE: + TIMEOUT: 'Timed out waiting for a client offer.' + UNEXPECTED: 'Unexpected status.'
clients: 0
@@ -38,15 +40,15 @@ class Broker xhr.onreadystatechange = -> return if xhr.DONE != xhr.readyState switch xhr.status - when Broker.STATUS_OK + when Broker.STATUS.OK fulfill xhr.responseText # Should contain offer. - when Broker.STATUS_GATEWAY_TIMEOUT - reject Broker.MESSAGE_TIMEOUT + when Broker.STATUS.GATEWAY_TIMEOUT + reject Broker.MESSAGE.TIMEOUT else log 'Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText snowflake.ui.setStatus ' failure. Please refresh.' - reject Broker.MESSAGE_UNEXPECTED + reject Broker.MESSAGE.UNEXPECTED @_xhr = xhr # Used by spec to fake async Broker interaction @_postRequest id, xhr, 'proxy', id
@@ -59,10 +61,10 @@ class Broker xhr.onreadystatechange = -> return if xhr.DONE != xhr.readyState switch xhr.status - when Broker.STATUS_OK + when Broker.STATUS.OK dbg 'Broker: Successfully replied with answer.' dbg xhr.responseText - when Broker.STATUS_GONE + when Broker.STATUS.GONE dbg 'Broker: No longer valid to reply with answer.' else dbg 'Broker ERROR: Unexpected ' + xhr.status + diff --git a/proxy/config.coffee b/proxy/config.coffee new file mode 100644 index 0000000..810c79f --- /dev/null +++ b/proxy/config.coffee @@ -0,0 +1,26 @@ +class Config + brokerUrl: 'snowflake-broker.bamsoftware.com' + relayAddr: + host: 'snowflake.bamsoftware.com' + port: '443' + # Original non-wss relay: + # host: '192.81.135.242' + # port: 9902 + + cookieName: "snowflake-allow" + + # Bytes per second. Set to undefined to disable limit. + rateLimitBytes: undefined + minRateLimit: 10 * 1024 + rateLimitHistory: 5.0 + defaultBrokerPollInterval: 5.0 * 1000 + + maxNumClients: 1 + connectionsPerClient: 1 + + # TODO: Different ICE servers. + pcConfig = { + iceServers: [ + { urls: ['stun:stun.l.google.com:19302'] } + ] + } diff --git a/proxy/init.coffee b/proxy/init.coffee index 497a54f..48a2c39 100644 --- a/proxy/init.coffee +++ b/proxy/init.coffee @@ -1,37 +1,9 @@ -# General snowflake proxy constants. -# For websocket-specific constants, see websocket.coffee. -BROKER = 'snowflake-broker.bamsoftware.com' -RELAY = - host: 'snowflake.bamsoftware.com' - port: '443' - # Original non-wss relay: - # host: '192.81.135.242' - # port: 9902 -COOKIE_NAME = "snowflake-allow"
-# Bytes per second. Set to undefined to disable limit. -DEFAULT_RATE_LIMIT = undefined -MIN_RATE_LIMIT = 10 * 1024 -RATE_LIMIT_HISTORY = 5.0 -DEFAULT_BROKER_POLL_INTERVAL = 5.0 * 1000 - -MAX_NUM_CLIENTS = 1 -CONNECTIONS_PER_CLIENT = 1 - -# TODO: Different ICE servers. -config = { - iceServers: [ - { urls: ['stun:stun.l.google.com:19302'] } - ] -} - -CONFIRMATION_MESSAGE = 'You're currently serving a Tor user via Snowflake.' +snowflake = null
query = Query.parse(location) -DEBUG = Params.getBool(query, 'debug', false) - -snowflake = null -silenceNotifications = false +debug = Params.getBool(query, 'debug', false) +silenceNotifications = Params.getBool(query, 'silent', false)
# Log to both console and UI if applicable. # Requires that the snowflake and UI objects are hooked up in order to @@ -40,13 +12,17 @@ log = (msg) -> console.log 'Snowflake: ' + msg snowflake?.ui.log msg
-dbg = (msg) -> log msg if DEBUG or (snowflake?.ui instanceof DebugUI) - +dbg = (msg) -> log msg if debug or (snowflake?.ui instanceof DebugUI)
### Entry point. ### init = () -> + config = new Config + + if 'off' != query['ratelimit'] + config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes) + ui = null if (document.getElementById('badge') != null) ui = new BadgeUI() @@ -57,29 +33,24 @@ init = () -> else ui = new UI()
- rateLimitBytes = undefined - if 'off' != query['ratelimit'] - rateLimitBytes = Params.getByteCount(query, 'ratelimit', DEFAULT_RATE_LIMIT) - - silenceNotifications = Params.getBool(query, 'silent', false) - broker = new Broker BROKER - snowflake = new Snowflake broker, ui, rateLimitBytes + broker = new Broker config.brokerUrl + snowflake = new Snowflake config, ui, broker
log '== snowflake proxy ==' - if Util.snowflakeIsDisabled() + if Util.snowflakeIsDisabled(config.cookieName) # Do not activate the proxy if any number of conditions are true. log 'Currently not active.' return
# Otherwise, begin setting up WebRTC and acting as a proxy. dbg 'Contacting Broker at ' + broker.url - snowflake.setRelayAddr RELAY + snowflake.setRelayAddr config.relayAddr snowflake.beginWebRTC()
# Notification of closing tab with active proxy. window.onbeforeunload = -> if !silenceNotifications && Snowflake.MODE.WEBRTC_READY == snowflake.state - return CONFIRMATION_MESSAGE + return Snowflake.MESSAGE.CONFIRMATION null
window.onunload = -> diff --git a/proxy/proxypair.coffee b/proxy/proxypair.coffee index bd6850b..879c6b6 100644 --- a/proxy/proxypair.coffee +++ b/proxy/proxypair.coffee @@ -24,14 +24,14 @@ class ProxyPair - @relayAddr is the destination relay - @rateLimit specifies a rate limit on traffic ### - constructor: (@relayAddr, @rateLimit) -> + constructor: (@relayAddr, @rateLimit, @pcConfig) -> @id = Util.genSnowflakeID() @c2rSchedule = [] @r2cSchedule = []
# Prepare a WebRTC PeerConnection and await for an SDP offer. begin: -> - @pc = new PeerConnection config, { + @pc = new PeerConnection @pcConfig, { optional: [ { DtlsSrtpKeyAgreement: true } { RtpDataChannels: false } @@ -126,15 +126,13 @@ class ProxyPair
# WebRTC --> websocket onClientToRelayMessage: (msg) => - if DEBUG - log 'WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes' + dbg 'WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes' @c2rSchedule.push msg.data @flush()
# websocket --> WebRTC onRelayToClientMessage: (event) => - if DEBUG - log 'websocket --> WebRTC data: ' + event.data.byteLength + ' bytes' + dbg 'websocket --> WebRTC data: ' + event.data.byteLength + ' bytes' @r2cSchedule.push event.data @flush()
diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee index fa421ee..66885d9 100644 --- a/proxy/snowflake.coffee +++ b/proxy/snowflake.coffee @@ -16,21 +16,26 @@ class Snowflake retries: 0
# Janky state machine - @MODE = + @MODE: INIT: 0 WEBRTC_CONNECTING: 1 WEBRTC_READY: 2
+ @MESSAGE: + CONFIRMATION: 'You're currently serving a Tor user via Snowflake.' + # Prepare the Snowflake with a Broker (to find clients) and optional UI. - constructor: (@broker, @ui, rateLimitBytes) -> + constructor: (@config, @ui, @broker) -> @state = Snowflake.MODE.INIT @proxyPairs = []
- if undefined == rateLimitBytes + if undefined == @config.rateLimitBytes @rateLimit = new DummyRateLimit() else - @rateLimit = new BucketRateLimit(rateLimitBytes * RATE_LIMIT_HISTORY, - RATE_LIMIT_HISTORY) + @rateLimit = new BucketRateLimit( + @config.rateLimitBytes * @config.rateLimitHistory, + @config.rateLimitHistory + ) @retries = 0
# Set the target relay address spec, which is expected to be websocket. @@ -45,7 +50,7 @@ class Snowflake # process. |pollBroker| automatically arranges signalling. beginWebRTC: -> @state = Snowflake.MODE.WEBRTC_CONNECTING - for i in [1..CONNECTIONS_PER_CLIENT] + for i in [1..@config.connectionsPerClient] @makeProxyPair @relayAddr log 'ProxyPair Slots: ' + @proxyPairs.length log 'Snowflake IDs: ' + (@proxyPairs.map (p) -> p.id).join ' | ' @@ -77,9 +82,9 @@ class Snowflake recv = @broker.getClientOffer pair.id recv.then (desc) => @receiveOffer pair, desc - countdown('Serving 1 new client.', DEFAULT_BROKER_POLL_INTERVAL / 1000) - , (err) -> - countdown(err, DEFAULT_BROKER_POLL_INTERVAL / 1000) + countdown('Serving 1 new client.', @config.defaultBrokerPollInterval / 1000) + , (err) => + countdown(err, @config.defaultBrokerPollInterval / 1000) @retries++
findClients() @@ -111,7 +116,7 @@ class Snowflake .catch fail
makeProxyPair: (relay) -> - pair = new ProxyPair relay, @rateLimit + pair = new ProxyPair relay, @rateLimit, @config.pcConfig @proxyPairs.push pair pair.onCleanup = (event) => # Delete from the list of active proxy pairs. diff --git a/proxy/spec/broker.spec.coffee b/proxy/spec/broker.spec.coffee index 3532fe1..2b1d2bd 100644 --- a/proxy/spec/broker.spec.coffee +++ b/proxy/spec/broker.spec.coffee @@ -25,7 +25,7 @@ describe 'Broker', -> # fake successful request and response from broker. spyOn(b, '_postRequest').and.callFake -> b._xhr.readyState = b._xhr.DONE - b._xhr.status = Broker.STATUS_OK + b._xhr.status = Broker.STATUS.OK b._xhr.responseText = 'fake offer' b._xhr.onreadystatechange() poll = b.getClientOffer() @@ -35,7 +35,7 @@ describe 'Broker', -> expect(desc).toEqual 'fake offer' done() .catch -> - fail 'should not reject on Broker.STATUS_OK' + fail 'should not reject on Broker.STATUS.OK' done()
it 'rejects if the broker timed-out', (done) -> @@ -43,16 +43,16 @@ describe 'Broker', -> # fake timed-out request from broker spyOn(b, '_postRequest').and.callFake -> b._xhr.readyState = b._xhr.DONE - b._xhr.status = Broker.STATUS_GATEWAY_TIMEOUT + b._xhr.status = Broker.STATUS.GATEWAY_TIMEOUT b._xhr.onreadystatechange() poll = b.getClientOffer() expect(poll).not.toBeNull() expect(b._postRequest).toHaveBeenCalled() poll.then (desc) -> - fail 'should not fulfill on GATEWAY_TIMEOUT' + fail 'should not fulfill on Broker.STATUS.GATEWAY_TIMEOUT' done() , (err) -> - expect(err).toBe Broker.MESSAGE_TIMEOUT + expect(err).toBe Broker.MESSAGE.TIMEOUT done()
it 'rejects on any other status', (done) -> @@ -69,7 +69,7 @@ describe 'Broker', -> fail 'should not fulfill on non-OK status' done() , (err) -> - expect(err).toBe Broker.MESSAGE_UNEXPECTED + expect(err).toBe Broker.MESSAGE.UNEXPECTED expect(b._xhr.status).toBe 1337 done()
diff --git a/proxy/spec/init.spec.coffee b/proxy/spec/init.spec.coffee new file mode 100644 index 0000000..4134a22 --- /dev/null +++ b/proxy/spec/init.spec.coffee @@ -0,0 +1,28 @@ + +# Fake snowflake to interact with +snowflake = + ui: new UI + broker: + sendAnswer: -> + state: Snowflake.MODE.INIT + +describe 'Init', -> + + it 'gives a dialog when closing, only while active', -> + silenceNotifications = false + snowflake.state = Snowflake.MODE.WEBRTC_READY + msg = window.onbeforeunload() + expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY + expect(msg).toBe Snowflake.MESSAGE.CONFIRMATION + + snowflake.state = Snowflake.MODE.INIT + msg = window.onbeforeunload() + expect(snowflake.state).toBe Snowflake.MODE.INIT + expect(msg).toBe null + + it 'does not give a dialog when silent flag is on', -> + silenceNotifications = true + snowflake.state = Snowflake.MODE.WEBRTC_READY + msg = window.onbeforeunload() + expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY + expect(msg).toBe null diff --git a/proxy/spec/proxypair.spec.coffee b/proxy/spec/proxypair.spec.coffee index 566c6f1..87aeb55 100644 --- a/proxy/spec/proxypair.spec.coffee +++ b/proxy/spec/proxypair.spec.coffee @@ -24,10 +24,11 @@ arrayMatching = (sample) -> {
describe 'ProxyPair', -> fakeRelay = Parse.address '0.0.0.0:12345' - rateLimit = new DummyRateLimit() + rateLimit = new DummyRateLimit + config = new Config destination = [] # Using the mock PeerConnection definition from spec/snowflake.spec.coffee. - pp = new ProxyPair(fakeRelay, rateLimit) + pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig)
beforeEach -> pp.begin() diff --git a/proxy/spec/snowflake.spec.coffee b/proxy/spec/snowflake.spec.coffee index a619a06..2c87204 100644 --- a/proxy/spec/snowflake.spec.coffee +++ b/proxy/spec/snowflake.spec.coffee @@ -15,40 +15,38 @@ class WebSocket constructor: -> @bufferedAmount = 0 send: (data) -> + log = -> -fakeUI = new UI() + +config = new Config +ui = new UI + class FakeBroker getClientOffer: -> new Promise((F,R) -> {}) -# Fake snowflake to interact with -snowflake = - ui: fakeUI - broker: - sendAnswer: -> - state: Snowflake.MODE.INIT
describe 'Snowflake', ->
it 'constructs correctly', -> - s = new Snowflake({ fake: 'broker' }, fakeUI) + s = new Snowflake(config, ui, { fake: 'broker' }) expect(s.rateLimit).not.toBeNull() expect(s.broker).toEqual { fake: 'broker' } expect(s.ui).not.toBeNull() expect(s.retries).toBe 0
it 'sets relay address correctly', -> - s = new Snowflake(null, fakeUI) + s = new Snowflake(config, ui, null) s.setRelayAddr 'foo' expect(s.relayAddr).toEqual 'foo'
it 'initalizes WebRTC connection', -> - s = new Snowflake(new FakeBroker(), fakeUI) + s = new Snowflake(config, ui, new FakeBroker()) spyOn(s.broker, 'getClientOffer').and.callThrough() s.beginWebRTC() expect(s.retries).toBe 1 expect(s.broker.getClientOffer).toHaveBeenCalled()
it 'receives SDP offer and sends answer', -> - s = new Snowflake(new FakeBroker(), fakeUI) + s = new Snowflake(config, ui, new FakeBroker()) pair = { receiveWebRTCOffer: -> } spyOn(pair, 'receiveWebRTCOffer').and.returnValue true spyOn(s, 'sendAnswer') @@ -56,7 +54,7 @@ describe 'Snowflake', -> expect(s.sendAnswer).toHaveBeenCalled()
it 'does not send answer when receiving invalid offer', -> - s = new Snowflake(new FakeBroker(), fakeUI) + s = new Snowflake(config, ui, new FakeBroker()) pair = { receiveWebRTCOffer: -> } spyOn(pair, 'receiveWebRTCOffer').and.returnValue false spyOn(s, 'sendAnswer') @@ -64,25 +62,6 @@ describe 'Snowflake', -> expect(s.sendAnswer).not.toHaveBeenCalled()
it 'can make a proxypair', -> - s = new Snowflake(new FakeBroker(), fakeUI) + s = new Snowflake(config, ui, new FakeBroker()) s.makeProxyPair() expect(s.proxyPairs.length).toBe 1 - - it 'gives a dialog when closing, only while active', -> - silenceNotifications = false - snowflake.state = Snowflake.MODE.WEBRTC_READY - msg = window.onbeforeunload() - expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY - expect(msg).toBe CONFIRMATION_MESSAGE - - snowflake.state = Snowflake.MODE.INIT - msg = window.onbeforeunload() - expect(snowflake.state).toBe Snowflake.MODE.INIT - expect(msg).toBe null - - it 'does not give a dialog when silent flag is on', -> - silenceNotifications = true - snowflake.state = Snowflake.MODE.WEBRTC_READY - msg = window.onbeforeunload() - expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY - expect(msg).toBe null diff --git a/proxy/util.coffee b/proxy/util.coffee index 15b4152..b8057fb 100644 --- a/proxy/util.coffee +++ b/proxy/util.coffee @@ -22,10 +22,10 @@ class Util @genSnowflakeID: -> Math.random().toString(36).substring(2)
- @snowflakeIsDisabled = -> + @snowflakeIsDisabled = (cookieName) -> cookies = Parse.cookie document.cookie # Do nothing if snowflake has not been opted in by user. - if cookies[COOKIE_NAME] != '1' + if cookies[cookieName] != '1' log 'Not opted-in. Please click the badge to change options.' return true # Also do nothing if running in Tor Browser.
tor-commits@lists.torproject.org