Initialize snowflake instance with a config

This commit is contained in:
Arlo Breault 2019-05-08 16:13:22 -04:00
parent edbbea1d03
commit 2d8a1690ba
11 changed files with 125 additions and 113 deletions

View file

@ -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'

View file

@ -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 +

26
proxy/config.coffee Normal file
View file

@ -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'] }
]
}

View file

@ -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.'
query = Query.parse(location)
DEBUG = Params.getBool(query, 'debug', false)
snowflake = null
silenceNotifications = false
query = Query.parse(location)
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 = ->

View file

@ -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()

View file

@ -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.

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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.