Skip to content

Commit

Permalink
feat(oonirun): implement OONIRun v1
Browse files Browse the repository at this point in the history
This diff adds support for running OONIRun v1 links.

Run with `miniooni` using:

```
./miniooni -i LINK oonirun
```

Part of ooni/probe#2184
  • Loading branch information
bassosimone committed Jul 8, 2022
1 parent 0b4a491 commit 1b63a87
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 1 deletion.
42 changes: 41 additions & 1 deletion internal/cmd/miniooni/libminiooni.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,14 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
log.Infof("- resolver's network: %s (%s)", sess.ResolverNetworkName(),
sess.ResolverASNString())

// Run OONI experiments as we normally do.
// We handle the oonirun experiment name specially. The user must specify
// `miniooni -i {OONIRunURL} oonirun` to run a OONI Run URL (v1 or v2).
if experimentName == "oonirun" {
ooniRunMain(ctx, sess, currentOptions, annotations)
return
}

// Otherwise just run OONI experiments as we normally do.
desc := &oonirun.Experiment{
Annotations: annotations,
ExtraOptions: extraOptions,
Expand All @@ -386,3 +393,36 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
err = desc.Run(ctx)
runtimex.PanicOnError(err, "cannot run experiment")
}

// ooniRunMain runs the experiments described by the given OONI Run URLs. This
// function works with both v1 and v2 OONI Run URLs.
func ooniRunMain(ctx context.Context,
sess *engine.Session, currentOptions Options, annotations map[string]string) {
runtimex.PanicIfTrue(
len(currentOptions.Inputs) <= 0,
"in oonirun mode you need to specify at least one URL using `-i URL`",
)
runtimex.PanicIfTrue(
len(currentOptions.InputFilePaths) > 0,
"in oonirun mode you cannot specify any `-f FILE` file",
)
logger := sess.Logger()
cfg := &oonirun.LinkConfig{
AcceptChanges: currentOptions.Yes,
Annotations: annotations,
KVStore: sess.KeyValueStore(),
MaxRuntime: currentOptions.MaxRuntime,
NoCollector: currentOptions.NoCollector,
NoJSON: currentOptions.NoJSON,
Random: currentOptions.Random,
ReportFile: currentOptions.ReportFile,
Session: sess,
}
for _, URL := range currentOptions.Inputs {
r := oonirun.NewLinkRunner(cfg, URL)
if err := r.Run(ctx); err != nil {
logger.Warnf("oonirun: Measure failed: %s", err.Error())
continue
}
}
}
92 changes: 92 additions & 0 deletions internal/oonirun/link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package oonirun

//
// OONI Run v1 and v2 links
//

import (
"context"
"strings"

"github.com/ooni/probe-cli/v3/internal/model"
)

// LinkConfig contains config for an OONI Run link. You MUST fill all the fields that
// are marked as MANDATORY, or the LinkConfig would cause crashes.
type LinkConfig struct {
// AcceptChanges is OPTIONAL and tells this library that the user is
// okay with running a new or modified OONI Run link without previously
// reviewing what it contains or what has changed.
AcceptChanges bool

// Annotations contains OPTIONAL Annotations for the experiment.
Annotations map[string]string

// KVStore is the MANDATORY key-value store to use to keep track of
// OONI Run links and know when they are new or modified.
KVStore model.KeyValueStore

// MaxRuntime is the OPTIONAL maximum runtime in seconds.
MaxRuntime int64

// NoCollector OPTIONALLY indicates we should not be using any collector.
NoCollector bool

// NoJSON OPTIONALLY indicates we don't want to save measurements to a JSON file.
NoJSON bool

// Random OPTIONALLY indicates we should randomize inputs.
Random bool

// ReportFile is the MANDATORY file in which to save reports, which is only
// used when noJSON is set to false.
ReportFile string

// Session is the MANDATORY Session to use.
Session Session
}

// LinkRunner knows how to run an OONI Run v1 or v2 link.
type LinkRunner interface {
Run(ctx context.Context) error
}

// linkRunner implements LinkRunner.
type linkRunner struct {
config *LinkConfig
f func(ctx context.Context, config *LinkConfig, URL string) error
url string
}

// Run implements LinkRunner.Rune.
func (lr *linkRunner) Run(ctx context.Context) error {
return lr.f(ctx, lr.config, lr.url)
}

