split and organize coffee files, improve Cake build tasks

This commit is contained in:
Serene Han 2016-01-14 16:28:32 -08:00
parent e433af26f8
commit fa9f58c9ab
10 changed files with 517 additions and 391 deletions

View file

@ -49,12 +49,20 @@ At this point the tor client should bootstrap to 100%.
#### Snowflake proxy
Otherwise, to connect through the WebRTC proxy in the browser, start a local
http server in the `proxy/` directory however you wish. For instance:
Otherwise, to connect through the WebRTC proxy in the browser, build with:
```
cd proxy/
python -m http.server
cake build
```
Then start a local http server in the `proxy/build/` however you wish
For instance:
'''
cd build/
python -m http.server
'''
Open a browser tab to `0.0.0.0:8000/snowflake.html`.
The page will ask you to input a relay.
Input your desired relay address, or input nothing/gibberish which will cause

View file

@ -1,17 +1,44 @@
fs = require 'fs'
{exec} = require 'child_process'
# All coffeescript files required.
FILES = [
'shims.coffee'
'util.coffee'
'proxypair.coffee'
'websocket.coffee'
'snowflake.coffee'
]
OUTFILE = 'build/snowflake.coffee'
STATIC = 'static'
task 'test', 'snowflake unit tests', ->
testFile = 'test/snowflake.bundle.coffee'
exec 'cat snowflake.coffee snowflake_test.coffee | cat > ' + testFile, (err, stdout, stderr) ->
exec 'cat ' + FILES.join(' ') + ' snowflake_test.coffee | cat > ' +
testFile, (err, stdout, stderr) ->
throw err if err
console.log stdout + stderr
exec 'coffee ' + testFile + ' -v', (err, stdout, stderr) ->
throw err if err
console.log stdout + stderr
task 'build', 'build the snowflake proxy', ->
exec 'coffee -o build -c snowflake.coffee', (err, stdout, stderr) ->
concatCoffeeFiles = -> exec 'cat ' + FILES.join(' ') + ' | cat > ' + OUTFILE
copyStaticFiles = -> exec 'cp ' + STATIC + '/* build/'
compileCoffee = ->
exec 'coffee -o build -c build/snowflake.coffee', (err, stdout, stderr) ->
throw err if err
console.log stdout + stderr
task 'build:concat', 'concatenate coffeescript files.', concatCoffeeFiles
task 'build', 'build the snowflake proxy', ->
exec 'mkdir build'
concatCoffeeFiles()
copyStaticFiles()
compileCoffee()
console.log 'Snowflake prepared.'
task 'clean', 'remove all built files', ->
exec 'rm -r build'
exec 'rm -r test'

142
proxy/proxypair.coffee Normal file
View file

@ -0,0 +1,142 @@
###
Represents a single:
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) ->
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
$msglog.className = 'active';
# 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.'
snowflake.state = MODE.INIT;
$msglog.className = ''
channel.onerror = =>
log 'Data channel error!'
channel.onmessage = @onClientToRelayMessage
# Assumes WebRTC datachannel is connected.
connectRelay: =>
log 'Connecting to relay...'
@relay = makeWebsocket @relayAddr
@relay.label = 'websocket-relay'
@relay.onopen = =>
log '\nRelay ' + @relay.label + ' connected!'
@relay.onclose = @onClose
@relay.onerror = @onError
@relay.onmessage = @onRelayToClientMessage
# 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()
console.log 'WebRTC --> websocket data: ' + line
@c2rSchedule.push recv
@flush()
# websocket --> WebRTC
onRelayToClientMessage: (event) =>
@r2cSchedule.push event.data
# log 'websocket-->WebRTC data: ' + event.data
@flush()
onClose: (event) =>
ws = event.target
log(ws.label + ': closed.')
@flush()
@maybeCleanup()
onError: (event) =>
ws = event.target
log ws.label + ': error.'
@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
relayIsReady: -> (null != @relay) && (WebSocket.OPEN == @relay.readyState)
isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
close: ->
@client.close() if @webrtcIsReady()
@relay.close() if @relayIsReady()
relay = null
maybeCleanup: =>
if @running
@running = false
# TODO: Call external callback
true
false
# 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 = =>
busy = false
# WebRTC --> websocket
if @relayIsReady() && @relay.bufferedAmount < @MAX_BUFFER && @c2rSchedule.length > 0
chunk = @c2rSchedule.shift()
@rateLimit.update chunk.length
@relay.send chunk
busy = true
# websocket --> WebRTC
if @webrtcIsReady() && @client.bufferedAmount < @MAX_BUFFER && @r2cSchedule.length > 0
chunk = @r2cSchedule.shift()
@rateLimit.update chunk.length
@client.send chunk
busy = true
checkChunks() while busy && !@rateLimit.isLimited()
if @r2cSchedule.length > 0 || @c2rSchedule.length > 0 || (@relayIsReady() && @relay.bufferedAmount > 0) || (@webrtcIsReady() && @client.bufferedAmount > 0)
@flush_timeout_id = setTimeout @flush, @rateLimit.when() * 1000

