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 }