move datachannel into ProxyPair as client, use hardcoded default Relay as fallback

This commit is contained in:
Serene Han 2016-01-12 10:11:47 -08:00
parent cfd87d1798
commit a8477ee402
2 changed files with 257 additions and 254 deletions

View file

@ -10,13 +10,15 @@ this must always act as the answerer.
TODO(keroserene): Complete the websocket + webrtc ProxyPair
###
DEFAULT_WEBSOCKET = '92.81.135.242:9901'
if 'undefined' != typeof module && 'undefined' != typeof module.exports
console.log 'not in browser.'
else
window.PeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate
window.RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription
@ -25,7 +27,7 @@ Query =
Parse a URL query string or application/x-www-form-urlencoded body. The
return type is an object mapping string keys to string values. By design,
this function doesn't support multiple values for the same named parameter,
for example "a=1&a=2&a=3"; the first definition always wins. Returns null on
for example 'a=1&a=2&a=3'; the first definition always wins. Returns null on
error.
Always decodes from UTF-8, not any other encoding.
@ -61,8 +63,8 @@ Params =
getBool: (query, param, defaultValue) ->
val = query[param]
return defaultValue if undefined == val
return true if "true" == val || "1" == val || "" == val
return false if "false" == val || "0" == val
return true if 'true' == val || '1' == val || '' == val
return false if 'false' == val || '0' == val
return null
@ -81,8 +83,8 @@ Params =
result[name] = value if !(name in result)
result
# Parse an address in the form "host:port". Returns an Object with keys "host"
# (String) and "port" (int). Returns null on error.
# Parse an address in the form 'host:port'. Returns an Object with keys 'host'
# (String) and 'port' (int). Returns null on error.
parseAddress: (spec) ->
m = null
# IPv6 syntax.
@ -104,191 +106,20 @@ Params =
# elems = []
# for k in x
# elems.push(maybe_quote(k) + ': ' + repr(x[k]));
# return "{ " + elems.join(", ") + " }";
# } else if (typeof x === "string") {
# return '{ ' + elems.join(', ') + ' }';
# } else if (typeof x === 'string') {
# return quote(x);
# } else {
# return x.toString();
# safe_repr = (s) -> SAFE_LOGGING ? "[scrubbed]" : repr(s)
safe_repr = (s) -> SAFE_LOGGING ? "[scrubbed]" : JSON.stringify(s)
# safe_repr = (s) -> SAFE_LOGGING ? '[scrubbed]' : repr(s)
safe_repr = (s) -> SAFE_LOGGING ? '[scrubbed]' : JSON.stringify(s)
# HEADLESS is true if we are running not in a browser with a DOM.
DEBUG = false
if window && window.location
query = Query.parse(window.location.search.substr(1))
DEBUG = Params.getBool(query, "debug", false)
HEADLESS = "undefined" == typeof(document)
# TODO: Different ICE servers.
config = {
iceServers: [
{ urls: ["stun:stun.l.google.com:19302"] }
]
}
# DOM elements
$chatlog = null
$send = null
$input = null
# 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
# PeerConnection
pc: null
rateLimit: 0
proxyPairs: []
relayAddr: null
badge: null
$badge: null
MAX_NUM_CLIENTS = 1
CONNECTIONS_PER_CLIENT = 1
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)
# TODO: User-supplied for now, but should fetch from facilitator later.
setRelayAddr: (relayAddr) ->
addr = Params.parseAddress relayAddr
if !addr
log 'Invalid address spec. Try again.'
return false
@relayAddr = addr
log 'Using ' + relayAddr + ' as Relay.'
log "Input offer from the snowflake client:"
@beginWebRTC()
return true
# Initialize WebRTC PeerConnection
beginWebRTC: ->
log "Starting up Snowflake..."
@state = MODE.WEBRTC_CONNECTING
@pc = new PeerConnection(config, {
optional: [
{ DtlsSrtpKeyAgreement: true }
{ RtpDataChannels: false }
]
})
@pc.onicecandidate = (evt) =>
# Browser sends a null candidate once the ICE gathering completes.
# In this case, it makes sense to send one copy-paste blob.
if null == evt.candidate
log "Finished gathering ICE candidates."
Signalling.send @pc.localDescription
# OnDataChannel triggered remotely from the client when connection succeeds.
@pc.ondatachannel = (dc) =>
console.log dc;
channel = dc.channel
log "Data Channel established..."
@prepareDataChannel channel
prepareDataChannel: (channel) ->
channel.onopen = =>
log "Data channel opened!"
@state = MODE.WEBRTC_READY
# This is the point when the WebRTC datachannel is done, so the next step
# is to establish websocket to the server.
@beginProxy(null, @relayAddr)
channel.onclose = =>
log "Data channel closed."
@state = MODE.INIT;
$chatlog.className = ""
channel.onerror = =>
log "Data channel error!"
channel.onmessage = (msg) =>
line = recv = msg.data
console.log(msg);
# Go sends only raw bytes...
if "[object ArrayBuffer]" == recv.toString()
bytes = new Uint8Array recv
line = String.fromCharCode.apply(null, bytes)
line = line.trim()
log "data: " + line
# Receive an SDP offer from client plugin.
receiveOffer: (desc) =>
sdp = new RTCSessionDescription desc
try
err = @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."
@pc.setLocalDescription sdp
promise = @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"]
beginProxy: (client, relay) ->
for i in [1..CONNECTIONS_PER_CLIENT]
@makeProxyPair client, relay
makeProxyPair: (client, relay) ->
pair = new ProxyPair(client, relay, @rate_limit);
@proxyPairs.push pair
pair.onCleanup = (event) =>
# Delete from the list of active proxy pairs.
@proxyPairs.splice(@proxy_pairs.indexOf(pair), 1)
@badge.endProxy() if @badge
try
pair.connectRelay()
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
DEBUG = Params.getBool(query, 'debug', false)
HEADLESS = 'undefined' == typeof(document)
DEFAULT_PORTS =
http: 80
@ -317,10 +148,10 @@ buildUrl = (scheme, host, port, path, params) ->
path = '/' + path
###
Slash is significant so we must protect it from encodeURIComponent, while
still encoding question mark and number sign. RFC 3986, section 3.3: "The
still encoding question mark and number sign. RFC 3986, section 3.3: 'The
path is terminated by the first question mark ('?') or number sign ('#')
character, or by the end of the URI. ... A path consists of a sequence of
path segments separated by a slash ('/') character."
path segments separated by a slash ('/') character.'
###
path = path.replace /[^\/]+/, (m) ->
encodeURIComponent m
@ -339,59 +170,244 @@ makeWebsocket = (addr) ->
# else
# ws = new WebSocket url 'base64'
###
"User agents can use this as a hint for how to handle incoming binary data: if
'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."
set to 'arraybuffer', it is likely more efficient to keep the data in memory.'
###
ws.binaryType = 'arraybuffer'
ws
# TODO: Different ICE servers.
config = {
iceServers: [
{ urls: ['stun:stun.l.google.com:19302'] }
]
}
# DOM elements
$chatlog = null
$send = null
$input = null
# 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
MAX_NUM_CLIENTS = 1
CONNECTIONS_PER_CLIENT = 1
relayAddr: null
# TODO: Actually support multiple ProxyPairs. (makes more sense once meek-
# signalling is ready)
proxyPairs: []
proxyPair: null
rateLimit: null # TODO
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)
# TODO: User-supplied for now, but should fetch from facilitator later.
setRelayAddr: (relayAddr) ->
addr = Params.parseAddress 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
###
Represents: client <-- webrtc --> snowflake <-- websocket --> relay
###
class ProxyPair
MAX_BUFFER: 10 * 1024 * 1024
pc: null
c2rSchedule: []
r2cSchedule: []
client: null # WebRTC Data channel
relay: null # websocket
running: true
flush_timeout_id: null
constructor: (@clientAddr, @relayAddr, @rateLimit) ->
@c2rSchedule = []
@r2cSchedule = []
# Assumes WebRTC part is already connected.
# TODO: Put the webrtc stuff in ProxyPair, so that multiple webrtc connections
# can be established.
connectClient: ->
@pc = new PeerConnection config, {
optional: [
{ DtlsSrtpKeyAgreement: true }
{ RtpDataChannels: false }
]}
@pc.onicecandidate = (evt) =>
# Browser sends a null candidate once the ICE gathering completes.
# In this case, it makes sense to send one copy-paste blob.
if null == evt.candidate
# TODO: Use a promise.all to tell Snowflake about all offers at once,
# once multiple proxypairs are supported.
log 'Finished gathering ICE candidates.'
Signalling.send @pc.localDescription
# OnDataChannel triggered remotely from the client when connection succeeds.
@pc.ondatachannel = (dc) =>
console.log dc;
channel = dc.channel
log 'Data Channel established...'
@prepareDataChannel channel
@client = channel
prepareDataChannel: (channel) ->
channel.onopen = =>
log 'Data channel opened!'
snowflake.state = MODE.WEBRTC_READY
# This is the point when the WebRTC datachannel is done, so the next step
# is to establish websocket to the server.
@connectRelay()
channel.onclose = =>
log 'Data channel closed.'
@state = MODE.INIT;
$chatlog.className = ''
channel.onerror = =>
log 'Data channel error!'
channel.onmessage = @onClientToRelayMessage
# Assumes WebRTC datachannel is connected.
connectRelay: ->
log "Snowflake: connecting to relay"
@relay = makeWebsocket(@relayAddr);
@relay.label = 'Relay'
log 'Connecting to relay...'
@relay = makeWebsocket @relayAddr
@relay.label = 'websocket-relay'
@relay.onopen = =>
log "Snowflake: " + ws.label + "connected"
log 'Relay ' + @relay.label + 'connected'
@relay.onclose = @onClose
@relay.onerror = @onError
@relay.onmessage = @onRelayToClientMessage
onClientToRelayMessage: (event) ->
@c2r_schedule.push event.data
# WebRTC --> websocket
onClientToRelayMessage: (msg) =>
line = recv = msg.data
console.log msg
# Go sends only raw bytes...
if '[object ArrayBuffer]' == recv.toString()
bytes = new Uint8Array recv
line = String.fromCharCode.apply(null, bytes)
line = line.trim()
log 'WebRTC-->websocket data: ' + line
@c2rSchedule.push recv
@flush()
onRelayToClientMessage: (event) ->
@r2c_schedule.push event.data
# websocket --> WebRTC
onRelayToClientMessage: (event) =>
@r2cSchedule.push event.data
log 'websocket-->WebRTC data: ' + event.data
@flush()
onClose: (event) ->
onClose: (event) =>
ws = event.target
log(ws.label + ': closed.')
@flush()
@maybeCleanup()
onError: (event) ->
onError: (event) =>
ws = event.target
log ws.label + ': error.'
this.close();
@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.
@maybeCleanup()
webrtcIsReady: -> null != @client && 'open' == @client.readyState
isOpen: (ws) -> undefined != ws && WebSocket.OPEN == ws.readyState
isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
close: ->
@client.close() if !(isClosed @client)
@relay.close() if !(isClosed @relay)
maybeCleanup: ->
if @running && @isClosed(client) && @isClosed @relay
@ -402,66 +418,44 @@ class ProxyPair
# Send as much data as the rate limit currently allows.
flush: ->
###
clearTimeout @flush_timeout_id if @flush_timeout_id
@flush_timeout_id = null
busy = true
checkChunks = ->
checkChunks = =>
busy = false
# if @isOpen @clientthis.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 @isOpen @relay &&
@relay.bufferedAmount < MAX_BUFFER &&
@c2r_schedule.length > 0
chunk = @c2r_schedule.shift()
@rate_limit.update chunk.length
# websocket --> WebRTC
if @webrtcIsReady() && @client.bufferedAmount < @MAX_BUFFER && @r2cSchedule.length > 0
chunk = @r2cSchedule.shift()
# this.rate_limit.update(chunk.length)
@client.send chunk
busy = true
# WebRTC --> websocket
if (@isOpen @relay) && (@relay.bufferedAmount < @MAX_BUFFER) && @c2rSchedule.length > 0
chunk = @c2rSchedule.shift()
# @rate_limit.update chunk.length
@relay.send chunk
busy = true
checkChunks() while busy && !@rate_limit.is_limited()
checkChunks() while busy # && !@rate_limit.is_limited()
if @isClosed @relay &&
# !isClosed(this.client_s) &&
# @client_s.bufferedAmount === 0 &&
@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();
# }
# TODO: rate limiting stuff
# if @r2cSchedule.length > 0 || (@client) && @client.bufferedAmount > 0) || @c2rSchedule.length > 0 || (@isOpen(@relay) && @relay.bufferedAmount > 0)
# @flush_timeout_id = setTimeout @flush, @rate_limit.when() * 1000
while busy && !@rate_limit.is_limited()
if this.r2c_schedule.length > 0 ||
(@isOpen(@client) && @client.bufferedAmount > 0) ||
@c2r_schedule.length > 0 ||
(@isOpen(@relay) && @relay.bufferedAmount > 0)
@flush_timeout_id = setTimeout @flush, @rate_limit.when() * 1000
###
#
## -- DOM & Input Functionality -- ##
#
snowflake = null
welcome = ->
log "== snowflake browser proxy =="
log "Input desired relay address:"
log '== snowflake browser proxy =='
log 'Input desired relay address:'
# Log to the message window.
log = (msg) ->
$chatlog.value += msg + "\n"
console.log msg
# Scroll to latest
if $chatlog
$chatlog.value += msg + '\n'
$chatlog.scrollTop = $chatlog.scrollHeight
Interface =
@ -471,44 +465,46 @@ Interface =
switch snowflake.state
when MODE.INIT
# Set target relay.
snowflake.setRelayAddr msg
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."
log 'No input expected - WebRTC connected.'
# data = msg
# log(data)
# channel.send(data)
else
log "ERROR: " + msg
$input.value = ""
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 '---- Please copy the below to peer ----\n'
log JSON.stringify(msg)
log "\n"
log '\n'
receive: (msg) ->
recv = ""
recv = ''
try
recv = JSON.parse msg
catch e
log "Invalid JSON."
log 'Invalid JSON.'
return
desc = recv['sdp']
if !desc
log "Invalid SDP."
log 'Invalid SDP.'
return false
snowflake.receiveOffer recv if desc
init = ->
$chatlog = document.getElementById('chatlog')
$chatlog.value = ""
$chatlog.value = ''
$send = document.getElementById('send')
$send.onclick = Interface.acceptInput

View file

@ -188,7 +188,14 @@ testParseQueryString = ->
else
fail test.qs, test.expected, actual
testProxyPair = ->
announce 'testProxyPair'
addr = Params.parseAddress '0.0.0.0:35302'
console.log addr
pair = new ProxyPair(null, addr, 0)
pair.connectRelay()
testBuildUrl()
testParseCookieString()
testParseQueryString()
# testProxyPair()