14
proxy/shims.coffee Normal file
View file

@ -0,0 +1,14 @@
###
WebrTC shims for multiple browsers.
###
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.RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription

View file

@ -7,137 +7,16 @@ 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
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.RTCSessionDescription = window.RTCSessionDescription ||
window.mozRTCSessionDescription
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 string in strings
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 not of result
result
# params is a list of (key, value) 2-tuples.
buildString: (params) ->
parts = []
for param in params
parts.push encodeURIComponent(param[0]) + '=' +
encodeURIComponent(param[1])
parts.join '&'
Parse =
# Parse a cookie data string (usually document.cookie). The return type is an
# object mapping cookies names to values. Returns null on error.
# http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
cookie: (cookies) ->
result = {}
strings = []
strings = cookies.split ';' if cookies
for string in strings
j = string.indexOf '='
return null if -1 == j
name = decodeURIComponent string.substr(0, j).trim()
value = decodeURIComponent string.substr(j + 1).trim()
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.
address: (spec) ->
m = null
# IPv6 syntax.
m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/) if !m
# IPv4 syntax.
m = spec.match(/^([0-9.]+):([0-9]+)$/) if !m
return null if !m
host = m[1]
port = parseInt(m[2], 10)
if isNaN(port) || port < 0 || port > 65535
return null
{ host: host, port: port }
# Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase)
# does what you would think. Returns null on error.
byteCount: (spec) ->
UNITS = {
k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024
K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
}
matches = spec.match /^(\d+(?:\.\d*)?)(\w*)$/
return null if null == matches
count = Number matches[1]
return null if isNaN count
if '' == matches[2]
units = 1
else
units = UNITS[matches[2]]
return null if null == units
count * Number(units)
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
# Get an object value and parse it as a byte count. Example byte counts are
# '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null on
# a parsing error.
getByteCount: (query, param, defaultValue) ->
spec = query[param]
return defaultValue if undefined == spec
Parse.byteCount spec
# Get an object value and parse it as an address spec. Returns |defaultValue|
# if param is not a key. Returns null on a parsing error.
getAddress: (query, param, defaultValue) ->
val = query[param]
return defaultValue if undefined == val
Parse.address val
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 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.
@ -145,102 +24,8 @@ DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT || undefined
MIN_RATE_LIMIT = 10 * 1024
RATE_LIMIT_HISTORY = 5.0
DEFAULT_PORTS =
http: 80
https: 443
# DOM elements.
$msglog = null
$send = null
$input = null
# Build an escaped URL string from unescaped components. Only scheme and host
# are required. See RFC 3986, section 3.
buildUrl = (scheme, host, port, path, params) ->
parts = []
parts.push(encodeURIComponent scheme)
parts.push '://'
# If it contains a colon but no square brackets, treat it as IPv6.
if host.match(/:/) && !host.match(/[[\]]/)
parts.push '['
parts.push host
parts.push ']'
else
parts.push(encodeURIComponent host)
if undefined != port && DEFAULT_PORTS[scheme] != port
parts.push ':'
parts.push(encodeURIComponent port.toString())
if undefined != path && '' != path
if !path.match(/^\//)
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
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 = path.replace /[^\/]+/, (m) ->
encodeURIComponent m
parts.push path
if undefined != params
parts.push '?'
parts.push Query.buildString params
parts.join ''
makeWebsocket = (addr) ->
url = buildUrl 'ws', addr.host, addr.port, '/'
# if have_websocket_binary_frames()
ws = new WebSocket url
# else
# ws = new WebSocket url 'base64'
###
'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.'
###
ws.binaryType = 'arraybuffer'
ws
class BucketRateLimit
amount: 0.0
lastUpdate: new Date()
constructor: (@capacity, @time) ->
age: ->
now = new Date()
delta = (now - @lastUpdate) / 1000.0
@lastUpdate = now
@amount -= delta * @capacity / @time
@amount = 0.0 if @amount < 0.0
update: (n) ->
@age()
@amount += n
@amount <= @capacity
# How many seconds in the future will the limit expire?
when: ->
age()
(@amount - @capacity) / (@capacity / @time)
isLimited: ->
@age()
@amount > @capacity
# A rate limiter that never limits.
class DummyRateLimit
constructor: (@capacity, @time) ->
update: (n) -> true
when: -> 0.0
isLimited: -> false
MAX_NUM_CLIENTS = 1
CONNECTIONS_PER_CLIENT = 1
# TODO: Different ICE servers.
config = {
@ -249,7 +34,6 @@ config = {
]
}
# TODO: Implement
class Badge
@ -262,9 +46,6 @@ MODE =
# 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)
@ -295,7 +76,7 @@ class Snowflake
@rateLimit = new BucketRateLimit(rateLimitBytes * RATE_LIMIT_HISTORY,
RATE_LIMIT_HISTORY)
# TODO: User-supplied for now, but should fetch from facilitator later.
# TODO: Should fetch from facilitator later.
setRelayAddr: (relayAddr) ->
addr = Parse.address relayAddr
if !addr
@ -323,7 +104,7 @@ class Snowflake
catch e
log 'Invalid SDP message.'
return false
log('SDP ' + sdp.type + ' successfully received.')
log 'SDP ' + sdp.type + ' successfully received.'
@sendAnswer() if 'offer' == sdp.type
true
@ -336,7 +117,7 @@ class Snowflake
# Poll facilitator when this snowflake can support more clients.
proxyMain: ->
if @proxyPairs.length >= @MAX_NUM_CLIENTS * @CONNECTIONS_PER_CLIENT
if @proxyPairs.length >= MAX_NUM_CLIENTS * CONNECTIONS_PER_CLIENT
setTimeout(@proxyMain, @facilitator_poll_interval * 1000)
return
params = [['r', '1']]
@ -372,159 +153,16 @@ class Snowflake
@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) ->
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
$msglog.className = 'active';
# 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.'
snowflake.state = MODE.INIT;
$msglog.className = ''
channel.onerror = =>
log 'Data channel error!'
channel.onmessage = @onClientToRelayMessage
# Assumes WebRTC datachannel is connected.
connectRelay: =>
log 'Connecting to relay...'
@relay = makeWebsocket @relayAddr
@relay.label = 'websocket-relay'
@relay.onopen = =>
log '\nRelay ' + @relay.label + ' connected!'
@relay.onclose = @onClose
@relay.onerror = @onError
@relay.onmessage = @onRelayToClientMessage
# 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()
console.log 'WebRTC --> websocket data: ' + line
@c2rSchedule.push recv
@flush()
# websocket --> WebRTC
onRelayToClientMessage: (event) =>
@r2cSchedule.push event.data
# log 'websocket-->WebRTC data: ' + event.data
@flush()
onClose: (event) =>
ws = event.target
log(ws.label + ': closed.')
@flush()
@maybeCleanup()
onError: (event) =>
ws = event.target
log ws.label + ': error.'
@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
relayIsReady: -> (null != @relay) && (WebSocket.OPEN == @relay.readyState)
isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
close: ->
@client.close() if @webrtcIsReady()
@relay.close() if @relayIsReady()
relay = null
maybeCleanup: =>
if @running
@running = false
# TODO: Call external callback
true
false
# 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 = =>
busy = false
# WebRTC --> websocket
if @relayIsReady() && @relay.bufferedAmount < @MAX_BUFFER && @c2rSchedule.length > 0
chunk = @c2rSchedule.shift()
@rateLimit.update chunk.length
@relay.send chunk
busy = true
# websocket --> WebRTC
if @webrtcIsReady() && @client.bufferedAmount < @MAX_BUFFER && @r2cSchedule.length > 0
chunk = @r2cSchedule.shift()
@rateLimit.update chunk.length
@client.send chunk
busy = true
checkChunks() while busy && !@rateLimit.isLimited()
if @r2cSchedule.length > 0 || @c2rSchedule.length > 0 || (@relayIsReady() && @relay.bufferedAmount > 0) || (@webrtcIsReady() && @client.bufferedAmount > 0)
@flush_timeout_id = setTimeout @flush, @rateLimit.when() * 1000
#
## -- DOM & Input Functionality -- ##
#
snowflake = null
welcome = ->
log '== snowflake browser proxy =='
log 'Input desired relay address:'
#
## -- DOM & Inputs -- #
#
# Log to the message window.
log = (msg) ->
console.log msg
# Scroll to latest
if $msglog
$msglog.value += msg + '\n'
$msglog.scrollTop = $msglog.scrollHeight
# DOM elements references.
$msglog = null
$send = null
$input = null
Interface =
# Local input from keyboard into message window.
@ -550,7 +188,7 @@ Interface =
Signalling =
send: (msg) ->
log '---- Please copy the below to peer ----\n'
log JSON.stringify(msg)
log JSON.stringify msg
log '\n'
receive: (msg) ->
@ -566,8 +204,18 @@ Signalling =
return false
snowflake.receiveOffer recv if desc
init = ->
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 = ''
@ -576,9 +224,8 @@ init = ->
$input = document.getElementById('input')
$input.focus()
$input.onkeydown = (e) =>
if 13 == e.keyCode # enter
$send.onclick()
$input.onkeydown = (e) => $send.onclick() if 13 == e.keyCode # enter
snowflake = new Snowflake()
window.snowflake = snowflake
welcome()

