mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 05:11:19 -04:00
Initialize snowflake instance with a config
This commit is contained in:
parent
edbbea1d03
commit
2d8a1690ba
11 changed files with 125 additions and 113 deletions
|
@ -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'
|
||||
|
|
|
@ -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
26
proxy/config.coffee
Normal 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'] }
|
||||
]
|
||||
}
|
|
@ -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 = ->
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
28
proxy/spec/init.spec.coffee
Normal file
28
proxy/spec/init.spec.coffee
Normal 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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue