ooni-probe-cli/internal/experiment/webconnectivity/measurer.go

155 lines
3.9 KiB
Go

package webconnectivity
//
// Measurer
//
import (
"context"
"errors"
"net/http/cookiejar"
"sync"
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
"github.com/ooni/probe-cli/v3/internal/model"
"golang.org/x/net/publicsuffix"
)
// Measurer for the web_connectivity experiment.
type Measurer struct {
// Contains the experiment's config.
Config *Config
}
// NewExperimentMeasurer creates a new model.ExperimentMeasurer.
func NewExperimentMeasurer(config *Config) model.ExperimentMeasurer {
return &Measurer{
Config: config,
}
}
// ExperimentName implements model.ExperimentMeasurer.
func (m *Measurer) ExperimentName() string {
return "web_connectivity"
}
// ExperimentVersion implements model.ExperimentMeasurer.
func (m *Measurer) ExperimentVersion() string {
return "0.5.18"
}
// Run implements model.ExperimentMeasurer.
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
// 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).
// make sure we have a cancellable context such that we can stop any
// goroutine running in the background (e.g., priority.go's ones)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// honour InputOrQueryBackend
input := measurement.Input
if input == "" {
return errors.New("no input provided")
}
// convert the input string to a URL
inputParser := &InputParser{
AcceptedSchemes: []string{
"http",
"https",
},
AllowEndpoints: false,
DefaultScheme: "",
}
URL, err := inputParser.Parse(string(measurement.Input))
if err != nil {
return err
}
// initialize the experiment's test keys
tk := NewTestKeys()
measurement.TestKeys = tk
// create variables required to run parallel tasks
idGenerator := &atomicx.Int64{}
wg := &sync.WaitGroup{}
// create cookiejar
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
return err
}
// obtain the test helper's address
testhelpers, _ := sess.GetTestHelpersByName("web-connectivity")
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")
tk.SetControlFailure(webconnectivity.ErrNoAvailableTestHelpers)
}
registerExtensions(measurement)
// start background tasks
resos := &DNSResolvers{
DNSCache: NewDNSCache(),
Domain: URL.Hostname(),
IDGenerator: idGenerator,
Logger: sess.Logger(),
NumRedirects: NewNumRedirects(5),
TestKeys: tk,
URL: URL,
ZeroTime: measurement.MeasurementStartTimeSaved,
WaitGroup: wg,
CookieJar: jar,
Referer: "",
Session: sess,
THAddr: thAddr,
UDPAddress: "",
}
resos.Start(ctx)
// wait for background tasks to join
wg.Wait()
// If the context passed to us has been cancelled, we cannot
// trust this experiment's results to be okay.
if err := ctx.Err(); err != nil {
return err
}
// perform any deferred computation on the test keys
tk.Finalize(sess.Logger())
// return whether there was a fundamental failure, which would prevent
// the measurement from being submitted to the OONI collector.
return tk.fundamentalFailure
}
// registerExtensions registers the extensions used by this
// experiment into the given measurement.
func registerExtensions(m *model.Measurement) {
model.ArchivalExtHTTP.AddTo(m)
model.ArchivalExtDNS.AddTo(m)
model.ArchivalExtNetevents.AddTo(m)
model.ArchivalExtTCPConnect.AddTo(m)
model.ArchivalExtTLSHandshake.AddTo(m)
model.ArchivalExtTunnel.AddTo(m)
}