View file

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Before After
Before After

View file

@ -0,0 +1,78 @@
<!doctype html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<script type="text/javascript" src="snowflake.js"></script>
<style>
* {
box-sizing: border-box;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
body {
position: absolute;
width: 100%; height: 100%; top: 0; margin: 0 auto;
background-color: #424;
color: #000;
text-align: center;
font-size: 24px;
font-family: monospace;
background-image: url('koch.jpg');
}
textarea {
background-color: rgba(0,0,0,0.8);
color: #fff;
resize: none;
}
.chatarea {
position: relative; border: none;
width: 50%; min-width: 40em;
padding: 0.5em; margin: auto;
}
.active { background-color: rgba(0,50,0,0.8); }
#msglog {
display: block;
width: 100%;
min-height: 40em;
margin-bottom: 1em;
padding: 8px;
}
.inputarea {
position: relative;
width: 100%;
height: 3em;
display: block;
}
#input {
display: inline-block;
position: absolute; left: 0;
width: 89%; height: 100%;
padding: 8px 30px;
font-size: 80%;
color: #fff;
background-color: rgba(0,0,0,0.9);
border: 1px solid #999;
}
#send {
display: inline-block; position: absolute;
right: 0; top: 0; height: 100%; width: 10%;
background-color: #202; color: #f8f;
font-variant: small-caps; font-size: 100%;
border: none; // box-shadow: 0 2px 5px #000;
}
#send:hover { background-color: #636; }
</style>
</head>
<body>
<div class="chatarea">
<textarea id="msglog" readonly>
</textarea>
<div class="inputarea">
<input type="text" id="input">
<input type="submit" id="send" value="send">
</div>
</div>
</body>
</html>

