snowflake/proxy/snowflake.coffee

292 lines
7.5 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)
# 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
# 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: null
badge: null
$badge: null
MAX_NUM_CLIENTS = 1
state: MODE.INIT
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")
# 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
# TODO: Prepare ProxyPair onw.
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 [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 = null
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
Interface =
# Local input from keyboard into message window.
acceptInput: ->
msg = $input.value
switch snowflake.state
when MODE.WEBRTC_CONNECTING
Signalling.receive msg
when MODE.WEBRTC_READY
log "No input expected - WebRTC connected."
# 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
desc = recv['sdp']
if !desc
log "Invalid SDP."
return false
snowflake.receiveOffer recv if desc
init = ->
$chatlog = document.getElementById('chatlog')
$chatlog.value = ""
$send = document.getElementById('send')
$send.onclick = Interface.acceptInput
$input = document.getElementById('input')
$input.focus()
$input.onkeydown = (e) =>
if 13 == e.keyCode # enter
$send.onclick()
snowflake = new Snowflake()
snowflake.beginWebRTC()
welcome()
window.onload = init