This diff enables `websteps` to use uTLS for TLS parroting. It integrates the `oohttp.StdlibTransport` wrapper which uses the `ooni/oohttp` fork. `oohttp` supports TLS-like connections like `utls.Conn`. As a prototype, the testhelper and `websteps` code now uses the `utls.HelloChrome_Auto` fingerprint, i.e. the simulated TLS fingerprint of the Google Chrome browser. It is a further contribution for my GSoC project. Reference issue: https://github.com/ooni/probe/issues/1733
470 lines
12 KiB
Go
470 lines
12 KiB
Go
package websteps
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/ooni/probe-cli/v3/internal/errorsx"
|
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
)
|
|
|
|
var generator = &DefaultGenerator{resolver: newResolver()}
|
|
|
|
type fakeTransport struct {
|
|
err error
|
|
}
|
|
|
|
func (txp fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return nil, txp.err
|
|
}
|
|
func (txp fakeTransport) CloseIdleConnections() {}
|
|
|
|
type fakeQUICDialer struct {
|
|
err error
|
|
}
|
|
|
|
func (d fakeQUICDialer) DialContext(ctx context.Context, network, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
|
|
return nil, d.err
|
|
}
|
|
|
|
type fakeDialer struct {
|
|
err error
|
|
}
|
|
|
|
func (d fakeDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return nil, d.err
|
|
}
|
|
|
|
func TestGenerateDNSFailure(t *testing.T) {
|
|
u, err := url.Parse("https://www.google.google")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rts := []*RoundTrip{
|
|
{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
}
|
|
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
|
|
if err != nil {
|
|
t.Fatal("unexpected error")
|
|
}
|
|
if len(urlMeasurements) != 1 {
|
|
t.Fatal("unexpected urlMeasurements length")
|
|
}
|
|
if urlMeasurements[0].DNS == nil {
|
|
t.Fatal("DNS should not be nil")
|
|
}
|
|
if urlMeasurements[0].DNS.Failure == nil || *urlMeasurements[0].DNS.Failure != errorsx.FailureDNSNXDOMAINError {
|
|
t.Fatal("unexpected DNS failure type")
|
|
}
|
|
}
|
|
|
|
func TestGenerate(t *testing.T) {
|
|
u, err := url.Parse("http://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
|
|
u2, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
|
|
rts := []*RoundTrip{
|
|
{
|
|
Proto: "http",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u2,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
{
|
|
Proto: "h3",
|
|
Request: &http.Request{
|
|
URL: u2,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
}
|
|
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if urlMeasurements == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if len(urlMeasurements) < 3 {
|
|
t.Fatal("unexpected number of urlMeasurements", len(urlMeasurements))
|
|
}
|
|
}
|
|
|
|
func TestGenerateUnexpectedProtocol(t *testing.T) {
|
|
u, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rts := []*RoundTrip{
|
|
{
|
|
Proto: "h3-27",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
}
|
|
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if urlMeasurements == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if len(urlMeasurements) != 1 {
|
|
t.Fatal("unexpected number of urlMeasurements")
|
|
}
|
|
measurement := urlMeasurements[0]
|
|
if measurement.URL != u.String() {
|
|
t.Fatal("unexpected URL")
|
|
}
|
|
if measurement.DNS == nil {
|
|
t.Fatal("DNS should not be nil")
|
|
}
|
|
if measurement.RoundTrip == nil {
|
|
t.Fatal("RoundTrip should not be nil")
|
|
}
|
|
if measurement.Endpoints != nil {
|
|
t.Fatal("Endpoints should be nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateURLWithClientResolutions(t *testing.T) {
|
|
u, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "h3",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
clientResolution := "142.250.186.36"
|
|
urlMeasurement := generator.GenerateURL(context.Background(), rt, []string{clientResolution})
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if urlMeasurement == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if urlMeasurement.DNS == nil {
|
|
t.Fatal("DNS should not be nil")
|
|
}
|
|
if len(urlMeasurement.Endpoints) < 2 {
|
|
t.Fatal("unexpected number of endpoints")
|
|
}
|
|
clientAddrsFound := false
|
|
for _, e := range urlMeasurement.Endpoints {
|
|
if e.Endpoint == clientResolution+":443" {
|
|
clientAddrsFound = true
|
|
}
|
|
}
|
|
if !clientAddrsFound {
|
|
t.Fatal("did not use provided client resolution")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTTP(t *testing.T) {
|
|
u, err := url.Parse("http://example.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "http",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateHTTPEndpoint(context.Background(), rt, "93.184.216.34:80")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if endpointMeasurement.TCPConnect == nil {
|
|
t.Fatal("TCPConnectMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.HTTPRoundTrip == nil {
|
|
t.Fatal("HTTPRoundTripMeasurement should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTTPS(t *testing.T) {
|
|
u, err := url.Parse("https://example.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "93.184.216.34:443")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if endpointMeasurement.TCPConnect == nil {
|
|
t.Fatal("TCPConnectMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.TLSHandshake == nil {
|
|
t.Fatal("TLSHandshakeMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.TLSHandshake.Failure != nil {
|
|
t.Fatal("unexpected failure at TLSHandshakeMeasurement")
|
|
}
|
|
if endpointMeasurement.HTTPRoundTrip == nil {
|
|
t.Fatal("HTTPRoundTripMeasurement should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTTPSTLSFailure(t *testing.T) {
|
|
u, err := url.Parse("https://wrong.host.badssl.com/")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "104.154.89.105:443")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if endpointMeasurement.TCPConnect == nil {
|
|
t.Fatal("TCPConnectMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.TLSHandshake == nil {
|
|
t.Fatal("TLSHandshakeMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.TLSHandshake.Failure == nil {
|
|
t.Fatal("expected failure at TLSHandshakeMeasurement")
|
|
}
|
|
if endpointMeasurement.HTTPRoundTrip != nil {
|
|
t.Fatal("HTTPRoundTripMeasurement should be nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateH3(t *testing.T) {
|
|
u, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "h3",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateH3Endpoint(context.Background(), rt, "173.194.76.103:443")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement == nil {
|
|
t.Fatal("unexpected nil urlMeasurement")
|
|
}
|
|
if endpointMeasurement.QUICHandshake == nil {
|
|
t.Fatal("TCPConnectMeasurement should not be nil")
|
|
}
|
|
if endpointMeasurement.HTTPRoundTrip == nil {
|
|
t.Fatal("HTTPRoundTripMeasurement should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateTCPDoFails(t *testing.T) {
|
|
expected := errors.New("expected")
|
|
generator := &DefaultGenerator{
|
|
dialer: fakeDialer{err: expected},
|
|
resolver: newResolver(),
|
|
}
|
|
u, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateHTTPSEndpoint(context.Background(), rt, "173.194.76.103:443")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement.TCPConnect == nil {
|
|
t.Fatal("QUIC handshake should not be nil")
|
|
}
|
|
if endpointMeasurement.TCPConnect.Failure == nil {
|
|
t.Fatal("expected an error here")
|
|
}
|
|
if *endpointMeasurement.TCPConnect.Failure != *newfailure(expected) {
|
|
t.Fatal("unexpected error type")
|
|
}
|
|
}
|
|
|
|
func TestGenerateQUICDoFails(t *testing.T) {
|
|
expected := errors.New("expected")
|
|
generator := &DefaultGenerator{
|
|
quicDialer: fakeQUICDialer{err: expected},
|
|
resolver: newResolver(),
|
|
}
|
|
u, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed")
|
|
rt := &RoundTrip{
|
|
Proto: "h3",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
}
|
|
endpointMeasurement := generator.GenerateH3Endpoint(context.Background(), rt, "173.194.76.103:443")
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if endpointMeasurement.QUICHandshake == nil {
|
|
t.Fatal("QUIC handshake should not be nil")
|
|
}
|
|
if endpointMeasurement.QUICHandshake.Failure == nil {
|
|
t.Fatal("expected an error here")
|
|
}
|
|
if *endpointMeasurement.QUICHandshake.Failure != *newfailure(expected) {
|
|
t.Fatal("unexpected error type")
|
|
}
|
|
}
|
|
|
|
func TestGenerateHTTPDoFails(t *testing.T) {
|
|
expected := errors.New("expected")
|
|
generator := &DefaultGenerator{
|
|
transport: fakeTransport{err: expected},
|
|
resolver: newResolver(),
|
|
}
|
|
u, err := url.Parse("http://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
|
|
u2, err := url.Parse("https://www.google.com")
|
|
runtimex.PanicOnError(err, "url.Parse failed for clearly good URL")
|
|
rts := []*RoundTrip{
|
|
{
|
|
Proto: "http",
|
|
Request: &http.Request{
|
|
URL: u,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
{
|
|
Proto: "https",
|
|
Request: &http.Request{
|
|
URL: u2,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
{
|
|
Proto: "h3",
|
|
Request: &http.Request{
|
|
URL: u2,
|
|
},
|
|
Response: &http.Response{
|
|
StatusCode: 200,
|
|
},
|
|
SortIndex: 0,
|
|
},
|
|
}
|
|
urlMeasurements, err := generator.Generate(context.Background(), rts, []string{})
|
|
if err != nil {
|
|
t.Fatal("unexpected err")
|
|
}
|
|
if len(urlMeasurements) != 3 {
|
|
t.Fatal("unexpected number of urlMeasurements")
|
|
}
|
|
for _, u := range urlMeasurements {
|
|
if u.DNS == nil {
|
|
t.Fatal("unexpected DNS failure")
|
|
}
|
|
if len(u.Endpoints) < 1 {
|
|
t.Fatal("unexpected number of endpoints", len(u.Endpoints))
|
|
}
|
|
// this can occur when the network is unreachable, but it is irrelevant for checking HTTP behavior
|
|
if u.Endpoints[0].TCPConnect != nil && u.Endpoints[0].TCPConnect.Failure != nil {
|
|
continue
|
|
}
|
|
if u.Endpoints[0].QUICHandshake != nil && u.Endpoints[0].QUICHandshake.Failure != nil {
|
|
continue
|
|
}
|
|
if u.Endpoints[0].HTTPRoundTrip == nil {
|
|
t.Fatal("roundtrip should not be nil", u.Endpoints[0].TCPConnect.Failure, "jaaaa")
|
|
}
|
|
if u.Endpoints[0].HTTPRoundTrip.Response == nil {
|
|
t.Fatal("roundtrip response should not be nil")
|
|
}
|
|
if u.Endpoints[0].HTTPRoundTrip.Response.Failure == nil {
|
|
t.Fatal("expected an HTTP error")
|
|
}
|
|
if !strings.HasSuffix(*u.Endpoints[0].HTTPRoundTrip.Response.Failure, expected.Error()) {
|
|
t.Fatal("unexpected failure type")
|
|
}
|
|
}
|
|
}
|