mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 05:11:19 -04:00
This continues to asserts the known version while decoding. The client will only ever generate the latest version while encoding and if the response needs to change, the impetus will be a new feature, set in the deserialized request, which can be used as a distinguisher.
273 lines
9.1 KiB
Go
273 lines
9.1 KiB
Go
package snowflake_client
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
|
|
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
|
|
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
)
|
|
|
|
// mockTransport's RoundTrip method returns a response with a fake status and
|
|
// body.
|
|
type mockTransport struct {
|
|
statusCode int
|
|
body []byte
|
|
}
|
|
|
|
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
Status: fmt.Sprintf("%d %s", t.statusCode, http.StatusText(t.statusCode)),
|
|
StatusCode: t.statusCode,
|
|
Body: ioutil.NopCloser(bytes.NewReader(t.body)),
|
|
}, nil
|
|
}
|
|
|
|
// errorTransport's RoundTrip method returns an error.
|
|
type errorTransport struct {
|
|
err error
|
|
}
|
|
|
|
func (t errorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return nil, t.err
|
|
}
|
|
|
|
// makeEncPollReq returns an encoded client poll request containing a given
|
|
// offer.
|
|
func makeEncPollReq(offer string) []byte {
|
|
encPollReq, err := (&messages.ClientPollRequest{
|
|
Offer: offer,
|
|
NAT: nat.NATUnknown,
|
|
}).EncodeClientPollRequest()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return encPollReq
|
|
}
|
|
|
|
// makeEncPollResp returns an encoded client poll response with given answer and
|
|
// error strings.
|
|
func makeEncPollResp(answer, errorStr string) []byte {
|
|
encPollResp, err := (&messages.ClientPollResponse{
|
|
Answer: answer,
|
|
Error: errorStr,
|
|
}).EncodePollResponse()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
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() {
|
|
transport := &mockTransport{http.StatusOK, []byte{}}
|
|
rend, err := newHTTPRendezvous("http://test.broker", "", transport)
|
|
So(err, ShouldBeNil)
|
|
So(rend.brokerURL, ShouldNotBeNil)
|
|
So(rend.brokerURL.Host, ShouldResemble, "test.broker")
|
|
So(rend.front, ShouldResemble, "")
|
|
So(rend.transport, ShouldEqual, transport)
|
|
})
|
|
|
|
Convey("Construct httpRendezvous *with* front domain", func() {
|
|
transport := &mockTransport{http.StatusOK, []byte{}}
|
|
rend, err := newHTTPRendezvous("http://test.broker", "front", transport)
|
|
So(err, ShouldBeNil)
|
|
So(rend.brokerURL, ShouldNotBeNil)
|
|
So(rend.brokerURL.Host, ShouldResemble, "test.broker")
|
|
So(rend.front, ShouldResemble, "front")
|
|
So(rend.transport, ShouldEqual, transport)
|
|
})
|
|
|
|
Convey("httpRendezvous.Exchange responds with answer", func() {
|
|
fakeEncPollResp := makeEncPollResp(
|
|
`{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`,
|
|
"",
|
|
)
|
|
rend, err := newHTTPRendezvous("http://test.broker", "",
|
|
&mockTransport{http.StatusOK, fakeEncPollResp})
|
|
So(err, ShouldBeNil)
|
|
answer, err := rend.Exchange(fakeEncPollReq)
|
|
So(err, ShouldBeNil)
|
|
So(answer, ShouldResemble, fakeEncPollResp)
|
|
})
|
|
|
|
Convey("httpRendezvous.Exchange responds with no answer", func() {
|
|
fakeEncPollResp := makeEncPollResp(
|
|
"",
|
|
`{"error": "no snowflake proxies currently available"}`,
|
|
)
|
|
rend, err := newHTTPRendezvous("http://test.broker", "",
|
|
&mockTransport{http.StatusOK, fakeEncPollResp})
|
|
So(err, ShouldBeNil)
|
|
answer, err := rend.Exchange(fakeEncPollReq)
|
|
So(err, ShouldBeNil)
|
|
So(answer, ShouldResemble, fakeEncPollResp)
|
|
})
|
|
|
|
Convey("httpRendezvous.Exchange fails with unexpected HTTP status code", func() {
|
|
rend, err := newHTTPRendezvous("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("httpRendezvous.Exchange fails with error", func() {
|
|
transportErr := errors.New("error")
|
|
rend, err := newHTTPRendezvous("http://test.broker", "",
|
|
&errorTransport{err: transportErr})
|
|
So(err, ShouldBeNil)
|
|
answer, err := rend.Exchange(fakeEncPollReq)
|
|
So(err, ShouldEqual, transportErr)
|
|
So(answer, ShouldBeNil)
|
|
})
|
|
|
|
Convey("httpRendezvous.Exchange fails with large read", func() {
|
|
rend, err := newHTTPRendezvous("http://test.broker", "",
|
|
&mockTransport{http.StatusOK, make([]byte, readLimit+1)})
|
|
So(err, ShouldBeNil)
|
|
_, err = rend.Exchange(fakeEncPollReq)
|
|
So(err, ShouldEqual, io.ErrUnexpectedEOF)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|