ba9151d4fa
This diff adds the prototype websteps implementation that used to live at https://github.com/ooni/probe-cli/pull/506. The code is reasonably good already and it's pointing to a roaming test helper that I've properly configured. You can run websteps with: ``` ./miniooni -n websteps ``` This will go over the test list for your country. At this stage the mechanics of the experiment is set, but we still need to have a conversation on the following topics: 1. whether we're okay with reusing the data format used by other OONI experiments, or we would like to use a more compact data format (which may either be a more compact JSON or we can choose to always submit compressed measurements for websteps); 2. the extent to which we would like to keep the measurement as a collection of "the experiment saw this" and "the test helper saw that" and let the pipeline choose an overall score: this is clearly an option, but there is also the opposite option to build a summary of the measurement on the probe. Compared to the previous prototype of websteps, the main architectural change we have here is that we are following the point of view of the probe and the test helper is much more dumb. Basically, the probe will choose which redirection to follow and ask the test helper every time it discovers a new URL to measure it w/o redirections. Reference issue: https://github.com/ooni/probe/issues/1733
218 lines
6.0 KiB
Go
218 lines
6.0 KiB
Go
package webstepsx
|
|
|
|
//
|
|
// Measurer
|
|
//
|
|
// This file contains the client implementation.
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
|
"github.com/ooni/probe-cli/v3/internal/measurex"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
)
|
|
|
|
const (
|
|
testName = "websteps"
|
|
testVersion = "0.0.2"
|
|
)
|
|
|
|
// Config contains the experiment config.
|
|
type Config struct{}
|
|
|
|
// TestKeys contains the experiment's test keys.
|
|
type TestKeys struct {
|
|
*measurex.URLMeasurement
|
|
}
|
|
|
|
// Measurer performs the measurement.
|
|
type Measurer struct {
|
|
Config Config
|
|
}
|
|
|
|
var (
|
|
_ model.ExperimentMeasurer = &Measurer{}
|
|
_ model.ExperimentMeasurerAsync = &Measurer{}
|
|
)
|
|
|
|
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
|
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
|
return &Measurer{Config: config}
|
|
}
|
|
|
|
// ExperimentName implements ExperimentMeasurer.ExperExperimentName.
|
|
func (mx *Measurer) ExperimentName() string {
|
|
return testName
|
|
}
|
|
|
|
// ExperimentVersion implements ExperimentMeasurer.ExperExperimentVersion.
|
|
func (mx *Measurer) ExperimentVersion() string {
|
|
return testVersion
|
|
}
|
|
|
|
var (
|
|
// ErrNoAvailableTestHelpers is emitted when there are no available test helpers.
|
|
ErrNoAvailableTestHelpers = errors.New("no available helpers")
|
|
|
|
// ErrNoInput indicates that no input was provided.
|
|
ErrNoInput = errors.New("no input provided")
|
|
|
|
// ErrInputIsNotAnURL indicates that the input is not an URL.
|
|
ErrInputIsNotAnURL = errors.New("input is not an URL")
|
|
|
|
// ErrUnsupportedInput indicates that the input URL scheme is unsupported.
|
|
ErrUnsupportedInput = errors.New("unsupported input scheme")
|
|
)
|
|
|
|
// RunAsync implements ExperimentMeasurerAsync.RunAsync.
|
|
func (mx *Measurer) RunAsync(
|
|
ctx context.Context, sess model.ExperimentSession, input string,
|
|
callbacks model.ExperimentCallbacks) (<-chan *model.ExperimentAsyncTestKeys, error) {
|
|
// 1. Parse and verify URL
|
|
URL, err := url.Parse(input)
|
|
if err != nil {
|
|
return nil, ErrInputIsNotAnURL
|
|
}
|
|
if URL.Scheme != "http" && URL.Scheme != "https" {
|
|
return nil, ErrUnsupportedInput
|
|
}
|
|
// 2. Find the testhelper
|
|
testhelpers, _ := sess.GetTestHelpersByName("web-connectivity")
|
|
var testhelper *model.Service
|
|
for _, th := range testhelpers {
|
|
if th.Type == "https" {
|
|
testhelper = &th
|
|
break
|
|
}
|
|
}
|
|
if testhelper == nil {
|
|
return nil, ErrNoAvailableTestHelpers
|
|
}
|
|
testhelper.Address = "https://1.th.ooni.org/api/v1/websteps" // TODO(bassosimone): remove!
|
|
out := make(chan *model.ExperimentAsyncTestKeys)
|
|
go mx.runAsync(ctx, sess, input, testhelper, out)
|
|
return out, nil
|
|
}
|
|
|
|
var measurerResolvers = []*measurex.ResolverInfo{{
|
|
Network: "system",
|
|
Address: "",
|
|
}, {
|
|
Network: "udp",
|
|
Address: "8.8.4.4:53",
|
|
}, {
|
|
Network: "udp",
|
|
Address: "1.1.1.1:53",
|
|
}}
|
|
|
|
func (mx *Measurer) runAsync(ctx context.Context, sess model.ExperimentSession,
|
|
URL string, th *model.Service, out chan<- *model.ExperimentAsyncTestKeys) {
|
|
defer close(out)
|
|
helper := &measurerMeasureURLHelper{
|
|
Clnt: sess.DefaultHTTPClient(),
|
|
Logger: sess.Logger(),
|
|
THURL: th.Address,
|
|
}
|
|
mmx := &measurex.Measurer{
|
|
Begin: time.Now(),
|
|
HTTPClient: sess.DefaultHTTPClient(),
|
|
MeasureURLHelper: helper,
|
|
Logger: sess.Logger(),
|
|
Resolvers: measurerResolvers,
|
|
TLSHandshaker: netxlite.NewTLSHandshakerStdlib(sess.Logger()),
|
|
}
|
|
cookies := measurex.NewCookieJar()
|
|
in := mmx.MeasureURLAndFollowRedirections(
|
|
ctx, URL, measurex.NewHTTPRequestHeaderForMeasuring(), cookies)
|
|
for m := range in {
|
|
out <- &model.ExperimentAsyncTestKeys{
|
|
MeasurementRuntime: m.TotalRuntime.Seconds(),
|
|
TestKeys: &TestKeys{URLMeasurement: m},
|
|
Extensions: map[string]int64{
|
|
archival.ExtHTTP.Name: archival.ExtHTTP.V,
|
|
archival.ExtDNS.Name: archival.ExtDNS.V,
|
|
archival.ExtNetevents.Name: archival.ExtNetevents.V,
|
|
archival.ExtTCPConnect.Name: archival.ExtTCPConnect.V,
|
|
archival.ExtTLSHandshake.Name: archival.ExtTLSHandshake.V,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// measurerMeasureURLHelper injects the TH into the normal
|
|
// URL measurement flow implemented by measurex.
|
|
type measurerMeasureURLHelper struct {
|
|
// Clnt is the MANDATORY client to use
|
|
Clnt measurex.HTTPClient
|
|
|
|
// Logger is the MANDATORY Logger to use
|
|
Logger model.Logger
|
|
|
|
// THURL is the MANDATORY TH URL.
|
|
THURL string
|
|
}
|
|
|
|
func (mth *measurerMeasureURLHelper) LookupExtraHTTPEndpoints(
|
|
ctx context.Context, URL *url.URL, headers http.Header,
|
|
curEndpoints ...*measurex.HTTPEndpoint) (
|
|
[]*measurex.HTTPEndpoint, interface{}, error) {
|
|
cc := &THClientCall{
|
|
Endpoints: measurex.HTTPEndpointsToEndpoints(curEndpoints),
|
|
HTTPClient: mth.Clnt,
|
|
Header: headers,
|
|
THURL: mth.THURL,
|
|
TargetURL: URL.String(),
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
ol := measurex.NewOperationLogger(
|
|
mth.Logger, "THClientCall %s", URL.String())
|
|
resp, err := cc.Call(ctx)
|
|
ol.Stop(err)
|
|
if err != nil {
|
|
return nil, resp, err
|
|
}
|
|
var out []*measurex.HTTPEndpoint
|
|
for _, epnt := range resp.Endpoints {
|
|
out = append(out, &measurex.HTTPEndpoint{
|
|
Domain: URL.Hostname(),
|
|
Network: epnt.Network,
|
|
Address: epnt.Address,
|
|
SNI: URL.Hostname(),
|
|
ALPN: measurex.ALPNForHTTPEndpoint(epnt.Network),
|
|
URL: URL,
|
|
Header: headers,
|
|
})
|
|
}
|
|
return out, resp, nil
|
|
}
|
|
|
|
// Run implements ExperimentMeasurer.Run.
|
|
func (mx *Measurer) Run(ctx context.Context, sess model.ExperimentSession,
|
|
measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
|
|
return errors.New("sync run is not implemented")
|
|
}
|
|
|
|
// SummaryKeys contains summary keys for this experiment.
|
|
//
|
|
// Note that this structure is part of the ABI contract with probe-cli
|
|
// therefore we should be careful when changing it.
|
|
type SummaryKeys struct {
|
|
Accessible bool `json:"accessible"`
|
|
Blocking string `json:"blocking"`
|
|
IsAnomaly bool `json:"-"`
|
|
}
|
|
|
|
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
|
func (mx *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
|
sk := SummaryKeys{}
|
|
return sk, nil
|
|
}
|