snowflake/proxy/snowflake.coffee
2016-01-09 21:10:09 -08:00

232 lines
5.7 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.
###
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
error.
Always decodes from UTF-8, not any other encoding.
http://dev.w3.org/html5/spec/Overview.html#url-encoded-form-data
###
parse: (qs) ->
result = {}
strings = []
strings = qs.split '&' if qs
return result if 0 == strings.length
for i in [1..strings.length]
string = strings[i]
j = string.indexOf '='
if j == -1
name = string
value = ''
else
name = string.substr(0, j)
value = string.substr(j + 1)
name = decodeURIComponent(name.replace(/\+/g, ' '))
value = decodeURIComponent(value.replace(/\+/g, ' '))
result[name] = value if !(name in result)
result
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 null
# HEADLESS is true if we are running not in a browser with a DOM.
query = Query.parse(window.location.search.substr(1))
HEADLESS = "undefined" == typeof(document)
DEBUG = Params.getBool(query, "debug", false)
# Janky state machine
MODE =
INIT: 0
CONNECTING: 1
CHAT: 2
currentMode = MODE.INIT
# TODO: Different ICE servers.
config = {
iceServers: [
{ urls: ["stun:stun.l.google.com:19302"] }
]
}
# DOM elements
$chatlog = null
$send = null
$input = null
window.PeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription
# TODO: Implement
class Badge
# Minimum viable snowflake for now - just 1 client.
class Snowflake
rateLimit: 0
proxyPairs: null
badge: null
$badge: null
MAX_NUM_CLIENTS = 1
constructor: ->
if HEADLESS
# No badge
else if DEBUG
@$badge = debug_div
else
@badge = new Badge()
@$badgem = @badge.elem
if (@$badge)
@$badge.setAttribute("id", "snowflake-badge")
start: ->
log "Starting up Snowflake..."
# 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 [0..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
proxy_pair.connect()
catch err
log "ProxyPair: exception while connecting: " + safe_repr(err.message) + "."
return
@badge.beginProxy if @badge
cease = ->
# @start = null
# @proxyMain = null
# @make_proxy_pair = function(client_addr, relay_addr) { };
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
# TODO: Implement
class ProxyPair
#
## -- DOM & Input Functionality -- ##
#
snowflake = new Snowflake()
welcome = ->
log "== snowflake browser proxy =="
log "Input offer from the snowflake client:"
# Log to the message window.
log = (msg) ->
$chatlog.value += msg + "\n"
console.log msg
# Scroll to latest
$chatlog.scrollTop = $chatlog.scrollHeight
# Local input from keyboard into message window.
acceptInput = () ->
msg = $input.value
switch currentMode
when MODE.INIT
if msg.startsWith("start")
start(true)
else
Signalling.receive msg
when MODE.CONNECTING
Signalling.receive msg
when MODE.CHAT
data = msg
log(data)
channel.send(data)
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
# Begin as answerer if peerconnection doesn't exist yet.
snowflake.start false if !pc
desc = recv['sdp']
ice = recv['candidate']
if !desc && ! ice
log "Invalid SDP."
return false
receiveDescription recv if desc
receiveICE recv if ice
init = ->
$chatlog = document.getElementById('chatlog')
$chatlog.value = ""
$send = document.getElementById('send')
$send.onclick = acceptInput
$input = document.getElementById('input')
$input.focus()
$input.onkeydown = (e) =>
if 13 == e.keyCode # enter
$send.onclick()
welcome()
window.onload = init