commit 5adb99402861569a0c3d0e46299d48a583f48725
Author: David Fifield <[email protected]>
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)
+               })
+       })
+}



_______________________________________________
tor-commits mailing list
[email protected]
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits

Reply via email to