mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 05:11:19 -04:00
Implement ampCacheRendezvous.
This commit is contained in:
parent
c13810192d
commit
5adb994028
2 changed files with 181 additions and 7 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue