[tor-commits] [snowflake/master] answer successfully roundtripped back from snowflake proxy through broker to client (#1)

arlo at torproject.org arlo at torproject.org
Thu Jan 21 22:15:14 UTC 2016


commit c9013b2f80aeda0aa6e86d984f1d7e9b89aabd46
Author: Serene Han <keroserene+git at gmail.com>
Date:   Thu Jan 21 13:02:46 2016 -0800

    answer successfully roundtripped back from snowflake proxy through broker to client (#1)
---
 broker/config.go           |   16 ------
 broker/snowflake-broker.go |  123 +++++++++++++++++++++++++++++++-------------
 proxy/broker.coffee        |   42 ++++++++++++---
 proxy/proxypair.coffee     |    5 +-
 proxy/snowflake.coffee     |   11 +---
 5 files changed, 127 insertions(+), 70 deletions(-)

diff --git a/broker/config.go b/broker/config.go
deleted file mode 100644
index 7e250aa..0000000
--- a/broker/config.go
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
-This is the server-side code that runs on Google App Engine for the
-"appspot" registration method.
-
-See doc/appspot-howto.txt for more details about setting up an
-application, and advice on running one.
-
-To upload a new version:
-$ torify ~/go_appengine/appcfg.py --no_cookies -A $YOUR_APP_ID update .
-*/
-package snowflake_broker
-
-// host:port/basepath of the broker you want to register with
-// for example, fp-broker.org or example.com:12345/broker
-// https:// and /reg/ will be prepended and appended respectively.
-const SNOWFLAKE_BROKER = ""
diff --git a/broker/snowflake-broker.go b/broker/snowflake-broker.go
index 1d26eb1..c2bde7b 100644
--- a/broker/snowflake-broker.go
+++ b/broker/snowflake-broker.go
@@ -1,3 +1,12 @@
+/*
+Broker acts as the HTTP signaling channel.
+It matches clients and snowflake proxies by passing corresponding
+SessionDescriptions in order to negotiate a WebRTC connection.
+
+TODO(serene): This code is currently the absolute minimum required to
+cause a successful negotiation.
+It's otherwise very unsafe and problematic, and needs quite some work...
+*/
 package snowflake_broker
 
 import (
@@ -8,8 +17,6 @@ import (
 	"net"
 	"net/http"
 	"time"
-	// "appengine"
-	// "appengine/urlfetch"
 )
 
 // This is an intermediate step - a basic hardcoded appengine rendezvous
@@ -23,10 +30,11 @@ import (
 // var snowflakes []chan []byte
 
 type Snowflake struct {
-	id         string
-	sigChannel chan []byte
-	clients    int
-	index      int
+	id            string
+	offerChannel  chan []byte
+	answerChannel chan []byte
+	clients       int
+	index         int
 }
 
 // Implements heap.Interface, and holds Snowflakes.
@@ -63,14 +71,17 @@ func (sh *SnowflakeHeap) Pop() interface{} {
 }
 
 var snowflakes *SnowflakeHeap
+var snowflakeMap map[string]*Snowflake
 
 // Create and add a Snowflake to the heap.
 func AddSnowflake(id string) *Snowflake {
 	snowflake := new(Snowflake)
 	snowflake.id = id
 	snowflake.clients = 0
-	snowflake.sigChannel = make(chan []byte)
+	snowflake.offerChannel = make(chan []byte)
+	snowflake.answerChannel = make(chan []byte)
 	heap.Push(snowflakes, snowflake)
+	snowflakeMap[id] = snowflake
 	return snowflake
 }
 
@@ -97,65 +108,108 @@ func clientHandler(w http.ResponseWriter, r *http.Request) {
 	offer, err := ioutil.ReadAll(r.Body)
 	if nil != err {
 		log.Println("Invalid data.")
+		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 	w.Header().Set("Access-Control-Allow-Origin", "*")
-	// Pop the most available snowflake proxy, and pass the offer to it.
-	// TODO: Make this much better.
+	w.Header().Set("Access-Control-Allow-Headers", "X-Session-ID")
+
+	// Find the most available snowflake proxy, and pass the offer to it.
+	// TODO: Needs improvement.
 	snowflake := heap.Pop(snowflakes).(*Snowflake)
 	if nil == snowflake {
-		// w.Header().Set("Status", http.StatusServiceUnavailable)
-		w.Write([]byte("no snowflake proxies available"))
+		w.Header().Set("Status", http.StatusServiceUnavailable)
+		// w.Write([]byte("no snowflake proxies available"))
 		return
 	}
-	// snowflakes = snowflakes[1:]
-	snowflake.sigChannel <- offer
-	w.Write([]byte("sent offer to proxy!"))
-	// TODO: Get browser snowflake to talkto this appengine instance
-	// so it can reply with an answer, and not just the offer again :)
-	// TODO: Real broker which matches clients and snowflake proxies.
-	w.Write(offer)
+	snowflake.offerChannel <- offer
+
+	// Wait for the answer to be returned on the channel.
+	select {
+	case answer := <-snowflake.answerChannel:
+		log.Println("Retrieving answer")
+		w.Write(answer)
+		// Only remove from the snowflake map once the answer is set.
+		delete(snowflakeMap, snowflake.id)
+
+	case <-time.After(time.Second * 10):
+		w.WriteHeader(http.StatusGatewayTimeout)
+		w.Write([]byte("timed out waiting for answer!"))
+	}
 }
 
 /*
-A snowflake browser proxy requests a client from the Broker.
+For snowflake proxies to request a client from the Broker.
 */
 func proxyHandler(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID")
+	// For CORS preflight.
+	if "OPTIONS" == r.Method {
+		return
+	}
+
+	id := r.Header.Get("X-Session-ID")
 	body, err := ioutil.ReadAll(r.Body)
 	if nil != err {
 		log.Println("Invalid data.")
+		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-	snowflakeSession := body
-	log.Println("Received snowflake: ", snowflakeSession)
-	snowflake := AddSnowflake(string(snowflakeSession))
+	if string(body) != id { // Mismatched IDs!
+		w.WriteHeader(http.StatusBadRequest)
+	}
+	// Maybe confirm that X-Session-ID is the same.
+	log.Println("Received snowflake: ", id)
+	snowflake := AddSnowflake(id)
+
+	// Wait for a client to avail an offer to the snowflake, or timeout
+	// and ask the snowflake to poll later.
 	select {
-	case offer := <-snowflake.sigChannel:
+	case offer := <-snowflake.offerChannel:
 		log.Println("Passing client offer to snowflake.")
 		w.Write(offer)
+
 	case <-time.After(time.Second * 10):
-		// s := fmt.Sprintf("%d snowflakes left.", snowflakes.Len())
-		// w.Write([]byte("timed out. " + s))
-		// w.Header().Set("Status", http.StatusRequestTimeout)
-		w.WriteHeader(http.StatusGatewayTimeout)
 		heap.Remove(snowflakes, snowflake.index)
+		w.WriteHeader(http.StatusGatewayTimeout)
 	}
 }
 
-func reflectHandler(w http.ResponseWriter, r *http.Request) {
+/*
+Expects snowflake proxes which have previously successfully received
+an offer from proxyHandler to respond with an answer in an HTTP POST,
+which the broker will pass back to the original client.
+*/
+func answerHandler(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Access-Control-Allow-Headers", "X-Session-ID")
+	// For CORS preflight.
+	if "OPTIONS" == r.Method {
+		return
+	}
+
+	id := r.Header.Get("X-Session-ID")
+	snowflake, ok := snowflakeMap[id]
+	if !ok || nil == snowflake {
+		// The snowflake took too long to respond with an answer,
+		// and the designated client is no longer around / recognized by the Broker.
+		w.WriteHeader(http.StatusGone)
+		return
+	}
 	body, err := ioutil.ReadAll(r.Body)
 	if nil != err {
 		log.Println("Invalid data.")
+		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-	w.Write(body)
+	log.Println("Received answer: ", body)
+	snowflake.answerChannel <- body
 }
 
 func init() {
-	// snowflakes = make([]chan []byte, 0)
 	snowflakes = new(SnowflakeHeap)
+	snowflakeMap = make(map[string]*Snowflake)
 	heap.Init(snowflakes)
 
 	http.HandleFunc("/robots.txt", robotsTxtHandler)
@@ -163,8 +217,5 @@ func init() {
 
 	http.HandleFunc("/client", clientHandler)
 	http.HandleFunc("/proxy", proxyHandler)
-	http.HandleFunc("/reflect", reflectHandler)
-	// if SNOWFLAKE_BROKER == "" {
-	// panic("SNOWFLAKE_BROKER empty; did you forget to edit config.go?")
-	// }
+	http.HandleFunc("/answer", answerHandler)
 }
diff --git a/proxy/broker.coffee b/proxy/broker.coffee
index 688d45d..91a81d2 100644
--- a/proxy/broker.coffee
+++ b/proxy/broker.coffee
@@ -6,19 +6,25 @@ to get assigned to clients.
 ###
 
 STATUS_OK = 200
+STATUS_GONE = 410
 STATUS_GATEWAY_TIMEOUT = 504
 
+genSnowflakeID = ->
+  Math.random().toString(36).substring(2)
+
 # Represents a broker running remotely.
 class Broker
 
   clients: 0
+  id: null
 
   # When interacting with the Broker, snowflake must generate a unique session
   # ID so the Broker can keep track of which signalling channel it's speaking
   # to.
   constructor: (@url) ->
-    log 'Using Broker at ' + @url
-    clients = 0
+    @clients = 0
+    @id = genSnowflakeID()
+    log 'Contacting Broker at ' + @url + '\nSnowflake ID: ' + @id
 
   # Snowflake registers with the broker using an HTTP POST request, and expects
   # a response from the broker containing some client offer.
@@ -27,7 +33,8 @@ class Broker
     new Promise (fulfill, reject) =>
       xhr = new XMLHttpRequest()
       try
-        xhr.open 'POST', @url
+        xhr.open 'POST', @url + 'proxy'
+        xhr.setRequestHeader('X-Session-ID', @id)
       catch err
         ###
         An exception happens here when, for example, NoScript allows the domain
@@ -47,10 +54,29 @@ class Broker
           else
             log 'Broker ERROR: Unexpected ' + xhr.status +
                 ' - ' + xhr.statusText
-
-      xhr.send 'snowflake-testing'
-      log "Broker: polling for client offer..."
+      xhr.send @id
+      log @id + " - polling for client offer..."
 
   sendAnswer: (answer) ->
-    log 'Sending answer to broker.'
-    log answer
+    log @id + ' - Sending answer back to broker...\n'
+    log answer.sdp
+    xhr = new XMLHttpRequest()
+    try
+      xhr.open 'POST', @url + 'answer'
+      xhr.setRequestHeader('X-Session-ID', @id)
+    catch err
+      log 'Broker: exception while connecting: ' + err.message
+      return
+    xhr.onreadystatechange = ->
+      return if xhr.DONE != xhr.readyState
+      log xhr
+      switch xhr.status
+        when STATUS_OK
+          log 'Broker: Successfully replied with answer.'
+          log xhr.responseText
+        when STATUS_GONE
+          log 'Broker: No longer valid to reply with answer.'
+        else
+          log 'Broker ERROR: Unexpected ' + xhr.status +
+              ' - ' + xhr.statusText
+    xhr.send JSON.stringify(answer)
diff --git a/proxy/proxypair.coffee b/proxy/proxypair.coffee
index a27ac92..f78e9f6 100644
--- a/proxy/proxypair.coffee
+++ b/proxy/proxypair.coffee
@@ -32,7 +32,10 @@ class ProxyPair
         # 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
+        if COPY_PASTE_ENABLED
+          Signalling.send @pc.localDescription
+        else
+          snowflake.broker.sendAnswer @pc.localDescription
     # OnDataChannel triggered remotely from the client when connection succeeds.
     @pc.ondatachannel = (dc) =>
       console.log dc
diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee
index 646b9b4..cc34f81 100644
--- a/proxy/snowflake.coffee
+++ b/proxy/snowflake.coffee
@@ -8,7 +8,7 @@ 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_BROKER = 'https://snowflake-reg.appspot.com/proxy'
+DEFAULT_BROKER = 'https://snowflake-reg.appspot.com/'
 COPY_PASTE_ENABLED = false
 DEFAULT_PORTS =
   http:  80
@@ -104,8 +104,8 @@ class Snowflake
     poll = =>
       recv = broker.getClientOffer()
       recv.then((desc) =>
-        log 'Received:\n\n' + desc + '\n'
         offer = JSON.parse desc
+        log 'Received:\n\n' + offer.sdp + '\n'
         @receiveOffer offer
       , (err) ->
         log err
@@ -113,13 +113,6 @@ class Snowflake
       )
     poll()
 
-    # if @proxyPairs.length >= MAX_NUM_CLIENTS * CONNECTIONS_PER_CLIENT
-      # setTimeout(@proxyMain, @broker_poll_interval * 1000)
-      # return
-    # params = [['r', '1']]
-    # params.push ['transport', 'websocket']
-    # params.push ['transport', 'webrtc']
-
   # Receive an SDP offer from client plugin.
   receiveOffer: (desc) =>
     sdp = new RTCSessionDescription desc





More information about the tor-commits mailing list