Compare commits

..

11 Commits

103 changed files with 1784 additions and 3297 deletions

View File

@ -156,16 +156,14 @@ response for every request whose `Host` contains the specified string.
### tls-proxy ### tls-proxy
TLS proxy is a TCP proxy that routes traffic to specific servers depending TLS proxy is a proxy that routes traffic to specific servers depending
on their SNI value. It is controlled by the following flags: on their SNI value. It is controlled by the following flags:
```bash ```bash
-tls-proxy-address string -tls-proxy-address string
Address where the TCP+TLS proxy should listen (default "127.0.0.1:443") Address where the HTTP proxy should listen (default "127.0.0.1:443")
-tls-proxy-block value -tls-proxy-block value
Register SNI header keyword triggering TLS censorship Register keyword triggering TLS censorship
-tls-proxy-outbound-port
Define the outbound port requests are proxied to (default "443 for HTTPS)
``` ```
The `-tls-proxy-address` flags has the same semantics it has for the DNS The `-tls-proxy-address` flags has the same semantics it has for the DNS

View File

@ -58,9 +58,8 @@ var (
tag *string tag *string
tlsProxyAddress *string tlsProxyAddress *string
tlsProxyBlock flagx.StringArray tlsProxyBlock flagx.StringArray
tlsProxyOutboundPort *string
uncensoredResolverDoH *string uncensoredResolverDoH *string
) )
@ -160,16 +159,12 @@ func init() {
// tlsProxy // tlsProxy
tlsProxyAddress = flag.String( tlsProxyAddress = flag.String(
"tls-proxy-address", "127.0.0.1:443", "tls-proxy-address", "127.0.0.1:443",
"Address where the TCP+TLS proxy should listen", "Address where the HTTP proxy should listen",
) )
flag.Var( flag.Var(
&tlsProxyBlock, "tls-proxy-block", &tlsProxyBlock, "tls-proxy-block",
"Register keyword triggering TLS censorship", "Register keyword triggering TLS censorship",
) )
tlsProxyOutboundPort = flag.String(
"tls-proxy-outbound-port", "443",
"The outbound port where requests should be proxied",
)
// uncensored // uncensored
uncensoredResolverDoH = flag.String( uncensoredResolverDoH = flag.String(
@ -232,7 +227,7 @@ func iptablesStart() *iptables.CensoringPolicy {
} }
func tlsProxyStart(uncensored *uncensored.Client) net.Listener { func tlsProxyStart(uncensored *uncensored.Client) net.Listener {
proxy := tlsproxy.NewCensoringProxy(tlsProxyBlock, uncensored, tlsProxyOutboundPort) proxy := tlsproxy.NewCensoringProxy(tlsProxyBlock, uncensored)
listener, err := proxy.Start(*tlsProxyAddress) listener, err := proxy.Start(*tlsProxyAddress)
runtimex.PanicOnError(err, "proxy.Start failed") runtimex.PanicOnError(err, "proxy.Start failed")
return listener return listener

View File

@ -21,9 +21,8 @@ type Dialer interface {
// CensoringProxy is a censoring TLS proxy // CensoringProxy is a censoring TLS proxy
type CensoringProxy struct { type CensoringProxy struct {
keywords []string keywords []string
dial func(network, address string) (net.Conn, error) dial func(network, address string) (net.Conn, error)
outboundPort string
} }
// NewCensoringProxy creates a new CensoringProxy instance using // NewCensoringProxy creates a new CensoringProxy instance using
@ -32,18 +31,13 @@ type CensoringProxy struct {
// the SNII record of a ClientHello. dnsNetwork and dnsAddress are // the SNII record of a ClientHello. dnsNetwork and dnsAddress are
// settings to configure the upstream, non censored DNS. // settings to configure the upstream, non censored DNS.
func NewCensoringProxy( func NewCensoringProxy(
keywords []string, uncensored Dialer, outboundPort *string, keywords []string, uncensored Dialer,
) *CensoringProxy { ) *CensoringProxy {
defaultPort := "443"
if outboundPort == nil {
outboundPort = &defaultPort
}
return &CensoringProxy{ return &CensoringProxy{
keywords: keywords, keywords: keywords,
dial: func(network, address string) (net.Conn, error) { dial: func(network, address string) (net.Conn, error) {
return uncensored.DialContext(context.Background(), network, address) return uncensored.DialContext(context.Background(), network, address)
}, },
outboundPort: *outboundPort,
} }
} }
@ -152,7 +146,7 @@ func (p *CensoringProxy) handle(clientconn net.Conn) {
return return
} }
} }
serverconn, err := p.dial("tcp", net.JoinHostPort(sni, p.outboundPort)) serverconn, err := p.dial("tcp", net.JoinHostPort(sni, "443"))
if err != nil { if err != nil {
log.WithError(err).Warn("tlsproxy: p.dial failed") log.WithError(err).Warn("tlsproxy: p.dial failed")
alertclose(clientconn) alertclose(clientconn)

View File

@ -94,7 +94,7 @@ func TestFailWriteAfterConnect(t *testing.T) {
func TestListenError(t *testing.T) { func TestListenError(t *testing.T) {
proxy := NewCensoringProxy( proxy := NewCensoringProxy(
[]string{""}, uncensored.NewClient("https://1.1.1.1/dns-query"), nil, []string{""}, uncensored.NewClient("https://1.1.1.1/dns-query"),
) )
listener, err := proxy.Start("8.8.8.8:80") listener, err := proxy.Start("8.8.8.8:80")
if err == nil { if err == nil {
@ -107,7 +107,7 @@ func TestListenError(t *testing.T) {
func newproxy(t *testing.T, blocked string) net.Listener { func newproxy(t *testing.T, blocked string) net.Listener {
proxy := NewCensoringProxy( proxy := NewCensoringProxy(
[]string{blocked}, uncensored.NewClient("https://1.1.1.1/dns-query"), nil, []string{blocked}, uncensored.NewClient("https://1.1.1.1/dns-query"),
) )
listener, err := proxy.Start("127.0.0.1:0") listener, err := proxy.Start("127.0.0.1:0")
if err != nil { if err != nil {

View File

@ -92,12 +92,7 @@ func (eaw *experimentAsyncWrapper) RunAsync(
out := make(chan *model.ExperimentAsyncTestKeys) out := make(chan *model.ExperimentAsyncTestKeys)
measurement := eaw.experiment.newMeasurement(input) measurement := eaw.experiment.newMeasurement(input)
start := time.Now() start := time.Now()
args := &model.ExperimentArgs{ err := eaw.experiment.measurer.Run(ctx, eaw.session, measurement, eaw.callbacks)
Callbacks: eaw.callbacks,
Measurement: measurement,
Session: eaw.session,
}
err := eaw.experiment.measurer.Run(ctx, args)
stop := time.Now() stop := time.Now()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -249,10 +249,10 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements model.ExperimentMeasurer.Run. // Run implements model.ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
tk := new(TestKeys) tk := new(TestKeys)
measurement.TestKeys = tk measurement.TestKeys = tk
saver := &tracex.Saver{} saver := &tracex.Saver{}

View File

@ -270,15 +270,15 @@ func TestMeasureWithCancelledContext(t *testing.T) {
cancel() // cause failure cancel() // cause failure
measurement := new(model.Measurement) measurement := new(model.Measurement)
m := &Measurer{} m := &Measurer{}
args := &model.ExperimentArgs{ err := m.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableHTTPClient: http.DefaultClient, MockableHTTPClient: http.DefaultClient,
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := m.Run(ctx, args) model.NewPrinterCallbacks(log.Log),
)
// See corresponding comment in Measurer.Run implementation to // See corresponding comment in Measurer.Run implementation to
// understand why here it's correct to return nil. // understand why here it's correct to return nil.
if !errors.Is(err, nil) { if !errors.Is(err, nil) {

View File

@ -120,11 +120,10 @@ var (
) )
// Run implements model.ExperimentSession.Run // Run implements model.ExperimentSession.Run
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
// 1. fill the measurement with test keys // 1. fill the measurement with test keys
tk := new(TestKeys) tk := new(TestKeys)
tk.Lookups = make(map[string]urlgetter.TestKeys) tk.Lookups = make(map[string]urlgetter.TestKeys)

View File

@ -56,12 +56,12 @@ func TestExperimentNameAndVersion(t *testing.T) {
func TestDNSCheckFailsWithoutInput(t *testing.T) { func TestDNSCheckFailsWithoutInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{Domain: "example.com"}) measurer := NewExperimentMeasurer(Config{Domain: "example.com"})
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: new(model.Measurement), newsession(),
Session: newsession(), new(model.Measurement),
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, ErrInputRequired) { if !errors.Is(err, ErrInputRequired) {
t.Fatal("expected no input error") t.Fatal("expected no input error")
} }
@ -69,12 +69,12 @@ func TestDNSCheckFailsWithoutInput(t *testing.T) {
func TestDNSCheckFailsWithInvalidURL(t *testing.T) { func TestDNSCheckFailsWithInvalidURL(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{Input: "Not a valid URL \x7f"}, newsession(),
Session: newsession(), &model.Measurement{Input: "Not a valid URL \x7f"},
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, ErrInvalidURL) { if !errors.Is(err, ErrInvalidURL) {
t.Fatal("expected invalid input error") t.Fatal("expected invalid input error")
} }
@ -82,12 +82,12 @@ func TestDNSCheckFailsWithInvalidURL(t *testing.T) {
func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) { func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{Input: "file://1.1.1.1"}, newsession(),
Session: newsession(), &model.Measurement{Input: "file://1.1.1.1"},
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, ErrUnsupportedURLScheme) { if !errors.Is(err, ErrUnsupportedURLScheme) {
t.Fatal("expected unsupported scheme error") t.Fatal("expected unsupported scheme error")
} }
@ -100,12 +100,12 @@ func TestWithCancelledContext(t *testing.T) {
DefaultAddrs: "1.1.1.1 1.0.0.1", DefaultAddrs: "1.1.1.1 1.0.0.1",
}) })
measurement := &model.Measurement{Input: "dot://one.one.one.one"} measurement := &model.Measurement{Input: "dot://one.one.one.one"}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, newsession(),
Session: newsession(), measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -147,12 +147,12 @@ func TestDNSCheckValid(t *testing.T) {
DefaultAddrs: "1.1.1.1 1.0.0.1", DefaultAddrs: "1.1.1.1 1.0.0.1",
}) })
measurement := model.Measurement{Input: "dot://one.one.one.one:853"} measurement := model.Measurement{Input: "dot://one.one.one.one:853"}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &measurement, newsession(),
Session: newsession(), &measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err.Error()) t.Fatalf("unexpected error: %s", err.Error())
} }
@ -195,12 +195,12 @@ func TestDNSCheckWait(t *testing.T) {
measurer := &Measurer{Endpoints: endpoints} measurer := &Measurer{Endpoints: endpoints}
run := func(input string) { run := func(input string) {
measurement := model.Measurement{Input: model.MeasurementTarget(input)} measurement := model.Measurement{Input: model.MeasurementTarget(input)}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &measurement, newsession(),
Session: newsession(), &measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err.Error()) t.Fatalf("unexpected error: %s", err.Error())
} }

View File

@ -85,10 +85,12 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errNoInputProvided return errNoInputProvided
} }

View File

@ -61,12 +61,7 @@ func TestMeasurer_run(t *testing.T) {
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
return meas, m, err return meas, m, err
} }

View File

@ -57,10 +57,10 @@ func (m Measurer) ExperimentVersion() string {
var ErrFailure = errors.New("mocked error") var ErrFailure = errors.New("mocked error")
// Run implements model.ExperimentMeasurer.Run. // Run implements model.ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
var err error var err error
if m.config.ReturnError { if m.config.ReturnError {
err = ErrFailure err = ErrFailure

View File

@ -26,12 +26,7 @@ func TestSuccess(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(sess.Logger()) callbacks := model.NewPrinterCallbacks(sess.Logger())
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := m.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -52,12 +47,7 @@ func TestFailure(t *testing.T) {
ctx := context.Background() ctx := context.Background()
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(sess.Logger()) callbacks := model.NewPrinterCallbacks(sess.Logger())
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, new(model.Measurement), callbacks)
Callbacks: callbacks,
Measurement: new(model.Measurement),
Session: sess,
}
err := m.Run(ctx, args)
if !errors.Is(err, example.ErrFailure) { if !errors.Is(err, example.ErrFailure) {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }

View File

@ -157,10 +157,10 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run // Run implements ExperimentMeasurer.Run
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
urlgetter.RegisterExtensions(measurement) urlgetter.RegisterExtensions(measurement)

View File

@ -35,12 +35,7 @@ func TestSuccess(t *testing.T) {
sess := newsession(t) sess := newsession(t)
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -102,12 +97,7 @@ func TestWithCancelledContext(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -90,10 +90,10 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
urlgetter.RegisterExtensions(measurement) urlgetter.RegisterExtensions(measurement)

View File

@ -45,12 +45,7 @@ func TestSuccess(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -158,12 +153,7 @@ func TestCancelledContext(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -269,12 +259,7 @@ func TestNoHelpers(t *testing.T) {
sess := &mockable.Session{} sess := &mockable.Session{}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) { if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -324,12 +309,7 @@ func TestNoActualHelpersInList(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) { if !errors.Is(err, hhfm.ErrNoAvailableTestHelpers) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -382,12 +362,7 @@ func TestWrongTestHelperType(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hhfm.ErrInvalidHelperType) { if !errors.Is(err, hhfm.ErrInvalidHelperType) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -440,12 +415,7 @@ func TestNewRequestFailure(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -502,12 +472,7 @@ func TestInvalidJSONBody(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -78,10 +78,10 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
tk := new(TestKeys) tk := new(TestKeys)
measurement.TestKeys = tk measurement.TestKeys = tk
if len(m.Methods) < 1 { if len(m.Methods) < 1 {

View File

@ -42,12 +42,7 @@ func TestSuccess(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -96,12 +91,7 @@ func TestCancelledContext(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -200,12 +190,7 @@ func TestWithFakeMethods(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -266,12 +251,7 @@ func TestWithNoMethods(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hirl.ErrNoMeasurementMethod) { if !errors.Is(err, hirl.ErrNoMeasurementMethod) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -299,12 +279,7 @@ func TestNoHelpers(t *testing.T) {
sess := &mockable.Session{} sess := &mockable.Session{}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) { if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -336,12 +311,7 @@ func TestNoActualHelperInList(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) { if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -376,12 +346,7 @@ func TestWrongTestHelperType(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, hirl.ErrInvalidHelperType) { if !errors.Is(err, hirl.ErrInvalidHelperType) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }

View File

@ -46,10 +46,12 @@ func (m *Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errors.New("experiment requires input") return errors.New("experiment requires input")
} }

View File

@ -30,12 +30,12 @@ func TestMeasurerMeasureNoMeasurementInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{ measurer := NewExperimentMeasurer(Config{
TestHelperURL: "http://www.google.com", TestHelperURL: "http://www.google.com",
}) })
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{}, newsession(),
Session: newsession(), new(model.Measurement),
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err == nil || err.Error() != "experiment requires input" { if err == nil || err.Error() != "experiment requires input" {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -44,12 +44,12 @@ func TestMeasurerMeasureNoMeasurementInput(t *testing.T) {
func TestMeasurerMeasureNoTestHelper(t *testing.T) { func TestMeasurerMeasureNoTestHelper(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := &model.Measurement{Input: "x.org"} measurement := &model.Measurement{Input: "x.org"}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, newsession(),
Session: newsession(), measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -75,12 +75,12 @@ func TestRunnerHTTPSetHostHeader(t *testing.T) {
measurement := &model.Measurement{ measurement := &model.Measurement{
Input: "x.org", Input: "x.org",
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, newsession(),
Session: newsession(), measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if host != "x.org" { if host != "x.org" {
t.Fatal("not the host we expected") t.Fatal("not the host we expected")
} }

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,416 @@
package imap
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"github.com/pkg/errors"
"net"
//"net/smtp"
"net/url"
"strings"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/tracex"
)
var (
// errNoInputProvided indicates you didn't provide any input
errNoInputProvided = errors.New("not input provided")
// errInputIsNotAnURL indicates that input is not an URL
errInputIsNotAnURL = errors.New("input is not an URL")
// errInvalidScheme indicates that the scheme is invalid
errInvalidScheme = errors.New("scheme must be smtp(s)")
)
const (
testName = "imap"
testVersion = "0.0.1"
)
// Config contains the experiment config.
type Config struct{}
type RuntimeConfig struct {
host string
port string
forced_tls bool
noop_count uint8
}
func config(input model.MeasurementTarget) (*RuntimeConfig, error) {
if input == "" {
// TODO: static input data (eg. gmail/riseup..)
return nil, errNoInputProvided
}
parsed, err := url.Parse(string(input))
if err != nil {
return nil, fmt.Errorf("%w: %s", errInputIsNotAnURL, err.Error())
}
if parsed.Scheme != "imap" && parsed.Scheme != "imaps" {
return nil, errInvalidScheme
}
port := ""
if parsed.Port() == "" {
// Default ports for StartTLS and forced TLS respectively
if parsed.Scheme == "imap" {
port = "143"
} else {
port = "993"
}
} else {
// Valid port is checked by URL parsing
port = parsed.Port()
}
valid_config := RuntimeConfig{
host: parsed.Hostname(),
forced_tls: parsed.Scheme == "imaps",
port: port,
noop_count: 10,
}
return &valid_config, nil
}
// TestKeys contains the experiment results
type TestKeys struct {
Queries []*model.ArchivalDNSLookupResult `json:"queries"`
Runs map[string]*IndividualTestKeys `json:"runs"`
// Used for global failure (DNS resolution)
Failure string `json:"failure"`
// Indicates global failure or individual test failure
Failed bool `json:"failed"`
}
// IndividualTestKeys contains results for TCP/IP level stuff for each address found
// in the DNS lookup
type IndividualTestKeys struct {
NoOpCounter uint8
TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"`
TLSHandshakes []*model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"`
// Individual failure aborting the test run for this address/port combo
Failure *string `json:"failure"`
}
type Measurer struct {
// Config contains the experiment settings. If empty we
// will be using default settings.
Config Config
// Getter is an optional getter to be used for testing.
Getter urlgetter.MultiGetter
}
// ExperimentName implements ExperimentMeasurer.ExperimentName
func (m Measurer) ExperimentName() string {
return testName
}
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion
func (m Measurer) ExperimentVersion() string {
return testVersion
}
// Manages sequential TCP sessions to the same hostname (over different IPs)
// don't use in parallel!
type TCPRunner struct {
trace *measurexlite.Trace
logger model.Logger
ctx context.Context
tk *TestKeys
tlsconfig *tls.Config
host string
port string
// addr is changed everytime TCPRunner.conn(addr) is called
addr string
}
type TCPSession struct {
addr string
port string
runner *TCPRunner
tk *IndividualTestKeys
tls bool
raw_conn *net.Conn
tls_conn *net.Conn
}
func (s *TCPSession) Close() {
if s.tls {
var conn = *s.tls_conn
conn.Close()
} else {
var conn = *s.raw_conn
conn.Close()
}
}
func (s *TCPSession) current_conn() net.Conn {
if s.tls {
return *s.tls_conn
} else {
return *s.raw_conn
}
}
func (r *TCPRunner) run_key() string {
return net.JoinHostPort(r.addr, r.port)
}
func (r *TCPRunner) get_run() *IndividualTestKeys {
if r.tk.Runs == nil {
r.tk.Runs = make(map[string]*IndividualTestKeys)
}
key := r.run_key()
val, exists := r.tk.Runs[key]
if exists {
return val
} else {
r.tk.Runs[key] = &IndividualTestKeys{}
return r.tk.Runs[key]
}
}
func (r *TCPRunner) conn(addr string, port string) (*TCPSession, bool) {
r.addr = addr
run := r.get_run()
s := new(TCPSession)
if !s.conn(addr, port, r, run) {
return nil, false
}
return s, true
}
func (r *TCPRunner) dial(addr string, port string) (net.Conn, error) {
dialer := r.trace.NewDialerWithoutResolver(r.logger)
conn, err := dialer.DialContext(r.ctx, "tcp", net.JoinHostPort(addr, port))
run := r.get_run()
run.TCPConnect = append(run.TCPConnect, r.trace.TCPConnects()...)
return conn, err
}
func (s *TCPSession) conn(addr string, port string, runner *TCPRunner, tk *IndividualTestKeys) bool {
// Initialize addr field and corresponding errors in TestKeys
s.addr = addr
s.port = port
s.tls = false
s.runner = runner
s.tk = tk
conn, err := runner.dial(addr, port)
if err != nil {
s.error(err)
return false
}
s.raw_conn = &conn
return true
}
func (s *TCPSession) error(err error) {
s.runner.tk.Failed = true
s.tk.Failure = tracex.NewFailure(err)
//s. = append(s.errors, tracex.NewFailure(err))
}
func (r *TCPRunner) resolve(host string) ([]string, bool) {
r.logger.Infof("Resolving DNS for %s", host)
resolver := r.trace.NewStdlibResolver(r.logger)
addrs, err := resolver.LookupHost(r.ctx, host)
r.tk.Queries = append(r.tk.Queries, r.trace.DNSLookupsFromRoundTrip()...)
if err != nil {
r.tk.Failure = *tracex.NewFailure(err)
return []string{}, false
}
r.logger.Infof("Finished DNS for %s: %v", host, addrs)
return addrs, true
}
func (s *TCPSession) handshake() bool {
if s.tls {
// TLS already initialized...
return true
}
s.runner.logger.Infof("Starting TLS handshake with %s:%s", s.addr, s.port)
thx := s.runner.trace.NewTLSHandshakerStdlib(s.runner.logger)
tconn, _, err := thx.Handshake(s.runner.ctx, *s.raw_conn, s.runner.tlsconfig)
s.tk.TLSHandshakes = append(s.tk.TLSHandshakes, s.runner.trace.FirstTLSHandshakeOrNil())
if err != nil {
s.error(err)
return false
}
s.tls = true
s.tls_conn = &tconn
s.runner.logger.Infof("Handshake succeeded")
return true
}
func (s *TCPSession) starttls(message string) bool {
if s.tls {
// TLS already initialized...
return true
}
if message != "" {
s.runner.logger.Infof("Asking for StartTLS upgrade")
s.current_conn().Write([]byte(message))
}
return s.handshake()
}
func (s *TCPSession) imap(noop uint8) bool {
conn := s.current_conn()
command, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
s.error(err)
return false
}
if !strings.Contains(command, "CAPABILITY") {
s.error(errors.New("Unexpected IMAP reply: " + command))
return false
}
if noop > 0 {
s.runner.logger.Infof("Trying to generate no-op traffic")
s.tk.NoOpCounter = 0
for s.tk.NoOpCounter < noop {
s.tk.NoOpCounter += 1
s.runner.logger.Infof("NoOp Iteration %d", s.tk.NoOpCounter)
conn.Write([]byte("A1 NOOP\n"))
command, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
s.error(err)
break
}
if !strings.Contains(command, "OK NOOP") {
s.error(errors.New("Unexpected IMAP reply: " + command))
break
}
}
if s.tk.NoOpCounter == noop {
s.runner.logger.Infof("Successfully generated no-op traffic")
return true
} else {
s.runner.logger.Infof("Failed no-op traffic at iteration %d", s.tk.NoOpCounter)
return false
}
}
return true
}
// Run implements ExperimentMeasurer.Run
func (m Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
log := sess.Logger()
trace := measurexlite.NewTrace(0, measurement.MeasurementStartTimeSaved)
config, err := config(measurement.Input)
if err != nil {
// Invalid input data, we don't even generate report
return err
}
tk := new(TestKeys)
measurement.TestKeys = tk
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
tlsconfig := tls.Config{
InsecureSkipVerify: false,
ServerName: config.host,
}
runner := &TCPRunner{
trace: trace,
logger: log,
ctx: ctx,
tk: tk,
tlsconfig: &tlsconfig,
host: config.host,
port: config.port,
}
// First resolve DNS
addrs, success := runner.resolve(config.host)
if !success {
return nil
}
for _, addr := range addrs {
tcp_session, success := runner.conn(addr, config.port)
if !success {
continue
}
defer tcp_session.Close()
if config.forced_tls {
// Direct TLS connection
if !tcp_session.handshake() {
continue
}
// Try EHLO + NoOps
if !tcp_session.imap(config.noop_count) {
continue
}
} else {
// StartTLS...
if !tcp_session.starttls("A1 STARTTLS\n") {
continue
}
if !tcp_session.imap(config.noop_count) {
continue
}
}
}
return nil
}
// NewExperimentMeasurer creates a new ExperimentMeasurer.
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return Measurer{Config: config}
}
// SummaryKeys contains summary keys for this experiment.
//
// Note that this structure is part of the ABI contract with ooniprobe
// therefore we should be careful when changing it.
type SummaryKeys struct {
//DNSBlocking bool `json:"facebook_dns_blocking"`
//TCPBlocking bool `json:"facebook_tcp_blocking"`
IsAnomaly bool `json:"-"`
}
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
sk := SummaryKeys{IsAnomaly: false}
_, ok := measurement.TestKeys.(*TestKeys)
if !ok {
return sk, errors.New("invalid test keys type")
}
return sk, nil
}

View File

@ -0,0 +1,186 @@
package imap
import (
"bufio"
"context"
"crypto/tls"
//"encoding/json"
"errors"
"fmt"
"net"
"strings"
"testing"
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
"github.com/ooni/probe-cli/v3/internal/model"
)
func plaintextListener() net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
}
}
return l
}
func tlsListener(l net.Listener) net.Listener {
return tls.NewListener(l, &tls.Config{})
}
func listener_addr(l net.Listener) string {
return l.Addr().String()
}
func ValidIMAPServer(conn net.Conn) {
for {
command, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return
}
if strings.Contains(command, "NOOP") {
conn.Write([]byte("A1 OK NOOP completed.\n"))
} else if command == "STARTTLS" {
conn.Write([]byte("A1 OK Begin TLS negotiation now.\n"))
// TODO: conn.Close does not actually close connection? or does client not detect it?
conn.Close()
return
}
conn.Write([]byte("\n"))
}
}
func TCPServer(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
continue
}
defer conn.Close()
conn.Write([]byte("* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ STARTTLS LOGINDISABLED] howdy, ready.\n"))
ValidIMAPServer(conn)
}
}
func TestMeasurer_run(t *testing.T) {
// runHelper is an helper function to run this set of tests.
runHelper := func(input string) (*model.Measurement, model.ExperimentMeasurer, error) {
m := NewExperimentMeasurer(Config{})
if m.ExperimentName() != "imap" {
t.Fatal("invalid experiment name")
}
if m.ExperimentVersion() != "0.0.1" {
t.Fatal("invalid experiment version")
}
ctx := context.Background()
meas := &model.Measurement{
Input: model.MeasurementTarget(input),
}
sess := &mockable.Session{
MockableLogger: model.DiscardLogger,
}
callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
err := m.Run(ctx, sess, meas, callbacks)
return meas, m, err
}
t.Run("with empty input", func(t *testing.T) {
_, _, err := runHelper("")
if !errors.Is(err, errNoInputProvided) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid URL", func(t *testing.T) {
_, _, err := runHelper("\t")
if !errors.Is(err, errInputIsNotAnURL) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid scheme", func(t *testing.T) {
_, _, err := runHelper("https://8.8.8.8:443/")
if !errors.Is(err, errInvalidScheme) {
t.Fatal("unexpected error", err)
}
})
t.Run("with broken TLS", func(t *testing.T) {
p := plaintextListener()
defer p.Close()
l := tlsListener(p)
defer l.Close()
addr := listener_addr(l)
go TCPServer(l)
meas, m, err := runHelper("imaps://" + addr)
if err != nil {
t.Fatal(err)
}
tk := meas.TestKeys.(*TestKeys)
for _, run := range tk.Runs {
for _, handshake := range run.TLSHandshakes {
if *handshake.Failure != "unknown_failure: remote error: tls: unrecognized name" {
t.Fatal("expected unrecognized_name in TLS handshake")
}
}
if run.NoOpCounter != 0 {
t.Fatalf("expected to not have any noops, not %d noops", run.NoOpCounter)
}
}
ask, err := m.GetSummaryKeys(meas)
if err != nil {
t.Fatal("cannot obtain summary")
}
summary := ask.(SummaryKeys)
if summary.IsAnomaly {
t.Fatal("expected no anomaly")
}
})
t.Run("with broken starttls", func(t *testing.T) {
l := plaintextListener()
defer l.Close()
addr := listener_addr(l)
go TCPServer(l)
meas, m, err := runHelper("imap://" + addr)
if err != nil {
t.Fatal(err)
}
tk := meas.TestKeys.(*TestKeys)
//bs, _ := json.Marshal(tk)
//fmt.Println(string(bs))
for _, run := range tk.Runs {
for _, handshake := range run.TLSHandshakes {
if *handshake.Failure != "unknown_failure: tls: first record does not look like a TLS handshake" {
t.Fatal("expected broken handshake")
}
}
if run.NoOpCounter != 0 {
t.Fatalf("expected to not have any noops, not %d noops", run.NoOpCounter)
}
}
ask, err := m.GetSummaryKeys(meas)
if err != nil {
t.Fatal("cannot obtain summary")
}
summary := ask.(SummaryKeys)
if summary.IsAnomaly {
t.Fatal("expected no anomaly")
}
})
}

View File

@ -210,10 +210,10 @@ func (m *Measurer) doUpload(
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
tk := new(TestKeys) tk := new(TestKeys)
tk.Protocol = 7 tk.Protocol = 7
measurement.TestKeys = tk measurement.TestKeys = tk

View File

@ -84,12 +84,7 @@ func TestRunWithCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel cancel() // immediately cancel
meas := &model.Measurement{} meas := &model.Measurement{}
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, model.NewPrinterCallbacks(log.Log))
Callbacks: model.NewPrinterCallbacks(log.Log),
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
// Here we get nil because we still want to submit this measurement // Here we get nil because we still want to submit this measurement
if !errors.Is(err, nil) { if !errors.Is(err, nil) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
@ -109,15 +104,15 @@ func TestGood(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableHTTPClient: http.DefaultClient, MockableHTTPClient: http.DefaultClient,
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -138,15 +133,15 @@ func TestFailDownload(t *testing.T) {
cancel() cancel()
} }
meas := &model.Measurement{} meas := &model.Measurement{}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: meas, &mockable.Session{
Session: &mockable.Session{
MockableHTTPClient: http.DefaultClient, MockableHTTPClient: http.DefaultClient,
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} meas,
err := measurer.Run(ctx, args) model.NewPrinterCallbacks(log.Log),
)
// We expect a nil failure here because we want to submit anyway // We expect a nil failure here because we want to submit anyway
// a measurement that failed to connect to m-lab. // a measurement that failed to connect to m-lab.
if err != nil { if err != nil {
@ -169,15 +164,15 @@ func TestFailUpload(t *testing.T) {
cancel() cancel()
} }
meas := &model.Measurement{} meas := &model.Measurement{}
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: meas, &mockable.Session{
Session: &mockable.Session{
MockableHTTPClient: http.DefaultClient, MockableHTTPClient: http.DefaultClient,
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} meas,
err := measurer.Run(ctx, args) model.NewPrinterCallbacks(log.Log),
)
// Here we expect a nil error because we want to submit this measurement // Here we expect a nil error because we want to submit this measurement
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -202,15 +197,15 @@ func TestDownloadJSONUnmarshalFail(t *testing.T) {
seenError = true seenError = true
return expected return expected
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{}, &mockable.Session{
Session: &mockable.Session{
MockableHTTPClient: http.DefaultClient, MockableHTTPClient: http.DefaultClient,
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} new(model.Measurement),
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -38,10 +38,12 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
// TODO(DecFox): Replace the localhost deployment with an OONI testhelper // TODO(DecFox): Replace the localhost deployment with an OONI testhelper
// Ensure that we only do this once we have a deployed testhelper // Ensure that we only do this once we have a deployed testhelper
testhelper := "http://127.0.0.1" testhelper := "http://127.0.0.1"

View File

@ -29,12 +29,7 @@ func TestMeasurer_run(t *testing.T) {
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
ctx := context.Background() ctx := context.Background()
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -66,10 +66,10 @@ func (m *Measurer) printprogress(
} }
// Run runs the measurement // Run runs the measurement
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
const maxruntime = 300 const maxruntime = 300
ctx, cancel := context.WithTimeout(ctx, maxruntime*time.Second) ctx, cancel := context.WithTimeout(ctx, maxruntime*time.Second)
var ( var (

View File

@ -33,12 +33,8 @@ func TestRunWithCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately cancel() // fail immediately
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, newfakesession(), measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: newfakesession(),
}
err := measurer.Run(ctx, args)
if !errors.Is(err, nil) { // nil because we want to submit the measurement if !errors.Is(err, nil) { // nil because we want to submit the measurement
t.Fatal("expected another error here") t.Fatal("expected another error here")
} }
@ -68,12 +64,8 @@ func TestRunWithCustomInputAndCancelledContext(t *testing.T) {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately cancel() // fail immediately
args := &model.ExperimentArgs{ err := measurer.Run(ctx, newfakesession(), measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: newfakesession(),
}
err := measurer.Run(ctx, args)
if !errors.Is(err, nil) { // nil because we want to submit the measurement if !errors.Is(err, nil) { // nil because we want to submit the measurement
t.Fatal("expected another error here") t.Fatal("expected another error here")
} }
@ -92,12 +84,7 @@ func TestRunWillPrintSomethingWithCancelledContext(t *testing.T) {
cancel() // fail after we've given the printer a chance to run cancel() // fail after we've given the printer a chance to run
} }
observer := observerCallbacks{progress: &atomicx.Int64{}} observer := observerCallbacks{progress: &atomicx.Int64{}}
args := &model.ExperimentArgs{ err := measurer.Run(ctx, newfakesession(), measurement, observer)
Callbacks: observer,
Measurement: measurement,
Session: newfakesession(),
}
err := measurer.Run(ctx, args)
if !errors.Is(err, nil) { // nil because we want to submit the measurement if !errors.Is(err, nil) { // nil because we want to submit the measurement
t.Fatal("expected another error here") t.Fatal("expected another error here")
} }

View File

@ -221,11 +221,12 @@ func (m *Measurer) receiver(
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
host := string(measurement.Input) host := string(measurement.Input)
// allow URL input // allow URL input
if u, err := url.ParseRequestURI(host); err == nil { if u, err := url.ParseRequestURI(host); err == nil {

View File

@ -33,12 +33,8 @@ func TestInvalidHost(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("a.a.a.a") measurement.Input = model.MeasurementTarget("a.a.a.a")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err == nil { if err == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
@ -57,12 +53,8 @@ func TestURLInput(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("https://google.com/") measurement.Input = model.MeasurementTarget("https://google.com/")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err != nil { if err != nil {
t.Fatal("unexpected error") t.Fatal("unexpected error")
} }
@ -81,12 +73,8 @@ func TestSuccess(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("google.com") measurement.Input = model.MeasurementTarget("google.com")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err != nil { if err != nil {
t.Fatal("did not expect an error here") t.Fatal("did not expect an error here")
} }
@ -129,12 +117,8 @@ func TestWithCancelledContext(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal("did not expect an error here") t.Fatal("did not expect an error here")
} }
@ -154,12 +138,8 @@ func TestListenFails(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("google.com") measurement.Input = model.MeasurementTarget("google.com")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err == nil { if err == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
@ -202,12 +182,8 @@ func TestWriteFails(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("google.com") measurement.Input = model.MeasurementTarget("google.com")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err != nil { if err != nil {
t.Fatal("unexpected error") t.Fatal("unexpected error")
} }
@ -263,12 +239,8 @@ func TestReadFails(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("google.com") measurement.Input = model.MeasurementTarget("google.com")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err != nil { if err != nil {
t.Fatal("unexpected error") t.Fatal("unexpected error")
} }
@ -299,12 +271,8 @@ func TestNoResponse(t *testing.T) {
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("ooni.org") measurement.Input = model.MeasurementTarget("ooni.org")
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
args := &model.ExperimentArgs{ err := measurer.Run(context.Background(), sess, measurement,
Callbacks: model.NewPrinterCallbacks(log.Log), model.NewPrinterCallbacks(log.Log))
Measurement: measurement,
Session: sess,
}
err := measurer.Run(context.Background(), args)
if err != nil { if err != nil {
t.Fatal("did not expect an error here") t.Fatal("did not expect an error here")
} }

View File

@ -175,11 +175,8 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession,
callbacks := args.Callbacks measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
measurement := args.Measurement
sess := args.Session
ctx, cancel := context.WithTimeout(ctx, 90*time.Second) ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel() defer cancel()
testkeys := NewTestKeys() testkeys := NewTestKeys()

View File

@ -100,7 +100,7 @@ const (
"cert": "XXXXXXXXXXXXXXXXXXXXXXXXX", "cert": "XXXXXXXXXXXXXXXXXXXXXXXXX",
"iatMode": "0" "iatMode": "0"
} }
}, },
{ {
"type":"openvpn", "type":"openvpn",
"protocols":[ "protocols":[
@ -328,12 +328,7 @@ func TestInvalidCaCert(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -604,12 +599,7 @@ func TestMissingTransport(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err = measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err = measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -800,14 +790,14 @@ func runDefaultMockTest(t *testing.T, multiGetter urlgetter.MultiGetter) *model.
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -21,10 +21,5 @@ func (m *dnsCheckMain) do(ctx context.Context, input StructuredInput,
measurement.TestName = exp.ExperimentName() measurement.TestName = exp.ExperimentName()
measurement.TestVersion = exp.ExperimentVersion() measurement.TestVersion = exp.ExperimentVersion()
measurement.Input = model.MeasurementTarget(input.Input) measurement.Input = model.MeasurementTarget(input.Input)
args := &model.ExperimentArgs{ return exp.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
return exp.Run(ctx, args)
} }

View File

@ -46,10 +46,10 @@ type StructuredInput struct {
} }
// Run implements ExperimentMeasurer.ExperimentVersion. // Run implements ExperimentMeasurer.ExperimentVersion.
func (Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
var input StructuredInput var input StructuredInput
if err := json.Unmarshal([]byte(measurement.Input), &input); err != nil { if err := json.Unmarshal([]byte(measurement.Input), &input); err != nil {
return err return err

View File

@ -31,12 +31,7 @@ func TestRunDNSCheckWithCancelledContext(t *testing.T) {
cancel() // fail immediately cancel() // fail immediately
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
// TODO(bassosimone): here we could improve the tests by checking // TODO(bassosimone): here we could improve the tests by checking
// whether the result makes sense for a cancelled context. // whether the result makes sense for a cancelled context.
if err != nil { if err != nil {
@ -67,12 +62,7 @@ func TestRunURLGetterWithCancelledContext(t *testing.T) {
cancel() // fail immediately cancel() // fail immediately
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { // here we expected nil b/c we want to submit the measurement if err != nil { // here we expected nil b/c we want to submit the measurement
t.Fatal(err) t.Fatal(err)
} }
@ -96,12 +86,7 @@ func TestRunWithInvalidJSON(t *testing.T) {
ctx := context.Background() ctx := context.Background()
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err == nil || err.Error() != "invalid character '}' looking for beginning of value" { if err == nil || err.Error() != "invalid character '}' looking for beginning of value" {
t.Fatalf("not the error we expected: %+v", err) t.Fatalf("not the error we expected: %+v", err)
} }
@ -115,12 +100,7 @@ func TestRunWithUnknownExperiment(t *testing.T) {
ctx := context.Background() ctx := context.Background()
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err == nil || err.Error() != "no such experiment: antani" { if err == nil || err.Error() != "no such experiment: antani" {
t.Fatalf("not the error we expected: %+v", err) t.Fatalf("not the error we expected: %+v", err)
} }

View File

@ -18,10 +18,5 @@ func (m *urlGetterMain) do(ctx context.Context, input StructuredInput,
measurement.TestName = exp.ExperimentName() measurement.TestName = exp.ExperimentName()
measurement.TestVersion = exp.ExperimentVersion() measurement.TestVersion = exp.ExperimentVersion()
measurement.Input = model.MeasurementTarget(input.Input) measurement.Input = model.MeasurementTarget(input.Input)
args := &model.ExperimentArgs{ return exp.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
return exp.Run(ctx, args)
} }

View File

@ -141,10 +141,8 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run // Run implements ExperimentMeasurer.Run
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession,
callbacks := args.Callbacks measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
measurement := args.Measurement
sess := args.Session
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
urlgetter.RegisterExtensions(measurement) urlgetter.RegisterExtensions(measurement)

View File

@ -25,14 +25,14 @@ func TestNewExperimentMeasurer(t *testing.T) {
func TestGood(t *testing.T) { func TestGood(t *testing.T) {
measurer := signal.NewExperimentMeasurer(signal.Config{}) measurer := signal.NewExperimentMeasurer(signal.Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -103,14 +103,14 @@ func TestBadSignalCA(t *testing.T) {
SignalCA: "INVALIDCA", SignalCA: "INVALIDCA",
}) })
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err.Error() != "AppendCertsFromPEM failed" { if err.Error() != "AppendCertsFromPEM failed" {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }

View File

@ -112,11 +112,12 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errNoInputProvided return errNoInputProvided
} }

View File

@ -65,12 +65,7 @@ func TestMeasurer_run(t *testing.T) {
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
return meas, m, err return meas, m, err
} }

View File

@ -0,0 +1,413 @@
package smtp
import (
"context"
"crypto/tls"
"fmt"
"github.com/pkg/errors"
"net"
"net/smtp"
"net/url"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/tracex"
)
var (
// errNoInputProvided indicates you didn't provide any input
errNoInputProvided = errors.New("not input provided")
// errInputIsNotAnURL indicates that input is not an URL
errInputIsNotAnURL = errors.New("input is not an URL")
// errInvalidScheme indicates that the scheme is invalid
errInvalidScheme = errors.New("scheme must be smtp(s)")
)
const (
testName = "smtp"
testVersion = "0.0.1"
)
// Config contains the experiment config.
type Config struct{}
type RuntimeConfig struct {
host string
port string
forced_tls bool
noop_count uint8
}
func config(input model.MeasurementTarget) (*RuntimeConfig, error) {
if input == "" {
// TODO: static input data (eg. gmail/riseup..)
return nil, errNoInputProvided
}
parsed, err := url.Parse(string(input))
if err != nil {
return nil, fmt.Errorf("%w: %s", errInputIsNotAnURL, err.Error())
}
if parsed.Scheme != "smtp" && parsed.Scheme != "smtps" {
return nil, errInvalidScheme
}
port := ""
if parsed.Port() == "" {
// Default ports for StartTLS and forced TLS respectively
if parsed.Scheme == "smtp" {
port = "587"
} else {
port = "465"
}
} else {
// Valid port is checked by URL parsing
port = parsed.Port()
}
valid_config := RuntimeConfig{
host: parsed.Hostname(),
forced_tls: parsed.Scheme == "smtps",
port: port,
noop_count: 10,
}
return &valid_config, nil
}
// TestKeys contains the experiment results
type TestKeys struct {
Queries []*model.ArchivalDNSLookupResult `json:"queries"`
Runs map[string]*IndividualTestKeys `json:"runs"`
// Used for global failure (DNS resolution)
Failure string `json:"failure"`
// Indicates global failure or individual test failure
Failed bool `json:"failed"`
}
// IndividualTestKeys contains results for TCP/IP level stuff for each address found
// in the DNS lookup
type IndividualTestKeys struct {
NoOpCounter uint8
TCPConnect []*model.ArchivalTCPConnectResult `json:"tcp_connect"`
TLSHandshakes []*model.ArchivalTLSOrQUICHandshakeResult `json:"tls_handshakes"`
// Individual failure aborting the test run for this address/port combo
Failure *string `json:"failure"`
}
type Measurer struct {
// Config contains the experiment settings. If empty we
// will be using default settings.
Config Config
// Getter is an optional getter to be used for testing.
Getter urlgetter.MultiGetter
}
// ExperimentName implements ExperimentMeasurer.ExperimentName
func (m Measurer) ExperimentName() string {
return testName
}
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion
func (m Measurer) ExperimentVersion() string {
return testVersion
}
// Manages sequential TCP sessions to the same hostname (over different IPs)
// don't use in parallel!
type TCPRunner struct {
trace *measurexlite.Trace
logger model.Logger
ctx context.Context
tk *TestKeys
tlsconfig *tls.Config
host string
port string
// addr is changed everytime TCPRunner.conn(addr) is called
addr string
}
type TCPSession struct {
addr string
port string
runner *TCPRunner
tk *IndividualTestKeys
tls bool
raw_conn *net.Conn
tls_conn *net.Conn
}
func (s *TCPSession) Close() {
if s.tls {
var conn = *s.tls_conn
conn.Close()
} else {
var conn = *s.raw_conn
conn.Close()
}
}
func (s *TCPSession) current_conn() net.Conn {
if s.tls {
return *s.tls_conn
} else {
return *s.raw_conn
}
}
func (r *TCPRunner) run_key() string {
return net.JoinHostPort(r.addr, r.port)
}
func (r *TCPRunner) get_run() *IndividualTestKeys {
if r.tk.Runs == nil {
r.tk.Runs = make(map[string]*IndividualTestKeys)
}
key := r.run_key()
val, exists := r.tk.Runs[key]
if exists {
return val
} else {
r.tk.Runs[key] = &IndividualTestKeys{}
return r.tk.Runs[key]
}
}
func (r *TCPRunner) conn(addr string, port string) (*TCPSession, bool) {
r.addr = addr
run := r.get_run()
s := new(TCPSession)
if !s.conn(addr, port, r, run) {
return nil, false
}
return s, true
}
func (r *TCPRunner) dial(addr string, port string) (net.Conn, error) {
dialer := r.trace.NewDialerWithoutResolver(r.logger)
conn, err := dialer.DialContext(r.ctx, "tcp", net.JoinHostPort(addr, port))
run := r.get_run()
run.TCPConnect = append(run.TCPConnect, r.trace.TCPConnects()...)
return conn, err
}
func (s *TCPSession) conn(addr string, port string, runner *TCPRunner, tk *IndividualTestKeys) bool {
// Initialize addr field and corresponding errors in TestKeys
s.addr = addr
s.port = port
s.tls = false
s.runner = runner
s.tk = tk
conn, err := runner.dial(addr, port)
if err != nil {
s.error(err)
return false
}
s.raw_conn = &conn
return true
}
func (s *TCPSession) error(err error) {
s.runner.tk.Failed = true
s.tk.Failure = tracex.NewFailure(err)
//s. = append(s.errors, tracex.NewFailure(err))
}
func (r *TCPRunner) resolve(host string) ([]string, bool) {
r.logger.Infof("Resolving DNS for %s", host)
resolver := r.trace.NewStdlibResolver(r.logger)
addrs, err := resolver.LookupHost(r.ctx, host)
r.tk.Queries = append(r.tk.Queries, r.trace.DNSLookupsFromRoundTrip()...)
if err != nil {
r.tk.Failure = *tracex.NewFailure(err)
return []string{}, false
}
r.logger.Infof("Finished DNS for %s: %v", host, addrs)
return addrs, true
}
func (s *TCPSession) handshake() bool {
if s.tls {
// TLS already initialized...
return true
}
s.runner.logger.Infof("Starting TLS handshake with %s:%s", s.addr, s.port)
thx := s.runner.trace.NewTLSHandshakerStdlib(s.runner.logger)
tconn, _, err := thx.Handshake(s.runner.ctx, *s.raw_conn, s.runner.tlsconfig)
s.tk.TLSHandshakes = append(s.tk.TLSHandshakes, s.runner.trace.FirstTLSHandshakeOrNil())
if err != nil {
s.error(err)
return false
}
s.tls = true
s.tls_conn = &tconn
s.runner.logger.Infof("Handshake succeeded")
return true
}
func (s *TCPSession) starttls(message string) bool {
if s.tls {
// TLS already initialized...
return true
}
if message != "" {
s.runner.logger.Infof("Asking for StartTLS upgrade")
s.current_conn().Write([]byte(message))
}
return s.handshake()
}
func (s *TCPSession) smtp(ehlo string, noop uint8) bool {
// Auto-choose plaintext/TCP session
client, err := smtp.NewClient(s.current_conn(), ehlo)
if err != nil {
s.error(err)
return false
}
err = client.Hello(ehlo)
if err != nil {
s.error(err)
return false
}
if noop > 0 {
s.runner.logger.Infof("Trying to generate more no-op traffic")
// TODO: noop counter per IP address
s.tk.NoOpCounter = 0
for s.tk.NoOpCounter < noop {
s.tk.NoOpCounter += 1
s.runner.logger.Infof("NoOp Iteration %d", s.tk.NoOpCounter)
err = client.Noop()
if err != nil {
s.error(err)
break
}
}
if s.tk.NoOpCounter == noop {
s.runner.logger.Infof("Successfully generated no-op traffic")
return true
} else {
s.runner.logger.Infof("Failed no-op traffic at iteration %d", s.tk.NoOpCounter)
return false
}
}
return true
}
// Run implements ExperimentMeasurer.Run
func (m Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
log := sess.Logger()
trace := measurexlite.NewTrace(0, measurement.MeasurementStartTimeSaved)
config, err := config(measurement.Input)
if err != nil {
// Invalid input data, we don't even generate report
return err
}
tk := new(TestKeys)
measurement.TestKeys = tk
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
tlsconfig := tls.Config{
InsecureSkipVerify: false,
ServerName: config.host,
}
runner := &TCPRunner{
trace: trace,
logger: log,
ctx: ctx,
tk: tk,
tlsconfig: &tlsconfig,
host: config.host,
port: config.port,
}
// First resolve DNS
addrs, success := runner.resolve(config.host)
if !success {
return nil
}
for _, addr := range addrs {
tcp_session, success := runner.conn(addr, config.port)
if !success {
continue
}
defer tcp_session.Close()
if config.forced_tls {
// Direct TLS connection
if !tcp_session.handshake() {
continue
}
// Try EHLO + NoOps
if !tcp_session.smtp("localhost", config.noop_count) {
continue
}
} else {
// StartTLS... first try plaintext EHLO
if !tcp_session.smtp("localhost", 0) {
continue
}
// Upgrade via StartTLS and try EHLO + NoOps
if !tcp_session.starttls("STARTTLS\n") {
continue
}
if !tcp_session.smtp("localhost", config.noop_count) {
continue
}
}
}
return nil
}
// NewExperimentMeasurer creates a new ExperimentMeasurer.
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return Measurer{Config: config}
}
// SummaryKeys contains summary keys for this experiment.
//
// Note that this structure is part of the ABI contract with ooniprobe
// therefore we should be careful when changing it.
type SummaryKeys struct {
//DNSBlocking bool `json:"facebook_dns_blocking"`
//TCPBlocking bool `json:"facebook_tcp_blocking"`
IsAnomaly bool `json:"-"`
}
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
sk := SummaryKeys{IsAnomaly: false}
_, ok := measurement.TestKeys.(*TestKeys)
if !ok {
return sk, errors.New("invalid test keys type")
}
return sk, nil
}

View File

@ -0,0 +1,185 @@
package smtp
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"testing"
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
"github.com/ooni/probe-cli/v3/internal/model"
)
func plaintextListener() net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
}
}
return l
}
func tlsListener(l net.Listener) net.Listener {
return tls.NewListener(l, &tls.Config{})
}
func listener_addr(l net.Listener) string {
return l.Addr().String()
}
func ValidSMTPServer(conn net.Conn) {
for {
command, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return
}
if command == "" {
} else if command == "NOOP" {
conn.Write([]byte("250 2.0.0 Ok\n"))
} else if command == "STARTTLS" {
conn.Write([]byte("220 2.0.0 Ready to start TLS\n"))
// TODO: conn.Close does not actually close connection? or does client not detect it?
conn.Close()
return
} else if strings.HasPrefix(command, "EHLO") {
conn.Write([]byte("250 mock.example.com\n"))
}
conn.Write([]byte("\n"))
}
}
func TCPServer(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
continue
}
defer conn.Close()
conn.Write([]byte("220 mock.example.com ESMTP (spam is not appreciated)\n"))
ValidSMTPServer(conn)
}
}
func TestMeasurer_run(t *testing.T) {
// runHelper is an helper function to run this set of tests.
runHelper := func(input string) (*model.Measurement, model.ExperimentMeasurer, error) {
m := NewExperimentMeasurer(Config{})
if m.ExperimentName() != "smtp" {
t.Fatal("invalid experiment name")
}
if m.ExperimentVersion() != "0.0.1" {
t.Fatal("invalid experiment version")
}
ctx := context.Background()
meas := &model.Measurement{
Input: model.MeasurementTarget(input),
}
sess := &mockable.Session{
MockableLogger: model.DiscardLogger,
}
callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
err := m.Run(ctx, sess, meas, callbacks)
return meas, m, err
}
t.Run("with empty input", func(t *testing.T) {
_, _, err := runHelper("")
if !errors.Is(err, errNoInputProvided) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid URL", func(t *testing.T) {
_, _, err := runHelper("\t")
if !errors.Is(err, errInputIsNotAnURL) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid scheme", func(t *testing.T) {
_, _, err := runHelper("https://8.8.8.8:443/")
if !errors.Is(err, errInvalidScheme) {
t.Fatal("unexpected error", err)
}
})
t.Run("with broken TLS", func(t *testing.T) {
p := plaintextListener()
defer p.Close()
l := tlsListener(p)
defer l.Close()
addr := listener_addr(l)
go TCPServer(l)
meas, m, err := runHelper("smtps://" + addr)
if err != nil {
t.Fatal(err)
}
tk := meas.TestKeys.(*TestKeys)
for _, run := range tk.Runs {
for _, handshake := range run.TLSHandshakes {
if *handshake.Failure != "unknown_failure: remote error: tls: unrecognized name" {
t.Fatal("expected unrecognized_name in TLS handshake")
}
}
if run.NoOpCounter != 0 {
t.Fatalf("expected to not have any noops, not %d noops", run.NoOpCounter)
}
}
ask, err := m.GetSummaryKeys(meas)
if err != nil {
t.Fatal("cannot obtain summary")
}
summary := ask.(SummaryKeys)
if summary.IsAnomaly {
t.Fatal("expected no anomaly")
}
})
t.Run("with broken starttls", func(t *testing.T) {
l := plaintextListener()
defer l.Close()
addr := listener_addr(l)
go TCPServer(l)
meas, m, err := runHelper("smtp://" + addr)
if err != nil {
t.Fatal(err)
}
tk := meas.TestKeys.(*TestKeys)
for _, run := range tk.Runs {
for _, handshake := range run.TLSHandshakes {
if *handshake.Failure != "generic_timeout_error" {
t.Fatal("expected timeout in TLS handshake")
}
}
if run.NoOpCounter != 0 {
t.Fatalf("expected to not have any noops, not %d noops", run.NoOpCounter)
}
}
ask, err := m.GetSummaryKeys(meas)
if err != nil {
t.Fatal("cannot obtain summary")
}
summary := ask.(SummaryKeys)
if summary.IsAnomaly {
t.Fatal("expected no anomaly")
}
})
}

View File

@ -233,10 +233,12 @@ func maybeURLToSNI(input model.MeasurementTarget) (model.MeasurementTarget, erro
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
m.mu.Lock() m.mu.Lock()
if m.cache == nil { if m.cache == nil {
m.cache = make(map[string]Subresult) m.cache = make(map[string]Subresult)

View File

@ -116,12 +116,12 @@ func TestMeasurerMeasureNoMeasurementInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{ measurer := NewExperimentMeasurer(Config{
ControlSNI: "example.com", ControlSNI: "example.com",
}) })
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{}, newsession(),
Session: newsession(), new(model.Measurement),
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err.Error() != "Experiment requires measurement.Input" { if err.Error() != "Experiment requires measurement.Input" {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -136,12 +136,12 @@ func TestMeasurerMeasureWithInvalidInput(t *testing.T) {
measurement := &model.Measurement{ measurement := &model.Measurement{
Input: "\t", Input: "\t",
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, newsession(),
Session: newsession(), measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err == nil { if err == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
@ -156,12 +156,12 @@ func TestMeasurerMeasureWithCancelledContext(t *testing.T) {
measurement := &model.Measurement{ measurement := &model.Measurement{
Input: "kernel.org", Input: "kernel.org",
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, newsession(),
Session: newsession(), measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -73,10 +73,10 @@ var errStunMissingPortInURL = errors.New("stun: missing port in URL")
var errUnsupportedURLScheme = errors.New("stun: unsupported URL scheme") var errUnsupportedURLScheme = errors.New("stun: unsupported URL scheme")
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
tk := new(TestKeys) tk := new(TestKeys)
measurement.TestKeys = tk measurement.TestKeys = tk
registerExtensions(measurement) registerExtensions(measurement)

View File

@ -32,12 +32,12 @@ func TestMeasurerExperimentNameVersion(t *testing.T) {
func TestRunWithoutInput(t *testing.T) { func TestRunWithoutInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, errStunMissingInput) { if !errors.Is(err, errStunMissingInput) {
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
@ -47,12 +47,12 @@ func TestRunWithInvalidURL(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("\t") // <- invalid URL measurement.Input = model.MeasurementTarget("\t") // <- invalid URL
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
@ -62,12 +62,12 @@ func TestRunWithNoPort(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("stun://stun.ekiga.net") measurement.Input = model.MeasurementTarget("stun://stun.ekiga.net")
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, errStunMissingPortInURL) { if !errors.Is(err, errStunMissingPortInURL) {
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
@ -77,12 +77,12 @@ func TestRunWithUnsupportedURLScheme(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget("https://stun.ekiga.net:3478") measurement.Input = model.MeasurementTarget("https://stun.ekiga.net:3478")
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if !errors.Is(err, errUnsupportedURLScheme) { if !errors.Is(err, errUnsupportedURLScheme) {
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
@ -92,14 +92,14 @@ func TestRunWithInput(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget(defaultInput) measurement.Input = model.MeasurementTarget(defaultInput)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -124,14 +124,14 @@ func TestCancelledContext(t *testing.T) {
measurer := NewExperimentMeasurer(Config{}) measurer := NewExperimentMeasurer(Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget(defaultInput) measurement.Input = model.MeasurementTarget(defaultInput)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
}, },
} measurement,
err := measurer.Run(ctx, args) model.NewPrinterCallbacks(log.Log),
)
if !errors.Is(err, nil) { // nil because we want to submit if !errors.Is(err, nil) { // nil because we want to submit
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
@ -166,14 +166,14 @@ func TestNewClientFailure(t *testing.T) {
measurer := NewExperimentMeasurer(*config) measurer := NewExperimentMeasurer(*config)
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget(defaultInput) measurement.Input = model.MeasurementTarget(defaultInput)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if !errors.Is(err, nil) { // nil because we want to submit if !errors.Is(err, nil) { // nil because we want to submit
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -202,14 +202,14 @@ func TestStartFailure(t *testing.T) {
measurer := NewExperimentMeasurer(*config) measurer := NewExperimentMeasurer(*config)
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget(defaultInput) measurement.Input = model.MeasurementTarget(defaultInput)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if !errors.Is(err, nil) { // nil because we want to submit if !errors.Is(err, nil) { // nil because we want to submit
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -242,14 +242,14 @@ func TestReadFailure(t *testing.T) {
measurer := NewExperimentMeasurer(*config) measurer := NewExperimentMeasurer(*config)
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = model.MeasurementTarget(defaultInput) measurement.Input = model.MeasurementTarget(defaultInput)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if !errors.Is(err, nil) { // nil because we want to submit if !errors.Is(err, nil) { // nil because we want to submit
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }

View File

@ -82,10 +82,12 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errNoInputProvided return errNoInputProvided
} }

View File

@ -51,12 +51,7 @@ func TestMeasurer_run(t *testing.T) {
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
return meas, m, err return meas, m, err
} }

View File

@ -101,11 +101,8 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run // Run implements ExperimentMeasurer.Run
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(ctx context.Context, sess model.ExperimentSession,
callbacks := args.Callbacks measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
measurement := args.Measurement
sess := args.Session
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
urlgetter.RegisterExtensions(measurement) urlgetter.RegisterExtensions(measurement)

View File

@ -28,14 +28,14 @@ func TestNewExperimentMeasurer(t *testing.T) {
func TestGood(t *testing.T) { func TestGood(t *testing.T) {
measurer := telegram.NewExperimentMeasurer(telegram.Config{}) measurer := telegram.NewExperimentMeasurer(telegram.Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -297,12 +297,7 @@ func TestWeConfigureWebChecksToFailOnHTTPError(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ if err := measurer.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := measurer.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if called.Load() < 1 { if called.Load() < 1 {

View File

@ -52,10 +52,12 @@ var (
) )
// // Run implements ExperimentMeasurer.Run. // // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errNoInputProvided return errNoInputProvided
} }

View File

@ -38,12 +38,7 @@ func TestMeasurer_input_failure(t *testing.T) {
}, },
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
return meas, m, err return meas, m, err
} }

View File

@ -112,10 +112,12 @@ var (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
if measurement.Input == "" { if measurement.Input == "" {
return errNoInputProvided return errNoInputProvided
} }

View File

@ -58,12 +58,7 @@ func TestMeasurer_run(t *testing.T) {
MockableLogger: model.DiscardLogger, MockableLogger: model.DiscardLogger,
} }
callbacks := model.NewPrinterCallbacks(model.DiscardLogger) callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, meas, callbacks)
Callbacks: callbacks,
Measurement: meas,
Session: sess,
}
err := m.Run(ctx, args)
return meas, m, err return meas, m, err
} }

View File

@ -78,11 +78,12 @@ var allMethods = []method{{
}} }}
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
// TODO(bassosimone): wondering whether this experiment should // TODO(bassosimone): wondering whether this experiment should
// actually be merged with sniblocking instead? // actually be merged with sniblocking instead?
tk := new(TestKeys) tk := new(TestKeys)

View File

@ -27,12 +27,12 @@ func TestRunWithExplicitSNI(t *testing.T) {
}) })
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = "8.8.8.8:853" measurement.Input = "8.8.8.8:853"
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -43,12 +43,12 @@ func TestRunWithImplicitSNI(t *testing.T) {
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{}) measurer := tlstool.NewExperimentMeasurer(tlstool.Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = "dns.google:853" measurement.Input = "dns.google:853"
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -60,12 +60,12 @@ func TestRunWithCancelledContext(t *testing.T) {
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{}) measurer := tlstool.NewExperimentMeasurer(tlstool.Config{})
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = "dns.google:853" measurement.Input = "dns.google:853"
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx,
Measurement: measurement, &mockable.Session{},
Session: &mockable.Session{}, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(ctx, args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -166,10 +166,12 @@ func (m *Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
targets, err := m.gimmeTargets(ctx, sess) targets, err := m.gimmeTargets(ctx, sess)
if err != nil { if err != nil {
return err // fail the measurement if we cannot get any target return err // fail the measurement if we cannot get any target

View File

@ -36,14 +36,14 @@ func TestMeasurerMeasureFetchTorTargetsError(t *testing.T) {
measurer.fetchTorTargets = func(ctx context.Context, sess model.ExperimentSession, cc string) (map[string]model.OOAPITorTarget, error) { measurer.fetchTorTargets = func(ctx context.Context, sess model.ExperimentSession, cc string) (map[string]model.OOAPITorTarget, error) {
return nil, expected return nil, expected
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{}, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} new(model.Measurement),
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if !errors.Is(err, expected) { if !errors.Is(err, expected) {
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -55,14 +55,14 @@ func TestMeasurerMeasureFetchTorTargetsEmptyList(t *testing.T) {
return nil, nil return nil, nil
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} measurement,
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -79,14 +79,14 @@ func TestMeasurerMeasureGoodWithMockedOrchestra(t *testing.T) {
measurer.fetchTorTargets = func(ctx context.Context, sess model.ExperimentSession, cc string) (map[string]model.OOAPITorTarget, error) { measurer.fetchTorTargets = func(ctx context.Context, sess model.ExperimentSession, cc string) (map[string]model.OOAPITorTarget, error) {
return nil, nil return nil, nil
} }
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: &model.Measurement{}, &mockable.Session{
Session: &mockable.Session{
MockableLogger: log.Log, MockableLogger: log.Log,
}, },
} new(model.Measurement),
err := measurer.Run(context.Background(), args) model.NewPrinterCallbacks(log.Log),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -99,12 +99,12 @@ func TestMeasurerMeasureGood(t *testing.T) {
measurer := NewMeasurer(Config{}) measurer := NewMeasurer(Config{})
sess := newsession() sess := newsession()
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, sess,
Session: sess, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -142,12 +142,12 @@ func TestMeasurerMeasureSanitiseOutput(t *testing.T) {
key: staticPrivateTestingTarget, key: staticPrivateTestingTarget,
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
args := &model.ExperimentArgs{ err := measurer.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), context.Background(),
Measurement: measurement, sess,
Session: sess, measurement,
} model.NewPrinterCallbacks(log.Log),
err := measurer.Run(context.Background(), args) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -34,12 +34,7 @@ func TestRunWithExistingTor(t *testing.T) {
MockableLogger: log.Log, MockableLogger: log.Log,
MockableTempDir: tempdir, MockableTempDir: tempdir,
} }
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View File

@ -124,10 +124,10 @@ const maxRuntime = 600 * time.Second
// set the relevant OONI error inside of the measurement and // set the relevant OONI error inside of the measurement and
// return nil. This is important because the caller may not submit // return nil. This is important because the caller may not submit
// the measurement if this method returns an error. // the measurement if this method returns an error.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
ptl, sfdialer, err := m.setup(ctx, sess.Logger()) ptl, sfdialer, err := m.setup(ctx, sess.Logger())
if err != nil { if err != nil {
// we cannot setup the experiment // we cannot setup the experiment

View File

@ -47,12 +47,7 @@ func TestFailureWithInvalidRendezvousMethod(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ err := m.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := m.Run(ctx, args)
if !errors.Is(err, ptx.ErrSnowflakeNoSuchRendezvousMethod) { if !errors.Is(err, ptx.ErrSnowflakeNoSuchRendezvousMethod) {
t.Fatal("unexpected error", err) t.Fatal("unexpected error", err)
} }
@ -75,12 +70,7 @@ func TestFailureToStartPTXListener(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); !errors.Is(err, expected) {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); !errors.Is(err, expected) {
t.Fatal("not the error we expected", err) t.Fatal("not the error we expected", err)
} }
if tk := measurement.TestKeys; tk != nil { if tk := measurement.TestKeys; tk != nil {
@ -118,12 +108,7 @@ func TestSuccessWithMockedTunnelStart(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if called.Load() != 1 { if called.Load() != 1 {
@ -183,12 +168,7 @@ func TestWithCancelledContext(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tk := measurement.TestKeys.(*TestKeys) tk := measurement.TestKeys.(*TestKeys)
@ -251,12 +231,7 @@ func TestFailureToStartTunnel(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tk := measurement.TestKeys.(*TestKeys) tk := measurement.TestKeys.(*TestKeys)

View File

@ -97,10 +97,10 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements model.ExperimentSession.Run // Run implements model.ExperimentSession.Run
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
_ = args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
// When using the urlgetter experiment directly, there is a nonconfigurable // When using the urlgetter experiment directly, there is a nonconfigurable
// default timeout that applies. When urlgetter is used as a library, it's // default timeout that applies. When urlgetter is used as a library, it's
// instead the responsibility of the user of urlgetter to set timeouts. Note // instead the responsibility of the user of urlgetter to set timeouts. Note

View File

@ -23,12 +23,10 @@ func TestMeasurer(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = "https://www.google.com" measurement.Input = "https://www.google.com"
args := &model.ExperimentArgs{ err := m.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx, &mockable.Session{},
Measurement: measurement, measurement, model.NewPrinterCallbacks(log.Log),
Session: &mockable.Session{}, )
}
err := m.Run(ctx, args)
if !errors.Is(err, nil) { // nil because we want to submit the measurement if !errors.Is(err, nil) { // nil because we want to submit the measurement
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }
@ -62,12 +60,10 @@ func TestMeasurerDNSCache(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
measurement.Input = "https://www.google.com" measurement.Input = "https://www.google.com"
args := &model.ExperimentArgs{ err := m.Run(
Callbacks: model.NewPrinterCallbacks(log.Log), ctx, &mockable.Session{},
Measurement: measurement, measurement, model.NewPrinterCallbacks(log.Log),
Session: &mockable.Session{}, )
}
err := m.Run(ctx, args)
if !errors.Is(err, nil) { // nil because we want to submit the measurement if !errors.Is(err, nil) { // nil because we want to submit the measurement
t.Fatal("not the error we expected") t.Fatal("not the error we expected")
} }

View File

@ -34,12 +34,7 @@ func TestRunWithExistingTor(t *testing.T) {
MockableLogger: log.Log, MockableLogger: log.Log,
MockableTempDir: tempdir, MockableTempDir: tempdir,
} }
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View File

@ -106,10 +106,10 @@ const maxRuntime = 200 * time.Second
// set the relevant OONI error inside of the measurement and // set the relevant OONI error inside of the measurement and
// return nil. This is important because the caller may not submit // return nil. This is important because the caller may not submit
// the measurement if this method returns an error. // the measurement if this method returns an error.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
m.registerExtensions(measurement) m.registerExtensions(measurement)
start := time.Now() start := time.Now()
ctx, cancel := context.WithTimeout(ctx, maxRuntime) ctx, cancel := context.WithTimeout(ctx, maxRuntime)

View File

@ -59,12 +59,7 @@ func TestSuccessWithMockedTunnelStart(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if called.Load() != 1 { if called.Load() != 1 {
@ -118,12 +113,7 @@ func TestWithCancelledContext(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tk := measurement.TestKeys.(*TestKeys) tk := measurement.TestKeys.(*TestKeys)
@ -180,12 +170,7 @@ func TestFailureToStartTunnel(t *testing.T) {
callbacks := &model.PrinterCallbacks{ callbacks := &model.PrinterCallbacks{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
} }
args := &model.ExperimentArgs{ if err := m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := m.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tk := measurement.TestKeys.(*TestKeys) tk := measurement.TestKeys.(*TestKeys)

View File

@ -4,10 +4,9 @@ import (
"context" "context"
"github.com/ooni/probe-cli/v3/internal/geoipx" "github.com/ooni/probe-cli/v3/internal/geoipx"
"github.com/ooni/probe-cli/v3/internal/httpapi" "github.com/ooni/probe-cli/v3/internal/httpx"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
) )
// Redirect to types defined inside the model package // Redirect to types defined inside the model package
@ -22,23 +21,22 @@ type (
// Control performs the control request and returns the response. // Control performs the control request and returns the response.
func Control( func Control(
ctx context.Context, sess model.ExperimentSession, ctx context.Context, sess model.ExperimentSession,
testhelpers []model.OOAPIService, creq ControlRequest) (ControlResponse, *model.OOAPIService, error) { thAddr string, creq ControlRequest) (out ControlResponse, err error) {
seqCaller := httpapi.NewSequenceCaller( clnt := &httpx.APIClientTemplate{
httpapi.MustNewPOSTJSONWithJSONResponseDescriptor(sess.Logger(), "/", creq).WithBodyLogging(true), BaseURL: thAddr,
httpapi.NewEndpointList(sess.DefaultHTTPClient(), sess.UserAgent(), testhelpers...)..., HTTPClient: sess.DefaultHTTPClient(),
) Logger: sess.Logger(),
sess.Logger().Infof("control for %s...", creq.HTTPRequest) UserAgent: sess.UserAgent(),
var out ControlResponse
idx, err := seqCaller.CallWithJSONResponse(ctx, &out)
sess.Logger().Infof("control for %s... %+v", creq.HTTPRequest, model.ErrorToStringOrOK(err))
if err != nil {
// make sure error is wrapped
err = netxlite.NewTopLevelGenericErrWrapper(err)
return ControlResponse{}, nil, err
} }
sess.Logger().Infof("control for %s...", creq.HTTPRequest)
// make sure error is wrapped
err = clnt.WithBodyLogging().Build().PostJSON(ctx, "/", creq, &out)
if err != nil {
err = netxlite.NewTopLevelGenericErrWrapper(err)
}
sess.Logger().Infof("control for %s... %+v", creq.HTTPRequest, model.ErrorToStringOrOK(err))
fillASNs(&out.DNS) fillASNs(&out.DNS)
runtimex.Assert(idx >= 0 && idx < len(testhelpers), "idx out of bounds") return
return out, &testhelpers[idx], nil
} }
// fillASNs fills the ASNs array of ControlDNSResult. For each Addr inside // fillASNs fills the ASNs array of ControlDNSResult. For each Addr inside

View File

@ -15,7 +15,7 @@ import (
const ( const (
testName = "web_connectivity" testName = "web_connectivity"
testVersion = "0.4.2" testVersion = "0.4.1"
) )
// Config contains the experiment config. // Config contains the experiment config.
@ -121,11 +121,12 @@ const (
) )
// Run implements ExperimentMeasurer.Run. // Run implements ExperimentMeasurer.Run.
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
_ = args.Callbacks ctx context.Context,
measurement := args.Measurement sess model.ExperimentSession,
sess := args.Session measurement *model.Measurement,
callbacks model.ExperimentCallbacks,
) error {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
tk := new(TestKeys) tk := new(TestKeys)
@ -144,9 +145,19 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
} }
// 1. find test helper // 1. find test helper
testhelpers, _ := sess.GetTestHelpersByName("web-connectivity") testhelpers, _ := sess.GetTestHelpersByName("web-connectivity")
if len(testhelpers) < 1 { var testhelper *model.OOAPIService
for _, th := range testhelpers {
if th.Type == "https" {
testhelper = &th
break
}
}
if testhelper == nil {
return ErrNoAvailableTestHelpers return ErrNoAvailableTestHelpers
} }
measurement.TestHelpers = map[string]interface{}{
"backend": testhelper,
}
// 2. perform the DNS lookup step // 2. perform the DNS lookup step
dnsBegin := time.Now() dnsBegin := time.Now()
dnsResult := DNSLookup(ctx, DNSLookupConfig{ dnsResult := DNSLookup(ctx, DNSLookupConfig{
@ -156,11 +167,10 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
tk.Queries = append(tk.Queries, dnsResult.TestKeys.Queries...) tk.Queries = append(tk.Queries, dnsResult.TestKeys.Queries...)
tk.DNSExperimentFailure = dnsResult.Failure tk.DNSExperimentFailure = dnsResult.Failure
epnts := NewEndpoints(URL, dnsResult.Addresses()) epnts := NewEndpoints(URL, dnsResult.Addresses())
sess.Logger().Infof("using control: %+v", testhelpers) sess.Logger().Infof("using control: %s", testhelper.Address)
// 3. perform the control measurement // 3. perform the control measurement
thBegin := time.Now() thBegin := time.Now()
var usedTH *model.OOAPIService tk.Control, err = Control(ctx, sess, testhelper.Address, ControlRequest{
tk.Control, usedTH, err = Control(ctx, sess, testhelpers, ControlRequest{
HTTPRequest: URL.String(), HTTPRequest: URL.String(),
HTTPRequestHeaders: map[string][]string{ HTTPRequestHeaders: map[string][]string{
"Accept": {model.HTTPHeaderAccept}, "Accept": {model.HTTPHeaderAccept},
@ -169,11 +179,6 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
}, },
TCPConnect: epnts.Endpoints(), TCPConnect: epnts.Endpoints(),
}) })
if usedTH != nil {
measurement.TestHelpers = map[string]interface{}{
"backend": usedTH,
}
}
tk.THRuntime = time.Since(thBegin) tk.THRuntime = time.Since(thBegin)
tk.ControlFailure = tracex.NewFailure(err) tk.ControlFailure = tracex.NewFailure(err)
// 4. analyze DNS results // 4. analyze DNS results

View File

@ -21,7 +21,7 @@ func TestNewExperimentMeasurer(t *testing.T) {
if measurer.ExperimentName() != "web_connectivity" { if measurer.ExperimentName() != "web_connectivity" {
t.Fatal("unexpected name") t.Fatal("unexpected name")
} }
if measurer.ExperimentVersion() != "0.4.2" { if measurer.ExperimentVersion() != "0.4.1" {
t.Fatal("unexpected version") t.Fatal("unexpected version")
} }
} }
@ -37,12 +37,7 @@ func TestSuccess(t *testing.T) {
sess := newsession(t, true) sess := newsession(t, true)
measurement := &model.Measurement{Input: "http://www.example.com"} measurement := &model.Measurement{Input: "http://www.example.com"}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -70,12 +65,7 @@ func TestMeasureWithCancelledContext(t *testing.T) {
sess := newsession(t, true) sess := newsession(t, true)
measurement := &model.Measurement{Input: "http://www.example.com"} measurement := &model.Measurement{Input: "http://www.example.com"}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ if err := measurer.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := measurer.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
tk := measurement.TestKeys.(*webconnectivity.TestKeys) tk := measurement.TestKeys.(*webconnectivity.TestKeys)
@ -109,12 +99,7 @@ func TestMeasureWithNoInput(t *testing.T) {
sess := newsession(t, true) sess := newsession(t, true)
measurement := &model.Measurement{Input: ""} measurement := &model.Measurement{Input: ""}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, webconnectivity.ErrNoInput) { if !errors.Is(err, webconnectivity.ErrNoInput) {
t.Fatal(err) t.Fatal(err)
} }
@ -142,12 +127,7 @@ func TestMeasureWithInputNotBeingAnURL(t *testing.T) {
sess := newsession(t, true) sess := newsession(t, true)
measurement := &model.Measurement{Input: "\t\t\t\t\t\t"} measurement := &model.Measurement{Input: "\t\t\t\t\t\t"}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, webconnectivity.ErrInputIsNotAnURL) { if !errors.Is(err, webconnectivity.ErrInputIsNotAnURL) {
t.Fatal(err) t.Fatal(err)
} }
@ -175,12 +155,7 @@ func TestMeasureWithUnsupportedInput(t *testing.T) {
sess := newsession(t, true) sess := newsession(t, true)
measurement := &model.Measurement{Input: "dnslookup://example.com"} measurement := &model.Measurement{Input: "dnslookup://example.com"}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, webconnectivity.ErrUnsupportedInput) { if !errors.Is(err, webconnectivity.ErrUnsupportedInput) {
t.Fatal(err) t.Fatal(err)
} }
@ -208,12 +183,7 @@ func TestMeasureWithNoAvailableTestHelpers(t *testing.T) {
sess := newsession(t, false) sess := newsession(t, false)
measurement := &model.Measurement{Input: "https://www.example.com"} measurement := &model.Measurement{Input: "https://www.example.com"}
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if !errors.Is(err, webconnectivity.ErrNoAvailableTestHelpers) { if !errors.Is(err, webconnectivity.ErrNoAvailableTestHelpers) {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -154,11 +154,10 @@ func (m Measurer) ExperimentVersion() string {
} }
// Run implements ExperimentMeasurer.Run // Run implements ExperimentMeasurer.Run
func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel() defer cancel()
urlgetter.RegisterExtensions(measurement) urlgetter.RegisterExtensions(measurement)

View File

@ -35,12 +35,7 @@ func TestSuccess(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -75,12 +70,7 @@ func TestFailureAllEndpoints(t *testing.T) {
sess := &mockable.Session{MockableLogger: log.Log} sess := &mockable.Session{MockableLogger: log.Log}
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ err := measurer.Run(ctx, sess, measurement, callbacks)
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
err := measurer.Run(ctx, args)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -608,12 +598,7 @@ func TestWeConfigureWebChecksCorrectly(t *testing.T) {
} }
measurement := new(model.Measurement) measurement := new(model.Measurement)
callbacks := model.NewPrinterCallbacks(log.Log) callbacks := model.NewPrinterCallbacks(log.Log)
args := &model.ExperimentArgs{ if err := measurer.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err := measurer.Run(ctx, args); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if called.Load() != 263 { if called.Load() != 263 {

View File

@ -475,7 +475,10 @@ func (am *antaniMeasurer) ExperimentVersion() string {
return "0.1.1" return "0.1.1"
} }
func (am *antaniMeasurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (am *antaniMeasurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
return nil return nil
} }

View File

@ -285,7 +285,7 @@ func (t *CleartextFlow) maybeFollowRedirects(ctx context.Context, resp *http.Res
WaitGroup: t.WaitGroup, WaitGroup: t.WaitGroup,
Referer: resp.Request.URL.String(), Referer: resp.Request.URL.String(),
Session: nil, // no need to issue another control request Session: nil, // no need to issue another control request
TestHelpers: nil, // ditto THAddr: "", // ditto
UDPAddress: t.UDPAddress, UDPAddress: t.UDPAddress,
} }
resolvers.Start(ctx) resolvers.Start(ctx)

View File

@ -8,11 +8,10 @@ import (
"time" "time"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity" "github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
"github.com/ooni/probe-cli/v3/internal/httpapi" "github.com/ooni/probe-cli/v3/internal/httpx"
"github.com/ooni/probe-cli/v3/internal/measurexlite" "github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
) )
// EndpointMeasurementsStarter is used by Control to start extra // EndpointMeasurementsStarter is used by Control to start extra
@ -52,8 +51,8 @@ type Control struct {
// Session is the MANDATORY session to use. // Session is the MANDATORY session to use.
Session model.ExperimentSession Session model.ExperimentSession
// TestHelpers is the MANDATORY list of test helpers. // THAddr is the MANDATORY TH's URL.
TestHelpers []model.OOAPIService THAddr string
// URL is the MANDATORY URL we are measuring. // URL is the MANDATORY URL we are measuring.
URL *url.URL URL *url.URL
@ -103,20 +102,26 @@ func (c *Control) Run(parentCtx context.Context) {
// create logger for this operation // create logger for this operation
ol := measurexlite.NewOperationLogger( ol := measurexlite.NewOperationLogger(
c.Logger, c.Logger,
"control for %s using %+v", "control for %s using %s",
creq.HTTPRequest, creq.HTTPRequest,
c.TestHelpers, c.THAddr,
) )
// create an httpapi sequence caller // create an API client
seqCaller := httpapi.NewSequenceCaller( clnt := (&httpx.APIClientTemplate{
httpapi.MustNewPOSTJSONWithJSONResponseDescriptor(c.Logger, "/", creq).WithBodyLogging(true), Accept: "",
httpapi.NewEndpointList(c.Session.DefaultHTTPClient(), c.Session.UserAgent(), c.TestHelpers...)..., Authorization: "",
) BaseURL: c.THAddr,
HTTPClient: c.Session.DefaultHTTPClient(),
Host: "", // use the one inside the URL
LogBody: true,
Logger: c.Logger,
UserAgent: c.Session.UserAgent(),
}).Build()
// issue the control request and wait for the response // issue the control request and wait for the response
var cresp webconnectivity.ControlResponse var cresp webconnectivity.ControlResponse
idx, err := seqCaller.CallWithJSONResponse(opCtx, &cresp) err := clnt.PostJSON(opCtx, "/", creq, &cresp)
if err != nil { if err != nil {
// make sure error is wrapped // make sure error is wrapped
err = netxlite.NewTopLevelGenericErrWrapper(err) err = netxlite.NewTopLevelGenericErrWrapper(err)
@ -129,10 +134,6 @@ func (c *Control) Run(parentCtx context.Context) {
c.TestKeys.SetControl(&cresp) c.TestKeys.SetControl(&cresp)
ol.Stop(nil) ol.Stop(nil)
// record the specific TH that worked
runtimex.Assert(idx >= 0 && idx < len(c.TestHelpers), "idx out of bounds")
c.TestKeys.setTestHelper(&c.TestHelpers[idx])
// if the TH returned us addresses we did not previously were // if the TH returned us addresses we did not previously were
// aware of, make sure we also measure them // aware of, make sure we also measure them
c.maybeStartExtraMeasurements(parentCtx, cresp.DNS.Addrs) c.maybeStartExtraMeasurements(parentCtx, cresp.DNS.Addrs)

View File

@ -67,9 +67,8 @@ type DNSResolvers struct {
// always follow the redirect chain caused by the provided URL. // always follow the redirect chain caused by the provided URL.
Session model.ExperimentSession Session model.ExperimentSession
// TestHelpers is the OPTIONAL list of test helpers. If the list is // THAddr is the OPTIONAL test helper address.
// empty, we are not going to try to contact any test helper. THAddr string
TestHelpers []model.OOAPIService
// UDPAddress is the OPTIONAL address of the UDP resolver to use. If this // UDPAddress is the OPTIONAL address of the UDP resolver to use. If this
// field is not set we use a default one (e.g., `8.8.8.8:53`). // field is not set we use a default one (e.g., `8.8.8.8:53`).
@ -499,15 +498,15 @@ func (t *DNSResolvers) startSecureFlows(
} }
} }
// maybeStartControlFlow starts the control flow iff .Session and .TestHelpers are set. // maybeStartControlFlow starts the control flow iff .Session and .THAddr are set.
func (t *DNSResolvers) maybeStartControlFlow( func (t *DNSResolvers) maybeStartControlFlow(
ctx context.Context, ctx context.Context,
ps *prioritySelector, ps *prioritySelector,
addresses []DNSEntry, addresses []DNSEntry,
) { ) {
// note: for subsequent requests we don't set .Session and .TestHelpers hence // note: for subsequent requests we don't set .Session and .THAddr hence
// we are not going to query the test helper more than once // we are not going to query the test helper more than once
if t.Session != nil && len(t.TestHelpers) > 0 { if t.Session != nil && t.THAddr != "" {
var addrs []string var addrs []string
for _, addr := range addresses { for _, addr := range addresses {
addrs = append(addrs, addr.Addr) addrs = append(addrs, addr.Addr)
@ -519,7 +518,7 @@ func (t *DNSResolvers) maybeStartControlFlow(
PrioSelector: ps, PrioSelector: ps,
TestKeys: t.TestKeys, TestKeys: t.TestKeys,
Session: t.Session, Session: t.Session,
TestHelpers: t.TestHelpers, THAddr: t.THAddr,
URL: t.URL, URL: t.URL,
WaitGroup: t.WaitGroup, WaitGroup: t.WaitGroup,
} }

View File

@ -36,17 +36,15 @@ func (m *Measurer) ExperimentName() string {
// ExperimentVersion implements model.ExperimentMeasurer. // ExperimentVersion implements model.ExperimentMeasurer.
func (m *Measurer) ExperimentVersion() string { func (m *Measurer) ExperimentVersion() string {
return "0.5.19" return "0.5.18"
} }
// Run implements model.ExperimentMeasurer. // Run implements model.ExperimentMeasurer.
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
// Reminder: when this function returns an error, the measurement result // Reminder: when this function returns an error, the measurement result
// WILL NOT be submitted to the OONI backend. You SHOULD only return an error // WILL NOT be submitted to the OONI backend. You SHOULD only return an error
// for fundamental errors (e.g., the input is invalid or missing). // for fundamental errors (e.g., the input is invalid or missing).
_ = args.Callbacks
measurement := args.Measurement
sess := args.Session
// make sure we have a cancellable context such that we can stop any // make sure we have a cancellable context such that we can stop any
// goroutine running in the background (e.g., priority.go's ones) // goroutine running in the background (e.g., priority.go's ones)
@ -91,7 +89,17 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
// obtain the test helper's address // obtain the test helper's address
testhelpers, _ := sess.GetTestHelpersByName("web-connectivity") testhelpers, _ := sess.GetTestHelpersByName("web-connectivity")
if len(testhelpers) < 1 { var thAddr string
for _, th := range testhelpers {
if th.Type == "https" {
thAddr = th.Address
measurement.TestHelpers = map[string]any{
"backend": &th,
}
break
}
}
if thAddr == "" {
sess.Logger().Warnf("continuing without a valid TH address") sess.Logger().Warnf("continuing without a valid TH address")
tk.SetControlFailure(webconnectivity.ErrNoAvailableTestHelpers) tk.SetControlFailure(webconnectivity.ErrNoAvailableTestHelpers)
} }
@ -112,7 +120,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
CookieJar: jar, CookieJar: jar,
Referer: "", Referer: "",
Session: sess, Session: sess,
TestHelpers: testhelpers, THAddr: thAddr,
UDPAddress: "", UDPAddress: "",
} }
resos.Start(ctx) resos.Start(ctx)
@ -129,16 +137,6 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
// perform any deferred computation on the test keys // perform any deferred computation on the test keys
tk.Finalize(sess.Logger()) tk.Finalize(sess.Logger())
// set the test helper we used
// TODO(bassosimone): it may be more informative to know about all the
// test helpers we _tried_ to use, however the data format does not have
// support for that as far as I can tell...
if th := tk.getTestHelper(); th != nil {
measurement.TestHelpers = map[string]interface{}{
"backend": th,
}
}
// return whether there was a fundamental failure, which would prevent // return whether there was a fundamental failure, which would prevent
// the measurement from being submitted to the OONI collector. // the measurement from being submitted to the OONI collector.
return tk.fundamentalFailure return tk.fundamentalFailure

View File

@ -337,7 +337,7 @@ func (t *SecureFlow) maybeFollowRedirects(ctx context.Context, resp *http.Respon
WaitGroup: t.WaitGroup, WaitGroup: t.WaitGroup,
Referer: resp.Request.URL.String(), Referer: resp.Request.URL.String(),
Session: nil, // no need to issue another control request Session: nil, // no need to issue another control request
TestHelpers: nil, // ditto THAddr: "", // ditto
UDPAddress: t.UDPAddress, UDPAddress: t.UDPAddress,
} }
resolvers.Start(ctx) resolvers.Start(ctx)

View File

@ -134,10 +134,6 @@ type TestKeys struct {
// mu provides mutual exclusion for accessing the test keys. // mu provides mutual exclusion for accessing the test keys.
mu *sync.Mutex mu *sync.Mutex
// testHelper is used to communicate the TH that worked to the main
// goroutine such that we can fill measurement.TestHelpers.
testHelper *model.OOAPIService
} }
// ConnPriorityLogEntry is an entry in the TestKeys.ConnPriorityLog slice. // ConnPriorityLogEntry is an entry in the TestKeys.ConnPriorityLog slice.
@ -306,21 +302,6 @@ func (tk *TestKeys) AppendConnPriorityLogEntry(entry *ConnPriorityLogEntry) {
tk.mu.Unlock() tk.mu.Unlock()
} }
// setTestHelper sets .testHelper in a thread safe way
func (tk *TestKeys) setTestHelper(th *model.OOAPIService) {
tk.mu.Lock()
tk.testHelper = th
tk.mu.Unlock()
}
// getTestHelper gets .testHelper in a thread safe way
func (tk *TestKeys) getTestHelper() (th *model.OOAPIService) {
tk.mu.Lock()
th = tk.testHelper
tk.mu.Unlock()
return
}
// NewTestKeys creates a new instance of TestKeys. // NewTestKeys creates a new instance of TestKeys.
func NewTestKeys() *TestKeys { func NewTestKeys() *TestKeys {
return &TestKeys{ return &TestKeys{
@ -367,7 +348,6 @@ func NewTestKeys() *TestKeys {
ControlRequest: nil, ControlRequest: nil,
fundamentalFailure: nil, fundamentalFailure: nil,
mu: &sync.Mutex{}, mu: &sync.Mutex{},
testHelper: nil,
} }
} }

View File

@ -1,181 +0,0 @@
package httpapi
//
// Calling HTTP APIs.
//
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
// joinURLPath appends |resourcePath| to |urlPath|.
func joinURLPath(urlPath, resourcePath string) string {
if resourcePath == "" {
if urlPath == "" {
return "/"
}
return urlPath
}
if !strings.HasSuffix(urlPath, "/") {
urlPath += "/"
}
resourcePath = strings.TrimPrefix(resourcePath, "/")
return urlPath + resourcePath
}
// newRequest creates a new http.Request from the given |ctx|, |endpoint|, and |desc|.
func newRequest(ctx context.Context, endpoint *Endpoint, desc *Descriptor) (*http.Request, error) {
URL, err := url.Parse(endpoint.BaseURL)
if err != nil {
return nil, err
}
// BaseURL and resource URL are joined if they have a path
URL.Path = joinURLPath(URL.Path, desc.URLPath)
if len(desc.URLQuery) > 0 {
URL.RawQuery = desc.URLQuery.Encode()
} else {
URL.RawQuery = "" // as documented we only honour desc.URLQuery
}
var reqBody io.Reader
if len(desc.RequestBody) > 0 {
reqBody = bytes.NewReader(desc.RequestBody)
desc.Logger.Debugf("httpapi: request body length: %d", len(desc.RequestBody))
if desc.LogBody {
desc.Logger.Debugf("httpapi: request body: %s", string(desc.RequestBody))
}
}
request, err := http.NewRequestWithContext(ctx, desc.Method, URL.String(), reqBody)
if err != nil {
return nil, err
}
request.Host = endpoint.Host // allow cloudfronting
if desc.Authorization != "" {
request.Header.Set("Authorization", desc.Authorization)
}
if desc.ContentType != "" {
request.Header.Set("Content-Type", desc.ContentType)
}
if desc.Accept != "" {
request.Header.Set("Accept", desc.Accept)
}
if endpoint.UserAgent != "" {
request.Header.Set("User-Agent", endpoint.UserAgent)
}
return request, nil
}
// ErrHTTPRequestFailed indicates that the server returned >= 400.
type ErrHTTPRequestFailed struct {
// StatusCode is the status code that failed.
StatusCode int
}
// Error implements error.
func (err *ErrHTTPRequestFailed) Error() string {
return fmt.Sprintf("httpapi: http request failed: %d", err.StatusCode)
}
// errMaybeCensorship indicates that there was an error at the networking layer
// including, e.g., DNS, TCP connect, TLS. When we see this kind of error, we
// will consider retrying with another endpoint under the assumption that it
// may be that the current endpoint is censored.
type errMaybeCensorship struct {
// Err is the underlying error
Err error
}
// Error implements error
func (err *errMaybeCensorship) Error() string {
return err.Err.Error()
}
// Unwrap allows to get the underlying error
func (err *errMaybeCensorship) Unwrap() error {
return err.Err
}
// docall calls the API represented by the given request |req| on the given |endpoint|
// and returns the response and its body or an error.
func docall(endpoint *Endpoint, desc *Descriptor, request *http.Request) (*http.Response, []byte, error) {
// Implementation note: remember to mark errors for which you want
// to retry with another endpoint using errMaybeCensorship.
response, err := endpoint.HTTPClient.Do(request)
if err != nil {
return nil, nil, &errMaybeCensorship{err}
}
defer response.Body.Close()
// Implementation note: always read and log the response body since
// it's quite useful to see the response JSON on API error.
r := io.LimitReader(response.Body, DefaultMaxBodySize)
data, err := netxlite.ReadAllContext(request.Context(), r)
if err != nil {
return response, nil, &errMaybeCensorship{err}
}
desc.Logger.Debugf("httpapi: response body length: %d bytes", len(data))
if desc.LogBody {
desc.Logger.Debugf("httpapi: response body: %s", string(data))
}
if response.StatusCode >= 400 {
return response, nil, &ErrHTTPRequestFailed{response.StatusCode}
}
return response, data, nil
}
// call is like Call but also returns the response.
func call(ctx context.Context, desc *Descriptor, endpoint *Endpoint) (*http.Response, []byte, error) {
timeout := desc.Timeout
if timeout <= 0 {
timeout = DefaultCallTimeout // as documented
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
request, err := newRequest(ctx, endpoint, desc)
if err != nil {
return nil, nil, err
}
return docall(endpoint, desc, request)
}
// Call invokes the API described by |desc| on the given HTTP |endpoint| and
// returns the response body (as a slice of bytes) or an error.
//
// Note: this function returns ErrHTTPRequestFailed if the HTTP status code is
// greater or equal than 400. You could use errors.As to obtain a copy of the
// error that was returned and see for yourself the actual status code.
func Call(ctx context.Context, desc *Descriptor, endpoint *Endpoint) ([]byte, error) {
_, rawResponseBody, err := call(ctx, desc, endpoint)
return rawResponseBody, err
}
// goodContentTypeForJSON tracks known-good content-types for JSON. If the content-type
// is not in this map, |CallWithJSONResponse| emits a warning message.
var goodContentTypeForJSON = map[string]bool{
applicationJSON: true,
}
// CallWithJSONResponse is like Call but also assumes that the response is a
// JSON body and attempts to parse it into the |response| field.
//
// Note: this function returns ErrHTTPRequestFailed if the HTTP status code is
// greater or equal than 400. You could use errors.As to obtain a copy of the
// error that was returned and see for yourself the actual status code.
func CallWithJSONResponse(ctx context.Context, desc *Descriptor, endpoint *Endpoint, response any) error {
httpResp, rawRespBody, err := call(ctx, desc, endpoint)
if err != nil {
return err
}
if ctype := httpResp.Header.Get("Content-Type"); !goodContentTypeForJSON[ctype] {
desc.Logger.Warnf("httpapi: unexpected content-type: %s", ctype)
// fallthrough
}
return json.Unmarshal(rawRespBody, response)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +0,0 @@
package httpapi
//
// HTTP API descriptor (e.g., GET /api/v1/test-list/urls)
//
import (
"encoding/json"
"net/http"
"net/url"
"time"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
// Descriptor contains the parameters for calling a given HTTP
// API (e.g., GET /api/v1/test-list/urls).
//
// The zero value of this struct is invalid. Please, fill all the
// fields marked as MANDATORY for correct initialization.
type Descriptor struct {
// Accept contains the OPTIONAL accept header.
Accept string
// Authorization is the OPTIONAL authorization.
Authorization string
// ContentType is the OPTIONAL content-type header.
ContentType string
// LogBody OPTIONALLY enables logging bodies.
LogBody bool
// Logger is the MANDATORY logger to use.
//
// For example, model.DiscardLogger.
Logger model.Logger
// MaxBodySize is the OPTIONAL maximum response body size. If
// not set, we use the |DefaultMaxBodySize| constant.
MaxBodySize int64
// Method is the MANDATORY request method.
Method string
// RequestBody is the OPTIONAL request body.
RequestBody []byte
// Timeout is the OPTIONAL timeout for this call. If no timeout
// is specified we will use the |DefaultCallTimeout| const.
Timeout time.Duration
// URLPath is the MANDATORY URL path.
URLPath string
// URLQuery is the OPTIONAL query.
URLQuery url.Values
}
// WithBodyLogging returns a SHALLOW COPY of |Descriptor| with LogBody set to |value|. You SHOULD
// only use this method when initializing the descriptor you want to use.
func (desc *Descriptor) WithBodyLogging(value bool) *Descriptor {
out := &Descriptor{}
*out = *desc
out.LogBody = value
return out
}
// DefaultMaxBodySize is the default value for the maximum
// body size you can fetch using the httpapi package.
const DefaultMaxBodySize = 1 << 22
// DefaultCallTimeout is the default timeout for an httpapi call.
const DefaultCallTimeout = 60 * time.Second
// NewGETJSONDescriptor is a convenience factory for creating a new descriptor
// that uses the GET method and expects a JSON response.
func NewGETJSONDescriptor(logger model.Logger, urlPath string) *Descriptor {
return NewGETJSONWithQueryDescriptor(logger, urlPath, url.Values{})
}
// applicationJSON is the content-type for JSON
const applicationJSON = "application/json"
// NewGETJSONWithQueryDescriptor is like NewGETJSONDescriptor but it also
// allows you to provide |query| arguments. Leaving |query| nil or empty
// is equivalent to calling NewGETJSONDescriptor directly.
func NewGETJSONWithQueryDescriptor(logger model.Logger, urlPath string, query url.Values) *Descriptor {
return &Descriptor{
Accept: applicationJSON,
Authorization: "",
ContentType: "",
LogBody: false,
Logger: logger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodGet,
RequestBody: nil,
Timeout: DefaultCallTimeout,
URLPath: urlPath,
URLQuery: query,
}
}
// NewPOSTJSONWithJSONResponseDescriptor creates a descriptor that POSTs a JSON document
// and expects to receive back a JSON document from the API.
//
// This function ONLY fails if we cannot serialize the |request| to JSON. So, if you know
// that |request| is JSON-serializable, you can safely call MustNewPostJSONWithJSONResponseDescriptor instead.
func NewPOSTJSONWithJSONResponseDescriptor(logger model.Logger, urlPath string, request any) (*Descriptor, error) {
rawRequest, err := json.Marshal(request)
if err != nil {
return nil, err
}
desc := &Descriptor{
Accept: applicationJSON,
Authorization: "",
ContentType: applicationJSON,
LogBody: false,
Logger: logger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodPost,
RequestBody: rawRequest,
Timeout: DefaultCallTimeout,
URLPath: urlPath,
URLQuery: nil,
}
return desc, nil
}
// MustNewPOSTJSONWithJSONResponseDescriptor is like NewPOSTJSONWithJSONResponseDescriptor except that
// it panics in case it's not possible to JSON serialize the |request|.
func MustNewPOSTJSONWithJSONResponseDescriptor(logger model.Logger, urlPath string, request any) *Descriptor {
desc, err := NewPOSTJSONWithJSONResponseDescriptor(logger, urlPath, request)
runtimex.PanicOnError(err, "NewPOSTJSONWithJSONResponseDescriptor failed")
return desc
}
// NewGETResourceDescriptor creates a generic descriptor for GETting a
// resource of unspecified type using the given |urlPath|.
func NewGETResourceDescriptor(logger model.Logger, urlPath string) *Descriptor {
return &Descriptor{
Accept: "",
Authorization: "",
ContentType: "",
LogBody: false,
Logger: logger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodGet,
RequestBody: nil,
Timeout: DefaultCallTimeout,
URLPath: urlPath,
URLQuery: url.Values{},
}
}

View File

@ -1,248 +0,0 @@
package httpapi
import (
"log"
"net/http"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestDescriptor_WithBodyLogging(t *testing.T) {
type fields struct {
Accept string
Authorization string
ContentType string
LogBody bool
Logger model.Logger
MaxBodySize int64
Method string
RequestBody []byte
Timeout time.Duration
URLPath string
URLQuery url.Values
}
tests := []struct {
name string
fields fields
want *Descriptor
}{{
name: "with empty fields",
fields: fields{}, // LogBody defaults to false
want: &Descriptor{
LogBody: true,
},
}, {
name: "with nonempty fields",
fields: fields{
Accept: "xx",
Authorization: "y",
ContentType: "zzz",
LogBody: false, // obviously must be false
Logger: model.DiscardLogger,
MaxBodySize: 123,
Method: "POST",
RequestBody: []byte("123"),
Timeout: 15555,
URLPath: "/",
URLQuery: map[string][]string{
"a": {"b"},
},
},
want: &Descriptor{
Accept: "xx",
Authorization: "y",
ContentType: "zzz",
LogBody: true,
Logger: model.DiscardLogger,
MaxBodySize: 123,
Method: "POST",
RequestBody: []byte("123"),
Timeout: 15555,
URLPath: "/",
URLQuery: map[string][]string{
"a": {"b"},
},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
desc := &Descriptor{
Accept: tt.fields.Accept,
Authorization: tt.fields.Authorization,
ContentType: tt.fields.ContentType,
LogBody: tt.fields.LogBody,
Logger: tt.fields.Logger,
MaxBodySize: tt.fields.MaxBodySize,
Method: tt.fields.Method,
RequestBody: tt.fields.RequestBody,
Timeout: tt.fields.Timeout,
URLPath: tt.fields.URLPath,
URLQuery: tt.fields.URLQuery,
}
got := desc.WithBodyLogging(true)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatal(diff)
}
})
}
}
func TestNewGetJSONDescriptor(t *testing.T) {
expected := &Descriptor{
Accept: "application/json",
Authorization: "",
ContentType: "",
LogBody: false,
Logger: model.DiscardLogger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodGet,
RequestBody: nil,
Timeout: DefaultCallTimeout,
URLPath: "/robots.txt",
URLQuery: url.Values{},
}
got := NewGETJSONDescriptor(model.DiscardLogger, "/robots.txt")
if diff := cmp.Diff(expected, got); diff != "" {
t.Fatal(diff)
}
}
func TestNewGetJSONWithQueryDescriptor(t *testing.T) {
query := url.Values{
"a": {"b"},
"c": {"d"},
}
expected := &Descriptor{
Accept: "application/json",
Authorization: "",
ContentType: "",
LogBody: false,
Logger: model.DiscardLogger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodGet,
RequestBody: nil,
Timeout: DefaultCallTimeout,
URLPath: "/robots.txt",
URLQuery: query,
}
got := NewGETJSONWithQueryDescriptor(model.DiscardLogger, "/robots.txt", query)
if diff := cmp.Diff(expected, got); diff != "" {
t.Fatal(diff)
}
}
func TestNewPOSTJSONWithJSONResponseDescriptor(t *testing.T) {
type request struct {
Name string
Age int64
}
t.Run("with failure", func(t *testing.T) {
request := make(chan int64)
got, err := NewPOSTJSONWithJSONResponseDescriptor(model.DiscardLogger, "/robots.txt", request)
if err == nil || err.Error() != "json: unsupported type: chan int64" {
log.Fatal("unexpected err", err)
}
if got != nil {
log.Fatal("expected to get a nil Descriptor")
}
})
t.Run("with success", func(t *testing.T) {
request := request{
Name: "sbs",
Age: 99,
}
expected := &Descriptor{
Accept: "application/json",
Authorization: "",
ContentType: "application/json",
LogBody: false,
Logger: model.DiscardLogger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodPost,
RequestBody: []byte(`{"Name":"sbs","Age":99}`),
Timeout: DefaultCallTimeout,
URLPath: "/robots.txt",
URLQuery: nil,
}
got, err := NewPOSTJSONWithJSONResponseDescriptor(model.DiscardLogger, "/robots.txt", request)
if err != nil {
log.Fatal(err)
}
if diff := cmp.Diff(expected, got); diff != "" {
t.Fatal(diff)
}
})
}
func TestMustNewPOSTJSONWithJSONResponseDescriptor(t *testing.T) {
type request struct {
Name string
Age int64
}
t.Run("with failure", func(t *testing.T) {
var panicked bool
func() {
defer func() {
if r := recover(); r != nil {
panicked = true
}
}()
request := make(chan int64)
_ = MustNewPOSTJSONWithJSONResponseDescriptor(model.DiscardLogger, "/robots.txt", request)
}()
if !panicked {
t.Fatal("did not panic")
}
})
t.Run("with success", func(t *testing.T) {
request := request{
Name: "sbs",
Age: 99,
}
expected := &Descriptor{
Accept: "application/json",
Authorization: "",
ContentType: "application/json",
LogBody: false,
Logger: model.DiscardLogger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodPost,
RequestBody: []byte(`{"Name":"sbs","Age":99}`),
Timeout: DefaultCallTimeout,
URLPath: "/robots.txt",
URLQuery: nil,
}
got := MustNewPOSTJSONWithJSONResponseDescriptor(model.DiscardLogger, "/robots.txt", request)
if diff := cmp.Diff(expected, got); diff != "" {
t.Fatal(diff)
}
})
}
func TestNewGetResourceDescriptor(t *testing.T) {
expected := &Descriptor{
Accept: "",
Authorization: "",
ContentType: "",
LogBody: false,
Logger: model.DiscardLogger,
MaxBodySize: DefaultMaxBodySize,
Method: http.MethodGet,
RequestBody: nil,
Timeout: DefaultCallTimeout,
URLPath: "/robots.txt",
URLQuery: url.Values{},
}
got := NewGETResourceDescriptor(model.DiscardLogger, "/robots.txt")
if diff := cmp.Diff(expected, got); diff != "" {
t.Fatal(diff)
}
}

View File

@ -1,15 +0,0 @@
// Package httpapi contains code for calling HTTP APIs.
//
// We model HTTP APIs as follows:
//
// 1. |Endpoint| is an API endpoint (e.g., https://api.ooni.io);
//
// 2. |Descriptor| describes the specific API you want to use (e.g.,
// GET /api/v1/test-list/urls with JSON response body).
//
// Generally, you use |Call| to call the API identified by a |Descriptor|
// on the specified |Endpoint|. However, there are cases where you
// need more complex calling patterns. For example, with |SequenceCaller|
// you can invoke the same API |Descriptor| with multiple equivalent
// API |Endpoint|s until one of them succeeds or all fail.
package httpapi

View File

@ -1,76 +0,0 @@
package httpapi
//
// HTTP API Endpoint (e.g., https://api.ooni.io)
//
import "github.com/ooni/probe-cli/v3/internal/model"
// Endpoint models an HTTP endpoint on which you can call
// several HTTP APIs (e.g., https://api.ooni.io) using a
// given HTTP client potentially using a circumvention tunnel
// mechanism such as psiphon or torsf.
//
// The zero value of this struct is invalid. Please, fill all the
// fields marked as MANDATORY for correct initialization.
type Endpoint struct {
// BaseURL is the MANDATORY endpoint base URL. We will honour the
// path of this URL and prepend it to the actual path specified inside
// a |Descriptor.URLPath|. However, we will always discard any query
// that may have been set inside the BaseURL. The only query string
// will be composed from the |Descriptor.URLQuery| values.
//
// For example, https://api.ooni.io.
BaseURL string
// HTTPClient is the MANDATORY HTTP client to use.
//
// For example, http.DefaultClient. You can introduce circumvention
// here by using an HTTPClient bound to a specific tunnel.
HTTPClient model.HTTPClient
// Host is the OPTIONAL host header to use.
//
// If this field is empty we use the BaseURL's hostname. A specific
// host header may be needed when using cloudfronting.
Host string
// User-Agent is the OPTIONAL user-agent to use. If empty,
// we'll use the stdlib's default user-agent string.
UserAgent string
}
// NewEndpointList constructs a list of API endpoints from |services|
// returned by the OONI backend (or known in advance).
//
// Arguments:
//
// - httpClient is the HTTP client to use for accessing the endpoints;
//
// - userAgent is the user agent you would like to use;
//
// - service is the list of services gathered from the backend.
func NewEndpointList(httpClient model.HTTPClient,
userAgent string, services ...model.OOAPIService) (out []*Endpoint) {
for _, svc := range services {
switch svc.Type {
case "https":
out = append(out, &Endpoint{
BaseURL: svc.Address,
HTTPClient: httpClient,
Host: "",
UserAgent: userAgent,
})
case "cloudfront":
out = append(out, &Endpoint{
BaseURL: svc.Address,
HTTPClient: httpClient,
Host: svc.Front,
UserAgent: userAgent,
})
default:
// nothing!
}
}
return
}

View File

@ -1,69 +0,0 @@
package httpapi
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
)
func TestNewEndpointList(t *testing.T) {
type args struct {
httpClient model.HTTPClient
userAgent string
services []model.OOAPIService
}
defaultHTTPClient := &mocks.HTTPClient{}
tests := []struct {
name string
args args
wantOut []*Endpoint
}{{
name: "with no services",
args: args{
httpClient: defaultHTTPClient,
userAgent: model.HTTPHeaderUserAgent,
services: nil,
},
wantOut: nil,
}, {
name: "common cases",
args: args{
httpClient: defaultHTTPClient,
userAgent: model.HTTPHeaderUserAgent,
services: []model.OOAPIService{{
Address: "https://www.example.com/",
Type: "https",
Front: "",
}, {
Address: "https://www.example.org/",
Type: "cloudfront",
Front: "example.org.it",
}, {
Address: "https://nonexistent.onion/",
Type: "onion",
Front: "",
}},
},
wantOut: []*Endpoint{{
BaseURL: "https://www.example.com/",
HTTPClient: defaultHTTPClient,
Host: "",
UserAgent: model.HTTPHeaderUserAgent,
}, {
BaseURL: "https://www.example.org/",
HTTPClient: defaultHTTPClient,
Host: "example.org.it",
UserAgent: model.HTTPHeaderUserAgent,
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOut := NewEndpointList(tt.args.httpClient, tt.args.userAgent, tt.args.services...)
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
t.Fatal(diff)
}
})
}
}

View File

@ -1,92 +0,0 @@
package httpapi
//
// Sequentially call available API endpoints until one succeed
// or all of them fail. A future implementation of this code may
// (probably should?) take into account knowledge of what is
// working and what is not working to optimize the order with
// which to try different alternatives.
//
import (
"context"
"errors"
"github.com/ooni/probe-cli/v3/internal/multierror"
)
// SequenceCaller calls the API specified by |Descriptor| once for each of
// the available |Endpoints| until one of them succeeds.
//
// CAVEAT: this code will ONLY retry API calls with subsequent endpoints when
// the error originates in the HTTP round trip or while reading the body.
type SequenceCaller struct {
// Descriptor is the API |Descriptor|.
Descriptor *Descriptor
// Endpoints is the list of |Endpoint| to use.
Endpoints []*Endpoint
}
// NewSequenceCaller is a factory for creating a |SequenceCaller|.
func NewSequenceCaller(desc *Descriptor, endpoints ...*Endpoint) *SequenceCaller {
return &SequenceCaller{
Descriptor: desc,
Endpoints: endpoints,
}
}
// ErrAllEndpointsFailed indicates that all endpoints failed.
var ErrAllEndpointsFailed = errors.New("httpapi: all endpoints failed")
// shouldRetry returns true when we should try with another endpoint given the
// value of |err| which could (obviously) be nil in case of success.
func (sc *SequenceCaller) shouldRetry(err error) bool {
var kind *errMaybeCensorship
belongs := errors.As(err, &kind)
return belongs
}
// Call calls |Call| for each |Endpoint| and |Descriptor| until one endpoint succeeds. The
// return value is the response body and the selected endpoint index or the error.
//
// CAVEAT: this code will ONLY retry API calls with subsequent endpoints when
// the error originates in the HTTP round trip or while reading the body.
func (sc *SequenceCaller) Call(ctx context.Context) ([]byte, int, error) {
var selected int
merr := multierror.New(ErrAllEndpointsFailed)
for _, epnt := range sc.Endpoints {
respBody, err := Call(ctx, sc.Descriptor, epnt)
if sc.shouldRetry(err) {
merr.Add(err)
selected++
continue
}
// Note: some errors will lead us to return
// early as documented for this method
return respBody, selected, err
}
return nil, -1, merr
}
// CallWithJSONResponse is like |SequenceCaller.Call| except that it invokes the
// underlying |CallWithJSONResponse| rather than invoking |Call|.
//
// CAVEAT: this code will ONLY retry API calls with subsequent endpoints when
// the error originates in the HTTP round trip or while reading the body.
func (sc *SequenceCaller) CallWithJSONResponse(ctx context.Context, response any) (int, error) {
var selected int
merr := multierror.New(ErrAllEndpointsFailed)
for _, epnt := range sc.Endpoints {
err := CallWithJSONResponse(ctx, sc.Descriptor, epnt, response)
if sc.shouldRetry(err) {
merr.Add(err)
selected++
continue
}
// Note: some errors will lead us to return
// early as documented for this method
return selected, err
}
return -1, merr
}

View File

@ -1,358 +0,0 @@
package httpapi
import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
)
func TestSequenceCaller(t *testing.T) {
t.Run("Call", func(t *testing.T) {
t.Run("first success", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("deadbeef")),
}
return resp, nil
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF
},
},
},
)
data, idx, err := sc.Call(context.Background())
if err != nil {
t.Fatal(err)
}
if idx != 0 {
t.Fatal("invalid idx")
}
if diff := cmp.Diff([]byte("deadbeef"), data); diff != "" {
t.Fatal(diff)
}
})
t.Run("first HTTP failure and we immediately stop", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 403, // should cause us to return early
Body: io.NopCloser(strings.NewReader("deadbeef")),
}
return resp, nil
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF
},
},
},
)
data, idx, err := sc.Call(context.Background())
var failure *ErrHTTPRequestFailed
if !errors.As(err, &failure) || failure.StatusCode != 403 {
t.Fatal("unexpected err", err)
}
if idx != 0 {
t.Fatal("invalid idx")
}
if len(data) > 0 {
t.Fatal("expected to see no response body")
}
})
t.Run("first network failure, second success", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to cycle to the second entry
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("abad1dea")),
}
return resp, nil
},
},
},
)
data, idx, err := sc.Call(context.Background())
if err != nil {
t.Fatal(err)
}
if idx != 1 {
t.Fatal("invalid idx")
}
if diff := cmp.Diff([]byte("abad1dea"), data); diff != "" {
t.Fatal(diff)
}
})
t.Run("all network failure", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to cycle to the next entry
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to cycle to the next entry
},
},
},
)
data, idx, err := sc.Call(context.Background())
if !errors.Is(err, ErrAllEndpointsFailed) {
t.Fatal("unexpected err", err)
}
if idx != -1 {
t.Fatal("invalid idx")
}
if len(data) > 0 {
t.Fatal("expected zero-length data")
}
})
})
t.Run("CallWithJSONResponse", func(t *testing.T) {
type response struct {
Name string
Age int64
}
t.Run("first success", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"Name":"sbs","Age":99}`)),
}
return resp, nil
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{}`)), // different
}
return resp, nil
},
},
},
)
expect := response{
Name: "sbs",
Age: 99,
}
var got response
idx, err := sc.CallWithJSONResponse(context.Background(), &got)
if err != nil {
t.Fatal(err)
}
if idx != 0 {
t.Fatal("invalid idx")
}
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
t.Run("first HTTP failure and we immediately stop", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 403, // should be enough to cause us fail immediately
Body: io.NopCloser(strings.NewReader(`{"Age": 155, "Name": "sbs"}`)),
}
return resp, nil
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF
},
},
},
)
// even though there is a JSON body we don't care about reading it
// and so we expect to see in output the zero-value struct
expect := response{
Name: "",
Age: 0,
}
var got response
idx, err := sc.CallWithJSONResponse(context.Background(), &got)
var failure *ErrHTTPRequestFailed
if !errors.As(err, &failure) || failure.StatusCode != 403 {
t.Fatal("unexpected err", err)
}
if idx != 0 {
t.Fatal("invalid idx")
}
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
t.Run("first network failure, second success", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to try the next entry
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"Age":155}`)),
}
return resp, nil
},
},
},
)
expect := response{
Name: "",
Age: 155,
}
var got response
idx, err := sc.CallWithJSONResponse(context.Background(), &got)
if err != nil {
t.Fatal(err)
}
if idx != 1 {
t.Fatal("invalid idx")
}
if diff := cmp.Diff(expect, got); diff != "" {
t.Fatal(diff)
}
})
t.Run("all network failure", func(t *testing.T) {
sc := NewSequenceCaller(
&Descriptor{
Logger: model.DiscardLogger,
Method: http.MethodGet,
URLPath: "/",
},
&Endpoint{
BaseURL: "https://a.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to try the next entry
},
},
},
&Endpoint{
BaseURL: "https://b.example.com/",
HTTPClient: &mocks.HTTPClient{
MockDo: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF // should cause us to try the next entry
},
},
},
)
var got response
idx, err := sc.CallWithJSONResponse(context.Background(), &got)
if !errors.Is(err, ErrAllEndpointsFailed) {
t.Fatal("unexpected err", err)
}
if idx != -1 {
t.Fatal("invalid idx")
}
})
})
}

View File

@ -1,8 +1,4 @@
// Package httpx contains http extensions. // Package httpx contains http extensions.
//
// Deprecated: new code should use httpapi instead. While this package and httpapi
// are basically using the same implementation, the API exposed by httpapi allows
// us to try the same request with multiple HTTP endpoints.
package httpx package httpx
import ( import (

View File

@ -117,19 +117,6 @@ func (d PrinterCallbacks) OnProgress(percentage float64, message string) {
d.Logger.Infof("[%5.1f%%] %s", percentage*100, message) d.Logger.Infof("[%5.1f%%] %s", percentage*100, message)
} }
// ExperimentArgs contains the arguments passed to an experiment.
type ExperimentArgs struct {
// Callbacks contains MANDATORY experiment callbacks.
Callbacks ExperimentCallbacks
// Measurement is the MANDATORY measurement in which the experiment
// must write the results of the measurement.
Measurement *Measurement
// Session is the MANDATORY session the experiment can use.
Session ExperimentSession
}
// ExperimentMeasurer is the interface that allows to run a // ExperimentMeasurer is the interface that allows to run a
// measurement for a specific experiment. // measurement for a specific experiment.
type ExperimentMeasurer interface { type ExperimentMeasurer interface {
@ -146,7 +133,10 @@ type ExperimentMeasurer interface {
// set the relevant OONI error inside of the measurement and // set the relevant OONI error inside of the measurement and
// return nil. This is important because the caller WILL NOT submit // return nil. This is important because the caller WILL NOT submit
// the measurement if this method returns an error. // the measurement if this method returns an error.
Run(ctx context.Context, args *ExperimentArgs) error Run(
ctx context.Context, sess ExperimentSession,
measurement *Measurement, callbacks ExperimentCallbacks,
) error
// GetSummaryKeys returns summary keys expected by ooni/probe-cli. // GetSummaryKeys returns summary keys expected by ooni/probe-cli.
GetSummaryKeys(*Measurement) (interface{}, error) GetSummaryKeys(*Measurement) (interface{}, error)

22
internal/registry/smtp.go Normal file
View File

@ -0,0 +1,22 @@
package registry
//
// Registers the `dnsping' experiment.
//
import (
"github.com/ooni/probe-cli/v3/internal/engine/experiment/smtp"
"github.com/ooni/probe-cli/v3/internal/model"
)
func init() {
AllExperiments["smtp"] = &Factory{
build: func(config interface{}) model.ExperimentMeasurer {
return smtp.NewExperimentMeasurer(
*config.(*smtp.Config),
)
},
config: &smtp.Config{},
inputPolicy: model.InputOrStaticDefault,
}
}

View File

@ -211,12 +211,7 @@ need any fancy context and we pass a `context.Background` to `Run`.
```Go ```Go
ctx := context.Background() ctx := context.Background()
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
log.WithError(err).Fatal("torsf experiment failed") log.WithError(err).Fatal("torsf experiment failed")
} }
``` ```

View File

@ -212,12 +212,7 @@ func main() {
// //
// ```Go // ```Go
ctx := context.Background() ctx := context.Background()
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
log.WithError(err).Fatal("torsf experiment failed") log.WithError(err).Fatal("torsf experiment failed")
} }
// ``` // ```

View File

@ -117,10 +117,10 @@ chapters, finally, we will modify this function until it is a
minimal implementation of the `torsf` experiment. minimal implementation of the `torsf` experiment.
```Go ```Go
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context, sess model.ExperimentSession,
_ = args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
``` ```
As you can see, this is just a stub implementation that sleeps As you can see, this is just a stub implementation that sleeps
for one second and prints a logging message. for one second and prints a logging message.

View File

@ -54,12 +54,7 @@ func main() {
MockableLogger: log.Log, MockableLogger: log.Log,
MockableTempDir: tempdir, MockableTempDir: tempdir,
} }
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
log.WithError(err).Fatal("torsf experiment failed") log.WithError(err).Fatal("torsf experiment failed")
} }
data, err := json.Marshal(measurement) data, err := json.Marshal(measurement)

View File

@ -93,10 +93,10 @@ func (m *Measurer) ExperimentVersion() string {
// minimal implementation of the `torsf` experiment. // minimal implementation of the `torsf` experiment.
// //
// ```Go // ```Go
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
_ = args.Callbacks ctx context.Context, sess model.ExperimentSession,
_ = args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
// ``` // ```
// As you can see, this is just a stub implementation that sleeps // As you can see, this is just a stub implementation that sleeps
// for one second and prints a logging message. // for one second and prints a logging message.

View File

@ -32,10 +32,10 @@ print periodic updates via the `callbacks`. We will defer the
real work to a private function called `run`. real work to a private function called `run`.
```Go ```Go
func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { func (m *Measurer) Run(
callbacks := args.Callbacks ctx context.Context, sess model.ExperimentSession,
measurement := args.Measurement measurement *model.Measurement, callbacks model.ExperimentCallbacks,
sess := args.Session ) error {
``` ```
Let's create an instance of `TestKeys` and let's modify Let's create an instance of `TestKeys` and let's modify

View File

@ -28,12 +28,7 @@ func main() {
MockableLogger: log.Log, MockableLogger: log.Log,
MockableTempDir: tempdir, MockableTempDir: tempdir,
} }
args := &model.ExperimentArgs{ if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
Callbacks: callbacks,
Measurement: measurement,
Session: sess,
}
if err = m.Run(ctx, args); err != nil {
log.WithError(err).Fatal("torsf experiment failed") log.WithError(err).Fatal("torsf experiment failed")
} }
data, err := json.Marshal(measurement) data, err := json.Marshal(measurement)

Some files were not shown because too many files have changed in this diff Show More