1package probeservices
2
3import (
4	"context"
5	"time"
6
7	"github.com/ooni/probe-cli/v3/internal/engine/model"
8)
9
10// Default returns the default probe services
11func Default() []model.Service {
12	return []model.Service{{
13		Address: "https://ps1.ooni.io",
14		Type:    "https",
15	}, {
16		Address: "https://ps2.ooni.io",
17		Type:    "https",
18	}, {
19		Front:   "dkyhjv0wpi2dk.cloudfront.net",
20		Type:    "cloudfront",
21		Address: "https://dkyhjv0wpi2dk.cloudfront.net",
22	}}
23}
24
25// SortEndpoints gives priority to https, then cloudfronted, then onion.
26func SortEndpoints(in []model.Service) (out []model.Service) {
27	for _, entry := range in {
28		if entry.Type == "https" {
29			out = append(out, entry)
30		}
31	}
32	for _, entry := range in {
33		if entry.Type == "cloudfront" {
34			out = append(out, entry)
35		}
36	}
37	for _, entry := range in {
38		if entry.Type == "onion" {
39			out = append(out, entry)
40		}
41	}
42	return
43}
44
45// OnlyHTTPS returns the HTTPS endpoints only.
46func OnlyHTTPS(in []model.Service) (out []model.Service) {
47	for _, entry := range in {
48		if entry.Type == "https" {
49			out = append(out, entry)
50		}
51	}
52	return
53}
54
55// OnlyFallbacks returns the fallback endpoints only.
56func OnlyFallbacks(in []model.Service) (out []model.Service) {
57	for _, entry := range SortEndpoints(in) {
58		if entry.Type != "https" {
59			out = append(out, entry)
60		}
61	}
62	return
63}
64
65// Candidate is a candidate probe service.
66type Candidate struct {
67	// Duration is the time it took to access the service.
68	Duration time.Duration
69
70	// Err indicates whether the service works.
71	Err error
72
73	// Endpoint is the service endpoint.
74	Endpoint model.Service
75
76	// TestHelpers contains the data returned by the endpoint.
77	TestHelpers map[string][]model.Service
78}
79
80func (c *Candidate) try(ctx context.Context, sess Session) {
81	client, err := NewClient(sess, c.Endpoint)
82	if err != nil {
83		c.Err = err
84		return
85	}
86	start := time.Now()
87	testhelpers, err := client.GetTestHelpers(ctx)
88	c.Duration = time.Since(start)
89	c.Err = err
90	c.TestHelpers = testhelpers
91	sess.Logger().Debugf("probe services: %+v: %+v %s", c.Endpoint, err, c.Duration)
92}
93
94func try(ctx context.Context, sess Session, svc model.Service) *Candidate {
95	candidate := &Candidate{Endpoint: svc}
96	candidate.try(ctx, sess)
97	return candidate
98}
99
100// TryAll tries all the input services using the provided context and session. It
101// returns a list containing information on each candidate that was tried. We will
102// try all the HTTPS candidates first. So, the beginning of the list will contain
103// all of them, and for each of them you will know whether it worked (by checking the
104// Err field) and how fast it was (by checking the Duration field). You should pick
105// the fastest one that worked. If none of them works, then TryAll will subsequently
106// attempt with all the available fallbacks, and return at the first success. In
107// such case, you will see a list of N failing HTTPS candidates, followed by a single
108// successful fallback candidate (e.g. cloudfronted). If all candidates fail, you
109// see in output a list containing all entries where Err is not nil.
110func TryAll(ctx context.Context, sess Session, in []model.Service) (out []*Candidate) {
111	var found bool
112	for _, svc := range OnlyHTTPS(in) {
113		candidate := try(ctx, sess, svc)
114		out = append(out, candidate)
115		if candidate.Err == nil {
116			found = true
117		}
118	}
119	if !found {
120		for _, svc := range OnlyFallbacks(in) {
121			candidate := try(ctx, sess, svc)
122			out = append(out, candidate)
123			if candidate.Err == nil {
124				return
125			}
126		}
127	}
128	return
129}
130
131// SelectBest selects the best among the candidates. If there is no
132// suitable candidate, then this function returns nil.
133func SelectBest(candidates []*Candidate) (selected *Candidate) {
134	for _, e := range candidates {
135		if e.Err != nil {
136			continue
137		}
138		if selected == nil {
139			selected = e
140			continue
141		}
142		if selected.Duration > e.Duration {
143			selected = e
144			continue
145		}
146	}
147	return
148}
149