// NewLinkRunner creates a suitable link runner for the current config
// and the given URL, which is one of the following:
//
// 1. OONI Run v1 link with https scheme (e.g., https://run.ooni.io/nettest?...)
//
// 2. OONI Run v1 link with ooni scheme (e.g., ooni://nettest?...)
//
// 3. arbitrary URL of the OONI Run v2 descriptor.
func NewLinkRunner(c *LinkConfig, URL string) LinkRunner {
// TODO(bassosimone): add support for v2 deeplinks.
out := &linkRunner{
config: c,
f: nil,
url: URL,
}
switch {
case strings.HasPrefix(URL, "https://run.ooni.io/nettest"):
out.f = v1Measure
case strings.HasPrefix(URL, "ooni://nettest"):
out.f = v1Measure
default:
// TODO(bassosimone): this panic will go away when we merge
// the next patch which will implement v2.
panic("unsupported OONI Run link")
}
return out
}
90 changes: 90 additions & 0 deletions internal/oonirun/v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package oonirun

//
// OONI Run v1 implementation
//

import (
"context"
"encoding/json"
"errors"
"net/url"
)

var (
// ErrInvalidV1URLScheme indicates a v1 OONI Run URL has an invalid scheme.
ErrInvalidV1URLScheme = errors.New("oonirun: invalid v1 URL scheme")

// ErrInvalidV1URLHost indicates a v1 OONI Run URL has an invalid host.
ErrInvalidV1URLHost = errors.New("oonirun: invalid v1 URL host")

// ErrInvalidV1URLPath indicates a v1 OONI Run URL has an invalid path.
ErrInvalidV1URLPath = errors.New("oonirun: invalid v1 URL path")

// ErrInvalidV1URLQueryArgument indicates a v1 OONI Run URL query argument is invalid.
ErrInvalidV1URLQueryArgument = errors.New("oonirun: invalid v1 URL query argument")
)

// v1Arguments contains arguments for a v1 OONI Run URL. These arguments are
// always encoded inside of the "ta" field, which is optional.
type v1Arguments struct {
URLs []string `json:"urls"`
}

// v1Measure performs a measurement using a v1 OONI Run URL.
func v1Measure(ctx context.Context, config *LinkConfig, URL string) error {
config.Session.Logger().Infof("oonirun/v1: running %s", URL)
pu, err := url.Parse(URL)
if err != nil {
return err
}
switch pu.Scheme {
case "https":
if pu.Host != "run.ooni.io" {
return ErrInvalidV1URLHost
}
if pu.Path != "/nettest" {
return ErrInvalidV1URLPath
}
case "ooni":
if pu.Host != "nettest" {
return ErrInvalidV1URLHost
}
if pu.Path != "" {
return ErrInvalidV1URLPath
}
default:
return ErrInvalidV1URLScheme
}
name := pu.Query().Get("tn")
if name == "" {
return ErrInvalidV1URLQueryArgument
}
var inputs []string
if ra := pu.Query().Get("ta"); ra != "" {
pa, err := url.QueryUnescape(ra)
if err != nil {
return err
}
var arguments v1Arguments
if err := json.Unmarshal([]byte(pa), &arguments); err != nil {
return err
}
inputs = arguments.URLs
}
// TODO(bassosimone): reject mv < 1.2.0
exp := &Experiment{
Annotations: config.Annotations,
ExtraOptions: nil, // no way to specify with v1 URLs
Inputs: inputs,
InputFilePaths: nil,
MaxRuntime: config.MaxRuntime,
Name: name,
NoCollector: config.NoCollector,
NoJSON: config.NoJSON,
Random: config.Random,
ReportFile: config.ReportFile,
Session: config.Session,
}
return exp.Run(ctx)
}
36 changes: 36 additions & 0 deletions internal/oonirun/v1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package oonirun

import (
"context"
"testing"

"github.com/ooni/probe-cli/v3/internal/kvstore"
)

// TODO(bassosimone): it would be cool to write unit tests. However, to do that
// we need to ~redesign the engine package for unit-testability.

func TestOONIRunV1Link(t *testing.T) {
ctx := context.Background()
config := &LinkConfig{
AcceptChanges: false,
Annotations: map[string]string{
"platform": "linux",
},
KVStore: &kvstore.Memory{},
MaxRuntime: 0,
NoCollector: true,
NoJSON: true,
Random: false,
ReportFile: "",
Session: newSession(ctx, t),
}
r := NewLinkRunner(config, "https://run.ooni.io/nettest?tn=example&mv=1.2.0")
if err := r.Run(ctx); err != nil {
t.Fatal(err)
}
r = NewLinkRunner(config, "ooni://nettest?tn=example&mv=1.2.0")
if err := r.Run(ctx); err != nil {
t.Fatal(err)
}
}

0 comments on commit 1b63a87

Please sign in to comment.