chore: merge probe-engine into probe-cli (#201)

This is how I did it:

1. `git clone https://github.com/ooni/probe-engine internal/engine`

2. ```
(cd internal/engine && git describe --tags)
v0.23.0
```

3. `nvim go.mod` (merging `go.mod` with `internal/engine/go.mod`

4. `rm -rf internal/.git internal/engine/go.{mod,sum}`

5. `git add internal/engine`

6. `find . -type f -name \*.go -exec sed -i 's@/ooni/probe-engine@/ooni/probe-cli/v3/internal/engine@g' {} \;`

7. `go build ./...` (passes)

8. `go test -race ./...` (temporary failure on RiseupVPN)

9. `go mod tidy`

10. this commit message

Once this piece of work is done, we can build a new version of `ooniprobe` that
is using `internal/engine` directly. We need to do more work to ensure all the
other functionality in `probe-engine` (e.g. making mobile packages) are still WAI.

Part of https://github.com/ooni/probe/issues/1335
This commit is contained in:
Simone Basso
2021-02-02 12:05:47 +01:00
committed by GitHub
parent b1ce300c8d
commit d57c78bc71
535 changed files with 66182 additions and 23 deletions
@@ -0,0 +1,73 @@
package httptransport
import (
"io"
"net/http"
"github.com/ooni/probe-cli/v3/internal/engine/netx/bytecounter"
)
// ByteCountingTransport is a RoundTripper that counts bytes.
type ByteCountingTransport struct {
RoundTripper
Counter *bytecounter.Counter
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp ByteCountingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Body != nil {
req.Body = byteCountingBody{
ReadCloser: req.Body, Account: txp.Counter.CountBytesSent}
}
txp.estimateRequestMetadata(req)
resp, err := txp.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
txp.estimateResponseMetadata(resp)
resp.Body = byteCountingBody{
ReadCloser: resp.Body, Account: txp.Counter.CountBytesReceived}
return resp, nil
}
func (txp ByteCountingTransport) estimateRequestMetadata(req *http.Request) {
txp.Counter.CountBytesSent(len(req.Method))
txp.Counter.CountBytesSent(len(req.URL.String()))
for key, values := range req.Header {
for _, value := range values {
txp.Counter.CountBytesSent(len(key))
txp.Counter.CountBytesSent(len(": "))
txp.Counter.CountBytesSent(len(value))
txp.Counter.CountBytesSent(len("\r\n"))
}
}
txp.Counter.CountBytesSent(len("\r\n"))
}
func (txp ByteCountingTransport) estimateResponseMetadata(resp *http.Response) {
txp.Counter.CountBytesReceived(len(resp.Status))
for key, values := range resp.Header {
for _, value := range values {
txp.Counter.CountBytesReceived(len(key))
txp.Counter.CountBytesReceived(len(": "))
txp.Counter.CountBytesReceived(len(value))
txp.Counter.CountBytesReceived(len("\r\n"))
}
}
txp.Counter.CountBytesReceived(len("\r\n"))
}
type byteCountingBody struct {
io.ReadCloser
Account func(int)
}
func (r byteCountingBody) Read(p []byte) (int, error) {
count, err := r.ReadCloser.Read(p)
if count > 0 {
r.Account(count)
}
return count, err
}
var _ RoundTripper = ByteCountingTransport{}
@@ -0,0 +1,128 @@
package httptransport_test
import (
"errors"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/ooni/probe-cli/v3/internal/engine/netx/bytecounter"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
)
func TestByteCounterFailure(t *testing.T) {
counter := bytecounter.New()
txp := httptransport.ByteCountingTransport{
Counter: counter,
RoundTripper: httptransport.FakeTransport{
Err: io.EOF,
},
}
client := &http.Client{Transport: txp}
req, err := http.NewRequest(
"POST", "https://www.google.com", strings.NewReader("AAAAAA"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("User-Agent", "antani-browser/1.0.0")
resp, err := client.Do(req)
if !errors.Is(err, io.EOF) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response here")
}
if counter.Sent.Load() != 68 {
t.Fatal("expected around 68 bytes sent")
}
if counter.Received.Load() != 0 {
t.Fatal("expected zero bytes received")
}
}
func TestByteCounterSuccess(t *testing.T) {
counter := bytecounter.New()
txp := httptransport.ByteCountingTransport{
Counter: counter,
RoundTripper: httptransport.FakeTransport{
Resp: &http.Response{
Body: ioutil.NopCloser(strings.NewReader("1234567")),
Header: http.Header{
"Server": []string{"antani/0.1.0"},
},
Status: "200 OK",
StatusCode: http.StatusOK,
},
},
}
client := &http.Client{Transport: txp}
req, err := http.NewRequest(
"POST", "https://www.google.com", strings.NewReader("AAAAAA"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("User-Agent", "antani-browser/1.0.0")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if string(data) != "1234567" {
t.Fatal("expected a different body here")
}
if counter.Sent.Load() != 68 {
t.Fatal("expected around 68 bytes sent")
}
if counter.Received.Load() != 37 {
t.Fatal("expected zero around 37 bytes received")
}
}
func TestByteCounterSuccessWithEOF(t *testing.T) {
counter := bytecounter.New()
txp := httptransport.ByteCountingTransport{
Counter: counter,
RoundTripper: httptransport.FakeTransport{
Resp: &http.Response{
Body: bodyReaderWithEOF{},
Header: http.Header{
"Server": []string{"antani/0.1.0"},
},
Status: "200 OK",
StatusCode: http.StatusOK,
},
},
}
client := &http.Client{Transport: txp}
resp, err := client.Get("https://www.google.com")
if err != nil {
t.Fatal(err)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if string(data) != "A" {
t.Fatal("expected a different body here")
}
}
type bodyReaderWithEOF struct{}
func (bodyReaderWithEOF) Read(p []byte) (int, error) {
if len(p) < 1 {
panic("should not happen")
}
p[0] = 'A'
return 1, io.EOF // we want code to be robust to this
}
func (bodyReaderWithEOF) Close() error {
return nil
}
@@ -0,0 +1,56 @@
package httptransport
import (
"context"
"io/ioutil"
"net"
"net/http"
"time"
)
type FakeDialer struct {
Conn net.Conn
Err error
}
func (d FakeDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
time.Sleep(10 * time.Microsecond)
return d.Conn, d.Err
}
type FakeTransport struct {
Err error
Func func(*http.Request) (*http.Response, error)
Resp *http.Response
}
func (txp FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
time.Sleep(10 * time.Microsecond)
if txp.Func != nil {
return txp.Func(req)
}
if req.Body != nil {
ioutil.ReadAll(req.Body)
req.Body.Close()
}
if txp.Err != nil {
return nil, txp.Err
}
txp.Resp.Request = req // non thread safe but it doesn't matter
return txp.Resp, nil
}
func (txp FakeTransport) CloseIdleConnections() {}
type FakeBody struct {
Err error
}
func (fb FakeBody) Read(p []byte) (int, error) {
time.Sleep(10 * time.Microsecond)
return 0, fb.Err
}
func (fb FakeBody) Close() error {
return nil
}
@@ -0,0 +1,43 @@
package httptransport
import (
"context"
"crypto/tls"
"net/http"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3"
"github.com/ooni/probe-cli/v3/internal/engine/netx/quicdialer"
)
// QUICWrapperDialer is a QUICDialer that wraps a ContextDialer
// This is necessary because the http3 RoundTripper does not support a DialContext method.
type QUICWrapperDialer struct {
Dialer quicdialer.ContextDialer
}
// Dial implements QUICDialer.Dial
func (d QUICWrapperDialer) Dial(network, host string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
return d.Dialer.DialContext(context.Background(), network, host, tlsCfg, cfg)
}
// HTTP3Transport is a httptransport.RoundTripper using the http3 protocol.
type HTTP3Transport struct {
http3.RoundTripper
}
// CloseIdleConnections closes all the connections opened by this transport.
func (t *HTTP3Transport) CloseIdleConnections() {
t.RoundTripper.Close()
}
// NewHTTP3Transport creates a new HTTP3Transport instance.
func NewHTTP3Transport(config Config) RoundTripper {
txp := &HTTP3Transport{}
txp.QuicConfig = &quic.Config{}
txp.TLSClientConfig = config.TLSConfig
txp.Dial = config.QUICDialer.Dial
return txp
}
var _ RoundTripper = &http.Transport{}
@@ -0,0 +1,157 @@
package httptransport_test
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"net/http"
"strings"
"testing"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
)
type MockQUICDialer struct{}
func (d MockQUICDialer) Dial(network, host string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
return quic.DialAddrEarly(host, tlsCfg, cfg)
}
type MockSNIQUICDialer struct {
namech chan string
}
func (d MockSNIQUICDialer) Dial(network, host string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
d.namech <- tlsCfg.ServerName
return quic.DialAddrEarly(host, tlsCfg, cfg)
}
type MockCertQUICDialer struct {
certch chan *x509.CertPool
}
func (d MockCertQUICDialer) Dial(network, host string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
d.certch <- tlsCfg.RootCAs
return quic.DialAddrEarly(host, tlsCfg, cfg)
}
func TestHTTP3TransportSNI(t *testing.T) {
namech := make(chan string, 1)
sni := "sni.org"
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err == nil {
t.Fatal("expected error here")
}
if resp != nil {
t.Fatal("expected nil resp here")
}
if !strings.Contains(err.Error(), "certificate is valid for www.google.com, not "+sni) {
t.Fatal("unexpected error type", err)
}
servername := <-namech
if servername != sni {
t.Fatal("unexpected server name", servername)
}
}
func TestHTTP3TransportSNINoVerify(t *testing.T) {
namech := make(chan string, 1)
sni := "sni.org"
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni, InsecureSkipVerify: true}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}
if resp == nil {
t.Fatal("unexpected nil resp")
}
servername := <-namech
if servername != sni {
t.Fatal("unexpected server name", servername)
}
}
func TestHTTP3TransportCABundle(t *testing.T) {
certch := make(chan *x509.CertPool, 1)
certpool := x509.NewCertPool()
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockCertQUICDialer{certch: certch}, TLSConfig: &tls.Config{RootCAs: certpool}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err == nil {
t.Fatal("expected error here")
}
if resp != nil {
t.Fatal("expected nil resp here")
}
// since the certificate pool is empty, the unknown authority error should be thrown
if !strings.Contains(err.Error(), "certificate signed by unknown authority") {
t.Fatal("unexpected error type")
}
certs := <-certch
if certs != certpool {
t.Fatal("not the certpool we expected")
}
}
func TestUnitHTTP3TransportSuccess(t *testing.T) {
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockQUICDialer{}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("unexpected nil response here")
}
if resp.StatusCode != 200 {
t.Fatal("HTTP statuscode should be 200 OK", resp.StatusCode)
}
}
func TestUnitHTTP3TransportFailure(t *testing.T) {
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockQUICDialer{}})
ctx, cancel := context.WithCancel(context.Background())
cancel() // so that the request immediately fails
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err == nil {
t.Fatal("expected error here")
}
// context.Canceled error occurs if the test host supports QUIC
// timeout error ("Handshake did not complete in time") occurs if the test host does not support QUIC
if !(errors.Is(err, context.Canceled) || strings.HasSuffix(err.Error(), "Handshake did not complete in time")) {
t.Fatal("not the error we expected", err)
}
if resp != nil {
t.Fatal("expected nil response here")
}
}
@@ -0,0 +1,47 @@
// Package httptransport contains HTTP transport extensions.
package httptransport
import (
"context"
"crypto/tls"
"net"
"net/http"
"github.com/lucas-clemente/quic-go"
)
// Config contains the configuration required for constructing an HTTP transport
type Config struct {
Dialer Dialer
QUICDialer QUICDialer
TLSDialer TLSDialer
TLSConfig *tls.Config
}
// Dialer is the definition of dialer assumed by this package.
type Dialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
// TLSDialer is the definition of a TLS dialer assumed by this package.
type TLSDialer interface {
DialTLSContext(ctx context.Context, network, address string) (net.Conn, error)
}
// QUICDialer is the definition of dialer for QUIC assumed by this package.
type QUICDialer interface {
Dial(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error)
}
// RoundTripper is the definition of http.RoundTripper used by this package.
type RoundTripper interface {
RoundTrip(req *http.Request) (*http.Response, error)
CloseIdleConnections()
}
// Resolver is the interface we expect from a resolver
type Resolver interface {
LookupHost(ctx context.Context, hostname string) (addrs []string, err error)
Network() string
Address() string
}
@@ -0,0 +1,50 @@
package httptransport
import "net/http"
// Logger is the logger assumed by this package
type Logger interface {
Debugf(format string, v ...interface{})
Debug(message string)
}
// LoggingTransport is a logging transport
type LoggingTransport struct {
RoundTripper
Logger Logger
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.Host
if host == "" {
host = req.URL.Host
}
req.Header.Set("Host", host) // anticipate what Go would do
return txp.logTrip(req)
}
func (txp LoggingTransport) logTrip(req *http.Request) (*http.Response, error) {
txp.Logger.Debugf("> %s %s", req.Method, req.URL.String())
for key, values := range req.Header {
for _, value := range values {
txp.Logger.Debugf("> %s: %s", key, value)
}
}
txp.Logger.Debug(">")
resp, err := txp.RoundTripper.RoundTrip(req)
if err != nil {
txp.Logger.Debugf("< %s", err)
return nil, err
}
txp.Logger.Debugf("< %d", resp.StatusCode)
for key, values := range resp.Header {
for _, value := range values {
txp.Logger.Debugf("< %s: %s", key, value)
}
}
txp.Logger.Debug("<")
return resp, nil
}
var _ RoundTripper = LoggingTransport{}
@@ -0,0 +1,77 @@
package httptransport_test
import (
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
)
func TestLoggingFailure(t *testing.T) {
txp := httptransport.LoggingTransport{
Logger: log.Log,
RoundTripper: httptransport.FakeTransport{
Err: io.EOF,
},
}
client := &http.Client{Transport: txp}
resp, err := client.Get("https://www.google.com")
if !errors.Is(err, io.EOF) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response here")
}
}
func TestLoggingFailureWithNoHostHeader(t *testing.T) {
txp := httptransport.LoggingTransport{
Logger: log.Log,
RoundTripper: httptransport.FakeTransport{
Err: io.EOF,
},
}
req := &http.Request{
Header: http.Header{},
URL: &url.URL{
Scheme: "https",
Host: "www.google.com",
Path: "/",
},
}
resp, err := txp.RoundTrip(req)
if !errors.Is(err, io.EOF) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response here")
}
}
func TestLoggingSuccess(t *testing.T) {
txp := httptransport.LoggingTransport{
Logger: log.Log,
RoundTripper: httptransport.FakeTransport{
Resp: &http.Response{
Body: ioutil.NopCloser(strings.NewReader("")),
Header: http.Header{
"Server": []string{"antani/0.1.0"},
},
StatusCode: 200,
},
},
}
client := &http.Client{Transport: txp}
resp, err := client.Get("https://www.google.com")
if err != nil {
t.Fatal(err)
}
ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
+158
View File
@@ -0,0 +1,158 @@
package httptransport
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/http/httptrace"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
)
// SaverPerformanceHTTPTransport is a RoundTripper that saves
// performance events occurring during the round trip
type SaverPerformanceHTTPTransport struct {
RoundTripper
Saver *trace.Saver
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp SaverPerformanceHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
tracep := httptrace.ContextClientTrace(req.Context())
if tracep == nil {
tracep = &httptrace.ClientTrace{
WroteHeaders: func() {
txp.Saver.Write(trace.Event{Name: "http_wrote_headers", Time: time.Now()})
},
WroteRequest: func(httptrace.WroteRequestInfo) {
txp.Saver.Write(trace.Event{Name: "http_wrote_request", Time: time.Now()})
},
GotFirstResponseByte: func() {
txp.Saver.Write(trace.Event{
Name: "http_first_response_byte", Time: time.Now()})
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), tracep))
}
return txp.RoundTripper.RoundTrip(req)
}
// SaverMetadataHTTPTransport is a RoundTripper that saves
// events related to HTTP request and response metadata
type SaverMetadataHTTPTransport struct {
RoundTripper
Saver *trace.Saver
Transport string
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp SaverMetadataHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
txp.Saver.Write(trace.Event{
HTTPHeaders: req.Header,
HTTPMethod: req.Method,
HTTPURL: req.URL.String(),
Transport: txp.Transport,
Name: "http_request_metadata",
Time: time.Now(),
})
resp, err := txp.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
txp.Saver.Write(trace.Event{
HTTPHeaders: resp.Header,
HTTPStatusCode: resp.StatusCode,
Name: "http_response_metadata",
Time: time.Now(),
})
return resp, err
}
// SaverTransactionHTTPTransport is a RoundTripper that saves
// events related to the HTTP transaction
type SaverTransactionHTTPTransport struct {
RoundTripper
Saver *trace.Saver
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp SaverTransactionHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
txp.Saver.Write(trace.Event{
Name: "http_transaction_start",
Time: time.Now(),
})
resp, err := txp.RoundTripper.RoundTrip(req)
txp.Saver.Write(trace.Event{
Err: err,
Name: "http_transaction_done",
Time: time.Now(),
})
return resp, err
}
// SaverBodyHTTPTransport is a RoundTripper that saves
// body events occurring during the round trip
type SaverBodyHTTPTransport struct {
RoundTripper
Saver *trace.Saver
SnapshotSize int
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp SaverBodyHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
const defaultSnapSize = 1 << 17
snapsize := defaultSnapSize
if txp.SnapshotSize != 0 {
snapsize = txp.SnapshotSize
}
if req.Body != nil {
data, err := saverSnapRead(req.Body, snapsize)
if err != nil {
return nil, err
}
req.Body = saverCompose(data, req.Body)
txp.Saver.Write(trace.Event{
DataIsTruncated: len(data) >= snapsize,
Data: data,
Name: "http_request_body_snapshot",
Time: time.Now(),
})
}
resp, err := txp.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
data, err := saverSnapRead(resp.Body, snapsize)
if err != nil {
resp.Body.Close()
return nil, err
}
resp.Body = saverCompose(data, resp.Body)
txp.Saver.Write(trace.Event{
DataIsTruncated: len(data) >= snapsize,
Data: data,
Name: "http_response_body_snapshot",
Time: time.Now(),
})
return resp, nil
}
func saverSnapRead(r io.ReadCloser, snapsize int) ([]byte, error) {
return ioutil.ReadAll(io.LimitReader(r, int64(snapsize)))
}
func saverCompose(data []byte, r io.ReadCloser) io.ReadCloser {
return saverReadCloser{Closer: r, Reader: io.MultiReader(bytes.NewReader(data), r)}
}
type saverReadCloser struct {
io.Closer
io.Reader
}
var _ RoundTripper = SaverPerformanceHTTPTransport{}
var _ RoundTripper = SaverMetadataHTTPTransport{}
var _ RoundTripper = SaverBodyHTTPTransport{}
var _ RoundTripper = SaverTransactionHTTPTransport{}
@@ -0,0 +1,429 @@
package httptransport_test
import (
"errors"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
)
func TestSaverPerformanceNoMultipleEvents(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &trace.Saver{}
// register twice - do we see events twice?
txp := httptransport.SaverPerformanceHTTPTransport{
RoundTripper: http.DefaultTransport.(*http.Transport),
Saver: saver,
}
txp = httptransport.SaverPerformanceHTTPTransport{
RoundTripper: txp,
Saver: saver,
}
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal("not the error we expected")
}
if resp == nil {
t.Fatal("expected non nil response here")
}
ev := saver.Read()
// we should specifically see the events not attached to any
// context being submitted twice. This is fine because they are
// explicit, while the context is implicit and hence leads to
// more subtle bugs. For example, this happens when you measure
// every event and combine HTTP with DoH.
if len(ev) != 3 {
t.Fatal("expected three events")
}
expected := []string{
"http_wrote_headers", // measured with context
"http_wrote_request", // measured with context
"http_first_response_byte", // measured with context
}
for i := 0; i < len(expected); i++ {
if ev[i].Name != expected[i] {
t.Fatal("unexpected event name")
}
}
}
func TestSaverMetadataSuccess(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &trace.Saver{}
txp := httptransport.SaverMetadataHTTPTransport{
RoundTripper: http.DefaultTransport.(*http.Transport),
Saver: saver,
}
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Add("User-Agent", "miniooni/0.1.0-dev")
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal("not the error we expected")
}
if resp == nil {
t.Fatal("expected non nil response here")
}
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("expected two events")
}
//
if ev[0].HTTPMethod != "GET" {
t.Fatal("unexpected Method")
}
if len(ev[0].HTTPHeaders) <= 0 {
t.Fatal("unexpected Headers")
}
if ev[0].HTTPURL != "https://www.google.com" {
t.Fatal("unexpected URL")
}
if ev[0].Name != "http_request_metadata" {
t.Fatal("unexpected Name")
}
if !ev[0].Time.Before(time.Now()) {
t.Fatal("unexpected Time")
}
//
if ev[1].HTTPStatusCode != 200 {
t.Fatal("unexpected StatusCode")
}
if len(ev[1].HTTPHeaders) <= 0 {
t.Fatal("unexpected Headers")
}
if ev[1].Name != "http_response_metadata" {
t.Fatal("unexpected Name")
}
if !ev[1].Time.After(ev[0].Time) {
t.Fatal("unexpected Time")
}
}
func TestSaverMetadataFailure(t *testing.T) {
expected := errors.New("mocked error")
saver := &trace.Saver{}
txp := httptransport.SaverMetadataHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Err: expected,
},
Saver: saver,
}
req, err := http.NewRequest("GET", "http://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Add("User-Agent", "miniooni/0.1.0-dev")
resp, err := txp.RoundTrip(req)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response here")
}
ev := saver.Read()
if len(ev) != 1 {
t.Fatal("expected one event")
}
if ev[0].HTTPMethod != "GET" {
t.Fatal("unexpected Method")
}
if len(ev[0].HTTPHeaders) <= 0 {
t.Fatal("unexpected Headers")
}
if ev[0].HTTPURL != "http://www.google.com" {
t.Fatal("unexpected URL")
}
if ev[0].Name != "http_request_metadata" {
t.Fatal("unexpected Name")
}
if !ev[0].Time.Before(time.Now()) {
t.Fatal("unexpected Time")
}
}
func TestSaverTransactionSuccess(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &trace.Saver{}
txp := httptransport.SaverTransactionHTTPTransport{
RoundTripper: http.DefaultTransport.(*http.Transport),
Saver: saver,
}
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal("not the error we expected")
}
if resp == nil {
t.Fatal("expected non nil response here")
}
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("expected two events")
}
//
if ev[0].Name != "http_transaction_start" {
t.Fatal("unexpected Name")
}
if !ev[0].Time.Before(time.Now()) {
t.Fatal("unexpected Time")
}
//
if ev[1].Err != nil {
t.Fatal("unexpected Err")
}
if ev[1].Name != "http_transaction_done" {
t.Fatal("unexpected Name")
}
if !ev[1].Time.After(ev[0].Time) {
t.Fatal("unexpected Time")
}
}
func TestSaverTransactionFailure(t *testing.T) {
expected := errors.New("mocked error")
saver := &trace.Saver{}
txp := httptransport.SaverTransactionHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Err: expected,
},
Saver: saver,
}
req, err := http.NewRequest("GET", "http://www.google.com", nil)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response here")
}
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("expected two events")
}
if ev[0].Name != "http_transaction_start" {
t.Fatal("unexpected Name")
}
if !ev[0].Time.Before(time.Now()) {
t.Fatal("unexpected Time")
}
if ev[1].Name != "http_transaction_done" {
t.Fatal("unexpected Name")
}
if !errors.Is(ev[1].Err, expected) {
t.Fatal("unexpected Err")
}
if !ev[1].Time.After(ev[0].Time) {
t.Fatal("unexpected Time")
}
}
func TestSaverBodySuccess(t *testing.T) {
saver := new(trace.Saver)
txp := httptransport.SaverBodyHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Func: func(req *http.Request) (*http.Response, error) {
data, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
if string(data) != "deadbeef" {
t.Fatal("invalid data")
}
return &http.Response{
StatusCode: 501,
Body: ioutil.NopCloser(strings.NewReader("abad1dea")),
}, nil
},
},
SnapshotSize: 4,
Saver: saver,
}
body := strings.NewReader("deadbeef")
req, err := http.NewRequest("POST", "http://x.org/y", body)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 501 {
t.Fatal("unexpected status code")
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(data) != "abad1dea" {
t.Fatal("unexpected body")
}
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("unexpected number of events")
}
if string(ev[0].Data) != "dead" {
t.Fatal("invalid Data")
}
if ev[0].DataIsTruncated != true {
t.Fatal("invalid DataIsTruncated")
}
if ev[0].Name != "http_request_body_snapshot" {
t.Fatal("invalid Name")
}
if ev[0].Time.After(time.Now()) {
t.Fatal("invalid Time")
}
if string(ev[1].Data) != "abad" {
t.Fatal("invalid Data")
}
if ev[1].DataIsTruncated != true {
t.Fatal("invalid DataIsTruncated")
}
if ev[1].Name != "http_response_body_snapshot" {
t.Fatal("invalid Name")
}
if ev[1].Time.Before(ev[0].Time) {
t.Fatal("invalid Time")
}
}
func TestSaverBodyRequestReadError(t *testing.T) {
saver := new(trace.Saver)
txp := httptransport.SaverBodyHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Func: func(req *http.Request) (*http.Response, error) {
panic("should not be called")
},
},
SnapshotSize: 4,
Saver: saver,
}
expected := errors.New("mocked error")
body := httptransport.FakeBody{Err: expected}
req, err := http.NewRequest("POST", "http://x.org/y", body)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response")
}
ev := saver.Read()
if len(ev) != 0 {
t.Fatal("unexpected number of events")
}
}
func TestSaverBodyRoundTripError(t *testing.T) {
saver := new(trace.Saver)
expected := errors.New("mocked error")
txp := httptransport.SaverBodyHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Err: expected,
},
SnapshotSize: 4,
Saver: saver,
}
body := strings.NewReader("deadbeef")
req, err := http.NewRequest("POST", "http://x.org/y", body)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response")
}
ev := saver.Read()
if len(ev) != 1 {
t.Fatal("unexpected number of events")
}
if string(ev[0].Data) != "dead" {
t.Fatal("invalid Data")
}
if ev[0].DataIsTruncated != true {
t.Fatal("invalid DataIsTruncated")
}
if ev[0].Name != "http_request_body_snapshot" {
t.Fatal("invalid Name")
}
if ev[0].Time.After(time.Now()) {
t.Fatal("invalid Time")
}
}
func TestSaverBodyResponseReadError(t *testing.T) {
saver := new(trace.Saver)
expected := errors.New("mocked error")
txp := httptransport.SaverBodyHTTPTransport{
RoundTripper: httptransport.FakeTransport{
Func: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: httptransport.FakeBody{
Err: expected,
},
}, nil
},
},
SnapshotSize: 4,
Saver: saver,
}
body := strings.NewReader("deadbeef")
req, err := http.NewRequest("POST", "http://x.org/y", body)
if err != nil {
t.Fatal(err)
}
resp, err := txp.RoundTrip(req)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if resp != nil {
t.Fatal("expected nil response")
}
ev := saver.Read()
if len(ev) != 1 {
t.Fatal("unexpected number of events")
}
if string(ev[0].Data) != "dead" {
t.Fatal("invalid Data")
}
if ev[0].DataIsTruncated != true {
t.Fatal("invalid DataIsTruncated")
}
if ev[0].Name != "http_request_body_snapshot" {
t.Fatal("invalid Name")
}
if ev[0].Time.After(time.Now()) {
t.Fatal("invalid Time")
}
}
@@ -0,0 +1,24 @@
package httptransport
import (
"net/http"
)
// NewSystemTransport creates a new "system" HTTP transport. That is a transport
// using the Go standard library with custom dialer and TLS dialer.
func NewSystemTransport(config Config) RoundTripper {
txp := http.DefaultTransport.(*http.Transport).Clone()
txp.DialContext = config.Dialer.DialContext
txp.DialTLSContext = config.TLSDialer.DialTLSContext
// Better for Cloudflare DNS and also better because we have less
// noisy events and we can better understand what happened.
txp.MaxConnsPerHost = 1
// The following (1) reduces the number of headers that Go will
// automatically send for us and (2) ensures that we always receive
// back the true headers, such as Content-Length. This change is
// functional to OONI's goal of observing the network.
txp.DisableCompression = true
return txp
}
var _ RoundTripper = &http.Transport{}
@@ -0,0 +1,19 @@
package httptransport
import "net/http"
// UserAgentTransport is a transport that ensures that we always
// set an OONI specific default User-Agent header.
type UserAgentTransport struct {
RoundTripper
}
// RoundTrip implements RoundTripper.RoundTrip
func (txp UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "miniooni/0.1.0-dev")
}
return txp.RoundTripper.RoundTrip(req)
}
var _ RoundTripper = UserAgentTransport{}
@@ -0,0 +1,51 @@
package httptransport_test
import (
"net/http"
"net/url"
"testing"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
)
func TestUserAgentWithDefault(t *testing.T) {
txp := httptransport.UserAgentTransport{
RoundTripper: httptransport.FakeTransport{
Resp: &http.Response{StatusCode: 200},
},
}
req := &http.Request{URL: &url.URL{
Scheme: "https",
Host: "www.google.com",
Path: "/",
}}
req.Header = http.Header{}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
if resp.Request.Header.Get("User-Agent") != "miniooni/0.1.0-dev" {
t.Fatal("not the User-Agent we expected")
}
}
func TestUserAgentWithExplicitValue(t *testing.T) {
txp := httptransport.UserAgentTransport{
RoundTripper: httptransport.FakeTransport{
Resp: &http.Response{StatusCode: 200},
},
}
req := &http.Request{URL: &url.URL{
Scheme: "https",
Host: "www.google.com",
Path: "/",
}}
req.Header = http.Header{"User-Agent": []string{"antani-client/0.1.1"}}
resp, err := txp.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
if resp.Request.Header.Get("User-Agent") != "antani-client/0.1.1" {
t.Fatal("not the User-Agent we expected")
}
}