1package oonimkall 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "runtime" 9 "sync" 10 11 engine "github.com/ooni/probe-engine" 12 "github.com/ooni/probe-engine/atomicx" 13 "github.com/ooni/probe-engine/internal/runtimex" 14 "github.com/ooni/probe-engine/model" 15 "github.com/ooni/probe-engine/probeservices" 16) 17 18// AtomicInt64 allows us to export atomicx.Int64 variables to 19// mobile libraries so we can use them in testing. 20type AtomicInt64 struct { 21 *atomicx.Int64 22} 23 24// The following two variables contain metrics pertaining to the number 25// of Sessions and Contexts that are currently being used. 26var ( 27 ActiveSessions = &AtomicInt64{atomicx.NewInt64()} 28 ActiveContexts = &AtomicInt64{atomicx.NewInt64()} 29) 30 31// Logger is the logger used by a Session. You should implement a class 32// compatible with this interface in Java/ObjC and then save a reference 33// to this instance in the SessionConfig object. All log messages that 34// the Session will generate will be routed to this Logger. 35type Logger interface { 36 Debug(msg string) 37 Info(msg string) 38 Warn(msg string) 39} 40 41// SessionConfig contains configuration for a Session. You should 42// fill all the mandatory fields and could also optionally fill some of 43// the optional fields. Then pass this struct to NewSession. 44type SessionConfig struct { 45 // AssetsDir is the mandatory directory where to store assets 46 // required by a Session, e.g. MaxMind DB files. 47 AssetsDir string 48 49 // Logger is the optional logger that will receive all the 50 // log messages generated by a Session. If this field is nil 51 // then the session will not emit any log message. 52 Logger Logger 53 54 // ProbeServicesURL allows you to optionally force the 55 // usage of an alternative probe service instance. This setting 56 // should only be used for implementing integration tests. 57 ProbeServicesURL string 58 59 // SoftwareName is the mandatory name of the application 60 // that will be using the new Session. 61 SoftwareName string 62 63 // SoftwareVersion is the mandatory version of the application 64 // that will be using the new Session. 65 SoftwareVersion string 66 67 // StateDir is the mandatory directory where to store state 68 // information required by a Session. 69 StateDir string 70 71 // TempDir is the mandatory directory where the Session shall 72 // store temporary files. Among other tasks, Session.Close will 73 // remove any temporary file created within this Session. 74 TempDir string 75 76 // Verbose is optional. If there is a non-null Logger and this 77 // field is true, then the Logger will also receive Debug messages, 78 // otherwise it will not receive such messages. 79 Verbose bool 80} 81 82// Session contains shared state for running experiments and/or other 83// OONI related task (e.g. geolocation). Note that the Session isn't 84// mean to be a long living object. The workflow is to create a Session, 85// do the operations you need to do with it now, then make sure it is 86// not referenced by other variables, so the Go GC can finalize it. 87// 88// Future directions 89// 90// We will eventually rewrite the code for running new experiments such 91// that a Task will be created from a Session, such that experiments 92// could share the same Session and save geolookups, etc. For now, we 93// are in the suboptimal situations where Tasks create, use, and close 94// their own session, thus running more lookups than needed. 95type Session struct { 96 cl []context.CancelFunc 97 mtx sync.Mutex 98 submitter *probeservices.Submitter 99 sessp *engine.Session 100 101 // Hooks for testing (should not appear in Java/ObjC) 102 TestingCheckInBeforeNewProbeServicesClient func(ctx *Context) 103 TestingCheckInBeforeCheckIn func(ctx *Context) 104} 105 106// NewSession creates a new session. You should use a session for running 107// a set of operations in a relatively short time frame. You SHOULD NOT create 108// a single session and keep it all alive for the whole app lifecyle, since 109// the Session code is not specifically designed for this use case. 110func NewSession(config *SessionConfig) (*Session, error) { 111 kvstore, err := engine.NewFileSystemKVStore(config.StateDir) 112 if err != nil { 113 return nil, err 114 } 115 var availableps []model.Service 116 if config.ProbeServicesURL != "" { 117 availableps = append(availableps, model.Service{ 118 Address: config.ProbeServicesURL, 119 Type: "https", 120 }) 121 } 122 engineConfig := engine.SessionConfig{ 123 AssetsDir: config.AssetsDir, 124 AvailableProbeServices: availableps, 125 KVStore: kvstore, 126 Logger: newLogger(config.Logger, config.Verbose), 127 SoftwareName: config.SoftwareName, 128 SoftwareVersion: config.SoftwareVersion, 129 TempDir: config.TempDir, 130 } 131 sessp, err := engine.NewSession(engineConfig) 132 if err != nil { 133 return nil, err 134 } 135 sess := &Session{sessp: sessp} 136 runtime.SetFinalizer(sess, sessionFinalizer) 137 ActiveSessions.Add(1) 138 return sess, nil 139} 140 141// sessionFinalizer finalizes a Session. While in general in Go code using a 142// finalizer is probably unclean, it seems that using a finalizer when binding 143// with Java/ObjC code is actually useful to simplify the apps. 144func sessionFinalizer(sess *Session) { 145 for _, fn := range sess.cl { 146 fn() 147 } 148 sess.sessp.Close() // ignore return value 149 ActiveSessions.Add(-1) 150} 151 152// Context is the context of an operation. You use this context 153// to cancel a long running operation by calling Cancel(). Because 154// you create a Context from a Session and because the Session is 155// keeping track of the Context instances it owns, you do don't 156// need to call the Cancel method when you're done. 157type Context struct { 158 cancel context.CancelFunc 159 ctx context.Context 160} 161 162// Cancel cancels pending operations using this context. 163func (ctx *Context) Cancel() { 164 ctx.cancel() 165} 166 167// NewContext creates an new interruptible Context. 168func (sess *Session) NewContext() *Context { 169 return sess.NewContextWithTimeout(-1) 170} 171 172// NewContextWithTimeout creates an new interruptible Context that will automatically 173// cancel itself after the given timeout. Setting a zero or negative timeout implies 174// there is no actual timeout configured for the Context. 175func (sess *Session) NewContextWithTimeout(timeout int64) *Context { 176 sess.mtx.Lock() 177 defer sess.mtx.Unlock() 178 ctx, origcancel := newContext(timeout) 179 ActiveContexts.Add(1) 180 var once sync.Once 181 cancel := func() { 182 once.Do(func() { 183 ActiveContexts.Add(-1) 184 origcancel() 185 }) 186 } 187 sess.cl = append(sess.cl, cancel) 188 return &Context{cancel: cancel, ctx: ctx} 189} 190 191// GeolocateResults contains the GeolocateTask results. 192type GeolocateResults struct { 193 // ASN is the autonomous system number. 194 ASN string 195 196 // Country is the country code. 197 Country string 198 199 // IP is the IP address. 200 IP string 201 202 // Org is the commercial name of the ASN. 203 Org string 204} 205 206// MaybeUpdateResources ensures that resources are up to date. 207func (sess *Session) MaybeUpdateResources(ctx *Context) error { 208 sess.mtx.Lock() 209 defer sess.mtx.Unlock() 210 return sess.sessp.MaybeUpdateResources(ctx.ctx) 211} 212 213// Geolocate performs a geolocate operation and returns the results. This method 214// is (in Java terminology) synchronized with the session instance. 215func (sess *Session) Geolocate(ctx *Context) (*GeolocateResults, error) { 216 sess.mtx.Lock() 217 defer sess.mtx.Unlock() 218 info, err := sess.sessp.LookupLocationContext(ctx.ctx) 219 if err != nil { 220 return nil, err 221 } 222 return &GeolocateResults{ 223 ASN: fmt.Sprintf("AS%d", info.ASN), 224 Country: info.CountryCode, 225 IP: info.ProbeIP, 226 Org: info.NetworkName, 227 }, nil 228} 229 230// SubmitMeasurementResults contains the results of a single measurement submission 231// to the OONI backends using the OONI collector API. 232type SubmitMeasurementResults struct { 233 UpdatedMeasurement string 234 UpdatedReportID string 235} 236 237// Submit submits the given measurement and returns the results. This method is (in 238// Java terminology) synchronized with the Session instance. 239func (sess *Session) Submit(ctx *Context, measurement string) (*SubmitMeasurementResults, error) { 240 sess.mtx.Lock() 241 defer sess.mtx.Unlock() 242 if sess.submitter == nil { 243 psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx) 244 if err != nil { 245 return nil, err 246 } 247 sess.submitter = probeservices.NewSubmitter(psc, sess.sessp.Logger()) 248 } 249 var mm model.Measurement 250 if err := json.Unmarshal([]byte(measurement), &mm); err != nil { 251 return nil, err 252 } 253 if err := sess.submitter.Submit(ctx.ctx, &mm); err != nil { 254 return nil, err 255 } 256 data, err := json.Marshal(mm) 257 runtimex.PanicOnError(err, "json.Marshal should not fail here") 258 return &SubmitMeasurementResults{ 259 UpdatedMeasurement: string(data), 260 UpdatedReportID: mm.ReportID, 261 }, nil 262} 263 264// CheckInConfigWebConnectivity is the configuration for the WebConnectivity test 265type CheckInConfigWebConnectivity struct { 266 CategoryCodes []string // CategoryCodes is an array of category codes 267} 268 269// Add a category code to the array in CheckInConfigWebConnectivity 270func (ckw *CheckInConfigWebConnectivity) Add(cat string) { 271 ckw.CategoryCodes = append(ckw.CategoryCodes, cat) 272} 273 274func (ckw *CheckInConfigWebConnectivity) toModel() model.CheckInConfigWebConnectivity { 275 return model.CheckInConfigWebConnectivity{ 276 CategoryCodes: ckw.CategoryCodes, 277 } 278} 279 280// CheckInConfig contains configuration for calling the checkin API. 281type CheckInConfig struct { 282 Charging bool // Charging indicate if the phone is actually charging 283 OnWiFi bool // OnWiFi indicate if the phone is actually connected to a WiFi network 284 Platform string // Platform of the probe 285 RunType string // RunType 286 SoftwareName string // SoftwareName of the probe 287 SoftwareVersion string // SoftwareVersion of the probe 288 WebConnectivity *CheckInConfigWebConnectivity // WebConnectivity class contain an array of categories 289} 290 291// CheckInInfoWebConnectivity contains the array of URLs returned by the checkin API 292type CheckInInfoWebConnectivity struct { 293 ReportID string 294 URLs []model.URLInfo 295} 296 297// URLInfo contains info on a test lists URL 298type URLInfo struct { 299 CategoryCode string 300 CountryCode string 301 URL string 302} 303 304// Size returns the number of URLs. 305func (ckw *CheckInInfoWebConnectivity) Size() int64 { 306 return int64(len(ckw.URLs)) 307} 308 309// At gets the URLInfo at position idx from CheckInInfoWebConnectivity.URLs 310func (ckw *CheckInInfoWebConnectivity) At(idx int64) *URLInfo { 311 if idx < 0 || int(idx) >= len(ckw.URLs) { 312 return nil 313 } 314 w := ckw.URLs[idx] 315 return &URLInfo{ 316 CategoryCode: w.CategoryCode, 317 CountryCode: w.CountryCode, 318 URL: w.URL, 319 } 320} 321 322func newCheckInInfoWebConnectivity(ckw *model.CheckInInfoWebConnectivity) *CheckInInfoWebConnectivity { 323 if ckw == nil { 324 return nil 325 } 326 out := new(CheckInInfoWebConnectivity) 327 out.ReportID = ckw.ReportID 328 out.URLs = ckw.URLs 329 return out 330} 331 332// CheckInInfo contains the return test objects from the checkin API 333type CheckInInfo struct { 334 WebConnectivity *CheckInInfoWebConnectivity 335} 336 337// CheckIn function is called by probes asking if there are tests to be run 338// The config argument contains the mandatory settings. 339// Returns the list of tests to run and the URLs, on success, or an explanatory error, in case of failure. 340func (sess *Session) CheckIn(ctx *Context, config *CheckInConfig) (*CheckInInfo, error) { 341 sess.mtx.Lock() 342 defer sess.mtx.Unlock() 343 if config.WebConnectivity == nil { 344 return nil, errors.New("oonimkall: missing webconnectivity config") 345 } 346 info, err := sess.sessp.LookupLocationContext(ctx.ctx) 347 if err != nil { 348 return nil, err 349 } 350 if sess.TestingCheckInBeforeNewProbeServicesClient != nil { 351 sess.TestingCheckInBeforeNewProbeServicesClient(ctx) 352 } 353 psc, err := sess.sessp.NewProbeServicesClient(ctx.ctx) 354 if err != nil { 355 return nil, err 356 } 357 if sess.TestingCheckInBeforeCheckIn != nil { 358 sess.TestingCheckInBeforeCheckIn(ctx) 359 } 360 cfg := model.CheckInConfig{ 361 Charging: config.Charging, 362 OnWiFi: config.OnWiFi, 363 Platform: config.Platform, 364 ProbeASN: info.ASNString(), 365 ProbeCC: info.CountryCode, 366 RunType: config.RunType, 367 SoftwareVersion: config.SoftwareVersion, 368 WebConnectivity: config.WebConnectivity.toModel(), 369 } 370 result, err := psc.CheckIn(ctx.ctx, cfg) 371 if err != nil { 372 return nil, err 373 } 374 return &CheckInInfo{ 375 WebConnectivity: newCheckInInfoWebConnectivity(result.WebConnectivity), 376 }, nil 377} 378