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:
@@ -0,0 +1,56 @@
|
||||
package hhfm_test
|
||||
|
||||
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,370 @@
|
||||
// Package hhfm contains the HTTP Header Field Manipulation network experiment.
|
||||
//
|
||||
// See https://github.com/ooni/spec/blob/master/nettests/ts-006-header-field-manipulation.md
|
||||
package hhfm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/randx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "http_header_field_manipulation"
|
||||
testVersion = "0.2.0"
|
||||
)
|
||||
|
||||
// Config contains the experiment config.
|
||||
type Config struct{}
|
||||
|
||||
// TestKeys contains the experiment test keys.
|
||||
//
|
||||
// Here we are emitting for the same set of test keys that are
|
||||
// produced by the MK implementation.
|
||||
type TestKeys struct {
|
||||
Agent string `json:"agent"`
|
||||
Failure *string `json:"failure"`
|
||||
Requests []archival.RequestEntry `json:"requests"`
|
||||
SOCKSProxy *string `json:"socksproxy"`
|
||||
Tampering Tampering `json:"tampering"`
|
||||
}
|
||||
|
||||
// Tampering describes the detected forms of tampering.
|
||||
//
|
||||
// The meaning of these fields is described in the specification.
|
||||
type Tampering struct {
|
||||
HeaderFieldName bool `json:"header_field_name"`
|
||||
HeaderFieldNumber bool `json:"header_field_number"`
|
||||
HeaderFieldValue bool `json:"header_field_value"`
|
||||
HeaderNameCapitalization bool `json:"header_name_capitalization"`
|
||||
HeaderNameDiff []string `json:"header_name_diff"`
|
||||
RequestLineCapitalization bool `json:"request_line_capitalization"`
|
||||
Total bool `json:"total"`
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return Measurer{Config: config}
|
||||
}
|
||||
|
||||
// Transport is the definition of http.RoundTripper used by this package.
|
||||
type Transport interface {
|
||||
RoundTrip(req *http.Request) (*http.Response, error)
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
Config Config
|
||||
Transport Transport // for testing
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperiExperimentName.
|
||||
func (m Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNoAvailableTestHelpers is emitted when there are no available test helpers.
|
||||
ErrNoAvailableTestHelpers = errors.New("no available helpers")
|
||||
|
||||
// ErrInvalidHelperType is emitted when the helper type is invalid.
|
||||
ErrInvalidHelperType = errors.New("invalid helper type")
|
||||
)
|
||||
|
||||
// Run implements ExperimentMeasurer.Run.
|
||||
func (m Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
urlgetter.RegisterExtensions(measurement)
|
||||
tk := new(TestKeys)
|
||||
tk.Agent = "agent"
|
||||
tk.Tampering.HeaderNameDiff = []string{}
|
||||
measurement.TestKeys = tk
|
||||
// parse helper
|
||||
const helperName = "http-return-json-headers"
|
||||
helpers, ok := sess.GetTestHelpersByName(helperName)
|
||||
if !ok || len(helpers) < 1 {
|
||||
return ErrNoAvailableTestHelpers
|
||||
}
|
||||
helper := helpers[0]
|
||||
if helper.Type != "legacy" {
|
||||
return ErrInvalidHelperType
|
||||
}
|
||||
measurement.TestHelpers = map[string]interface{}{
|
||||
"backend": helper.Address,
|
||||
}
|
||||
// prepare request
|
||||
req, err := http.NewRequest("GeT", helper.Address, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers := map[string]string{
|
||||
randx.ChangeCapitalization("Accept"): httpheader.Accept(),
|
||||
randx.ChangeCapitalization("Accept-Charset"): "ISO-8859-1,utf-8;q=0.7,*;q=0.3",
|
||||
randx.ChangeCapitalization("Accept-Encoding"): "gzip,deflate,sdch",
|
||||
randx.ChangeCapitalization("Accept-Language"): httpheader.AcceptLanguage(),
|
||||
randx.ChangeCapitalization("Host"): randx.Letters(15) + ".com",
|
||||
randx.ChangeCapitalization("User-Agent"): httpheader.UserAgent(),
|
||||
}
|
||||
for key, value := range headers {
|
||||
// Implementation note: Golang will normalize the header names. We will use
|
||||
// a custom dialer to restore the random capitalisation.
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req.Host = req.Header.Get("Host")
|
||||
// fill tk.Requests[0]
|
||||
tk.Requests = NewRequestEntryList(req, headers)
|
||||
// prepare transport
|
||||
txp := m.Transport
|
||||
if txp == nil {
|
||||
ht := http.DefaultTransport.(*http.Transport).Clone() // basically: use defaults
|
||||
ht.DisableCompression = true // disable sending Accept: gzip
|
||||
ht.ForceAttemptHTTP2 = false
|
||||
ht.DialContext = Dialer{Headers: headers}.DialContext
|
||||
txp = ht
|
||||
}
|
||||
defer txp.CloseIdleConnections()
|
||||
// round trip and read body
|
||||
// TODO(bassosimone): this implementation will lead to false positives if the
|
||||
// network is really bad. Yet, this seems what MK does, so I'd rather start
|
||||
// from that and then see to improve the robustness in the future.
|
||||
resp, data, err := Transact(txp, req.WithContext(ctx), callbacks)
|
||||
if err != nil {
|
||||
tk.Failure = archival.NewFailure(err)
|
||||
tk.Requests[0].Failure = tk.Failure
|
||||
tk.Tampering.Total = true
|
||||
return nil // measurement did not fail, we measured tampering
|
||||
}
|
||||
// fill tk.Requests[0].Response
|
||||
tk.Requests[0].Response = NewHTTPResponse(resp, data)
|
||||
// parse response body
|
||||
var jsonHeaders JSONHeaders
|
||||
if err := json.Unmarshal(data, &jsonHeaders); err != nil {
|
||||
failure := errorx.FailureJSONParseError
|
||||
tk.Failure = &failure
|
||||
tk.Tampering.Total = true
|
||||
return nil // measurement did not fail, we measured tampering
|
||||
}
|
||||
// fill tampering
|
||||
tk.FillTampering(req, jsonHeaders, headers)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transact performs the HTTP transaction which consists of performing
|
||||
// the HTTP round trip and then reading the body.
|
||||
func Transact(txp Transport, req *http.Request,
|
||||
callbacks model.ExperimentCallbacks) (*http.Response, []byte, error) {
|
||||
// make sure that we return a wrapped error here
|
||||
resp, data, err := transact(txp, req, callbacks)
|
||||
err = errorx.SafeErrWrapperBuilder{
|
||||
Error: err, Operation: errorx.TopLevelOperation}.MaybeBuild()
|
||||
return resp, data, err
|
||||
}
|
||||
|
||||
func transact(txp Transport, req *http.Request,
|
||||
callbacks model.ExperimentCallbacks) (*http.Response, []byte, error) {
|
||||
callbacks.OnProgress(0.25, "sending request...")
|
||||
resp, err := txp.RoundTrip(req)
|
||||
callbacks.OnProgress(0.50, fmt.Sprintf("got reseponse headers... %+v", err))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, nil, urlgetter.ErrHTTPRequestFailed
|
||||
}
|
||||
callbacks.OnProgress(0.75, "reading response body...")
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
callbacks.OnProgress(1.00, fmt.Sprintf("got reseponse body... %+v", err))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, data, nil
|
||||
}
|
||||
|
||||
// FillTampering fills the tampering structure in the TestKeys
|
||||
// based on the value of other fields of the TestKeys, the original
|
||||
// HTTP request, the response from the test helper, and the
|
||||
// headers with modified capitalisation.
|
||||
func (tk *TestKeys) FillTampering(
|
||||
req *http.Request, jsonHeaders JSONHeaders, headers map[string]string) {
|
||||
tk.Tampering.RequestLineCapitalization = (fmt.Sprintf(
|
||||
"%s / HTTP/1.1", req.Method) != jsonHeaders.RequestLine)
|
||||
tk.Tampering.HeaderFieldNumber = len(headers) != len(jsonHeaders.HeadersDict)
|
||||
expectedHeaderKeys := make(map[string]string)
|
||||
for key := range headers {
|
||||
expectedHeaderKeys[http.CanonicalHeaderKey(key)] = key
|
||||
}
|
||||
receivedHeaderKeys := make(map[string]string)
|
||||
for key := range jsonHeaders.HeadersDict {
|
||||
receivedHeaderKeys[http.CanonicalHeaderKey(key)] = key
|
||||
}
|
||||
commonHeaderKeys := make(map[string]int)
|
||||
for key := range expectedHeaderKeys {
|
||||
commonHeaderKeys[key]++
|
||||
}
|
||||
for key := range receivedHeaderKeys {
|
||||
commonHeaderKeys[key]++
|
||||
}
|
||||
for key, count := range commonHeaderKeys {
|
||||
if count != 2 {
|
||||
continue // not in common
|
||||
}
|
||||
expectedKey, receivedKey := expectedHeaderKeys[key], receivedHeaderKeys[key]
|
||||
if expectedKey != receivedKey {
|
||||
tk.Tampering.HeaderNameCapitalization = true
|
||||
tk.Tampering.HeaderNameDiff = append(tk.Tampering.HeaderNameDiff, expectedKey)
|
||||
tk.Tampering.HeaderNameDiff = append(tk.Tampering.HeaderNameDiff, receivedKey)
|
||||
}
|
||||
expectedValue := headers[expectedKey]
|
||||
receivedValue := jsonHeaders.HeadersDict[receivedKey]
|
||||
if len(receivedValue) != 1 || expectedValue != receivedValue[0] {
|
||||
tk.Tampering.HeaderFieldValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestEntryList creates a new []archival.RequestEntry given a
|
||||
// specific *http.Request and headers with random case.
|
||||
func NewRequestEntryList(req *http.Request, headers map[string]string) (out []archival.RequestEntry) {
|
||||
out = []archival.RequestEntry{{
|
||||
Request: archival.HTTPRequest{
|
||||
Headers: make(map[string]archival.MaybeBinaryValue),
|
||||
HeadersList: []archival.HTTPHeader{},
|
||||
Method: req.Method,
|
||||
URL: req.URL.String(),
|
||||
},
|
||||
}}
|
||||
for key, value := range headers {
|
||||
// Using the random capitalization headers here
|
||||
mbv := archival.MaybeBinaryValue{Value: value}
|
||||
out[0].Request.Headers[key] = mbv
|
||||
out[0].Request.HeadersList = append(out[0].Request.HeadersList,
|
||||
archival.HTTPHeader{Key: key, Value: mbv})
|
||||
}
|
||||
sort.Slice(out[0].Request.HeadersList, func(i, j int) bool {
|
||||
return out[0].Request.HeadersList[i].Key < out[0].Request.HeadersList[j].Key
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// NewHTTPResponse creates a new archival.HTTPResponse given a
|
||||
// specific *http.Response instance and its body.
|
||||
func NewHTTPResponse(resp *http.Response, data []byte) (out archival.HTTPResponse) {
|
||||
out = archival.HTTPResponse{
|
||||
Body: archival.HTTPBody{Value: string(data)},
|
||||
Code: int64(resp.StatusCode),
|
||||
Headers: make(map[string]archival.MaybeBinaryValue),
|
||||
HeadersList: []archival.HTTPHeader{},
|
||||
}
|
||||
for key := range resp.Header {
|
||||
mbv := archival.MaybeBinaryValue{Value: resp.Header.Get(key)}
|
||||
out.Headers[key] = mbv
|
||||
out.HeadersList = append(out.HeadersList, archival.HTTPHeader{Key: key, Value: mbv})
|
||||
}
|
||||
sort.Slice(out.HeadersList, func(i, j int) bool {
|
||||
return out.HeadersList[i].Key < out.HeadersList[j].Key
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// JSONHeaders contains the response from the backend server.
|
||||
//
|
||||
// Here we're defining only the fields we care about.
|
||||
type JSONHeaders struct {
|
||||
HeadersDict map[string][]string `json:"headers_dict"`
|
||||
RequestLine string `json:"request_line"`
|
||||
}
|
||||
|
||||
// Dialer is a dialer that performs headers transformations.
|
||||
//
|
||||
// Because Golang will canonicalize header names, we need to reintroduce
|
||||
// the random capitalization when emitting the request.
|
||||
//
|
||||
// This implementation rests on the assumption that we shall use the
|
||||
// same connection just once, which is guarantee by the implementation
|
||||
// of HHFM above. If using this code elsewhere, make sure that you
|
||||
// guarantee that the connection is used for a single request and that
|
||||
// such a request does not contain any body.
|
||||
type Dialer struct {
|
||||
Dialer netx.Dialer // used for testing
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// DialContext dials a specific connection and arranges such that
|
||||
// headers in the outgoing request are transformed.
|
||||
func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
dialer := d.Dialer
|
||||
if dialer == nil {
|
||||
dialer = selfcensor.DefaultDialer
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Conn{Conn: conn, Headers: d.Headers}, nil
|
||||
}
|
||||
|
||||
// Conn is a connection where headers in the outgoing request
|
||||
// are transformed according to a transform table.
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// Write implements Conn.Write.
|
||||
func (c Conn) Write(b []byte) (int, error) {
|
||||
for key := range c.Headers {
|
||||
b = bytes.Replace(b, []byte(http.CanonicalHeaderKey(key)+":"), []byte(key+":"), 1)
|
||||
}
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
//
|
||||
// Note that this structure is part of the ABI contract with probe-cli
|
||||
// therefore we should be careful when changing it.
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
sk := SummaryKeys{IsAnomaly: false}
|
||||
tk, ok := measurement.TestKeys.(*TestKeys)
|
||||
if !ok {
|
||||
return sk, errors.New("invalid test keys type")
|
||||
}
|
||||
sk.IsAnomaly = (tk.Tampering.HeaderFieldName ||
|
||||
tk.Tampering.HeaderFieldNumber ||
|
||||
tk.Tampering.HeaderFieldValue ||
|
||||
tk.Tampering.HeaderNameCapitalization ||
|
||||
tk.Tampering.RequestLineCapitalization ||
|
||||
tk.Tampering.Total)
|
||||
return sk, nil
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
package hhfm_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
engine "github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
||||
func TestNewExperimentMeasurer(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
if measurer.ExperimentName() != "http_header_field_manipulation" {
|
||||
t.Fatal("unexpected name")
|
||||
}
|
||||
if measurer.ExperimentVersion() != "0.2.0" {
|
||||
t.Fatal("unexpected version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": {{
|
||||
Address: "http://37.218.241.94:80",
|
||||
Type: "legacy",
|
||||
}},
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 1 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
request := tk.Requests[0]
|
||||
if request.Failure != nil {
|
||||
t.Fatal("invalid Requests[0].Failure")
|
||||
}
|
||||
if request.Request.Body.Value != "" {
|
||||
t.Fatal("invalid Requests[0].Request.Body.Value")
|
||||
}
|
||||
if request.Request.BodyIsTruncated != false {
|
||||
t.Fatal("invalid Requests[0].Request.BodyIsTruncated")
|
||||
}
|
||||
if len(request.Request.HeadersList) != 6 {
|
||||
t.Fatal("invalid Requests[0].Request.HeadersList length")
|
||||
}
|
||||
if len(request.Request.Headers) != 6 {
|
||||
t.Fatal("invalid Requests[0].Request.Headers length")
|
||||
}
|
||||
if strings.ToUpper(request.Request.Method) != "GET" {
|
||||
t.Fatal("invalid Requests[0].Request.Method")
|
||||
}
|
||||
if request.Request.Tor.ExitIP != nil {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.ExitIP")
|
||||
}
|
||||
if request.Request.Tor.ExitName != nil {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.ExitName")
|
||||
}
|
||||
if request.Request.Tor.IsTor != false {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.IsTor")
|
||||
}
|
||||
ths, ok := sess.GetTestHelpersByName("http-return-json-headers")
|
||||
if !ok || len(ths) < 1 || ths[0].Type != "legacy" {
|
||||
t.Fatal("cannot get the test helper")
|
||||
}
|
||||
if request.Request.URL != ths[0].Address {
|
||||
t.Fatal("invalid Requests[0].Request.URL")
|
||||
}
|
||||
if len(request.Response.Body.Value) < 1 {
|
||||
t.Fatal("invalid Requests[0].Response.Body.Value length")
|
||||
}
|
||||
if request.Response.BodyIsTruncated != false {
|
||||
t.Fatal("invalid Requests[0].Response.BodyIsTruncated")
|
||||
}
|
||||
if request.Response.Code != 200 {
|
||||
t.Fatal("invalid Requests[0].Code")
|
||||
}
|
||||
if len(request.Response.HeadersList) != 0 {
|
||||
t.Fatal("invalid Requests[0].HeadersList length")
|
||||
}
|
||||
if len(request.Response.Headers) != 0 {
|
||||
t.Fatal("invalid Requests[0].Headers length")
|
||||
}
|
||||
if request.T != 0 {
|
||||
t.Fatal("invalid Requests[0].T")
|
||||
}
|
||||
if request.TransactionID != 0 {
|
||||
t.Fatal("invalid Requests[0].TransactionID")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != false {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledContext(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": {{
|
||||
Address: "http://37.218.241.94:80",
|
||||
Type: "legacy",
|
||||
}},
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if *tk.Failure != errorx.FailureInterrupted {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 1 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
request := tk.Requests[0]
|
||||
if *request.Failure != errorx.FailureInterrupted {
|
||||
t.Fatal("invalid Requests[0].Failure")
|
||||
}
|
||||
if request.Request.Body.Value != "" {
|
||||
t.Fatal("invalid Requests[0].Request.Body.Value")
|
||||
}
|
||||
if request.Request.BodyIsTruncated != false {
|
||||
t.Fatal("invalid Requests[0].Request.BodyIsTruncated")
|
||||
}
|
||||
if len(request.Request.HeadersList) != 6 {
|
||||
t.Fatal("invalid Requests[0].Request.HeadersList length")
|
||||
}
|
||||
if len(request.Request.Headers) != 6 {
|
||||
t.Fatal("invalid Requests[0].Request.Headers length")
|
||||
}
|
||||
if strings.ToUpper(request.Request.Method) != "GET" {
|
||||
t.Fatal("invalid Requests[0].Request.Method")
|
||||
}
|
||||
if request.Request.Tor.ExitIP != nil {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.ExitIP")
|
||||
}
|
||||
if request.Request.Tor.ExitName != nil {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.ExitName")
|
||||
}
|
||||
if request.Request.Tor.IsTor != false {
|
||||
t.Fatal("invalid Requests[0].Request.Tor.IsTor")
|
||||
}
|
||||
ths, ok := sess.GetTestHelpersByName("http-return-json-headers")
|
||||
if !ok || len(ths) < 1 || ths[0].Type != "legacy" {
|
||||
t.Fatal("cannot get the test helper")
|
||||
}
|
||||
if request.Request.URL != ths[0].Address {
|
||||
t.Fatal("invalid Requests[0].Request.URL")
|
||||
}
|
||||
if len(request.Response.Body.Value) != 0 {
|
||||
t.Fatal("invalid Requests[0].Response.Body.Value length")
|
||||
}
|
||||
if request.Response.BodyIsTruncated != false {
|
||||
t.Fatal("invalid Requests[0].Response.BodyIsTruncated")
|
||||
}
|
||||
if request.Response.Code != 0 {
|
||||
t.Fatal("invalid Requests[0].Code")
|
||||
}
|
||||
if len(request.Response.HeadersList) != 0 {
|
||||
t.Fatal("invalid Requests[0].HeadersList length")
|
||||
}
|
||||
if len(request.Response.Headers) != 0 {
|
||||
t.Fatal("invalid Requests[0].Headers length")
|
||||
}
|
||||
if request.T != 0 {
|
||||
t.Fatal("invalid Requests[0].T")
|
||||
}
|
||||
if request.TransactionID != 0 {
|
||||
t.Fatal("invalid Requests[0].TransactionID")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != true {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
sk, err := measurer.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := sk.(hhfm.SummaryKeys); !ok {
|
||||
t.Fatal("invalid type for summary keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoHelpers(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 0 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != false {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActualHelpersInList(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": nil,
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 0 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != false {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongTestHelperType(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": {{
|
||||
Address: "http://127.0.0.1",
|
||||
Type: "antani",
|
||||
}},
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if !errors.Is(err, hhfm.ErrInvalidHelperType) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 0 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != false {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequestFailure(t *testing.T) {
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": {{
|
||||
Address: "http://127.0.0.1\t\t\t", // invalid
|
||||
Type: "legacy",
|
||||
}},
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if tk.Failure != nil {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 0 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != false {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidJSONBody(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello, client") // not valid JSON
|
||||
}))
|
||||
defer server.Close()
|
||||
measurer := hhfm.NewExperimentMeasurer(hhfm.Config{})
|
||||
ctx := context.Background()
|
||||
sess := &mockable.Session{
|
||||
MockableTestHelpers: map[string][]model.Service{
|
||||
"http-return-json-headers": {{
|
||||
Address: server.URL,
|
||||
Type: "legacy",
|
||||
}},
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
err := measurer.Run(ctx, sess, measurement, callbacks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*hhfm.TestKeys)
|
||||
if tk.Agent != "agent" {
|
||||
t.Fatal("invalid Agent")
|
||||
}
|
||||
if *tk.Failure != errorx.FailureJSONParseError {
|
||||
t.Fatal("invalid Failure")
|
||||
}
|
||||
if len(tk.Requests) != 1 {
|
||||
t.Fatal("invalid Requests")
|
||||
}
|
||||
// we already check the content of Requests in other tests
|
||||
if tk.SOCKSProxy != nil {
|
||||
t.Fatal("invalid SOCKSProxy")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldName != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldName")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldNumber != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldNumber")
|
||||
}
|
||||
if tk.Tampering.HeaderFieldValue != false {
|
||||
t.Fatal("invalid Tampering.HeaderFieldValue")
|
||||
}
|
||||
if tk.Tampering.HeaderNameCapitalization != false {
|
||||
t.Fatal("invalid Tampering.HeaderNameCapitalization")
|
||||
}
|
||||
if len(tk.Tampering.HeaderNameDiff) != 0 {
|
||||
t.Fatal("invalid Tampering.HeaderNameDiff")
|
||||
}
|
||||
if tk.Tampering.RequestLineCapitalization != false {
|
||||
t.Fatal("invalid Tampering.RequestLineCapitalization")
|
||||
}
|
||||
if tk.Tampering.Total != true {
|
||||
t.Fatal("invalid Tampering.Total")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactStatusCodeFailure(t *testing.T) {
|
||||
txp := FakeTransport{Resp: &http.Response{
|
||||
Body: ioutil.NopCloser(strings.NewReader("")),
|
||||
StatusCode: 500,
|
||||
}}
|
||||
resp, body, err := hhfm.Transact(txp, &http.Request{},
|
||||
model.NewPrinterCallbacks(log.Log))
|
||||
if !errors.Is(err, urlgetter.ErrHTTPRequestFailed) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("resp is not nil")
|
||||
}
|
||||
if body != nil {
|
||||
t.Fatal("body is not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactCannotReadBody(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
txp := FakeTransport{Resp: &http.Response{
|
||||
Body: &FakeBody{Err: expected},
|
||||
StatusCode: 200,
|
||||
}}
|
||||
resp, body, err := hhfm.Transact(txp, &http.Request{},
|
||||
model.NewPrinterCallbacks(log.Log))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("resp is not nil")
|
||||
}
|
||||
if body != nil {
|
||||
t.Fatal("body is not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func newsession(t *testing.T) model.ExperimentSession {
|
||||
sess, err := engine.NewSession(engine.SessionConfig{
|
||||
AssetsDir: "../../testdata",
|
||||
AvailableProbeServices: []model.Service{{
|
||||
Address: "https://ams-pg-test.ooni.org",
|
||||
Type: "https",
|
||||
}},
|
||||
Logger: log.Log,
|
||||
SoftwareName: "ooniprobe-engine",
|
||||
SoftwareVersion: "0.0.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sess.MaybeLookupBackends(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
func TestTestKeys_FillTampering(t *testing.T) {
|
||||
type fields struct {
|
||||
Agent string
|
||||
Failure *string
|
||||
Requests []archival.RequestEntry
|
||||
SOCKSProxy *string
|
||||
Tampering hhfm.Tampering
|
||||
}
|
||||
type args struct {
|
||||
req *http.Request
|
||||
jsonHeaders hhfm.JSONHeaders
|
||||
headers map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
}{{
|
||||
name: "Request line capitalisation",
|
||||
fields: fields{
|
||||
Tampering: hhfm.Tampering{
|
||||
RequestLineCapitalization: true,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
},
|
||||
jsonHeaders: hhfm.JSONHeaders{
|
||||
RequestLine: "GET / HTTP/1.1",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Header field number",
|
||||
fields: fields{
|
||||
Tampering: hhfm.Tampering{
|
||||
HeaderFieldNumber: true,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
},
|
||||
jsonHeaders: hhfm.JSONHeaders{
|
||||
RequestLine: "GeT / HTTP/1.1",
|
||||
},
|
||||
headers: map[string]string{
|
||||
"UsEr-AgENt": "miniooni/0.1.0-dev",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Header name diff",
|
||||
fields: fields{
|
||||
Tampering: hhfm.Tampering{
|
||||
HeaderNameCapitalization: true,
|
||||
HeaderNameDiff: []string{"UsEr-AgENt", "User-Agent"},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
},
|
||||
jsonHeaders: hhfm.JSONHeaders{
|
||||
RequestLine: "GeT / HTTP/1.1",
|
||||
HeadersDict: map[string][]string{
|
||||
"User-Agent": {"miniooni/0.1.0-dev"},
|
||||
},
|
||||
},
|
||||
headers: map[string]string{
|
||||
"UsEr-AgENt": "miniooni/0.1.0-dev",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Header value diff",
|
||||
fields: fields{
|
||||
Tampering: hhfm.Tampering{
|
||||
HeaderFieldValue: true,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
},
|
||||
jsonHeaders: hhfm.JSONHeaders{
|
||||
RequestLine: "GeT / HTTP/1.1",
|
||||
HeadersDict: map[string][]string{
|
||||
"UsEr-AgENt": {"MINIOONI/0.1.0-dev"},
|
||||
},
|
||||
},
|
||||
headers: map[string]string{
|
||||
"UsEr-AgENt": "miniooni/0.1.0-dev",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Number of headers per key diffs",
|
||||
fields: fields{
|
||||
Tampering: hhfm.Tampering{
|
||||
HeaderFieldValue: true,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
},
|
||||
jsonHeaders: hhfm.JSONHeaders{
|
||||
RequestLine: "GeT / HTTP/1.1",
|
||||
HeadersDict: map[string][]string{
|
||||
"UsEr-AgENt": {"miniooni/0.1.0-dev", "ooniprobe-engine/0.1.0-dev"},
|
||||
},
|
||||
},
|
||||
headers: map[string]string{
|
||||
"UsEr-AgENt": "miniooni/0.1.0-dev",
|
||||
},
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tk := &hhfm.TestKeys{
|
||||
Agent: tt.fields.Agent,
|
||||
Failure: tt.fields.Failure,
|
||||
Requests: tt.fields.Requests,
|
||||
SOCKSProxy: tt.fields.SOCKSProxy,
|
||||
}
|
||||
tk.FillTampering(tt.args.req, tt.args.jsonHeaders, tt.args.headers)
|
||||
if diff := cmp.Diff(tt.fields.Tampering, tk.Tampering); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequestEntryList(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
headers map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut []archival.RequestEntry
|
||||
}{{
|
||||
name: "common case",
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "10.0.0.1",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
headers: map[string]string{
|
||||
"ContENt-tYPE": "text/plain",
|
||||
"User-aGENT": "foo/1.0",
|
||||
},
|
||||
},
|
||||
wantOut: []archival.RequestEntry{{
|
||||
Request: archival.HTTPRequest{
|
||||
HeadersList: []archival.HTTPHeader{{
|
||||
Key: "ContENt-tYPE",
|
||||
Value: archival.MaybeBinaryValue{Value: "text/plain"},
|
||||
}, {
|
||||
Key: "User-aGENT",
|
||||
Value: archival.MaybeBinaryValue{Value: "foo/1.0"},
|
||||
}},
|
||||
Headers: map[string]archival.MaybeBinaryValue{
|
||||
"ContENt-tYPE": {Value: "text/plain"},
|
||||
"User-aGENT": {Value: "foo/1.0"},
|
||||
},
|
||||
Method: "GeT",
|
||||
URL: "http://10.0.0.1/",
|
||||
},
|
||||
}},
|
||||
}, {
|
||||
name: "without headers",
|
||||
args: args{
|
||||
req: &http.Request{
|
||||
Method: "GeT",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "10.0.0.1",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantOut: []archival.RequestEntry{{
|
||||
Request: archival.HTTPRequest{
|
||||
Method: "GeT",
|
||||
Headers: make(map[string]archival.MaybeBinaryValue),
|
||||
HeadersList: []archival.HTTPHeader{},
|
||||
URL: "http://10.0.0.1/",
|
||||
},
|
||||
}},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotOut := hhfm.NewRequestEntryList(tt.args.req, tt.args.headers)
|
||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPResponse(t *testing.T) {
|
||||
type args struct {
|
||||
resp *http.Response
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut archival.HTTPResponse
|
||||
}{{
|
||||
name: "common case",
|
||||
args: args{
|
||||
resp: &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"text/plain"},
|
||||
"User-Agent": []string{"foo/1.0"},
|
||||
},
|
||||
},
|
||||
data: []byte("deadbeef"),
|
||||
},
|
||||
wantOut: archival.HTTPResponse{
|
||||
Body: archival.MaybeBinaryValue{Value: "deadbeef"},
|
||||
Code: 200,
|
||||
HeadersList: []archival.HTTPHeader{{
|
||||
Key: "Content-Type",
|
||||
Value: archival.MaybeBinaryValue{Value: "text/plain"},
|
||||
}, {
|
||||
Key: "User-Agent",
|
||||
Value: archival.MaybeBinaryValue{Value: "foo/1.0"},
|
||||
}},
|
||||
Headers: map[string]archival.MaybeBinaryValue{
|
||||
"Content-Type": {Value: "text/plain"},
|
||||
"User-Agent": {Value: "foo/1.0"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "with no HTTP header and body",
|
||||
args: args{
|
||||
resp: &http.Response{StatusCode: 200},
|
||||
},
|
||||
wantOut: archival.HTTPResponse{
|
||||
Body: archival.MaybeBinaryValue{Value: ""},
|
||||
Code: 200,
|
||||
HeadersList: []archival.HTTPHeader{},
|
||||
Headers: map[string]archival.MaybeBinaryValue{},
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotOut := hhfm.NewHTTPResponse(tt.args.resp, tt.args.data)
|
||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialerDialContext(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
d := hhfm.Dialer{Dialer: FakeDialer{Err: expected}}
|
||||
conn, err := d.DialContext(context.Background(), "tcp", "127.0.0.1:80")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("conn is not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryKeysInvalidType(t *testing.T) {
|
||||
measurement := new(model.Measurement)
|
||||
m := &hhfm.Measurer{}
|
||||
_, err := m.GetSummaryKeys(measurement)
|
||||
if err.Error() != "invalid test keys type" {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryKeysWorksAsIntended(t *testing.T) {
|
||||
tests := []struct {
|
||||
tampering hhfm.Tampering
|
||||
isAnomaly bool
|
||||
}{{
|
||||
tampering: hhfm.Tampering{},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{HeaderFieldName: true},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{HeaderFieldNumber: true},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{HeaderFieldValue: true},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{HeaderNameCapitalization: true},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{RequestLineCapitalization: true},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tampering: hhfm.Tampering{Total: true},
|
||||
isAnomaly: true,
|
||||
}}
|
||||
for idx, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
|
||||
m := &hhfm.Measurer{}
|
||||
measurement := &model.Measurement{TestKeys: &hhfm.TestKeys{
|
||||
Tampering: tt.tampering,
|
||||
}}
|
||||
got, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
sk := got.(hhfm.SummaryKeys)
|
||||
if sk.IsAnomaly != tt.isAnomaly {
|
||||
t.Fatal("unexpected isAnomaly value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user