commit 5adb99402861569a0c3d0e46299d48a583f48725 Author: David Fifield david@bamsoftware.com Date: Sun Jul 18 14:57:45 2021 -0600
Implement ampCacheRendezvous. --- client/lib/rendezvous_ampcache.go | 56 ++++++++++++++-- client/lib/rendezvous_test.go | 132 +++++++++++++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 7 deletions(-)
diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go index 89745f4..4856893 100644 --- a/client/lib/rendezvous_ampcache.go +++ b/client/lib/rendezvous_ampcache.go @@ -1,11 +1,14 @@ package lib
import ( - "bytes" "errors" + "io" + "io/ioutil" "log" "net/http" "net/url" + + "git.torproject.org/pluggable-transports/snowflake.git/common/amp" )
// ampCacheRendezvous is a rendezvousMethod that communicates with the @@ -49,9 +52,22 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) { log.Println("AMP cache URL:", r.cacheURL) log.Println("Front domain:", r.front)
- // Suffix the path with the broker's client registration handler. - reqURL := r.brokerURL.ResolveReference(&url.URL{Path: "client"}) - req, err := http.NewRequest("POST", reqURL.String(), bytes.NewReader(encPollReq)) + // We cannot POST a body through an AMP cache, so instead we GET and + // encode the client poll request message into the URL. + reqURL := r.brokerURL.ResolveReference(&url.URL{ + Path: "amp/client/" + amp.EncodePath(encPollReq), + }) + + if r.cacheURL != nil { + // Rewrite reqURL to its AMP cache version. + var err error + reqURL, err = amp.CacheURL(reqURL, r.cacheURL, "c") + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest("GET", reqURL.String(), nil) if err != nil { return nil, err } @@ -71,8 +87,38 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
log.Printf("AMP cache rendezvous response: %s", resp.Status) if resp.StatusCode != http.StatusOK { + // A non-200 status indicates an error: + // * If the broker returns a page with invalid AMP, then the AMP + // cache returns a redirect that would bypass the cache. + // * If the broker returns a 5xx status, the AMP cache + // translates it to a 404. + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors... + return nil, errors.New(BrokerErrorUnexpected) + } + if _, err := resp.Location(); err == nil { + // The Google AMP Cache may return a "silent redirect" with + // status 200, a Location header set, and a JavaScript redirect + // in the body. The redirect points directly at the origin + // server for the request (bypassing the AMP cache). We do not + // follow redirects nor execute JavaScript, but in any case we + // cannot extract information from this response and can only + // treat it as an error. return nil, errors.New(BrokerErrorUnexpected) }
- return limitedRead(resp.Body, readLimit) + lr := io.LimitReader(resp.Body, readLimit+1) + dec, err := amp.NewArmorDecoder(lr) + if err != nil { + return nil, err + } + encPollResp, err := ioutil.ReadAll(dec) + if err != nil { + return nil, err + } + if lr.(*io.LimitedReader).N == 0 { + // We hit readLimit while decoding AMP armor, that's an error. + return nil, io.ErrUnexpectedEOF + } + + return encPollResp, err } diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go index c263e37..6a1a071 100644 --- a/client/lib/rendezvous_test.go +++ b/client/lib/rendezvous_test.go @@ -9,6 +9,7 @@ import ( "net/http" "testing"
+ "git.torproject.org/pluggable-transports/snowflake.git/common/amp" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/nat" . "github.com/smartystreets/goconvey/convey" @@ -64,6 +65,8 @@ func makeEncPollResp(answer, errorStr string) []byte { return encPollResp }
+var fakeEncPollReq = makeEncPollReq(`{"type":"offer","sdp":"test"}`) + func TestHTTPRendezvous(t *testing.T) { Convey("HTTP rendezvous", t, func() { Convey("Construct httpRendezvous with no front domain", func() { @@ -86,8 +89,6 @@ func TestHTTPRendezvous(t *testing.T) { So(rend.transport, ShouldEqual, transport) })
- fakeEncPollReq := makeEncPollReq(`{"type":"offer","sdp":"test"}`) - Convey("httpRendezvous.Exchange responds with answer", func() { fakeEncPollResp := makeEncPollResp( `{"answer": "{"type":"answer","sdp":"fake"}" }`, @@ -143,3 +144,130 @@ func TestHTTPRendezvous(t *testing.T) { }) }) } + +func ampArmorEncode(p []byte) []byte { + var buf bytes.Buffer + enc, err := amp.NewArmorEncoder(&buf) + if err != nil { + panic(err) + } + _, err = enc.Write(p) + if err != nil { + panic(err) + } + err = enc.Close() + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func TestAMPCacheRendezvous(t *testing.T) { + Convey("AMP cache rendezvous", t, func() { + Convey("Construct ampCacheRendezvous with no cache and no front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldBeNil) + So(rend.front, ShouldResemble, "") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with cache and no front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldNotBeNil) + So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/") + So(rend.front, ShouldResemble, "") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with no cache and front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "", "front", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldBeNil) + So(rend.front, ShouldResemble, "front") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with cache and front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "front", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldNotBeNil) + So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/") + So(rend.front, ShouldResemble, "front") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("ampCacheRendezvous.Exchange responds with answer", func() { + fakeEncPollResp := makeEncPollResp( + `{"answer": "{"type":"answer","sdp":"fake"}" }`, + "", + ) + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("ampCacheRendezvous.Exchange responds with no answer", func() { + fakeEncPollResp := makeEncPollResp( + "", + `{"error": "no snowflake proxies currently available"}`, + ) + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("ampCacheRendezvous.Exchange fails with unexpected HTTP status code", func() { + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusInternalServerError, []byte{}}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldNotBeNil) + So(answer, ShouldBeNil) + So(err.Error(), ShouldResemble, BrokerErrorUnexpected) + }) + + Convey("ampCacheRendezvous.Exchange fails with error", func() { + transportErr := errors.New("error") + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &errorTransport{err: transportErr}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldEqual, transportErr) + So(answer, ShouldBeNil) + }) + + Convey("ampCacheRendezvous.Exchange fails with large read", func() { + // readLimit should apply to the raw HTTP body, not the + // encoded bytes. Encode readLimit bytes—the encoded + // size will be larger—and try to read the body. It + // should fail. + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(make([]byte, readLimit))}) + So(err, ShouldBeNil) + _, err = rend.Exchange(fakeEncPollReq) + // We may get io.ErrUnexpectedEOF here, or something + // like "missing </pre> tag". + So(err, ShouldNotBeNil) + }) + }) +}
tor-commits@lists.torproject.org