diff --git a/proxy/Cakefile b/proxy/Cakefile index de1982d..3179247 100644 --- a/proxy/Cakefile +++ b/proxy/Cakefile @@ -2,7 +2,11 @@ fs = require 'fs' {exec} = require 'child_process' -task 'test', 'test snowflake.coffee', () -> - exec 'coffee snowflake_test.coffee -v', (err, stdout, stderr) -> +task 'test', 'snowflake unit tests', () -> + testFile = 'test/snowflake.bundle.coffee' + exec 'cat snowflake.coffee 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 diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee index 2723773..dfc236f 100644 --- a/proxy/snowflake.coffee +++ b/proxy/snowflake.coffee @@ -10,6 +10,16 @@ this must always act as the answerer. TODO(keroserene): Complete the websocket + webrtc ProxyPair ### +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 @@ -26,8 +36,7 @@ Query = strings = [] strings = qs.split '&' if qs return result if 0 == strings.length - for i in [1..strings.length] - string = strings[i] + for string in strings j = string.indexOf '=' if j == -1 name = string @@ -37,7 +46,7 @@ Query = value = string.substr(j + 1) name = decodeURIComponent(name.replace(/\+/g, ' ')) value = decodeURIComponent(value.replace(/\+/g, ' ')) - result[name] = value if !(name in result) + result[name] = value if name not of result result # params is a list of (key, value) 2-tuples. @@ -56,6 +65,22 @@ Params = return false if "false" == val || "0" == val return null + + # 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 + parseCookie: (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 + # repr = (x) -> # return 'null' if null == x # return 'undefined' if 'undefined' == typeof x @@ -72,9 +97,11 @@ Params = safe_repr = (s) -> SAFE_LOGGING ? "[scrubbed]" : JSON.stringify(s) # HEADLESS is true if we are running not in a browser with a DOM. -query = Query.parse(window.location.search.substr(1)) +DEBUG = false +if window && window.location + query = Query.parse(window.location.search.substr(1)) + DEBUG = Params.getBool(query, "debug", false) HEADLESS = "undefined" == typeof(document) -DEBUG = Params.getBool(query, "debug", false) # TODO: Different ICE servers. config = { @@ -88,12 +115,6 @@ $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 @@ -112,6 +133,7 @@ class Snowflake pc: null rateLimit: 0 proxyPairs: [] + relayAddr: null badge: null $badge: null MAX_NUM_CLIENTS = 1 @@ -126,8 +148,13 @@ class Snowflake else @badge = new Badge() @$badgem = @badge.elem - if (@$badge) - @$badge.setAttribute("id", "snowflake-badge") + @$badge.setAttribute("id", "snowflake-badge") if (@$badge) + + setRelayAddr: (relayAddr) -> + # TODO: User-supplied for now, but should fetch from facilitator later. + @relayAddr = relayAddr + log "Input offer from the snowflake client:" + @beginWebRTC() # Initialize WebRTC PeerConnection beginWebRTC: -> @@ -240,6 +267,9 @@ class Snowflake @badge.die() if @badge +DEFAULT_PORTS = + http: 80 + https: 443 # 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) -> @@ -298,9 +328,6 @@ makeWebsocket = (addr) -> # TODO: Implement class ProxyPair - # TODO: Hardcoded for now, but should fetch from facilitator later. - relayAddr: null - constructor: (@clientAddr, @relayAddr, @rateLimit) -> # Assumes WebRTC part is already connected. @@ -405,7 +432,7 @@ snowflake = null welcome = -> log "== snowflake browser proxy ==" - log "Input offer from the snowflake client:" + log "Input desired relay address:" # Log to the message window. log = (msg) -> @@ -419,6 +446,9 @@ Interface = acceptInput: -> msg = $input.value switch snowflake.state + when MODE.INIT + # Set target relay. + snowflake.setRelayAddr msg when MODE.WEBRTC_CONNECTING Signalling.receive msg when MODE.WEBRTC_READY @@ -453,6 +483,7 @@ Signalling = snowflake.receiveOffer recv if desc init = -> + $chatlog = document.getElementById('chatlog') $chatlog.value = "" @@ -465,7 +496,6 @@ init = -> if 13 == e.keyCode # enter $send.onclick() snowflake = new Snowflake() - snowflake.beginWebRTC() welcome() -window.onload = init +window.onload = init if window diff --git a/proxy/snowflake_test.coffee b/proxy/snowflake_test.coffee index 9db56b3..8164b6a 100644 --- a/proxy/snowflake_test.coffee +++ b/proxy/snowflake_test.coffee @@ -1,80 +1,194 @@ -s = require './snowflake' +# s = require './snowflake' + +window = {} VERBOSE = false -if process.argv.indexOf("-v") >= 0 +if process.argv.indexOf('-v') >= 0 VERBOSE = true numTests = 0 numFailed = 0 announce = (testName) -> if VERBOSE - # if (!top) - # console.log(); - console.log testName - # top = false + console.log '\n --- ' + testName + ' ---' pass = (test) -> numTests++; if VERBOSE - console.log "PASS " + test + console.log 'PASS ' + test fail = (test, expected, actual) -> numTests++ numFailed++ - console.log "FAIL " + test + " expected: " + expected + " actual: " + actual + console.log 'FAIL ' + test + + ' expected: ' + JSON.stringify(expected) + + ' actual: ' + JSON.stringify(actual) testBuildUrl = -> TESTS = [{ - args: ["http", "example.com"] - expected: "http://example.com" + args: ['http', 'example.com'] + expected: 'http://example.com' },{ - args: ["http", "example.com", 80] - expected: "http://example.com" + args: ['http', 'example.com', 80] + expected: 'http://example.com' },{ - args: ["http", "example.com", 81], - expected: "http://example.com:81" + args: ['http', 'example.com', 81], + expected: 'http://example.com:81' },{ - args: ["https", "example.com", 443] - expected: "https://example.com" + args: ['https', 'example.com', 443] + expected: 'https://example.com' },{ - args: ["https", "example.com", 444] - expected: "https://example.com:444" + args: ['https', 'example.com', 444] + expected: 'https://example.com:444' },{ - args: ["http", "example.com", 80, "/"] - expected: "http://example.com/" + args: ['http', 'example.com', 80, '/'] + expected: 'http://example.com/' },{ - args: ["http", "example.com", 80, "/test?k=%#v"] - expected: "http://example.com/test%3Fk%3D%25%23v" + args: ['http', 'example.com', 80, '/test?k=%#v'] + expected: 'http://example.com/test%3Fk%3D%25%23v' },{ - args: ["http", "example.com", 80, "/test", []] - expected: "http://example.com/test?" + args: ['http', 'example.com', 80, '/test', []] + expected: 'http://example.com/test?' },{ - args: ["http", "example.com", 80, "/test", [["k", "%#v"]]] - expected: "http://example.com/test?k=%25%23v" + args: ['http', 'example.com', 80, '/test', [['k', '%#v']]] + expected: 'http://example.com/test?k=%25%23v' },{ - args: ["http", "example.com", 80, "/test", [["a", "b"], ["c", "d"]]] - expected: "http://example.com/test?a=b&c=d" + args: ['http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']]] + expected: 'http://example.com/test?a=b&c=d' },{ - args: ["http", "1.2.3.4"] - expected: "http://1.2.3.4" + args: ['http', '1.2.3.4'] + expected: 'http://1.2.3.4' },{ - args: ["http", "1:2::3:4"] - expected: "http://[1:2::3:4]" + args: ['http', '1:2::3:4'] + expected: 'http://[1:2::3:4]' },{ - args: ["http", "bog][us"] - expected: "http://bog%5D%5Bus" + args: ['http', 'bog][us'] + expected: 'http://bog%5D%5Bus' },{ - args: ["http", "bog:u]s"] - expected: "http://bog%3Au%5Ds" - } - ] - announce "-- testBuildUrl --" + args: ['http', 'bog:u]s'] + expected: 'http://bog%3Au%5Ds' + }] + + announce 'testBuildUrl' for test in TESTS - actual = s.buildUrl.apply undefined, test.args + actual = buildUrl.apply undefined, test.args if actual == test.expected pass test.args else fail test.args, test.expected, actual +### +This test only checks that things work for strings formatted like +document.cookie. Browsers maintain several properties about this string, for +example cookie names are unique with no trailing whitespace. See +http://www.ietf.org/rfc/rfc2965.txt for the grammar. +### +testParseCookieString = -> + TESTS = [{ + cs: '' + expected: { } + },{ + cs: 'a=b' + expected: { a: 'b'} + },{ + cs: 'a=b=c' + expected: { a: 'b=c' } + },{ + cs: 'a=b; c=d' + expected: { a: 'b', c: 'd' } + },{ + cs: 'a=b ; c=d' + expected: { a: 'b', c: 'd' } + },{ + cs: 'a= b', + expected: {a: 'b' } + },{ + cs: 'a=' + expected: { a: '' } + }, { + cs: 'key', + expected: null + }, { + cs: 'key=%26%20' + expected: { key: '& ' } + }, { + cs: 'a=\'\'' + expected: { a: '\'\'' } + }] + + announce 'testParseCookieString' + for test in TESTS + actual = Params.parseCookie test.cs + if JSON.stringify(actual) == JSON.stringify(test.expected) + pass test.cs + else + fail test.cs, test.expected, actual + +testParseQueryString = -> + TESTS = [{ + qs: '' + expected: {} + },{ + qs: 'a=b' + expected: { a: 'b' } + },{ + qs: 'a=b=c' + expected: { a: 'b=c' } + },{ + qs: 'a=b&c=d' + expected: { a: 'b', c: 'd' } + },{ + qs: 'client=&relay=1.2.3.4%3A9001' + expected: { client: '', relay: '1.2.3.4:9001' } + },{ + qs: 'a=b%26c=d' + expected: { a: 'b&c=d' } + },{ + qs: 'a%3db=d' + expected: { 'a=b': 'd' } + },{ + qs: 'a=b+c%20d' + expected: { 'a': 'b c d' } + },{ + qs: 'a=b+c%2bd' + expected: { 'a': 'b c+d' } + },{ + qs: 'a+b=c' + expected: { 'a b': 'c' } + },{ + qs: 'a=b+c+d' + expected: { a: 'b c d' } + # First appearance wins. + },{ + qs: 'a=b&c=d&a=e' + expected: { a: 'b', c: 'd' } + },{ + qs: 'a' + expected: { a: '' } + },{ + qs: '=b', + expected: { '': 'b' } + },{ + qs: '&a=b' + expected: { '': '', a: 'b' } + },{ + qs: 'a=b&' + expected: { a: 'b', '':'' } + },{ + qs: 'a=b&&c=d' + expected: { a: 'b', '':'', c: 'd' } + }] + + announce 'testParseQueryString' + for test in TESTS + actual = Query.parse test.qs + if JSON.stringify(actual) == JSON.stringify(test.expected) + pass test.qs + else + fail test.qs, test.expected, actual + + testBuildUrl() +testParseCookieString() +testParseQueryString()