forked from ooni/probe-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
session.go
553 lines (486 loc) · 17.7 KB
/
session.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
package oonimkall
import (
"context"
"encoding/json"
"errors"
"net/url"
"runtime"
"sync"
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/engine"
"github.com/ooni/probe-cli/v3/internal/engine/probeservices"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/legacy/assetsdir"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
// AtomicInt64 allows us to export atomicx.Int64 variables to
// mobile libraries so we can use them in testing.
type AtomicInt64 struct {
*atomicx.Int64
}
// These two variables contain metrics pertaining to the number
// of Sessions and Contexts that are currently being used.
var (
ActiveSessions = &AtomicInt64{&atomicx.Int64{}}
ActiveContexts = &AtomicInt64{&atomicx.Int64{}}
)
// Logger is the logger used by a Session. You should implement a class
// compatible with this interface in Java/ObjC and then save a reference
// to this instance in the SessionConfig object. All log messages that
// the Session will generate will be routed to this Logger.
type Logger interface {
// Debug handles debug messages.
Debug(msg string)
// Info handles informational messages.
Info(msg string)
// Warn handles warning messages.
Warn(msg string)
}
// ExperimentCallbacks contains experiment callbacks.
type ExperimentCallbacks interface {
// OnProgress provides information about an experiment progress.
OnProgress(percentage float64, message string)
}
// SessionConfig contains configuration for a Session. You should
// fill all the mandatory fields and could also optionally fill some of
// the optional fields. Then pass this struct to NewSession.
type SessionConfig struct {
// AssetsDir is the mandatory directory where to store assets
// required by a Session, e.g. MaxMind DB files.
//
// This field is currently deprecated and unused. We will
// remove it when we'll bump the major number.
AssetsDir string
// Logger is the optional logger that will receive all the
// log messages generated by a Session. If this field is nil
// then the session will not emit any log message.
Logger Logger
// Proxy allows you to optionally force a specific proxy
// rather than using no proxy (the default).
//
// Use `psiphon:///` to force using Psiphon with the
// embedded configuration file. Not all builds have
// an embedded configuration file, but OONI builds have
// such a file, so they can use this functionality.
//
// Use `socks5://10.0.0.1:9050/` to connect to a SOCKS5
// proxy running on 10.0.0.1:9050. This could be, for
// example, a suitably configured `tor` instance.
Proxy string
// ProbeServicesURL allows you to optionally force the
// usage of an alternative probe service instance. This setting
// should only be used for implementing integration tests.
ProbeServicesURL string
// SoftwareName is the mandatory name of the application
// that will be using the new Session.
SoftwareName string
// SoftwareVersion is the mandatory version of the application
// that will be using the new Session.
SoftwareVersion string
// StateDir is the mandatory directory where to store state
// information required by a Session.
StateDir string
// TempDir is the mandatory directory where the Session shall
// store temporary files. Among other tasks, Session.Close will
// remove any temporary file created within this Session.
TempDir string
// TunnelDir is the directory where the Session shall store
// persistent data regarding circumvention tunnels. This directory
// is mandatory if you want to use tunnels.
TunnelDir string
// Verbose is optional. If there is a non-null Logger and this
// field is true, then the Logger will also receive Debug messages,
// otherwise it will not receive such messages.
Verbose bool
}
// Session contains shared state for running experiments and/or other
// OONI related task (e.g. geolocation). Note that the Session isn't
// mean to be a long living object. The workflow is to create a Session,
// do the operations you need to do with it now, then make sure it is
// not referenced by other variables, so the Go GC can finalize it. This
// is what you would normally done with Java/ObjC.
type Session struct {
// Hooks for testing (should not appear in Java/ObjC, because they
// cannot be automatically transformed to Java/ObjC code.)
TestingCheckInBeforeNewProbeServicesClient func(ctx *Context)
TestingCheckInBeforeCheckIn func(ctx *Context)
cl []context.CancelFunc
mtx sync.Mutex
submitter *probeservices.Submitter
sessp *engine.Session
}
// NewSession is like NewSessionWithContext but without context. This
// factory is deprecated and will be removed when we bump the major
// version number of ooni/probe-cli.
func NewSession(config *SessionConfig) (*Session, error) {
return newSessionWithContext(context.Background(), config)
}
// NewSessionWithContext creates a new session. You should use a session for running
// a set of operations in a relatively short time frame. You SHOULD NOT create
// a single session and keep it all alive for the whole app lifecyle, since
// the Session code is not specifically designed for this use case.
func NewSessionWithContext(ctx *Context, config *SessionConfig) (*Session, error) {
return newSessionWithContext(ctx.ctx, config)
}
// newSessionWithContext implements NewSessionWithContext.
func newSessionWithContext(ctx context.Context, config *SessionConfig) (*Session, error) {
kvstore, err := kvstore.NewFS(config.StateDir)
if err != nil {
return nil, err
}
// We cleanup the assets files used by versions of ooniprobe
// older than v3.9.0, where we started embedding the assets
// into the binary and use that directly. This cleanup doesn't
// remove the whole directory but only known files inside it
// and then the directory itself, if empty. We explicitly discard
// the return value as it does not matter to us here.
_, _ = assetsdir.Cleanup(config.AssetsDir)
var availableps []model.OOAPIService
if config.ProbeServicesURL != "" {
availableps = append(availableps, model.OOAPIService{
Address: config.ProbeServicesURL,
Type: "https",
})
}
// TODO(bassosimone): write tests for this functionality.
// See https://github.com/ooni/probe/issues/1465.
var proxyURL *url.URL
if config.Proxy != "" {
var err error
proxyURL, err = url.Parse(config.Proxy)
if err != nil {
return nil, err
}
}
engineConfig := engine.SessionConfig{
AvailableProbeServices: availableps,
KVStore: kvstore,
Logger: newLogger(config.Logger, config.Verbose),
ProxyURL: proxyURL,
SoftwareName: config.SoftwareName,
SoftwareVersion: config.SoftwareVersion,
TempDir: config.TempDir,
TunnelDir: config.TunnelDir,
}
sessp, err := engine.NewSession(ctx, engineConfig)
if err != nil {
return nil, err
}
sess := &Session{sessp: sessp}
// We use finalizers to reduce the burden of managing the
// session from languages with a garbage collector.
runtime.SetFinalizer(sess, sessionFinalizer)
ActiveSessions.Add(1)
return sess, nil
}
// sessionFinalizer finalizes a Session. While in general in Go code using a
// finalizer is probably unclean, it seems that using a finalizer when binding
// with Java/ObjC code is actually useful to simplify the apps.
func sessionFinalizer(sess *Session) {
for _, fn := range sess.cl {
fn()
}
sess.sessp.Close() // ignore return value
ActiveSessions.Add(-1)
}
// Context is the context of an operation. You use this context
// to cancel a long running operation by calling Cancel(). Because
// you create a Context from a Session and because the Session is
// keeping track of the Context instances it owns, you do don't
// need to call the Cancel method when you're done.
type Context struct {
cancel context.CancelFunc
ctx context.Context
}
// Cancel cancels pending operations using this context. This method
// is idempotent. Calling it more than once is fine. The first invocation
// cancels the context. Subsequent invocations are no-operations.
func (ctx *Context) Cancel() {
ctx.cancel()
}
// NewContext creates an new interruptible Context.
func (sess *Session) NewContext() *Context {
return sess.NewContextWithTimeout(-1)
}
// NewContextWithTimeout creates an new interruptible Context that will automatically
// cancel itself after the given timeout. Setting a zero or negative timeout implies
// there is no actual timeout configured for the Context, making this invocation
// equivalent to calling NewContext().
func (sess *Session) NewContextWithTimeout(timeout int64) *Context {
sess.mtx.Lock()
defer sess.mtx.Unlock()
ctx, origcancel := newContext(timeout)
ActiveContexts.Add(1)
var once sync.Once
cancel := func() {
once.Do(func() {
ActiveContexts.Add(-1)
origcancel()
})
}
sess.cl = append(sess.cl, cancel)
return &Context{cancel: cancel, ctx: ctx}
}
// GeolocateResults contains the results of session.Geolocate.
type GeolocateResults struct {
// ASN is the autonomous system number.
ASN string
// Country is the country code.
Country string
// IP is the IP address.
IP string
// Org is the commercial name of the ASN.
Org string
}
// MaybeUpdateResources is a legacy stub. It does nothing. We will
// remove it when we're ready to bump the major number.
func (sess *Session) MaybeUpdateResources(ctx *Context) error {
return nil
}
// Geolocate performs a geolocate operation and returns the results.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) Geolocate(ctx *Context) (*GeolocateResults, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
if err != nil {
return nil, err
}
return &GeolocateResults{
ASN: info.ASNString(),
Country: info.CountryCode,
IP: info.ProbeIP,
Org: info.NetworkName,
}, nil
}
// SubmitMeasurementResults contains the results of a single measurement submission
// to the OONI backends using the OONI collector API.
type SubmitMeasurementResults struct {
// UpdateMeasurement is the measurement with updated report ID.
UpdatedMeasurement string
// UpdatedReportID is the report ID used for the measurement.
UpdatedReportID string
}
// Submit submits the given measurement and returns the results.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) Submit(ctx *Context, measurement string) (*SubmitMeasurementResults, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
if sess.submitter == nil {
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
sess.submitter = probeservices.NewSubmitter(psc, sess.sessp.Logger())
}
var mm model.Measurement
if err := json.Unmarshal([]byte(measurement), &mm); err != nil {
return nil, err
}
if err := sess.submitter.Submit(ctx.ctx, &mm); err != nil {
return nil, err
}
data, err := json.Marshal(mm)
runtimex.PanicOnError(err, "json.Marshal should not fail here")
return &SubmitMeasurementResults{
UpdatedMeasurement: string(data),
UpdatedReportID: mm.ReportID,
}, nil
}
// CheckInConfigWebConnectivity contains WebConnectivity
// configuration for the check-in API.
type CheckInConfigWebConnectivity struct {
// CategoryCodes contains zero or more category codes (e.g. "HUMR").
CategoryCodes []string
}
// Add adds a category code to ckw.CategoryCode. This method allows you to
// edit ckw.CategoryCodes, which is inaccessible from Java/ObjC.
func (ckw *CheckInConfigWebConnectivity) Add(cat string) {
ckw.CategoryCodes = append(ckw.CategoryCodes, cat)
}
func (ckw *CheckInConfigWebConnectivity) toModel() model.OOAPICheckInConfigWebConnectivity {
return model.OOAPICheckInConfigWebConnectivity{
CategoryCodes: ckw.CategoryCodes,
}
}
// CheckInConfig contains configuration for the check-in API.
type CheckInConfig struct {
// Charging indicates whether the phone is charging.
Charging bool
// OnWiFi indicates whether the phone is using the Wi-Fi.
OnWiFi bool
// Platform is the mobile platform (e.g. "android")
Platform string
// RunType indicates whether this is an automated (model.RunTypeTimed) run
// or otherwise a manual run initiated by the user.
RunType string
// SoftwareName is the name of the application.
SoftwareName string
// SoftwareVersion is the version of the application.
SoftwareVersion string
// WebConnectivity contains configuration items specific of
// the WebConnectivity experiment.
WebConnectivity *CheckInConfigWebConnectivity
}
// CheckInInfoWebConnectivity contains the WebConnectivity
// specific results of the check-in API call.
type CheckInInfoWebConnectivity struct {
// ReportID is the report ID we should be using.
ReportID string
// URLs contains the list of URLs to measure.
URLs []model.OOAPIURLInfo
}
// URLInfo contains info on a specific URL to measure.
type URLInfo struct {
// CategoryCode is the URL's category code (e.g. "HUMR").
CategoryCode string
// CountryCode is the test list from which this URL
// comes from (e.g. "IT", "FR").
CountryCode string
// URL is the URL itself.
URL string
}
// Size returns the number of URLs included into the result.
func (ckw *CheckInInfoWebConnectivity) Size() int64 {
return int64(len(ckw.URLs))
}
// At returns the URLInfo at index idx. Note that this function will
// return nil/null if the index is out of bounds.
func (ckw *CheckInInfoWebConnectivity) At(idx int64) *URLInfo {
if idx < 0 || int(idx) >= len(ckw.URLs) {
return nil
}
w := ckw.URLs[idx]
return &URLInfo{
CategoryCode: w.CategoryCode,
CountryCode: w.CountryCode,
URL: w.URL,
}
}
func newCheckInInfoWebConnectivity(ckw *model.OOAPICheckInInfoWebConnectivity) *CheckInInfoWebConnectivity {
if ckw == nil {
return nil
}
return &CheckInInfoWebConnectivity{
ReportID: ckw.ReportID,
URLs: ckw.URLs,
}
}
// CheckInInfo contains the result of the check-in API.
type CheckInInfo struct {
// WebConnectivity contains results that are specific to
// the WebConnectivity experiment. This field MAY be null
// if the server's response did not contain any info.
WebConnectivity *CheckInInfoWebConnectivity
}
// CheckIn calls the check-in API. Both ctx and config MUST NOT be nil. This
// function will fail if config is missing required settings. The return value
// is either an error or a valid CheckInInfo instance. Beware that the returned
// object MAY still contain nil fields depending on the server's response.
//
// This function locks the session until it's done. That is, no other operation
// can be performed as long as this function is pending.
func (sess *Session) CheckIn(ctx *Context, config *CheckInConfig) (*CheckInInfo, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
if config.WebConnectivity == nil {
return nil, errors.New("oonimkall: missing webconnectivity config")
}
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
if err != nil {
return nil, err
}
if sess.TestingCheckInBeforeNewProbeServicesClient != nil {
sess.TestingCheckInBeforeNewProbeServicesClient(ctx) // for testing
}
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
if sess.TestingCheckInBeforeCheckIn != nil {
sess.TestingCheckInBeforeCheckIn(ctx) // for testing
}
cfg := model.OOAPICheckInConfig{
Charging: config.Charging,
OnWiFi: config.OnWiFi,
Platform: config.Platform,
ProbeASN: info.ASNString(),
ProbeCC: info.CountryCode,
RunType: model.RunType(config.RunType),
SoftwareVersion: config.SoftwareVersion,
WebConnectivity: config.WebConnectivity.toModel(),
}
result, err := psc.CheckIn(ctx.ctx, cfg)
if err != nil {
return nil, err
}
return &CheckInInfo{
WebConnectivity: newCheckInInfoWebConnectivity(result.WebConnectivity),
}, nil
}
// URLListConfig contains configuration for fetching the URL list.
type URLListConfig struct {
Categories []string // Categories to query for (empty means all)
CountryCode string // CountryCode is the optional country code
Limit int64 // Max number of URLs (<= 0 means no limit)
}
// URLListResult contains the URLs returned from the FetchURL API
type URLListResult struct {
Results []model.OOAPIURLInfo
}
// AddCategory adds category code to the array in URLListConfig
func (ckw *URLListConfig) AddCategory(cat string) {
ckw.Categories = append(ckw.Categories, cat)
}
// At gets the URLInfo at position idx from CheckInInfoWebConnectivity.URLs. It returns
// nil if you are using an outs of bound index.
func (ckw *URLListResult) At(idx int64) *URLInfo {
if idx < 0 || int(idx) >= len(ckw.Results) {
return nil
}
w := ckw.Results[idx]
return &URLInfo{
CategoryCode: w.CategoryCode,
CountryCode: w.CountryCode,
URL: w.URL,
}
}
// Size returns the number of URLs.
func (ckw *URLListResult) Size() int64 {
return int64(len(ckw.Results))
}
// FetchURLList fetches the list of URLs to test
func (sess *Session) FetchURLList(ctx *Context, config *URLListConfig) (*URLListResult, error) {
sess.mtx.Lock()
defer sess.mtx.Unlock()
psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx)
if err != nil {
return nil, err
}
if config.CountryCode == "" {
config.CountryCode = "XX"
info, err := sess.sessp.LookupLocationContext(ctx.ctx)
// TODO(bassosimone): this piece of code feels wrong to me. We don't
// want to continue if we cannot discover the country.
if err == nil && info != nil {
config.CountryCode = info.CountryCode
}
}
cfg := model.OOAPIURLListConfig{
Categories: config.Categories,
CountryCode: config.CountryCode,
Limit: config.Limit,
}
result, err := psc.FetchURLList(ctx.ctx, cfg)
if err != nil {
return nil, err
}
return &URLListResult{
Results: result,
}, nil
}