154
proxy/util.coffee Normal file
View file

@ -0,0 +1,154 @@
###
A Coffeescript WebRTC snowflake proxy
Contains helpers for parsing query strings and other utilities.
###
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 string in strings
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 not of result
result
# params is a list of (key, value) 2-tuples.
buildString: (params) ->
parts = []
for param in params
parts.push encodeURIComponent(param[0]) + '=' +
encodeURIComponent(param[1])
parts.join '&'
Parse =
# Parse a cookie data string (usually document.cookie). The return type is an
# object mapping cookies names to values. Returns null on error.
# http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
cookie: (cookies) ->
result = {}
strings = []
strings = cookies.split ';' if cookies
for string in strings
j = string.indexOf '='
return null if -1 == j
name = decodeURIComponent string.substr(0, j).trim()
value = decodeURIComponent string.substr(j + 1).trim()
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.
address: (spec) ->
m = null
# IPv6 syntax.
m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/) if !m
# IPv4 syntax.
m = spec.match(/^([0-9.]+):([0-9]+)$/) if !m
return null if !m
host = m[1]
port = parseInt(m[2], 10)
if isNaN(port) || port < 0 || port > 65535
return null
{ host: host, port: port }
# Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase)
# does what you would think. Returns null on error.
byteCount: (spec) ->
UNITS = {
k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024
K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
}
matches = spec.match /^(\d+(?:\.\d*)?)(\w*)$/
return null if null == matches
count = Number matches[1]
return null if isNaN count
if '' == matches[2]
units = 1
else
units = UNITS[matches[2]]
return null if null == units
count * Number(units)
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
# Get an object value and parse it as a byte count. Example byte counts are
# '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null on
# a parsing error.
getByteCount: (query, param, defaultValue) ->
spec = query[param]
return defaultValue if undefined == spec
Parse.byteCount spec
# Get an object value and parse it as an address spec. Returns |defaultValue|
# if param is not a key. Returns null on a parsing error.
getAddress: (query, param, defaultValue) ->
val = query[param]
return defaultValue if undefined == val
Parse.address val
class BucketRateLimit
amount: 0.0
lastUpdate: new Date()
constructor: (@capacity, @time) ->
age: ->
now = new Date()
delta = (now - @lastUpdate) / 1000.0
@lastUpdate = now
@amount -= delta * @capacity / @time
@amount = 0.0 if @amount < 0.0
update: (n) ->
@age()
@amount += n
@amount <= @capacity
# How many seconds in the future will the limit expire?
when: ->
age()
(@amount - @capacity) / (@capacity / @time)
isLimited: ->
@age()
@amount > @capacity
# A rate limiter that never limits.
class DummyRateLimit
constructor: (@capacity, @time) ->
update: (n) -> true
when: -> 0.0
isLimited: -> false

56
proxy/websocket.coffee Normal file
View file

@ -0,0 +1,56 @@
###
Only websocket-specific stuff.
###
# Build an escaped URL string from unescaped components. Only scheme and host
# are required. See RFC 3986, section 3.
buildUrl = (scheme, host, port, path, params) ->
parts = []
parts.push(encodeURIComponent scheme)
parts.push '://'
# If it contains a colon but no square brackets, treat it as IPv6.
if host.match(/:/) && !host.match(/[[\]]/)
parts.push '['
parts.push host
parts.push ']'
else
parts.push(encodeURIComponent host)
if undefined != port && DEFAULT_PORTS[scheme] != port
parts.push ':'
parts.push(encodeURIComponent port.toString())
if undefined != path && '' != path
if !path.match(/^\//)
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
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 = path.replace /[^\/]+/, (m) ->
encodeURIComponent m
parts.push path
if undefined != params
parts.push '?'
parts.push Query.buildString params
parts.join ''
makeWebsocket = (addr) ->
url = buildUrl 'ws', addr.host, addr.port, '/'
# TODO: Do we need to worry about the base64 version?
ws = new WebSocket url
###
'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.'
###
ws.binaryType = 'arraybuffer'
ws