Redo protocol for proxy--broker messages

Switch to containing all communication between the proxy and the broker
in the HTTP response body. This will make things easier if we ever use
something other than HTTP communicate between different actors in the
snowflake system.

Other changes to the protocol are as follows:
- requests are accompanied by a version number so the broker can be
backwards compatable if desired in the future
- all responses are 200 OK unless the request was badly formatted
This commit is contained in:
Cecylia Bocovich 2019-10-07 14:02:01 -04:00
parent abefae1587
commit c4ae64905b
6 changed files with 489 additions and 53 deletions

View file

@ -21,6 +21,7 @@ import (
"syscall"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
"golang.org/x/crypto/acme/autocert"
)
@ -151,15 +152,16 @@ func (ctx *BrokerContext) AddSnowflake(id string) *Snowflake {
For snowflake proxies to request a client from the Broker.
*/
func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Session-ID")
body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit))
if nil != err {
if err != nil {
log.Println("Invalid data.")
w.WriteHeader(http.StatusBadRequest)
return
}
if string(body) != id {
log.Println("Mismatched IDs!")
sid, err := messages.DecodePollRequest(body)
if err != nil {
log.Println("Invalid data.")
w.WriteHeader(http.StatusBadRequest)
return
}
@ -173,14 +175,26 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) {
}
// Wait for a client to avail an offer to the snowflake, or timeout if nil.
offer := ctx.RequestOffer(id)
offer := ctx.RequestOffer(sid)
var b []byte
if nil == offer {
ctx.metrics.proxyIdleCount++
w.WriteHeader(http.StatusGatewayTimeout)
b, err = messages.EncodePollResponse("", false)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(b)
return
}
log.Println("Passing client offer to snowflake.")
if _, err := w.Write(offer); err != nil {
b, err = messages.EncodePollResponse(string(offer), true)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if _, err := w.Write(b); err != nil {
log.Printf("proxyPolls unable to write offer with error: %v", err)
}
}
@ -235,14 +249,7 @@ an offer from proxyHandler to respond with an answer in an HTTP POST,
which the broker will pass back to the original client.
*/
func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Session-ID")
snowflake, ok := ctx.idToSnowflake[id]
if !ok || nil == snowflake {
// The snowflake took too long to respond with an answer, so its client
// disappeared / the snowflake is no longer recognized by the Broker.
w.WriteHeader(http.StatusGone)
return
}
body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit))
if nil != err || nil == body || len(body) <= 0 {
log.Println("Invalid data.")
@ -250,7 +257,32 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) {
return
}
snowflake.answerChannel <- body
answer, id, err := messages.DecodeAnswerRequest(body)
if err != nil || answer == "" {
log.Println("Invalid data.")
w.WriteHeader(http.StatusBadRequest)
return
}
var success = true
snowflake, ok := ctx.idToSnowflake[id]
if !ok || nil == snowflake {
// The snowflake took too long to respond with an answer, so its client
// disappeared / the snowflake is no longer recognized by the Broker.
success = false
}
b, err := messages.EncodeAnswerResponse(success)
if err != nil {
log.Printf("Error encoding answer: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(b)
if success {
snowflake.answerChannel <- []byte(answer)
}
}
func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) {

View file

@ -113,9 +113,8 @@ func TestBroker(t *testing.T) {
Convey("Responds to proxy polls...", func() {
done := make(chan bool)
w := httptest.NewRecorder()
data := bytes.NewReader([]byte("test"))
data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`))
r, err := http.NewRequest("POST", "snowflake.broker/proxy", data)
r.Header.Set("X-Session-ID", "test")
So(err, ShouldBeNil)
Convey("with a client offer if available.", func() {
@ -125,57 +124,59 @@ func TestBroker(t *testing.T) {
}(ctx)
// Pass a fake client offer to this proxy
p := <-ctx.proxyPolls
So(p.id, ShouldEqual, "test")
So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
p.offerChannel <- []byte("fake offer")
<-done
So(w.Code, ShouldEqual, http.StatusOK)
So(w.Body.String(), ShouldEqual, "fake offer")
So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer"}`)
})
Convey("times out when no client offer is available.", func() {
Convey("return empty 200 OK when no client offer is available.", func() {
go func(ctx *BrokerContext) {
proxyPolls(ctx, w, r)
done <- true
}(ctx)
p := <-ctx.proxyPolls
So(p.id, ShouldEqual, "test")
So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
// nil means timeout
p.offerChannel <- nil
<-done
So(w.Body.String(), ShouldEqual, "")
So(w.Code, ShouldEqual, http.StatusGatewayTimeout)
So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":""}`)
So(w.Code, ShouldEqual, http.StatusOK)
})
})
Convey("Responds to proxy answers...", func() {
s := ctx.AddSnowflake("test")
w := httptest.NewRecorder()
data := bytes.NewReader([]byte("fake answer"))
data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`))
Convey("by passing to the client if valid.", func() {
r, err := http.NewRequest("POST", "snowflake.broker/answer", data)
So(err, ShouldBeNil)
r.Header.Set("X-Session-ID", "test")
go func(ctx *BrokerContext) {
proxyAnswers(ctx, w, r)
}(ctx)
answer := <-s.answerChannel
So(w.Code, ShouldEqual, http.StatusOK)
So(answer, ShouldResemble, []byte("fake answer"))
So(answer, ShouldResemble, []byte("test"))
})
Convey("with error if the proxy is not recognized", func() {
r, err := http.NewRequest("POST", "snowflake.broker/answer", nil)
Convey("with client gone status if the proxy is not recognized", func() {
data = bytes.NewReader([]byte(`{"Version":"1.0","Sid":"invalid","Answer":"test"}`))
r, err := http.NewRequest("POST", "snowflake.broker/answer", data)
So(err, ShouldBeNil)
r.Header.Set("X-Session-ID", "invalid")
proxyAnswers(ctx, w, r)
So(w.Code, ShouldEqual, http.StatusGone)
So(w.Code, ShouldEqual, http.StatusOK)
b, err := ioutil.ReadAll(w.Body)
So(err, ShouldBeNil)
So(b, ShouldResemble, []byte(`{"Status":"client gone"}`))
})
Convey("with error if the proxy gives invalid answer", func() {
data := bytes.NewReader(nil)
r, err := http.NewRequest("POST", "snowflake.broker/answer", data)
r.Header.Set("X-Session-ID", "test")
So(err, ShouldBeNil)
proxyAnswers(ctx, w, r)
So(w.Code, ShouldEqual, http.StatusBadRequest)
@ -184,7 +185,6 @@ func TestBroker(t *testing.T) {
Convey("with error if the proxy writes too much data", func() {
data := bytes.NewReader(make([]byte, 100001))
r, err := http.NewRequest("POST", "snowflake.broker/answer", data)
r.Header.Set("X-Session-ID", "test")
So(err, ShouldBeNil)
proxyAnswers(ctx, w, r)
So(w.Code, ShouldEqual, http.StatusBadRequest)
@ -199,11 +199,10 @@ func TestBroker(t *testing.T) {
ctx := NewBrokerContext(NullLogger())
// Proxy polls with its ID first...
dataP := bytes.NewReader([]byte("test"))
dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`))
wP := httptest.NewRecorder()
rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP)
So(err, ShouldBeNil)
rP.Header.Set("X-Session-ID", "test")
go func() {
proxyPolls(ctx, wP, rP)
polled <- true
@ -211,13 +210,13 @@ func TestBroker(t *testing.T) {
// Manually do the Broker goroutine action here for full control.
p := <-ctx.proxyPolls
So(p.id, ShouldEqual, "test")
So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
s := ctx.AddSnowflake(p.id)
go func() {
offer := <-s.offerChannel
p.offerChannel <- offer
}()
So(ctx.idToSnowflake["test"], ShouldNotBeNil)
So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil)
// Client request blocks until proxy answer arrives.
dataC := bytes.NewReader([]byte("fake offer"))
@ -231,20 +230,19 @@ func TestBroker(t *testing.T) {
<-polled
So(wP.Code, ShouldEqual, http.StatusOK)
So(wP.Body.String(), ShouldResemble, "fake offer")
So(ctx.idToSnowflake["test"], ShouldNotBeNil)
So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer"}`)
So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil)
// Follow up with the answer request afterwards
wA := httptest.NewRecorder()
dataA := bytes.NewReader([]byte("fake answer"))
rA, err := http.NewRequest("POST", "snowflake.broker/proxy", dataA)
dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`))
rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA)
So(err, ShouldBeNil)
rA.Header.Set("X-Session-ID", "test")
proxyAnswers(ctx, wA, rA)
So(wA.Code, ShouldEqual, http.StatusOK)
<-done
So(wC.Code, ShouldEqual, http.StatusOK)
So(wC.Body.String(), ShouldEqual, "fake answer")
So(wC.Body.String(), ShouldEqual, "test")
})
}
@ -408,7 +406,7 @@ func TestMetrics(t *testing.T) {
//Test addition of proxy polls
Convey("for proxy polls", func() {
w := httptest.NewRecorder()
data := bytes.NewReader([]byte("test"))
data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\"}"))
r, err := http.NewRequest("POST", "snowflake.broker/proxy", data)
r.Header.Set("X-Session-ID", "test")
r.RemoteAddr = "129.97.208.23:8888" //CA geoip
@ -492,7 +490,7 @@ func TestMetrics(t *testing.T) {
//Test unique ip
Convey("proxy counts by unique ip", func() {
w := httptest.NewRecorder()
data := bytes.NewReader([]byte("test"))
data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`))
r, err := http.NewRequest("POST", "snowflake.broker/proxy", data)
r.Header.Set("X-Session-ID", "test")
r.RemoteAddr = "129.97.208.23:8888" //CA geoip
@ -505,7 +503,7 @@ func TestMetrics(t *testing.T) {
p.offerChannel <- nil
<-done
data = bytes.NewReader([]byte("test"))
data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`))
r, err = http.NewRequest("POST", "snowflake.broker/proxy", data)
if err != nil {
log.Printf("unable to get NewRequest with error: %v", err)