1// Package probeservices contains code to contact OONI probe services.
2//
3// The probe services are HTTPS endpoints distributed across a bunch of data
4// centres implementing a bunch of OONI APIs. When started, OONI will benchmark
5// the available probe services and select the fastest one. Eventually all the
6// possible OONI APIs will run as probe services.
7//
8// This package implements the following APIs:
9//
10// 1. v2.0.0 of the OONI bouncer specification defined
11// in https://github.com/ooni/spec/blob/master/backends/bk-004-bouncer;
12//
13// 2. v2.0.0 of the OONI collector specification defined
14// in https://github.com/ooni/spec/blob/master/backends/bk-003-collector.md;
15//
16// 3. most of the OONI orchestra API: login, register, fetch URLs for
17// the Web Connectivity experiment, input for Tor and Psiphon.
18//
19// Orchestra is a set of OONI APIs for probe orchestration. We currently mainly
20// using it for fetching inputs for the tor, psiphon, and web experiments.
21//
22// In addition, this package also contains code to benchmark the available
23// probe services, discard non working ones, select the fastest.
24package probeservices
25
26import (
27	"errors"
28	"net/http"
29	"net/url"
30
31	"github.com/ooni/probe-cli/v3/internal/engine/atomicx"
32	"github.com/ooni/probe-cli/v3/internal/engine/httpx"
33	"github.com/ooni/probe-cli/v3/internal/engine/model"
34)
35
36var (
37	// ErrUnsupportedEndpoint indicates that we don't support this endpoint type.
38	ErrUnsupportedEndpoint = errors.New("probe services: unsupported endpoint type")
39
40	// ErrUnsupportedCloudFrontAddress indicates that we don't support this
41	// cloudfront address (e.g. wrong scheme, presence of port).
42	ErrUnsupportedCloudFrontAddress = errors.New(
43		"probe services: unsupported cloud front address",
44	)
45
46	// ErrNotRegistered indicates that the probe is not registered
47	// with the OONI orchestra backend.
48	ErrNotRegistered = errors.New("not registered")
49
50	// ErrNotLoggedIn indicates that we are not logged in
51	ErrNotLoggedIn = errors.New("not logged in")
52
53	// ErrInvalidMetadata indicates that the metadata is not valid
54	ErrInvalidMetadata = errors.New("invalid metadata")
55)
56
57// Session is how this package sees a Session.
58type Session interface {
59	DefaultHTTPClient() *http.Client
60	KeyValueStore() model.KeyValueStore
61	Logger() model.Logger
62	ProxyURL() *url.URL
63	UserAgent() string
64}
65
66// Client is a client for the OONI probe services API.
67type Client struct {
68	httpx.Client
69	LoginCalls    *atomicx.Int64
70	RegisterCalls *atomicx.Int64
71	StateFile     StateFile
72}
73
74// GetCredsAndAuth is an utility function that returns the credentials with
75// which we are registered and the token with which we're logged in. If we're
76// not registered or not logged in, an error is returned instead.
77func (c Client) GetCredsAndAuth() (*LoginCredentials, *LoginAuth, error) {
78	state := c.StateFile.Get()
79	creds := state.Credentials()
80	if creds == nil {
81		return nil, nil, ErrNotRegistered
82	}
83	auth := state.Auth()
84	if auth == nil {
85		return nil, nil, ErrNotLoggedIn
86	}
87	return creds, auth, nil
88}
89
90// NewClient creates a new client for the specified probe services endpoint. This
91// function fails, e.g., we don't support the specified endpoint.
92func NewClient(sess Session, endpoint model.Service) (*Client, error) {
93	client := &Client{
94		Client: httpx.Client{
95			BaseURL:    endpoint.Address,
96			HTTPClient: sess.DefaultHTTPClient(),
97			Logger:     sess.Logger(),
98			ProxyURL:   sess.ProxyURL(),
99			UserAgent:  sess.UserAgent(),
100		},
101		LoginCalls:    atomicx.NewInt64(),
102		RegisterCalls: atomicx.NewInt64(),
103		StateFile:     NewStateFile(sess.KeyValueStore()),
104	}
105	switch endpoint.Type {
106	case "https":
107		return client, nil
108	case "cloudfront":
109		// Do the cloudfronting dance. The front must appear inside of the
110		// URL, so that we use it for DNS resolution and SNI. The real domain
111		// must instead appear inside of the Host header.
112		URL, err := url.Parse(client.BaseURL)
113		if err != nil {
114			return nil, err
115		}
116		if URL.Scheme != "https" || URL.Host != URL.Hostname() {
117			return nil, ErrUnsupportedCloudFrontAddress
118		}
119		client.Client.Host = URL.Hostname()
120		URL.Host = endpoint.Front
121		client.BaseURL = URL.String()
122		if _, err := url.Parse(client.BaseURL); err != nil {
123			return nil, err
124		}
125		return client, nil
126	default:
127		return nil, ErrUnsupportedEndpoint
128	}
129}
130