diff --git a/internal/engine/experiment/tor/tor.go b/internal/engine/experiment/tor/tor.go index 666ca41..d563adf 100644 --- a/internal/engine/experiment/tor/tor.go +++ b/internal/engine/experiment/tor/tor.go @@ -13,10 +13,8 @@ import ( "time" "github.com/ooni/probe-cli/v3/internal/atomicx" - "github.com/ooni/probe-cli/v3/internal/engine/httpheader" - "github.com/ooni/probe-cli/v3/internal/engine/legacy/netxlogger" - "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel" - "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates" + "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" + "github.com/ooni/probe-cli/v3/internal/measurex" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/runtimex" @@ -31,7 +29,7 @@ const ( testName = "tor" // testVersion is the version of this experiment - testVersion = "0.3.0" + testVersion = "0.4.0" ) // Config contains the experiment config. @@ -44,18 +42,18 @@ type Summary struct { // TargetResults contains the results of measuring a target. type TargetResults struct { - Agent string `json:"agent"` - Failure *string `json:"failure"` - NetworkEvents oonidatamodel.NetworkEventsList `json:"network_events"` - Queries oonidatamodel.DNSQueriesList `json:"queries"` - Requests oonidatamodel.RequestList `json:"requests"` - Summary map[string]Summary `json:"summary"` - TargetAddress string `json:"target_address"` - TargetName string `json:"target_name,omitempty"` - TargetProtocol string `json:"target_protocol"` - TargetSource string `json:"target_source,omitempty"` - TCPConnect oonidatamodel.TCPConnectList `json:"tcp_connect"` - TLSHandshakes oonidatamodel.TLSHandshakesList `json:"tls_handshakes"` + Agent string `json:"agent"` + Failure *string `json:"failure"` + NetworkEvents []*measurex.ArchivalNetworkEvent `json:"network_events"` + Queries []*measurex.ArchivalDNSLookupEvent `json:"queries"` + Requests []*measurex.ArchivalHTTPRoundTripEvent `json:"requests"` + Summary map[string]Summary `json:"summary"` + TargetAddress string `json:"target_address"` + TargetName string `json:"target_name,omitempty"` + TargetProtocol string `json:"target_protocol"` + TargetSource string `json:"target_source,omitempty"` + TCPConnect []*measurex.ArchivalTCPConnect `json:"tcp_connect"` + TLSHandshakes []*measurex.ArchivalQUICTLSHandshakeEvent `json:"tls_handshakes"` // Only for testing. We don't care about this field otherwise. We // cannot make this private because otherwise the IP address sanitizer @@ -64,11 +62,11 @@ type TargetResults struct { } func registerExtensions(m *model.Measurement) { - oonidatamodel.ExtHTTP.AddTo(m) - oonidatamodel.ExtNetevents.AddTo(m) - oonidatamodel.ExtDNS.AddTo(m) - oonidatamodel.ExtTCPConnect.AddTo(m) - oonidatamodel.ExtTLSHandshake.AddTo(m) + archival.ExtHTTP.AddTo(m) + archival.ExtNetevents.AddTo(m) + archival.ExtDNS.AddTo(m) + archival.ExtTCPConnect.AddTo(m) + archival.ExtTLSHandshake.AddTo(m) } // fillSummary fills the Summary field used by the UI. @@ -178,10 +176,6 @@ func (m *Measurer) Run( if err != nil { return err } - ctx, cancel := context.WithTimeout( - ctx, 15*time.Second*time.Duration(len(targets)), - ) - defer cancel() registerExtensions(measurement) m.measureTargets(ctx, sess, measurement, callbacks, targets) return nil @@ -251,7 +245,7 @@ func (m *Measurer) measureTargets( type resultsCollector struct { callbacks model.ExperimentCallbacks completed *atomicx.Int64 - flexibleConnect func(context.Context, keytarget) (oonitemplates.Results, error) + flexibleConnect func(context.Context, keytarget) (*measurex.ArchivalMeasurement, *string) measurement *model.Measurement mu sync.Mutex sess model.ExperimentSession @@ -293,15 +287,16 @@ func maybeSanitize(input TargetResults, kt keytarget) TargetResults { func (rc *resultsCollector) measureSingleTarget( ctx context.Context, kt keytarget, total int, ) { - tk, err := rc.flexibleConnect(ctx, kt) + tk, failure := rc.flexibleConnect(ctx, kt) + runtimex.PanicIfNil(tk, "measurex should guarantee non-nil here") tr := TargetResults{ Agent: "redirect", - Failure: setFailure(err), - NetworkEvents: oonidatamodel.NewNetworkEventsList(tk), - Queries: oonidatamodel.NewDNSQueriesList(tk), - Requests: oonidatamodel.NewRequestList(tk), - TCPConnect: oonidatamodel.NewTCPConnectList(tk), - TLSHandshakes: oonidatamodel.NewTLSHandshakesList(tk), + Failure: failure, + NetworkEvents: tk.NetworkEvents, + Queries: tk.Queries, + Requests: tk.Requests, + TCPConnect: tk.TCPConnect, + TLSHandshakes: tk.TLSHandshakes, } tr.fillSummary() tr = maybeSanitize(tr, kt) @@ -319,7 +314,7 @@ func (rc *resultsCollector) measureSingleTarget( } rc.callbacks.OnProgress(percentage, fmt.Sprintf( "tor: access %s/%s: %s", kt.maybeTargetAddress(), kt.target.Protocol, - errString(err), + failureString(failure), )) } @@ -330,56 +325,48 @@ func maybeScrubbingLogger(input model.Logger, kt keytarget) model.Logger { return &scrubber.Logger{Logger: input} } -func (rc *resultsCollector) defaultFlexibleConnect( - ctx context.Context, kt keytarget, -) (tk oonitemplates.Results, err error) { - logger := maybeScrubbingLogger(rc.sess.Logger(), kt) +// defaultFlexibleConnect is the default implementation of the +// rc.flexibleConnect testable function. +// +// Arguments: +// +// - ctx is the context for timeout/cancellation; +// +// - kt contains information about the target. +// +// Returns: +// +// - tk is the measurement, which is always non nil because +// the measurex "easy" API provides this guarantee; +// +// - failure is nil or an OONI failure string. +func (rc *resultsCollector) defaultFlexibleConnect(ctx context.Context, + kt keytarget) (tk *measurex.ArchivalMeasurement, failure *string) { + mx := measurex.NewMeasurerWithDefaultSettings() + mx.Begin = rc.measurement.MeasurementStartTimeSaved + mx.Logger = maybeScrubbingLogger(rc.sess.Logger(), kt) switch kt.target.Protocol { case "dir_port": - url := url.URL{ + URL := url.URL{ Host: kt.target.Address, Path: "/tor/status-vote/current/consensus.z", Scheme: "http", } const snapshotsize = 1 << 8 // no need to include all in report - r := oonitemplates.HTTPDo(ctx, oonitemplates.HTTPDoConfig{ - Accept: httpheader.Accept(), - AcceptLanguage: httpheader.AcceptLanguage(), - Beginning: rc.measurement.MeasurementStartTimeSaved, - MaxEventsBodySnapSize: snapshotsize, - MaxResponseBodySnapSize: snapshotsize, - Handler: netxlogger.NewHandler(logger), - Method: "GET", - URL: url.String(), - UserAgent: httpheader.UserAgent(), - }) - tk, err = r.TestKeys, r.Error + mx.HTTPMaxBodySnapshotSize = snapshotsize + const timeout = 15 * time.Second + return mx.EasyHTTPRoundTripGET(ctx, timeout, URL.String()) case "or_port", "or_port_dirauth": - r := oonitemplates.TLSConnect(ctx, oonitemplates.TLSConnectConfig{ - Address: kt.target.Address, - Beginning: rc.measurement.MeasurementStartTimeSaved, - InsecureSkipVerify: true, - Handler: netxlogger.NewHandler(logger), - }) - tk, err = r.TestKeys, r.Error + tlsConfig := measurex.NewEasyTLSConfig().InsecureSkipVerify(true) + return mx.EasyTLSConnectAndHandshake(ctx, kt.target.Address, tlsConfig) case "obfs4": - r := oonitemplates.OBFS4Connect(ctx, oonitemplates.OBFS4ConnectConfig{ - Address: kt.target.Address, - Beginning: rc.measurement.MeasurementStartTimeSaved, - Handler: netxlogger.NewHandler(logger), - Params: kt.target.Params, - StateBaseDir: rc.sess.TempDir(), - }) - tk, err = r.TestKeys, r.Error + const timeout = 15 * time.Second + return mx.EasyOBFS4ConnectAndHandshake( + ctx, timeout, kt.target.Address, rc.sess.TempDir(), + kt.target.Params) default: - r := oonitemplates.TCPConnect(ctx, oonitemplates.TCPConnectConfig{ - Address: kt.target.Address, - Beginning: rc.measurement.MeasurementStartTimeSaved, - Handler: netxlogger.NewHandler(logger), - }) - tk, err = r.TestKeys, r.Error + return mx.EasyTCPConnect(ctx, kt.target.Address) } - return } // NewExperimentMeasurer creates a new ExperimentMeasurer. @@ -387,18 +374,10 @@ func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { return NewMeasurer(config) } -func errString(err error) (s string) { +func failureString(failure *string) (s string) { s = "success" - if err != nil { - s = err.Error() - } - return -} - -func setFailure(err error) (s *string) { - if err != nil { - descr := err.Error() - s = &descr + if failure != nil { + s = *failure } return } diff --git a/internal/engine/experiment/tor/tor_test.go b/internal/engine/experiment/tor/tor_test.go index eba3437..f2eabeb 100644 --- a/internal/engine/experiment/tor/tor_test.go +++ b/internal/engine/experiment/tor/tor_test.go @@ -13,9 +13,8 @@ import ( "github.com/apex/log" "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonidatamodel" - "github.com/ooni/probe-cli/v3/internal/engine/legacy/oonitemplates" "github.com/ooni/probe-cli/v3/internal/engine/mockable" + "github.com/ooni/probe-cli/v3/internal/measurex" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/scrubber" @@ -26,7 +25,7 @@ func TestNewExperimentMeasurer(t *testing.T) { if measurer.ExperimentName() != "tor" { t.Fatal("unexpected name") } - if measurer.ExperimentVersion() != "0.3.0" { + if measurer.ExperimentVersion() != "0.4.0" { t.Fatal("unexpected version") } } @@ -118,15 +117,15 @@ func TestMeasurerMeasureGood(t *testing.T) { } } -var staticPrivateTestingTargetEndpoint = "192.95.36.142:443" +var staticPrivateTestingTargetEndpoint = "209.148.46.65:443" var staticPrivateTestingTarget = model.OOAPITorTarget{ Address: staticPrivateTestingTargetEndpoint, Params: map[string][]string{ "cert": { - "qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ", + "ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw", }, - "iat-mode": {"1"}, + "iat-mode": {"0"}, }, Protocol: "obfs4", Source: "bridgedb", @@ -159,7 +158,7 @@ func TestMeasurerMeasureSanitiseOutput(t *testing.T) { tk := measurement.TestKeys.(*TestKeys) entry := tk.Targets[key] if entry.Failure != nil { - t.Fatal("measurement failed unexpectedly") + t.Fatal("measurement failed unexpectedly", *entry.Failure) } if !bytes.Contains(data, []byte(key)) { t.Fatal("cannot find expected key") @@ -258,8 +257,8 @@ func TestResultsCollectorMeasureSingleTargetGood(t *testing.T) { new(model.Measurement), model.NewPrinterCallbacks(log.Log), ) - rc.flexibleConnect = func(context.Context, keytarget) (oonitemplates.Results, error) { - return oonitemplates.Results{}, nil + rc.flexibleConnect = func(context.Context, keytarget) (*measurex.ArchivalMeasurement, *string) { + return &measurex.ArchivalMeasurement{}, nil } rc.measureSingleTarget( context.Background(), wrapTestingTarget(staticTestingTargets[0]), @@ -292,8 +291,9 @@ func TestResultsCollectorMeasureSingleTargetWithFailure(t *testing.T) { new(model.Measurement), model.NewPrinterCallbacks(log.Log), ) - rc.flexibleConnect = func(context.Context, keytarget) (oonitemplates.Results, error) { - return oonitemplates.Results{}, errors.New("mocked error") + rc.flexibleConnect = func(context.Context, keytarget) (*measurex.ArchivalMeasurement, *string) { + failure := "mocked error" + return &measurex.ArchivalMeasurement{}, &failure } rc.measureSingleTarget( context.Background(), keytarget{ @@ -331,14 +331,14 @@ func TestDefautFlexibleConnectDirPort(t *testing.T) { ) ctx, cancel := context.WithCancel(context.Background()) cancel() - tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[1])) - if err == nil { - t.Fatal("expected an error here") + tk, failure := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[1])) + if failure == nil { + t.Fatal("expected a failure here") } - if !strings.HasSuffix(err.Error(), "interrupted") { + if !strings.HasSuffix(*failure, "interrupted") { t.Fatal("not the error we expected") } - if tk.HTTPRequests == nil { + if tk.Requests == nil { t.Fatal("expected HTTP data here") } } @@ -353,18 +353,18 @@ func TestDefautFlexibleConnectOrPort(t *testing.T) { ) ctx, cancel := context.WithCancel(context.Background()) cancel() - tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[2])) - if err == nil { - t.Fatal("expected an error here") + tk, failure := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[2])) + if failure == nil { + t.Fatal("expected a failure here") } - if err.Error() != "interrupted" { + if *failure != "interrupted" { t.Fatal("not the error we expected") } - if tk.Connects == nil { + if tk.TCPConnect == nil { t.Fatal("expected connects data here") } - if tk.NetworkEvents == nil { - t.Fatal("expected network events data here") + if tk.NetworkEvents != nil { + t.Fatal("expected no network events data here") } } @@ -378,18 +378,18 @@ func TestDefautFlexibleConnectOBFS4(t *testing.T) { ) ctx, cancel := context.WithCancel(context.Background()) cancel() - tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[0])) - if err == nil { - t.Fatal("expected an error here") + tk, failure := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[0])) + if failure == nil { + t.Fatal("expected a failure here") } - if err.Error() != "interrupted" { + if *failure != "interrupted" { t.Fatal("not the error we expected") } - if tk.Connects == nil { + if tk.TCPConnect == nil { t.Fatal("expected connects data here") } - if tk.NetworkEvents == nil { - t.Fatal("expected network events data here") + if tk.NetworkEvents != nil { + t.Fatal("expected no network events data here") } } @@ -403,24 +403,25 @@ func TestDefautFlexibleConnectDefault(t *testing.T) { ) ctx, cancel := context.WithCancel(context.Background()) cancel() - tk, err := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[3])) - if err == nil { - t.Fatal("expected an error here") + tk, failure := rc.defaultFlexibleConnect(ctx, wrapTestingTarget(staticTestingTargets[3])) + if failure == nil { + t.Fatal("expected a failure here") } - if err.Error() != "interrupted" { - t.Fatalf("not the error we expected: %+v", err) + if *failure != "interrupted" { + t.Fatalf("not the error we expected: %+v", *failure) } - if tk.Connects == nil { - t.Fatalf("expected connects data here, found: %+v", tk.Connects) + if tk.TCPConnect == nil { + t.Fatalf("expected connects data here, found: %+v", tk.TCPConnect) } } -func TestErrString(t *testing.T) { - if errString(nil) != "success" { +func TestFailureString(t *testing.T) { + if failureString(nil) != "success" { t.Fatal("not working with nil") } - if errString(errors.New("antani")) != "antani" { - t.Fatal("not working with error") + s := "antani" + if failureString(&s) != "antani" { + t.Fatal("not working with non-nil string") } } @@ -436,8 +437,8 @@ func TestSummary(t *testing.T) { t.Run("with a TCP connect and nothing else", func(t *testing.T) { tr := new(TargetResults) failure := "mocked_error" - tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{ - Status: oonidatamodel.TCPConnectStatus{ + tr.TCPConnect = append(tr.TCPConnect, &measurex.ArchivalTCPConnect{ + Status: &measurex.ArchivalTCPConnectStatus{ Success: true, Failure: &failure, }, @@ -453,8 +454,8 @@ func TestSummary(t *testing.T) { t.Run("for OBFS4", func(t *testing.T) { tr := new(TargetResults) - tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{ - Status: oonidatamodel.TCPConnectStatus{ + tr.TCPConnect = append(tr.TCPConnect, &measurex.ArchivalTCPConnect{ + Status: &measurex.ArchivalTCPConnectStatus{ Success: true, }, }) @@ -474,16 +475,16 @@ func TestSummary(t *testing.T) { }) t.Run("for or_port/or_port_dirauth", func(t *testing.T) { - doit := func(targetProtocol string, handshake *oonidatamodel.TLSHandshake) { + doit := func(targetProtocol string, handshake *measurex.ArchivalQUICTLSHandshakeEvent) { tr := new(TargetResults) - tr.TCPConnect = append(tr.TCPConnect, oonidatamodel.TCPConnectEntry{ - Status: oonidatamodel.TCPConnectStatus{ + tr.TCPConnect = append(tr.TCPConnect, &measurex.ArchivalTCPConnect{ + Status: &measurex.ArchivalTCPConnectStatus{ Success: true, }, }) tr.TargetProtocol = targetProtocol if handshake != nil { - tr.TLSHandshakes = append(tr.TLSHandshakes, *handshake) + tr.TLSHandshakes = append(tr.TLSHandshakes, handshake) } tr.fillSummary() if len(tr.Summary) < 1 { @@ -507,7 +508,7 @@ func TestSummary(t *testing.T) { } doit("or_port_dirauth", nil) doit("or_port", nil) - doit("or_port", &oonidatamodel.TLSHandshake{ + doit("or_port", &measurex.ArchivalQUICTLSHandshakeEvent{ Failure: (func() *string { s := io.EOF.Error() return &s @@ -796,10 +797,10 @@ func TestSummaryKeysWorksAsIntended(t *testing.T) { func TestTargetResultsFillSummaryDirPort(t *testing.T) { tr := &TargetResults{ TargetProtocol: "dir_port", - TCPConnect: oonidatamodel.TCPConnectList{{ + TCPConnect: []*measurex.ArchivalTCPConnect{{ IP: "1.2.3.4", Port: 443, - Status: oonidatamodel.TCPConnectStatus{ + Status: &measurex.ArchivalTCPConnectStatus{ Failure: nil, }, }}, diff --git a/internal/measurex/easy.go b/internal/measurex/easy.go new file mode 100644 index 0000000..3695ffc --- /dev/null +++ b/internal/measurex/easy.go @@ -0,0 +1,273 @@ +package measurex + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "time" + + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/ptx" +) + +// +// API for reducing boilerplate for simple measurements. +// + +// EasyHTTPRoundTripGET performs a GET with the given URL +// and default headers. This function will perform just +// a single HTTP round trip (i.e., no redirections). +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation; +// +// - timeout is the timeout for the whole operation; +// +// - URL is the URL to GET; +// +// Returns: +// +// - meas is a JSON serializable OONI measurement (this +// field will never be a nil pointer); +// +// - failure is either nil or a pointer to a OONI failure. +func (mx *Measurer) EasyHTTPRoundTripGET(ctx context.Context, timeout time.Duration, + URL string) (meas *ArchivalMeasurement, failure *string) { + ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout + defer cancel() + db := &MeasurementDB{} + req, err := NewHTTPRequestWithContext(ctx, "GET", URL, nil) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + txp := mx.NewTracingHTTPTransportWithDefaultSettings(db) + resp, err := txp.RoundTrip(req) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + resp.Body.Close() + return NewArchivalMeasurement(db.AsMeasurement()), nil +} + +// EasyTLSConfig helps you to generate a *tls.Config. +type EasyTLSConfig struct { + config *tls.Config +} + +// NewEasyTLSConfig creates a new EasyTLSConfig instance. +func NewEasyTLSConfig() *EasyTLSConfig { + return &EasyTLSConfig{ + config: &tls.Config{ + RootCAs: netxlite.NewDefaultCertPool(), + }, + } +} + +// NewEasyTLSConfigWithServerName creates a new EasyTLSConfig +// with an already configured value for ServerName. +func NewEasyTLSConfigWithServerName(serverName string) *EasyTLSConfig { + return NewEasyTLSConfig().ServerName(serverName) +} + +// ServerName sets the SNI value. +func (easy *EasyTLSConfig) ServerName(v string) *EasyTLSConfig { + easy.config.ServerName = v + return easy +} + +// InsecureSkipVerify disables TLS verification. +func (easy *EasyTLSConfig) InsecureSkipVerify(v bool) *EasyTLSConfig { + easy.config.InsecureSkipVerify = v + return easy +} + +// RootCAs allows the set the CA pool. +func (easy *EasyTLSConfig) RootCAs(v *x509.CertPool) *EasyTLSConfig { + easy.config.RootCAs = v + return easy +} + +// asTLSConfig converts an *EasyTLSConfig to a *tls.Config. +func (easy *EasyTLSConfig) asTLSConfig() *tls.Config { + if easy == nil || easy.config == nil { + return &tls.Config{} + } + return easy.config +} + +// EasyTLSConnectAndHandshake performs a TCP connect to a TCP endpoint +// followed by a TLS handshake using the given config. +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation; +// +// - endpoint is the TCP endpoint to connect to (e.g., +// 8.8.8.8:443 where the address part of the endpoint MUST +// be an IPv4 or IPv6 address and MUST NOT be a domain); +// +// - tlsConfig is the EasyTLSConfig to use (MUST NOT be nil). +// +// Returns: +// +// - meas is a JSON serializable OONI measurement (this +// field will never be a nil pointer); +// +// - failure is either nil or a pointer to a OONI failure. +// +// Note: +// +// - we use the Measurer's TCPConnectTimeout and TLSHandshakeTimeout. +func (mx *Measurer) EasyTLSConnectAndHandshake(ctx context.Context, endpoint string, + tlsConfig *EasyTLSConfig) (meas *ArchivalMeasurement, failure *string) { + // Note: TLSConnectAndHandshakeWithDB uses the timeout configured inside mx. + db := &MeasurementDB{} + conn, err := mx.TLSConnectAndHandshakeWithDB(ctx, db, endpoint, tlsConfig.asTLSConfig()) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + conn.Close() + return NewArchivalMeasurement(db.AsMeasurement()), nil +} + +// EasyTCPConnect performs a TCP connect to a TCP endpoint. +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation; +// +// - endpoint is the TCP endpoint to connect to (e.g., +// 8.8.8.8:443 where the address part of the endpoint MUST +// be an IPv4 or IPv6 address and MUST NOT be a domain). +// +// Returns: +// +// - meas is a JSON serializable OONI measurement (this +// field will never be a nil pointer); +// +// - failure is either nil or a pointer to a OONI failure. +// +// Note: +// +// - we use the Measurer's TCPConnectTimeout. +func (mx *Measurer) EasyTCPConnect(ctx context.Context, + endpoint string) (meas *ArchivalMeasurement, failure *string) { + // Note: TCPConnectWithDB uses the timeout configured inside mx. + db := &MeasurementDB{} + conn, err := mx.TCPConnectWithDB(ctx, db, endpoint) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + conn.Close() + return NewArchivalMeasurement(db.AsMeasurement()), nil +} + +// easyOBFS4Params contains params for OBFS4. +type easyOBFS4Params struct { + // Cert contains the MANDATORY certificate parameter. + Cert string + + // DataDir is the MANDATORY directory where to store obfs4 data. + DataDir string + + // Fingerprint is the MANDATORY bridge fingerprint. + Fingerprint string + + // IATMode contains the MANDATORY iat-mode parameter. + IATMode string +} + +// newEasyOBFS4Params constructs an EasyOBFS4Params structure +// from the map[string][]string returned by the OONI API. +// +// This function will only fail when the rawParams contains +// more than one entry for each input key. +func newEasyOBFS4Params(dataDir string, rawParams map[string][]string) (*easyOBFS4Params, error) { + out := &easyOBFS4Params{DataDir: dataDir} + for key, values := range rawParams { + var field *string + switch key { + case "cert": + field = &out.Cert + case "fingerprint": + field = &out.Fingerprint + case "iat-mode": + field = &out.IATMode + default: + continue // not interested + } + if len(values) != 1 { + return nil, fmt.Errorf("obfs4: expected exactly one value for %s", key) + } + *field = values[0] + } + // Assume that the API knows what it's returning, so don't bother + // checking whether some fields are missing. If this happens, it + // will be the obfs4 library task to tell us about that. + return out, nil +} + +// EasyOBFS4ConnectAndHandshake performs a TCP connect to a TCP endpoint +// followed by an OBFS4 handshake. This function is designed to receive +// in input the Tor bridges from the OONI API. +// +// Arguments: +// +// - ctx is the context for deadline/timeout/cancellation; +// +// - timeout is the timeout for the whole operation; +// +// - endpoint is the TCP endpoint to connect to (e.g., +// 8.8.8.8:443 where the address part of the endpoint MUST +// be an IPv4 or IPv6 address and MUST NOT be a domain); +// +// - dataDir is the data directory to use for obfs4; +// +// - rawParams contains raw obfs4 params from the OONI API. +// +// Returns: +// +// - meas is a JSON serializable OONI measurement (this +// field will never be a nil pointer); +// +// - failure is either nil or a pointer to a OONI failure. +func (mx *Measurer) EasyOBFS4ConnectAndHandshake(ctx context.Context, + timeout time.Duration, endpoint string, dataDir string, + rawParams map[string][]string) (meas *ArchivalMeasurement, failure *string) { + ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout + defer cancel() + db := &MeasurementDB{} + params, err := newEasyOBFS4Params(dataDir, rawParams) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + conn, err := mx.TCPConnectWithDB(ctx, db, endpoint) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + defer conn.Close() + dialer := netxlite.NewSingleUseDialer(conn) + obfs4 := ptx.OBFS4Dialer{ + Address: endpoint, + Cert: params.Cert, + DataDir: params.DataDir, + Fingerprint: params.Fingerprint, + IATMode: params.IATMode, + UnderlyingDialer: dialer, + } + o4conn, err := obfs4.DialContext(ctx) + if err != nil { + failure := err.Error() + return NewArchivalMeasurement(db.AsMeasurement()), &failure + } + o4conn.Close() + return NewArchivalMeasurement(db.AsMeasurement()), nil +} diff --git a/internal/runtimex/runtimex.go b/internal/runtimex/runtimex.go index 0be3a0c..140a15e 100644 --- a/internal/runtimex/runtimex.go +++ b/internal/runtimex/runtimex.go @@ -22,3 +22,8 @@ func PanicIfFalse(assertion bool, message string) { func PanicIfTrue(assertion bool, message string) { PanicIfFalse(!assertion, message) } + +// PanicIfNil calls panic if the given interface is nil. +func PanicIfNil(v interface{}, message string) { + PanicIfTrue(v == nil, message) +} diff --git a/internal/runtimex/runtimex_test.go b/internal/runtimex/runtimex_test.go index 4649540..9977622 100644 --- a/internal/runtimex/runtimex_test.go +++ b/internal/runtimex/runtimex_test.go @@ -12,12 +12,12 @@ func TestPanicOnError(t *testing.T) { defer func() { out = recover().(error) }() - runtimex.PanicOnError(in, "antani failed") + runtimex.PanicOnError(in, "we expect this assertion to fail") return } t.Run("error is nil", func(t *testing.T) { - runtimex.PanicOnError(nil, "antani failed") + runtimex.PanicOnError(nil, "this assertion should not fail") }) t.Run("error is not nil", func(t *testing.T) { @@ -38,7 +38,7 @@ func TestPanicIfFalse(t *testing.T) { } t.Run("assertion is true", func(t *testing.T) { - runtimex.PanicIfFalse(true, "antani failed") + runtimex.PanicIfFalse(true, "this assertion should not fail") }) t.Run("assertion is false", func(t *testing.T) { @@ -60,7 +60,7 @@ func TestPanicIfTrue(t *testing.T) { } t.Run("assertion is false", func(t *testing.T) { - runtimex.PanicIfTrue(false, "antani failed") + runtimex.PanicIfTrue(false, "this assertion should not fail") }) t.Run("assertion is true", func(t *testing.T) { @@ -71,3 +71,25 @@ func TestPanicIfTrue(t *testing.T) { } }) } + +func TestPanicIfNil(t *testing.T) { + badfunc := func(in interface{}, message string) (out error) { + defer func() { + out = errors.New(recover().(string)) + }() + runtimex.PanicIfNil(in, message) + return + } + + t.Run("value is not nil", func(t *testing.T) { + runtimex.PanicIfNil(false, "this assertion should not fail") + }) + + t.Run("value is nil", func(t *testing.T) { + message := "mocked error" + err := badfunc(nil, message) + if err == nil || err.Error() != message { + t.Fatal("not the error we expected", err) + } + }) +}