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