[tor-commits] [snowflake/main] Implement ampCacheRendezvous.

dcf at torproject.org dcf at torproject.org
Thu Aug 5 22:18:28 UTC 2021


commit 5adb99402861569a0c3d0e46299d48a583f48725
Author: David Fifield <david at 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/amp-cache-urls/#redirect-%26-error-handling
+		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)
+		})
+	})
+}





More information about the tor-commits mailing list