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,476 @@
|
||||
// Package tor contains the tor experiment.
|
||||
//
|
||||
// Spec: https://github.com/ooni/spec/blob/master/nettests/ts-023-tor.md
|
||||
package tor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/runtimex"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/netxlogger"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
)
|
||||
|
||||
const (
|
||||
// parallelism is the number of parallel threads we use for this experiment
|
||||
parallelism = 2
|
||||
|
||||
// testName is the name of this experiment
|
||||
testName = "tor"
|
||||
|
||||
// testVersion is the version of this experiment
|
||||
testVersion = "0.3.0"
|
||||
)
|
||||
|
||||
// Config contains the experiment config.
|
||||
type Config struct{}
|
||||
|
||||
// Summary contains a summary of what happened.
|
||||
type Summary struct {
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// TargetResults contains the results of measuring a target.
|
||||
type TargetResults struct {
|
||||
Agent string `json:"agent"`
|
||||
Failure *string `json:"failure"`
|
||||
NetworkEvents oonidatamodel.NetworkEventsList `json:"network_events"`
|
||||
Queries oonidatamodel.DNSQueriesList `json:"queries"`
|
||||
Requests oonidatamodel.RequestList `json:"requests"`
|
||||
Summary map[string]Summary `json:"summary"`
|
||||
TargetAddress string `json:"target_address"`
|
||||
TargetName string `json:"target_name,omitempty"`
|
||||
TargetProtocol string `json:"target_protocol"`
|
||||
TargetSource string `json:"target_source,omitempty"`
|
||||
TCPConnect oonidatamodel.TCPConnectList `json:"tcp_connect"`
|
||||
TLSHandshakes oonidatamodel.TLSHandshakesList `json:"tls_handshakes"`
|
||||
}
|
||||
|
||||
func registerExtensions(m *model.Measurement) {
|
||||
oonidatamodel.ExtHTTP.AddTo(m)
|
||||
oonidatamodel.ExtNetevents.AddTo(m)
|
||||
oonidatamodel.ExtDNS.AddTo(m)
|
||||
oonidatamodel.ExtTCPConnect.AddTo(m)
|
||||
oonidatamodel.ExtTLSHandshake.AddTo(m)
|
||||
}
|
||||
|
||||
// fillSummary fills the Summary field used by the UI.
|
||||
func (tr *TargetResults) fillSummary() {
|
||||
tr.Summary = make(map[string]Summary)
|
||||
if len(tr.TCPConnect) < 1 {
|
||||
return
|
||||
}
|
||||
tr.Summary[errorx.ConnectOperation] = Summary{
|
||||
Failure: tr.TCPConnect[0].Status.Failure,
|
||||
}
|
||||
switch tr.TargetProtocol {
|
||||
case "dir_port":
|
||||
// The UI currently doesn't care about this protocol
|
||||
// as long as drawing a table is concerned.
|
||||
case "obfs4":
|
||||
// We currently only perform an OBFS4 handshake, hence
|
||||
// the final Failure is the handshake result
|
||||
tr.Summary["handshake"] = Summary{
|
||||
Failure: tr.Failure,
|
||||
}
|
||||
case "or_port_dirauth", "or_port":
|
||||
if len(tr.TLSHandshakes) < 1 {
|
||||
return
|
||||
}
|
||||
tr.Summary["handshake"] = Summary{
|
||||
Failure: tr.TLSHandshakes[0].Failure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeys contains tor test keys.
|
||||
type TestKeys struct {
|
||||
DirPortTotal int64 `json:"dir_port_total"`
|
||||
DirPortAccessible int64 `json:"dir_port_accessible"`
|
||||
OBFS4Total int64 `json:"obfs4_total"`
|
||||
OBFS4Accessible int64 `json:"obfs4_accessible"`
|
||||
ORPortDirauthTotal int64 `json:"or_port_dirauth_total"`
|
||||
ORPortDirauthAccessible int64 `json:"or_port_dirauth_accessible"`
|
||||
ORPortTotal int64 `json:"or_port_total"`
|
||||
ORPortAccessible int64 `json:"or_port_accessible"`
|
||||
Targets map[string]TargetResults `json:"targets"`
|
||||
}
|
||||
|
||||
func (tk *TestKeys) fillToplevelKeys() {
|
||||
for _, value := range tk.Targets {
|
||||
switch value.TargetProtocol {
|
||||
case "dir_port":
|
||||
tk.DirPortTotal++
|
||||
if value.Failure == nil {
|
||||
tk.DirPortAccessible++
|
||||
}
|
||||
case "obfs4":
|
||||
tk.OBFS4Total++
|
||||
if value.Failure == nil {
|
||||
tk.OBFS4Accessible++
|
||||
}
|
||||
case "or_port_dirauth":
|
||||
tk.ORPortDirauthTotal++
|
||||
if value.Failure == nil {
|
||||
tk.ORPortDirauthAccessible++
|
||||
}
|
||||
case "or_port":
|
||||
tk.ORPortTotal++
|
||||
if value.Failure == nil {
|
||||
tk.ORPortAccessible++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
fetchTorTargets func(ctx context.Context, clnt model.ExperimentOrchestraClient, cc string) (map[string]model.TorTarget, error)
|
||||
newOrchestraClient func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error)
|
||||
}
|
||||
|
||||
// NewMeasurer creates a new Measurer
|
||||
func NewMeasurer(config Config) *Measurer {
|
||||
return &Measurer{
|
||||
config: config,
|
||||
fetchTorTargets: func(ctx context.Context, clnt model.ExperimentOrchestraClient, cc string) (map[string]model.TorTarget, error) {
|
||||
return clnt.FetchTorTargets(ctx, cc)
|
||||
},
|
||||
newOrchestraClient: func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error) {
|
||||
return sess.NewOrchestraClient(ctx)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperiExperimentName.
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
// Run implements ExperimentMeasurer.Run.
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context,
|
||||
sess model.ExperimentSession,
|
||||
measurement *model.Measurement,
|
||||
callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
targets, err := m.gimmeTargets(ctx, sess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(
|
||||
ctx, 15*time.Second*time.Duration(len(targets)),
|
||||
)
|
||||
defer cancel()
|
||||
registerExtensions(measurement)
|
||||
m.measureTargets(ctx, sess, measurement, callbacks, targets)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Measurer) gimmeTargets(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
) (map[string]model.TorTarget, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
clnt, err := m.newOrchestraClient(ctx, sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.fetchTorTargets(ctx, clnt, sess.ProbeCC())
|
||||
}
|
||||
|
||||
// keytarget contains a key and the related target
|
||||
type keytarget struct {
|
||||
key string
|
||||
target model.TorTarget
|
||||
}
|
||||
|
||||
// private returns whether a target is private. We consider private
|
||||
// every target coming from a non-empty data source.
|
||||
func (kt keytarget) private() bool {
|
||||
return kt.target.Source != ""
|
||||
}
|
||||
|
||||
// maybeTargetAddress returns the target address if the target is
|
||||
// not private, otherwise it returns `"[scrubbed]""`.
|
||||
func (kt keytarget) maybeTargetAddress() (address string) {
|
||||
address = "[scrubbed]"
|
||||
if !kt.private() {
|
||||
address = kt.target.Address
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Measurer) measureTargets(
|
||||
ctx context.Context,
|
||||
sess model.ExperimentSession,
|
||||
measurement *model.Measurement,
|
||||
callbacks model.ExperimentCallbacks,
|
||||
targets map[string]model.TorTarget,
|
||||
) {
|
||||
// run measurements in parallel
|
||||
var waitgroup sync.WaitGroup
|
||||
rc := newResultsCollector(sess, measurement, callbacks)
|
||||
waitgroup.Add(len(targets))
|
||||
workch := make(chan keytarget)
|
||||
for i := 0; i < parallelism; i++ {
|
||||
go func(ch <-chan keytarget, total int) {
|
||||
for kt := range ch {
|
||||
rc.measureSingleTarget(ctx, kt, total)
|
||||
waitgroup.Done()
|
||||
}
|
||||
}(workch, len(targets))
|
||||
}
|
||||
for key, target := range targets {
|
||||
workch <- keytarget{key: key, target: target}
|
||||
}
|
||||
close(workch)
|
||||
waitgroup.Wait()
|
||||
// fill the measurement entry
|
||||
testkeys := &TestKeys{Targets: rc.targetresults}
|
||||
testkeys.fillToplevelKeys()
|
||||
measurement.TestKeys = testkeys
|
||||
}
|
||||
|
||||
type resultsCollector struct {
|
||||
callbacks model.ExperimentCallbacks
|
||||
completed *atomicx.Int64
|
||||
flexibleConnect func(context.Context, keytarget) (oonitemplates.Results, error)
|
||||
measurement *model.Measurement
|
||||
mu sync.Mutex
|
||||
sess model.ExperimentSession
|
||||
targetresults map[string]TargetResults
|
||||
}
|
||||
|
||||
func newResultsCollector(
|
||||
sess model.ExperimentSession,
|
||||
measurement *model.Measurement,
|
||||
callbacks model.ExperimentCallbacks,
|
||||
) *resultsCollector {
|
||||
rc := &resultsCollector{
|
||||
callbacks: callbacks,
|
||||
completed: atomicx.NewInt64(),
|
||||
measurement: measurement,
|
||||
sess: sess,
|
||||
targetresults: make(map[string]TargetResults),
|
||||
}
|
||||
rc.flexibleConnect = rc.defaultFlexibleConnect
|
||||
return rc
|
||||
}
|
||||
|
||||
func maybeSanitize(input TargetResults, kt keytarget) TargetResults {
|
||||
if !kt.private() {
|
||||
return input
|
||||
}
|
||||
data, err := json.Marshal(input)
|
||||
runtimex.PanicOnError(err, "json.Marshal should not fail here")
|
||||
// Implementation note: here we are using a strict scrubbing policy where
|
||||
// we remove all IP _endpoints_, mainly for convenience, because we already
|
||||
// have a well tested implementation that does that.
|
||||
data = []byte(errorx.Scrub(string(data)))
|
||||
var out TargetResults
|
||||
err = json.Unmarshal(data, &out)
|
||||
runtimex.PanicOnError(err, "json.Unmarshal should not fail here")
|
||||
return out
|
||||
}
|
||||
|
||||
func (rc *resultsCollector) measureSingleTarget(
|
||||
ctx context.Context, kt keytarget, total int,
|
||||
) {
|
||||
tk, err := rc.flexibleConnect(ctx, kt)
|
||||
tr := TargetResults{
|
||||
Agent: "redirect",
|
||||
Failure: setFailure(err),
|
||||
NetworkEvents: oonidatamodel.NewNetworkEventsList(tk),
|
||||
Queries: oonidatamodel.NewDNSQueriesList(tk),
|
||||
Requests: oonidatamodel.NewRequestList(tk),
|
||||
TCPConnect: oonidatamodel.NewTCPConnectList(tk),
|
||||
TLSHandshakes: oonidatamodel.NewTLSHandshakesList(tk),
|
||||
}
|
||||
tr.fillSummary()
|
||||
tr = maybeSanitize(tr, kt)
|
||||
rc.mu.Lock()
|
||||
tr.TargetAddress = kt.maybeTargetAddress()
|
||||
tr.TargetName = kt.target.Name
|
||||
tr.TargetProtocol = kt.target.Protocol
|
||||
tr.TargetSource = kt.target.Source
|
||||
rc.targetresults[kt.key] = tr
|
||||
rc.mu.Unlock()
|
||||
sofar := rc.completed.Add(1)
|
||||
percentage := 0.0
|
||||
if total > 0 {
|
||||
percentage = float64(sofar) / float64(total)
|
||||
}
|
||||
rc.callbacks.OnProgress(percentage, fmt.Sprintf(
|
||||
"tor: access %s/%s: %s", kt.maybeTargetAddress(), kt.target.Protocol,
|
||||
errString(err),
|
||||
))
|
||||
}
|
||||
|
||||
// scrubbingLogger is a logger that scrubs endpoints from its output. We are using
|
||||
// it only here, currently, since we pay some performance penalty in that we evaluate
|
||||
// the string to be logged regardless of the logging level.
|
||||
//
|
||||
// TODO(bassosimone): find a more efficient way of scrubbing logs.
|
||||
type scrubbingLogger struct {
|
||||
model.Logger
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Debug(message string) {
|
||||
sl.Logger.Debug(errorx.Scrub(message))
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Debugf(format string, v ...interface{}) {
|
||||
sl.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Info(message string) {
|
||||
sl.Logger.Info(errorx.Scrub(message))
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Infof(format string, v ...interface{}) {
|
||||
sl.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Warn(message string) {
|
||||
sl.Logger.Warn(errorx.Scrub(message))
|
||||
}
|
||||
|
||||
func (sl scrubbingLogger) Warnf(format string, v ...interface{}) {
|
||||
sl.Warn(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func maybeScrubbingLogger(input model.Logger, kt keytarget) model.Logger {
|
||||
if !kt.private() {
|
||||
return input
|
||||
}
|
||||
return scrubbingLogger{Logger: input}
|
||||
}
|
||||
|
||||
func (rc *resultsCollector) defaultFlexibleConnect(
|
||||
ctx context.Context, kt keytarget,
|
||||
) (tk oonitemplates.Results, err error) {
|
||||
logger := maybeScrubbingLogger(rc.sess.Logger(), kt)
|
||||
switch kt.target.Protocol {
|
||||
case "dir_port":
|
||||
url := url.URL{
|
||||
Host: kt.target.Address,
|
||||
Path: "/tor/status-vote/current/consensus.z",
|
||||
Scheme: "http",
|
||||
}
|
||||
const snapshotsize = 1 << 8 // no need to include all in report
|
||||
r := oonitemplates.HTTPDo(ctx, oonitemplates.HTTPDoConfig{
|
||||
Accept: httpheader.Accept(),
|
||||
AcceptLanguage: httpheader.AcceptLanguage(),
|
||||
Beginning: rc.measurement.MeasurementStartTimeSaved,
|
||||
MaxEventsBodySnapSize: snapshotsize,
|
||||
MaxResponseBodySnapSize: snapshotsize,
|
||||
Handler: netxlogger.NewHandler(logger),
|
||||
Method: "GET",
|
||||
URL: url.String(),
|
||||
UserAgent: httpheader.UserAgent(),
|
||||
})
|
||||
tk, err = r.TestKeys, r.Error
|
||||
case "or_port", "or_port_dirauth":
|
||||
r := oonitemplates.TLSConnect(ctx, oonitemplates.TLSConnectConfig{
|
||||
Address: kt.target.Address,
|
||||
Beginning: rc.measurement.MeasurementStartTimeSaved,
|
||||
InsecureSkipVerify: true,
|
||||
Handler: netxlogger.NewHandler(logger),
|
||||
})
|
||||
tk, err = r.TestKeys, r.Error
|
||||
case "obfs4":
|
||||
r := oonitemplates.OBFS4Connect(ctx, oonitemplates.OBFS4ConnectConfig{
|
||||
Address: kt.target.Address,
|
||||
Beginning: rc.measurement.MeasurementStartTimeSaved,
|
||||
Handler: netxlogger.NewHandler(logger),
|
||||
Params: kt.target.Params,
|
||||
StateBaseDir: rc.sess.TempDir(),
|
||||
})
|
||||
tk, err = r.TestKeys, r.Error
|
||||
default:
|
||||
r := oonitemplates.TCPConnect(ctx, oonitemplates.TCPConnectConfig{
|
||||
Address: kt.target.Address,
|
||||
Beginning: rc.measurement.MeasurementStartTimeSaved,
|
||||
Handler: netxlogger.NewHandler(logger),
|
||||
})
|
||||
tk, err = r.TestKeys, r.Error
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return NewMeasurer(config)
|
||||
}
|
||||
|
||||
func errString(err error) (s string) {
|
||||
s = "success"
|
||||
if err != nil {
|
||||
s = err.Error()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func setFailure(err error) (s *string) {
|
||||
if err != nil {
|
||||
descr := err.Error()
|
||||
s = &descr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
DirPortTotal int64 `json:"dir_port_total"`
|
||||
DirPortAccessible int64 `json:"dir_port_accessible"`
|
||||
OBFS4Total int64 `json:"obfs4_total"`
|
||||
OBFS4Accessible int64 `json:"obfs4_accessible"`
|
||||
ORPortDirauthTotal int64 `json:"or_port_dirauth_total"`
|
||||
ORPortDirauthAccessible int64 `json:"or_port_dirauth_accessible"`
|
||||
ORPortTotal int64 `json:"or_port_total"`
|
||||
ORPortAccessible int64 `json:"or_port_accessible"`
|
||||
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.DirPortTotal = tk.DirPortTotal
|
||||
sk.DirPortAccessible = tk.DirPortAccessible
|
||||
sk.OBFS4Total = tk.OBFS4Total
|
||||
sk.OBFS4Accessible = tk.OBFS4Accessible
|
||||
sk.ORPortDirauthTotal = tk.ORPortDirauthTotal
|
||||
sk.ORPortDirauthAccessible = tk.ORPortDirauthAccessible
|
||||
sk.ORPortTotal = tk.ORPortTotal
|
||||
sk.ORPortAccessible = tk.ORPortAccessible
|
||||
sk.IsAnomaly = ((sk.DirPortAccessible <= 0 && sk.DirPortTotal > 0) ||
|
||||
(sk.OBFS4Accessible <= 0 && sk.OBFS4Total > 0) ||
|
||||
(sk.ORPortDirauthAccessible <= 0 && sk.ORPortDirauthTotal > 0) ||
|
||||
(sk.ORPortAccessible <= 0 && sk.ORPortTotal > 0))
|
||||
return sk, nil
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
package tor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/probeservices"
|
||||
)
|
||||
|
||||
func TestNewExperimentMeasurer(t *testing.T) {
|
||||
measurer := NewExperimentMeasurer(Config{})
|
||||
if measurer.ExperimentName() != "tor" {
|
||||
t.Fatal("unexpected name")
|
||||
}
|
||||
if measurer.ExperimentVersion() != "0.3.0" {
|
||||
t.Fatal("unexpected version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureNewOrchestraClientError(t *testing.T) {
|
||||
measurer := NewMeasurer(Config{})
|
||||
expected := errors.New("mocked error")
|
||||
measurer.newOrchestraClient = func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error) {
|
||||
return nil, expected
|
||||
}
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureFetchTorTargetsError(t *testing.T) {
|
||||
measurer := NewMeasurer(Config{})
|
||||
expected := errors.New("mocked error")
|
||||
measurer.newOrchestraClient = func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error) {
|
||||
return new(probeservices.Client), nil
|
||||
}
|
||||
measurer.fetchTorTargets = func(ctx context.Context, clnt model.ExperimentOrchestraClient, cc string) (map[string]model.TorTarget, error) {
|
||||
return nil, expected
|
||||
}
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureFetchTorTargetsEmptyList(t *testing.T) {
|
||||
measurer := NewMeasurer(Config{})
|
||||
measurer.newOrchestraClient = func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error) {
|
||||
return new(probeservices.Client), nil
|
||||
}
|
||||
measurer.fetchTorTargets = func(ctx context.Context, clnt model.ExperimentOrchestraClient, cc string) (map[string]model.TorTarget, error) {
|
||||
return nil, nil
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
if len(tk.Targets) != 0 {
|
||||
t.Fatal("expected no targets here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureGoodWithMockedOrchestra(t *testing.T) {
|
||||
// This test mocks orchestra to return a nil list of targets, so the code runs
|
||||
// but we don't perform any actualy network actions.
|
||||
measurer := NewMeasurer(Config{})
|
||||
measurer.newOrchestraClient = func(ctx context.Context, sess model.ExperimentSession) (model.ExperimentOrchestraClient, error) {
|
||||
return new(probeservices.Client), nil
|
||||
}
|
||||
measurer.fetchTorTargets = func(ctx context.Context, clnt model.ExperimentOrchestraClient, cc string) (map[string]model.TorTarget, error) {
|
||||
return nil, nil
|
||||
}
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureGood(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
measurer := NewMeasurer(Config{})
|
||||
sess := newsession()
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
sess,
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sk, err := measurer.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := sk.(SummaryKeys); !ok {
|
||||
t.Fatal("invalid type for summary keys")
|
||||
}
|
||||
}
|
||||
|
||||
var staticPrivateTestingTargetEndpoint = "192.95.36.142:443"
|
||||
|
||||
var staticPrivateTestingTarget = model.TorTarget{
|
||||
Address: staticPrivateTestingTargetEndpoint,
|
||||
Params: map[string][]string{
|
||||
"cert": {
|
||||
"qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ",
|
||||
},
|
||||
"iat-mode": {"1"},
|
||||
},
|
||||
Protocol: "obfs4",
|
||||
Source: "bridgedb",
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureSanitiseOutput(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skip test in short mode")
|
||||
}
|
||||
measurer := NewMeasurer(Config{})
|
||||
sess := newsession()
|
||||
key := "xyz-xyz-xyz-theCh2ju-ahG4chei-Ai2eka0a"
|
||||
sess.MockableOrchestraClient = &mockable.ExperimentOrchestraClient{
|
||||
MockableFetchTorTargetsResult: map[string]model.TorTarget{
|
||||
key: staticPrivateTestingTarget,
|
||||
},
|
||||
}
|
||||
measurement := new(model.Measurement)
|
||||
err := measurer.Run(
|
||||
context.Background(),
|
||||
sess,
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tk := measurement.TestKeys.(*TestKeys)
|
||||
entry := tk.Targets[key]
|
||||
if entry.Failure != nil {
|
||||
t.Fatal("measurement failed unexpectedly")
|
||||
}
|
||||
if !bytes.Contains(data, []byte(key)) {
|
||||
t.Fatal("cannot find expected key")
|
||||
}
|
||||
if bytes.Contains(data, []byte(staticPrivateTestingTargetEndpoint)) {
|
||||
t.Fatal("endpoint found in serialized measurement")
|
||||
}
|
||||
if !bytes.Contains(data, []byte("[scrubbed]")) {
|
||||
t.Fatal("[scrubbed] not found in serialized measurement")
|
||||
}
|
||||
}
|
||||
|
||||
var staticTestingTargets = []model.TorTarget{
|
||||
{
|
||||
Address: "192.95.36.142:443",
|
||||
Params: map[string][]string{
|
||||
"cert": {
|
||||
"qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ",
|
||||
},
|
||||
"iat-mode": {"1"},
|
||||
},
|
||||
Protocol: "obfs4",
|
||||
},
|
||||
{
|
||||
Address: "66.111.2.131:9030",
|
||||
Protocol: "dir_port",
|
||||
},
|
||||
{
|
||||
Address: "66.111.2.131:9001",
|
||||
Protocol: "or_port",
|
||||
},
|
||||
{
|
||||
Address: "1.1.1.1:80",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureTargetsNoInput(t *testing.T) {
|
||||
var measurement model.Measurement
|
||||
measurer := new(Measurer)
|
||||
measurer.measureTargets(
|
||||
context.Background(),
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
&measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
nil,
|
||||
)
|
||||
if len(measurement.TestKeys.(*TestKeys).Targets) != 0 {
|
||||
t.Fatal("expected no measurements here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurerMeasureTargetsCanceledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // so we don't actually do anything
|
||||
var measurement model.Measurement
|
||||
measurer := new(Measurer)
|
||||
measurer.measureTargets(
|
||||
ctx,
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
&measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
map[string]model.TorTarget{
|
||||
"xx": staticTestingTargets[0],
|
||||
},
|
||||
)
|
||||
targets := measurement.TestKeys.(*TestKeys).Targets
|
||||
if len(targets) != 1 {
|
||||
t.Fatal("expected single measurements here")
|
||||
}
|
||||
if _, found := targets["xx"]; !found {
|
||||
t.Fatal("the target we expected is missing")
|
||||
}
|
||||
tgt := targets["xx"]
|
||||
if *tgt.Failure != "interrupted" {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func wrapTestingTarget(tt model.TorTarget) keytarget {
|
||||
return keytarget{
|
||||
key: "xx", // using an super simple key; should work anyway
|
||||
target: tt,
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultsCollectorMeasureSingleTargetGood(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
rc.flexibleConnect = func(context.Context, keytarget) (oonitemplates.Results, error) {
|
||||
return oonitemplates.Results{}, nil
|
||||
}
|
||||
rc.measureSingleTarget(
|
||||
context.Background(), wrapTestingTarget(staticTestingTargets[0]),
|
||||
len(staticTestingTargets),
|
||||
)
|
||||
if len(rc.targetresults) != 1 {
|
||||
t.Fatal("wrong number of entries")
|
||||
}
|
||||
// Implementation note: here we won't bother with checking that
|
||||
// oonidatamodel works correctly because we already test that.
|
||||
if rc.targetresults["xx"].Agent != "redirect" {
|
||||
t.Fatal("agent is invalid")
|
||||
}
|
||||
if rc.targetresults["xx"].Failure != nil {
|
||||
t.Fatal("failure is invalid")
|
||||
}
|
||||
if rc.targetresults["xx"].TargetAddress != staticTestingTargets[0].Address {
|
||||
t.Fatal("target address is invalid")
|
||||
}
|
||||
if rc.targetresults["xx"].TargetProtocol != staticTestingTargets[0].Protocol {
|
||||
t.Fatal("target protocol is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultsCollectorMeasureSingleTargetWithFailure(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
rc.flexibleConnect = func(context.Context, keytarget) (oonitemplates.Results, error) {
|
||||
return oonitemplates.Results{}, errors.New("mocked error")
|
||||
}
|
||||
rc.measureSingleTarget(
|
||||
context.Background(), keytarget{
|
||||
key: "xx", // using an super simple key; should work anyway
|
||||
target: staticTestingTargets[0],
|
||||
},
|
||||
len(staticTestingTargets),
|
||||
)
|
||||
if len(rc.targetresults) != 1 {
|
||||
t.Fatal("wrong number of entries")
|
||||
}
|
||||
// Implementation note: here we won't bother with checking that
|
||||
// oonidatamodel works correctly because we already test that.
|
||||
if rc.targetresults["xx"].Agent != "redirect" {
|
||||
t.Fatal("agent is invalid")
|
||||
}
|
||||
if *rc.targetresults["xx"].Failure != "mocked error" {
|
||||
t.Fatal("failure is invalid")
|
||||
}
|
||||
if rc.targetresults["xx"].TargetAddress != staticTestingTargets[0].Address {
|
||||
t.Fatal("target address is invalid")
|
||||
}
|
||||
if rc.targetresults["xx"].TargetProtocol != staticTestingTargets[0].Protocol {
|
||||
t.Fatal("target protocol is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautFlexibleConnectDirPort(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[1]))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if !strings.HasSuffix(err.Error(), "interrupted") {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tk.HTTPRequests == nil {
|
||||
t.Fatal("expected HTTP data here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautFlexibleConnectOrPort(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[2]))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if err.Error() != "interrupted" {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tk.Connects == nil {
|
||||
t.Fatal("expected connects data here")
|
||||
}
|
||||
if tk.NetworkEvents == nil {
|
||||
t.Fatal("expected network events data here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautFlexibleConnectOBFS4(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[0]))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if err.Error() != "interrupted" {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if tk.Connects == nil {
|
||||
t.Fatal("expected connects data here")
|
||||
}
|
||||
if tk.NetworkEvents == nil {
|
||||
t.Fatal("expected network events data here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefautFlexibleConnectDefault(t *testing.T) {
|
||||
rc := newResultsCollector(
|
||||
&mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
},
|
||||
new(model.Measurement),
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[3]))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if err.Error() != "interrupted" {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if tk.Connects == nil {
|
||||
t.Fatalf("expected connects data here, found: %+v", tk.Connects)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrString(t *testing.T) {
|
||||
if errString(nil) != "success" {
|
||||
t.Fatal("not working with nil")
|
||||
}
|
||||
if errString(errors.New("antani")) != "antani" {
|
||||
t.Fatal("not working with error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummary(t *testing.T) {
|
||||
t.Run("without any piece of data", func(t *testing.T) {
|
||||
tr := new(TargetResults)
|
||||
tr.fillSummary()
|
||||
if len(tr.Summary) != 0 {
|
||||
t.Fatal("summary must be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with a TCP connect and nothing else", func(t *testing.T) {
|
||||
tr := new(TargetResults)
|
||||
failure := "mocked_error"
|
||||
tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{
|
||||
Status: oonidatamodel.TCPConnectStatus{
|
||||
Success: true,
|
||||
Failure: &failure,
|
||||
},
|
||||
})
|
||||
tr.fillSummary()
|
||||
if len(tr.Summary) != 1 {
|
||||
t.Fatal("cannot find expected entry")
|
||||
}
|
||||
if *tr.Summary[errorx.ConnectOperation].Failure != failure {
|
||||
t.Fatal("invalid failure")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for OBFS4", func(t *testing.T) {
|
||||
tr := new(TargetResults)
|
||||
tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{
|
||||
Status: oonidatamodel.TCPConnectStatus{
|
||||
Success: true,
|
||||
},
|
||||
})
|
||||
failure := "mocked_error"
|
||||
tr.TargetProtocol = "obfs4"
|
||||
tr.Failure = &failure
|
||||
tr.fillSummary()
|
||||
if len(tr.Summary) != 2 {
|
||||
t.Fatal("cannot find expected entry")
|
||||
}
|
||||
if tr.Summary[errorx.ConnectOperation].Failure != nil {
|
||||
t.Fatal("invalid failure")
|
||||
}
|
||||
if *tr.Summary["handshake"].Failure != failure {
|
||||
t.Fatal("invalid failure")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for or_port/or_port_dirauth", func(t *testing.T) {
|
||||
doit := func(targetProtocol string, handshake *oonidatamodel.TLSHandshake) {
|
||||
tr := new(TargetResults)
|
||||
tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{
|
||||
Status: oonidatamodel.TCPConnectStatus{
|
||||
Success: true,
|
||||
},
|
||||
})
|
||||
tr.TargetProtocol = targetProtocol
|
||||
if handshake != nil {
|
||||
tr.TLSHandshakes = append(tr.TLSHandshakes, *handshake)
|
||||
}
|
||||
tr.fillSummary()
|
||||
if len(tr.Summary) < 1 {
|
||||
t.Fatal("cannot find expected entry")
|
||||
}
|
||||
if tr.Summary[errorx.ConnectOperation].Failure != nil {
|
||||
t.Fatal("invalid failure")
|
||||
}
|
||||
if handshake == nil {
|
||||
if len(tr.Summary) != 1 {
|
||||
t.Fatal("unexpected summary length")
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(tr.Summary) != 2 {
|
||||
t.Fatal("unexpected summary length")
|
||||
}
|
||||
if tr.Summary["handshake"].Failure != handshake.Failure {
|
||||
t.Fatal("the failure value is unexpected")
|
||||
}
|
||||
}
|
||||
doit("or_port_dirauth", nil)
|
||||
doit("or_port", nil)
|
||||
doit("or_port", &oonidatamodel.TLSHandshake{
|
||||
Failure: (func() *string {
|
||||
s := io.EOF.Error()
|
||||
return &s
|
||||
})(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestFillToplevelKeys(t *testing.T) {
|
||||
var tr TargetResults
|
||||
tr.TargetProtocol = "or_port"
|
||||
tk := new(TestKeys)
|
||||
tk.Targets = make(map[string]TargetResults)
|
||||
tk.Targets["xxx"] = tr
|
||||
tk.fillToplevelKeys()
|
||||
if tk.ORPortTotal != 1 {
|
||||
t.Fatal("unexpected ORPortTotal value")
|
||||
}
|
||||
}
|
||||
|
||||
func newsession() *mockable.Session {
|
||||
return &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableHTTPClient: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
var referenceTargetResult = []byte(`{
|
||||
"agent": "redirect",
|
||||
"failure": null,
|
||||
"network_events": [
|
||||
{
|
||||
"address": "85.31.186.98:443",
|
||||
"conn_id": 19,
|
||||
"dial_id": 21,
|
||||
"failure": null,
|
||||
"operation": "connect",
|
||||
"proto": "tcp",
|
||||
"t": 8.639313
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1915,
|
||||
"operation": "write",
|
||||
"proto": "tcp",
|
||||
"t": 8.639686
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1440,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.691708
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1440,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.691912
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1383,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.69234
|
||||
}
|
||||
],
|
||||
"queries": null,
|
||||
"requests": null,
|
||||
"summary": {
|
||||
"connect": {
|
||||
"failure": null
|
||||
}
|
||||
},
|
||||
"target_address": "85.31.186.98:443",
|
||||
"target_protocol": "obfs4",
|
||||
"tcp_connect": [
|
||||
{
|
||||
"conn_id": 19,
|
||||
"dial_id": 21,
|
||||
"ip": "85.31.186.98",
|
||||
"port": 443,
|
||||
"status": {
|
||||
"failure": null,
|
||||
"success": true
|
||||
},
|
||||
"t": 8.639313
|
||||
}
|
||||
],
|
||||
"tls_handshakes": null
|
||||
}`)
|
||||
|
||||
var scrubbedTargetResult = []byte(`{
|
||||
"agent": "redirect",
|
||||
"failure": null,
|
||||
"network_events": [
|
||||
{
|
||||
"address": "[scrubbed]",
|
||||
"conn_id": 19,
|
||||
"dial_id": 21,
|
||||
"failure": null,
|
||||
"operation": "connect",
|
||||
"proto": "tcp",
|
||||
"t": 8.639313
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1915,
|
||||
"operation": "write",
|
||||
"proto": "tcp",
|
||||
"t": 8.639686
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1440,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.691708
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1440,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.691912
|
||||
},
|
||||
{
|
||||
"conn_id": 19,
|
||||
"failure": null,
|
||||
"num_bytes": 1383,
|
||||
"operation": "read",
|
||||
"proto": "tcp",
|
||||
"t": 8.69234
|
||||
}
|
||||
],
|
||||
"queries": null,
|
||||
"requests": null,
|
||||
"summary": {
|
||||
"connect": {
|
||||
"failure": null
|
||||
}
|
||||
},
|
||||
"target_address": "[scrubbed]",
|
||||
"target_protocol": "obfs4",
|
||||
"tcp_connect": [
|
||||
{
|
||||
"conn_id": 19,
|
||||
"dial_id": 21,
|
||||
"ip": "[scrubbed]",
|
||||
"port": 443,
|
||||
"status": {
|
||||
"failure": null,
|
||||
"success": true
|
||||
},
|
||||
"t": 8.639313
|
||||
}
|
||||
],
|
||||
"tls_handshakes": null
|
||||
}`)
|
||||
|
||||
func TestMaybeSanitize(t *testing.T) {
|
||||
var input TargetResults
|
||||
if err := json.Unmarshal(referenceTargetResult, &input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("nothing to do", func(t *testing.T) {
|
||||
out := maybeSanitize(input, keytarget{target: model.TorTarget{Source: ""}})
|
||||
diff := cmp.Diff(input, out)
|
||||
if diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
t.Run("scrubbing to do", func(t *testing.T) {
|
||||
var expected TargetResults
|
||||
if err := json.Unmarshal(scrubbedTargetResult, &expected); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := maybeSanitize(input, keytarget{target: model.TorTarget{
|
||||
Address: "85.31.186.98:443",
|
||||
Source: "bridgedb",
|
||||
}})
|
||||
diff := cmp.Diff(expected, out)
|
||||
if diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type savingLogger struct {
|
||||
debug []string
|
||||
info []string
|
||||
warn []string
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Debug(message string) {
|
||||
sl.debug = append(sl.debug, message)
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Debugf(format string, v ...interface{}) {
|
||||
sl.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Info(message string) {
|
||||
sl.info = append(sl.info, message)
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Infof(format string, v ...interface{}) {
|
||||
sl.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Warn(message string) {
|
||||
sl.warn = append(sl.warn, message)
|
||||
}
|
||||
|
||||
func (sl *savingLogger) Warnf(format string, v ...interface{}) {
|
||||
sl.Warn(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func TestScrubLogger(t *testing.T) {
|
||||
input := "failure: 130.192.91.211:443: no route the host"
|
||||
expect := "failure: [scrubbed]: no route the host"
|
||||
|
||||
t.Run("for debug", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Debug(input)
|
||||
if len(logger.debug) != 1 && len(logger.info) != 0 && len(logger.warn) != 0 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.debug[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for debugf", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Debugf("%s", input)
|
||||
if len(logger.debug) != 1 && len(logger.info) != 0 && len(logger.warn) != 0 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.debug[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for info", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Info(input)
|
||||
if len(logger.debug) != 0 && len(logger.info) != 1 && len(logger.warn) != 0 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.info[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for infof", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Infof("%s", input)
|
||||
if len(logger.debug) != 0 && len(logger.info) != 1 && len(logger.warn) != 0 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.info[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for warn", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Warn(input)
|
||||
if len(logger.debug) != 0 && len(logger.info) != 0 && len(logger.warn) != 1 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.warn[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for warnf", func(t *testing.T) {
|
||||
logger := new(savingLogger)
|
||||
scrubber := scrubbingLogger{Logger: logger}
|
||||
scrubber.Warnf("%s", input)
|
||||
if len(logger.debug) != 0 && len(logger.info) != 0 && len(logger.warn) != 1 {
|
||||
t.Fatal("unexpected number of log lines written")
|
||||
}
|
||||
if logger.warn[0] != expect {
|
||||
t.Fatal("unexpected output written")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMaybeScrubbingLogger(t *testing.T) {
|
||||
var input model.Logger = new(savingLogger)
|
||||
|
||||
t.Run("for when we don't need to save", func(t *testing.T) {
|
||||
kt := keytarget{target: model.TorTarget{
|
||||
Source: "",
|
||||
}}
|
||||
out := maybeScrubbingLogger(input, kt)
|
||||
if out != input {
|
||||
t.Fatal("not the output we expected")
|
||||
}
|
||||
if _, ok := out.(*savingLogger); !ok {
|
||||
t.Fatal("not the output type we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("for when we need to save", func(t *testing.T) {
|
||||
kt := keytarget{target: model.TorTarget{
|
||||
Source: "bridgedb",
|
||||
}}
|
||||
out := maybeScrubbingLogger(input, kt)
|
||||
if out == input {
|
||||
t.Fatal("not the output value we expected")
|
||||
}
|
||||
if _, ok := out.(scrubbingLogger); !ok {
|
||||
t.Fatal("not the output type we expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSummaryKeysInvalidType(t *testing.T) {
|
||||
measurement := new(model.Measurement)
|
||||
m := &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 {
|
||||
tk TestKeys
|
||||
isAnomaly bool
|
||||
}{{
|
||||
tk: TestKeys{},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tk: TestKeys{DirPortAccessible: 1, DirPortTotal: 3},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tk: TestKeys{DirPortAccessible: 0, DirPortTotal: 3},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tk: TestKeys{OBFS4Accessible: 1, OBFS4Total: 3},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tk: TestKeys{OBFS4Accessible: 0, OBFS4Total: 3},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tk: TestKeys{ORPortDirauthAccessible: 1, ORPortDirauthTotal: 3},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tk: TestKeys{ORPortDirauthAccessible: 0, ORPortDirauthTotal: 3},
|
||||
isAnomaly: true,
|
||||
}, {
|
||||
tk: TestKeys{ORPortAccessible: 1, ORPortTotal: 3},
|
||||
isAnomaly: false,
|
||||
}, {
|
||||
tk: TestKeys{ORPortAccessible: 0, ORPortTotal: 3},
|
||||
isAnomaly: true,
|
||||
}}
|
||||
for idx, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
|
||||
m := &Measurer{}
|
||||
measurement := &model.Measurement{TestKeys: &tt.tk}
|
||||
got, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
sk := got.(SummaryKeys)
|
||||
if sk.IsAnomaly != tt.isAnomaly {
|
||||
t.Fatal("unexpected isAnomaly value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user