mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 14:11:23 -04:00
234 lines
5.8 KiB
CoffeeScript
234 lines
5.8 KiB
CoffeeScript
###
|
|
A Coffeescript WebRTC snowflake proxy
|
|
Using Copy-paste signaling for now.
|
|
|
|
Uses WebRTC from the client, and websocket to the server.
|
|
|
|
Assume that the webrtc client plugin is always the offerer, in which case
|
|
this must always act as the answerer.
|
|
###
|
|
DEFAULT_WEBSOCKET = '192.81.135.242:9901'
|
|
DEFAULT_PORTS =
|
|
http: 80
|
|
https: 443
|
|
|
|
DEBUG = false
|
|
if window && window.location
|
|
query = Query.parse(window.location.search.substr(1))
|
|
DEBUG = Params.getBool(query, 'debug', false)
|
|
# HEADLESS is true if we are running not in a browser with a DOM.
|
|
HEADLESS = 'undefined' == typeof(document)
|
|
|
|
# Bytes per second. Set to undefined to disable limit.
|
|
DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT || undefined
|
|
MIN_RATE_LIMIT = 10 * 1024
|
|
RATE_LIMIT_HISTORY = 5.0
|
|
|
|
MAX_NUM_CLIENTS = 1
|
|
CONNECTIONS_PER_CLIENT = 1
|
|
|
|
# TODO: Different ICE servers.
|
|
config = {
|
|
iceServers: [
|
|
{ urls: ['stun:stun.l.google.com:19302'] }
|
|
]
|
|
}
|
|
|
|
# TODO: Implement
|
|
class Badge
|
|
|
|
# Janky state machine
|
|
MODE =
|
|
INIT: 0
|
|
WEBRTC_CONNECTING: 1
|
|
WEBRTC_READY: 2
|
|
|
|
# Minimum viable snowflake for now - just 1 client.
|
|
class Snowflake
|
|
|
|
relayAddr: null
|
|
# TODO: Actually support multiple ProxyPairs. (makes more sense once meek-
|
|
# signalling is ready)
|
|
proxyPairs: []
|
|
proxyPair: null
|
|
|
|
rateLimit: null
|
|
badge: null
|
|
$badge: null
|
|
state: MODE.INIT
|
|
|
|
constructor: ->
|
|
if HEADLESS
|
|
# No badge
|
|
else if DEBUG
|
|
@$badge = debug_div
|
|
else
|
|
@badge = new Badge()
|
|
@$badgem = @badge.elem
|
|
@$badge.setAttribute('id', 'snowflake-badge') if (@$badge)
|
|
|
|
rateLimitBytes = undefined
|
|
if 'off' != query['ratelimit']
|
|
rateLimitBytes = Params.getByteCount(query, 'ratelimit',
|
|
DEFAULT_RATE_LIMIT)
|
|
if undefined == rateLimitBytes
|
|
@rateLimit = new DummyRateLimit()
|
|
else
|
|
@rateLimit = new BucketRateLimit(rateLimitBytes * RATE_LIMIT_HISTORY,
|
|
RATE_LIMIT_HISTORY)
|
|
|
|
# TODO: Should fetch from facilitator later.
|
|
setRelayAddr: (relayAddr) ->
|
|
addr = Parse.address relayAddr
|
|
if !addr
|
|
log 'Invalid address spec.'
|
|
return false
|
|
@relayAddr = addr
|
|
log 'Using ' + relayAddr + ' as Relay.'
|
|
@beginWebRTC()
|
|
log 'Input offer from the snowflake client:'
|
|
return true
|
|
|
|
# Initialize WebRTC PeerConnection
|
|
beginWebRTC: ->
|
|
log 'Starting up Snowflake...\n'
|
|
@state = MODE.WEBRTC_CONNECTING
|
|
for i in [1..CONNECTIONS_PER_CLIENT]
|
|
@makeProxyPair @relayAddr
|
|
@proxyPair = @proxyPairs[0]
|
|
|
|
# Receive an SDP offer from client plugin.
|
|
receiveOffer: (desc) =>
|
|
sdp = new RTCSessionDescription desc
|
|
try
|
|
err = @proxyPair.pc.setRemoteDescription sdp
|
|
catch e
|
|
log 'Invalid SDP message.'
|
|
return false
|
|
log 'SDP ' + sdp.type + ' successfully received.'
|
|
@sendAnswer() if 'offer' == sdp.type
|
|
true
|
|
|
|
sendAnswer: =>
|
|
next = (sdp) =>
|
|
log 'webrtc: Answer ready.'
|
|
@proxyPair.pc.setLocalDescription sdp
|
|
promise = @proxyPair.pc.createAnswer next
|
|
promise.then next if promise
|
|
|
|
# Poll facilitator when this snowflake can support more clients.
|
|
proxyMain: ->
|
|
if @proxyPairs.length >= MAX_NUM_CLIENTS * CONNECTIONS_PER_CLIENT
|
|
setTimeout(@proxyMain, @facilitator_poll_interval * 1000)
|
|
return
|
|
params = [['r', '1']]
|
|
params.push ['transport', 'websocket']
|
|
params.push ['transport', 'webrtc']
|
|
|
|
makeProxyPair: (relay) ->
|
|
pair = new ProxyPair null, relay, @rateLimit
|
|
@proxyPairs.push pair
|
|
pair.onCleanup = (event) =>
|
|
# Delete from the list of active proxy pairs.
|
|
@proxyPairs.splice(@proxyPairs.indexOf(pair), 1)
|
|
@badge.endProxy() if @badge
|
|
try
|
|
pair.connectClient()
|
|
catch err
|
|
log 'ERROR: ProxyPair exception while connecting.'
|
|
log err
|
|
return
|
|
@badge.beginProxy if @badge
|
|
|
|
cease: ->
|
|
while @proxyPairs.length > 0
|
|
@proxyPairs.pop().close()
|
|
|
|
disable: ->
|
|
log 'Disabling Snowflake.'
|
|
@cease()
|
|
@badge.disable() if @badge
|
|
|
|
die: ->
|
|
log 'Snowflake died.'
|
|
@cease()
|
|
@badge.die() if @badge
|
|
|
|
snowflake = null
|
|
|
|
#
|
|
## -- DOM & Inputs -- #
|
|
#
|
|
|
|
# DOM elements references.
|
|
$msglog = null
|
|
$send = null
|
|
$input = null
|
|
|
|
Interface =
|
|
# Local input from keyboard into message window.
|
|
acceptInput: ->
|
|
msg = $input.value
|
|
switch snowflake.state
|
|
when MODE.INIT
|
|
# Set target relay.
|
|
if !(snowflake.setRelayAddr msg)
|
|
log 'Defaulting to websocket relay at ' + DEFAULT_WEBSOCKET
|
|
snowflake.setRelayAddr DEFAULT_WEBSOCKET
|
|
when MODE.WEBRTC_CONNECTING
|
|
Signalling.receive msg
|
|
when MODE.WEBRTC_READY
|
|
log 'No input expected - WebRTC connected.'
|
|
else
|
|
log 'ERROR: ' + msg
|
|
$input.value = ''
|
|
$input.focus()
|
|
|
|
# Signalling channel - just tells user to copy paste to the peer.
|
|
# Eventually this should go over the facilitator.
|
|
Signalling =
|
|
send: (msg) ->
|
|
log '---- Please copy the below to peer ----\n'
|
|
log JSON.stringify msg
|
|
log '\n'
|
|
|
|
receive: (msg) ->
|
|
recv = ''
|
|
try
|
|
recv = JSON.parse msg
|
|
catch e
|
|
log 'Invalid JSON.'
|
|
return
|
|
desc = recv['sdp']
|
|
if !desc
|
|
log 'Invalid SDP.'
|
|
return false
|
|
snowflake.receiveOffer recv if desc
|
|
|
|
log = (msg) -> # Log to the message window.
|
|
console.log msg
|
|
# Scroll to latest
|
|
if $msglog
|
|
$msglog.value += msg + '\n'
|
|
$msglog.scrollTop = $msglog.scrollHeight
|
|
|
|
welcome = ->
|
|
log '== snowflake browser proxy =='
|
|
log 'Input desired relay address:'
|
|
|
|
init = ->
|
|
$msglog = document.getElementById('msglog')
|
|
$msglog.value = ''
|
|
|
|
$send = document.getElementById('send')
|
|
$send.onclick = Interface.acceptInput
|
|
|
|
$input = document.getElementById('input')
|
|
$input.focus()
|
|
$input.onkeydown = (e) -> $send.onclick() if 13 == e.keyCode # enter
|
|
|
|
snowflake = new Snowflake()
|
|
window.snowflake = snowflake
|
|
welcome()
|
|
|
|
window.onload = init if window
|