// Package hirl contains the HTTP Invalid Request Line network experiment. // // See https://github.com/ooni/spec/blob/master/nettests/ts-007-http-invalid-request-line.md package hirl import ( "context" "errors" "fmt" "net" "strings" "time" "github.com/ooni/probe-cli/v3/internal/engine/netx" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/randx" "github.com/ooni/probe-cli/v3/internal/tracex" ) const ( testName = "http_invalid_request_line" testVersion = "0.2.0" timeout = 5 * time.Second ) // Config contains the experiment config. type Config struct{} // TestKeys contains the experiment test keys. type TestKeys struct { FailureList []*string `json:"failure_list"` Received []tracex.MaybeBinaryValue `json:"received"` Sent []string `json:"sent"` TamperingList []bool `json:"tampering_list"` Tampering bool `json:"tampering"` } // NewExperimentMeasurer creates a new ExperimentMeasurer. func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { return Measurer{ Config: config, Methods: []Method{ randomInvalidMethod{}, randomInvalidFieldCount{}, randomBigRequestMethod{}, randomInvalidVersionNumber{}, squidCacheManager{}, }, } } // Measurer performs the measurement. type Measurer struct { Config Config Methods []Method } // ExperimentName implements ExperimentMeasurer.ExperiExperimentName. func (m Measurer) ExperimentName() string { return testName } // ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. func (m Measurer) ExperimentVersion() string { return testVersion } var ( // ErrNoAvailableTestHelpers is emitted when there are no available test helpers. ErrNoAvailableTestHelpers = errors.New("no available helpers") // ErrInvalidHelperType is emitted when the helper type is invalid. ErrInvalidHelperType = errors.New("invalid helper type") // ErrNoMeasurementMethod is emitted when Measurer.Methods is empty. ErrNoMeasurementMethod = errors.New("no configured measurement method") ) // Run implements ExperimentMeasurer.Run. func (m Measurer) Run( ctx context.Context, sess model.ExperimentSession, measurement *model.Measurement, callbacks model.ExperimentCallbacks, ) error { tk := new(TestKeys) measurement.TestKeys = tk if len(m.Methods) < 1 { return ErrNoMeasurementMethod } const helperName = "tcp-echo" helpers, ok := sess.GetTestHelpersByName(helperName) if !ok || len(helpers) < 1 { return ErrNoAvailableTestHelpers } helper := helpers[0] if helper.Type != "legacy" { return ErrInvalidHelperType } measurement.TestHelpers = map[string]interface{}{ "backend": helper.Address, } out := make(chan MethodResult) for _, method := range m.Methods { callbacks.OnProgress(0.0, fmt.Sprintf("%s...", method.Name())) go method.Run(ctx, MethodConfig{ Address: helper.Address, Logger: sess.Logger(), Out: out, }) } var ( completed int progress float64 result MethodResult ) for { select { case result = <-out: case <-time.After(500 * time.Millisecond): if completed <= 0 { progress += 0.05 callbacks.OnProgress(progress, "waiting for results...") } continue } failure := tracex.NewFailure(result.Err) tk.FailureList = append(tk.FailureList, failure) tk.Received = append(tk.Received, result.Received) tk.Sent = append(tk.Sent, result.Sent) tk.TamperingList = append(tk.TamperingList, result.Tampering) tk.Tampering = (tk.Tampering || result.Tampering) completed++ percentage := (float64(completed)/float64(len(m.Methods)))*0.5 + 0.5 callbacks.OnProgress(percentage, fmt.Sprintf("%s... %+v", result.Name, model.ErrorToStringOrOK(result.Err))) if completed >= len(m.Methods) { break } } return nil } // MethodConfig contains the settings for a specific measuring method. type MethodConfig struct { Address string Logger model.Logger Out chan<- MethodResult } // MethodResult is the result of one of the methods implemented by this experiment. type MethodResult struct { Err error Name string Received tracex.MaybeBinaryValue Sent string Tampering bool } // Method is one of the methods implemented by this experiment. type Method interface { Name() string Run(ctx context.Context, config MethodConfig) } type randomInvalidMethod struct{} func (randomInvalidMethod) Name() string { return "random_invalid_method" } func (meth randomInvalidMethod) Run(ctx context.Context, config MethodConfig) { RunMethod(ctx, RunMethodConfig{ MethodConfig: config, Name: meth.Name(), RequestLine: randx.LettersUppercase(4) + " / HTTP/1.1\n\r", }) } type randomInvalidFieldCount struct{} func (randomInvalidFieldCount) Name() string { return "random_invalid_field_count" } func (meth randomInvalidFieldCount) Run(ctx context.Context, config MethodConfig) { RunMethod(ctx, RunMethodConfig{ MethodConfig: config, Name: meth.Name(), RequestLine: strings.Join([]string{ randx.LettersUppercase(5), " ", randx.LettersUppercase(5), " ", randx.LettersUppercase(5), " ", randx.LettersUppercase(5), "\r\n", }, ""), }) } type randomBigRequestMethod struct{} func (randomBigRequestMethod) Name() string { return "random_big_request_method" } func (meth randomBigRequestMethod) Run(ctx context.Context, config MethodConfig) { RunMethod(ctx, RunMethodConfig{ MethodConfig: config, Name: meth.Name(), RequestLine: strings.Join([]string{ randx.LettersUppercase(1024), " / HTTP/1.1\r\n", }, ""), }) } type randomInvalidVersionNumber struct{} func (randomInvalidVersionNumber) Name() string { return "random_invalid_version_number" } func (meth randomInvalidVersionNumber) Run(ctx context.Context, config MethodConfig) { RunMethod(ctx, RunMethodConfig{ MethodConfig: config, Name: meth.Name(), RequestLine: strings.Join([]string{ "GET / HTTP/", randx.LettersUppercase(3), "\r\n", }, ""), }) } type squidCacheManager struct{} func (squidCacheManager) Name() string { return "squid_cache_manager" } func (meth squidCacheManager) Run(ctx context.Context, config MethodConfig) { RunMethod(ctx, RunMethodConfig{ MethodConfig: config, Name: meth.Name(), RequestLine: "GET cache_object://localhost/ HTTP/1.0\n\r", }) } // RunMethodConfig contains the config for RunMethod type RunMethodConfig struct { MethodConfig Name string NewDialer func(config netx.Config) model.Dialer RequestLine string } // RunMethod runs the specific method using the given config and context func RunMethod(ctx context.Context, config RunMethodConfig) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() result := MethodResult{Name: config.Name} defer func() { config.Out <- result }() if config.NewDialer == nil { config.NewDialer = netx.NewDialer } dialer := config.NewDialer(netx.Config{ ContextByteCounting: true, Logger: config.Logger, }) conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(config.Address, "80")) if err != nil { result.Err = err return } deadline := time.Now().Add(timeout) if err := conn.SetDeadline(deadline); err != nil { result.Err = err return } if _, err := conn.Write([]byte(config.RequestLine)); err != nil { result.Err = err return } result.Sent = config.RequestLine data := make([]byte, 4096) defer func() { result.Tampering = (result.Sent != result.Received.Value) }() for { count, err := conn.Read(data) if err != nil { // We expect this method to terminate w/ timeout if err.Error() == netxlite.FailureGenericTimeoutError { err = nil } result.Err = err return } result.Received.Value += string(data[:count]) } } // 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 { IsAnomaly bool `json:"-"` } // GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { sk := SummaryKeys{IsAnomaly: false} tk, ok := measurement.TestKeys.(*TestKeys) if !ok { return sk, errors.New("invalid test keys type") } sk.IsAnomaly = tk.Tampering return sk, nil }