commit c9013b2f80aeda0aa6e86d984f1d7e9b89aabd46 Author: Serene Han keroserene+git@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