1package engine
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io/ioutil"
8	"net/http"
9	"net/url"
10	"os"
11	"sync"
12
13	"github.com/ooni/probe-cli/v3/internal/engine/atomicx"
14	"github.com/ooni/probe-cli/v3/internal/engine/geolocate"
15	"github.com/ooni/probe-cli/v3/internal/engine/internal/platform"
16	"github.com/ooni/probe-cli/v3/internal/engine/internal/sessionresolver"
17	"github.com/ooni/probe-cli/v3/internal/engine/kvstore"
18	"github.com/ooni/probe-cli/v3/internal/engine/model"
19	"github.com/ooni/probe-cli/v3/internal/engine/netx"
20	"github.com/ooni/probe-cli/v3/internal/engine/netx/bytecounter"
21	"github.com/ooni/probe-cli/v3/internal/engine/probeservices"
22	"github.com/ooni/probe-cli/v3/internal/engine/tunnel"
23	"github.com/ooni/probe-cli/v3/internal/version"
24)
25
26// SessionConfig contains the Session config
27type SessionConfig struct {
28	AvailableProbeServices []model.Service
29	KVStore                KVStore
30	Logger                 model.Logger
31	ProxyURL               *url.URL
32	SoftwareName           string
33	SoftwareVersion        string
34	TempDir                string
35	TorArgs                []string
36	TorBinary              string
37
38	// TunnelDir is the directory where we should store
39	// the state of persistent tunnels. This field is
40	// optional _unless_ you want to use tunnels. In such
41	// case, starting a tunnel will fail because there
42	// is no directory where to store state.
43	TunnelDir string
44}
45
46// Session is a measurement session. It contains shared information
47// required to run a measurement session, and it controls the lifecycle
48// of such resources. It is not possible to reuse a Session. You MUST
49// NOT attempt to use a Session again after Session.Close.
50type Session struct {
51	availableProbeServices   []model.Service
52	availableTestHelpers     map[string][]model.Service
53	byteCounter              *bytecounter.Counter
54	httpDefaultTransport     netx.HTTPRoundTripper
55	kvStore                  model.KeyValueStore
56	location                 *geolocate.Results
57	logger                   model.Logger
58	proxyURL                 *url.URL
59	queryProbeServicesCount  *atomicx.Int64
60	resolver                 *sessionresolver.Resolver
61	selectedProbeServiceHook func(*model.Service)
62	selectedProbeService     *model.Service
63	softwareName             string
64	softwareVersion          string
65	tempDir                  string
66
67	// closeOnce allows us to call Close just once.
68	closeOnce sync.Once
69
70	// mu provides mutual exclusion.
71	mu sync.Mutex
72
73	// testLookupLocationContext is a an optional hook for testing
74	// allowing us to mock LookupLocationContext.
75	testLookupLocationContext func(ctx context.Context) (*geolocate.Results, error)
76
77	// testMaybeLookupBackendsContext is an optional hook for testing
78	// allowing us to mock MaybeLookupBackendsContext.
79	testMaybeLookupBackendsContext func(ctx context.Context) error
80
81	// testMaybeLookupLocationContext is an optional hook for testing
82	// allowing us to mock MaybeLookupLocationContext.
83	testMaybeLookupLocationContext func(ctx context.Context) error
84
85	// testNewProbeServicesClientForCheckIn is an optional hook for testing
86	// allowing us to mock NewProbeServicesClient when calling CheckIn.
87	testNewProbeServicesClientForCheckIn func(ctx context.Context) (
88		sessionProbeServicesClientForCheckIn, error)
89
90	// torArgs contains the optional arguments for tor that we may need
91	// to pass to urlgetter when it uses a tor tunnel.
92	torArgs []string
93
94	// torBinary contains the optional path to the tor binary that we
95	// may need to pass to urlgetter when it uses a tor tunnel.
96	torBinary string
97
98	// tunnel is the optional tunnel that we may be using. It is created
99	// by NewSession and it is cleaned up by Close.
100	tunnel tunnel.Tunnel
101}
102
103// sessionProbeServicesClientForCheckIn returns the probe services
104// client that we should be using for performing the check-in.
105type sessionProbeServicesClientForCheckIn interface {
106	CheckIn(ctx context.Context, config model.CheckInConfig) (*model.CheckInInfo, error)
107}
108
109// NewSession creates a new session. This factory function will
110// execute the following steps:
111//
112// 1. Make sure the config is sane, apply reasonable defaults
113// where possible, otherwise return an error.
114//
115// 2. Create a temporary directory.
116//
117// 3. Create an instance of the session.
118//
119// 4. If the user requested for a proxy that entails a tunnel (at the
120// moment of writing this note, either psiphon or tor), then start the
121// requested tunnel and configure it as our proxy.
122//
123// 5. Create a compound resolver for the session that will attempt
124// to use a bunch of DoT/DoH servers before falling back to the system
125// resolver if nothing else works (see the sessionresolver pkg). This
126// sessionresolver will be using the configured proxy, if any.
127//
128// 6. Create the default HTTP transport that we should be using when
129// we communicate with the OONI backends. This transport will be
130// using the configured proxy, if any.
131//
132// If any of these steps fails, then we cannot create a measurement
133// session and we return an error.
134func NewSession(ctx context.Context, config SessionConfig) (*Session, error) {
135	if config.Logger == nil {
136		return nil, errors.New("Logger is empty")
137	}
138	if config.SoftwareName == "" {
139		return nil, errors.New("SoftwareName is empty")
140	}
141	if config.SoftwareVersion == "" {
142		return nil, errors.New("SoftwareVersion is empty")
143	}
144	if config.KVStore == nil {
145		config.KVStore = kvstore.NewMemoryKeyValueStore()
146	}
147	// Implementation note: if config.TempDir is empty, then Go will
148	// use the temporary directory on the current system. This should
149	// work on Desktop. We tested that it did also work on iOS, but
150	// we have also seen on 2020-06-10 that it does not work on Android.
151	tempDir, err := ioutil.TempDir(config.TempDir, "ooniengine")
152	if err != nil {
153		return nil, err
154	}
155	sess := &Session{
156		availableProbeServices:  config.AvailableProbeServices,
157		byteCounter:             bytecounter.New(),
158		kvStore:                 config.KVStore,
159		logger:                  config.Logger,
160		queryProbeServicesCount: atomicx.NewInt64(),
161		softwareName:            config.SoftwareName,
162		softwareVersion:         config.SoftwareVersion,
163		tempDir:                 tempDir,
164		torArgs:                 config.TorArgs,
165		torBinary:               config.TorBinary,
166	}
167	proxyURL := config.ProxyURL
168	if proxyURL != nil {
169		switch proxyURL.Scheme {
170		case "psiphon", "tor", "fake":
171			config.Logger.Infof(
172				"starting '%s' tunnel; please be patient...", proxyURL.Scheme)
173			tunnel, err := tunnel.Start(ctx, &tunnel.Config{
174				Logger:    config.Logger,
175				Name:      proxyURL.Scheme,
176				Session:   &sessionTunnelEarlySession{},
177				TorArgs:   config.TorArgs,
178				TorBinary: config.TorBinary,
179				TunnelDir: config.TunnelDir,
180			})
181			if err != nil {
182				return nil, err
183			}
184			config.Logger.Infof("tunnel '%s' running...", proxyURL.Scheme)
185			sess.tunnel = tunnel
186			proxyURL = tunnel.SOCKS5ProxyURL()
187		}
188	}
189	sess.proxyURL = proxyURL
190	httpConfig := netx.Config{
191		ByteCounter:  sess.byteCounter,
192		BogonIsError: true,
193		Logger:       sess.logger,
194		ProxyURL:     proxyURL,
195	}
196	sess.resolver = &sessionresolver.Resolver{
197		ByteCounter: sess.byteCounter,
198		KVStore:     config.KVStore,
199		Logger:      sess.logger,
200		ProxyURL:    proxyURL,
201	}
202	httpConfig.FullResolver = sess.resolver
203	sess.httpDefaultTransport = netx.NewHTTPTransport(httpConfig)
204	return sess, nil
205}
206
207// KibiBytesReceived accounts for the KibiBytes received by the HTTP clients
208// managed by this session so far, including experiments.
209func (s *Session) KibiBytesReceived() float64 {
210	return s.byteCounter.KibiBytesReceived()
211}
212
213// KibiBytesSent is like KibiBytesReceived but for the bytes sent.
214func (s *Session) KibiBytesSent() float64 {
215	return s.byteCounter.KibiBytesSent()
216}
217
218// CheckIn calls the check-in API. The input arguments MUST NOT
219// be nil. Before querying the API, this function will ensure
220// that the config structure does not contain any field that
221// SHOULD be initialized and is not initialized. Whenever there
222// is a field that is not initialized, we will attempt to set
223// a reasonable default value for such a field. This list describes
224// the current defaults we'll choose:
225//
226// - Platform: if empty, set to Session.Platform();
227//
228// - ProbeASN: if empty, set to Session.ProbeASNString();
229//
230// - ProbeCC: if empty, set to Session.ProbeCC();
231//
232// - RunType: if empty, set to "timed";
233//
234// - SoftwareName: if empty, set to Session.SoftwareName();
235//
236// - SoftwareVersion: if empty, set to Session.SoftwareVersion();
237//
238// - WebConnectivity.CategoryCodes: if nil, we will allocate
239// an empty array (the API does not like nil).
240//
241// Because we MAY need to know the current ASN and CC, this
242// function MAY call MaybeLookupLocationContext.
243//
244// The return value is either the check-in response or an error.
245func (s *Session) CheckIn(
246	ctx context.Context, config *model.CheckInConfig) (*model.CheckInInfo, error) {
247	if err := s.maybeLookupLocationContext(ctx); err != nil {
248		return nil, err
249	}
250	client, err := s.newProbeServicesClientForCheckIn(ctx)
251	if err != nil {
252		return nil, err
253	}
254	if config.Platform == "" {
255		config.Platform = s.Platform()
256	}
257	if config.ProbeASN == "" {
258		config.ProbeASN = s.ProbeASNString()
259	}
260	if config.ProbeCC == "" {
261		config.ProbeCC = s.ProbeCC()
262	}
263	if config.RunType == "" {
264		config.RunType = "timed" // most conservative choice
265	}
266	if config.SoftwareName == "" {
267		config.SoftwareName = s.SoftwareName()
268	}
269	if config.SoftwareVersion == "" {
270		config.SoftwareVersion = s.SoftwareVersion()
271	}
272	if config.WebConnectivity.CategoryCodes == nil {
273		config.WebConnectivity.CategoryCodes = []string{}
274	}
275	return client.CheckIn(ctx, *config)
276}
277
278// maybeLookupLocationContext is a wrapper for MaybeLookupLocationContext that calls
279// the configurable testMaybeLookupLocationContext mock, if configured, and the
280// real MaybeLookupLocationContext API otherwise.
281func (s *Session) maybeLookupLocationContext(ctx context.Context) error {
282	if s.testMaybeLookupLocationContext != nil {
283		return s.testMaybeLookupLocationContext(ctx)
284	}
285	return s.MaybeLookupLocationContext(ctx)
286}
287
288// newProbeServicesClientForCheckIn is a wrapper for NewProbeServicesClientForCheckIn
289// that calls the configurable testNewProbeServicesClientForCheckIn mock, if
290// configured, and the real NewProbeServicesClient API otherwise.
291func (s *Session) newProbeServicesClientForCheckIn(
292	ctx context.Context) (sessionProbeServicesClientForCheckIn, error) {
293	if s.testNewProbeServicesClientForCheckIn != nil {
294		return s.testNewProbeServicesClientForCheckIn(ctx)
295	}
296	client, err := s.NewProbeServicesClient(ctx)
297	if err != nil {
298		return nil, err
299	}
300	return client, nil
301}
302
303// Close ensures that we close all the idle connections that the HTTP clients
304// we are currently using may have created. It will also remove the temp dir
305// that contains data from this session. Not calling this function may likely
306// cause memory leaks in your application because of open idle connections,
307// as well as excessive usage of disk space.
308func (s *Session) Close() error {
309	s.closeOnce.Do(s.doClose)
310	return nil
311}
312
313// doClose implements Close. This function is called just once.
314func (s *Session) doClose() {
315	s.httpDefaultTransport.CloseIdleConnections()
316	s.resolver.CloseIdleConnections()
317	s.logger.Infof("%s", s.resolver.Stats())
318	if s.tunnel != nil {
319		s.tunnel.Stop()
320	}
321	_ = os.RemoveAll(s.tempDir)
322}
323
324// GetTestHelpersByName returns the available test helpers that
325// use the specified name, or false if there's none.
326func (s *Session) GetTestHelpersByName(name string) ([]model.Service, bool) {
327	defer s.mu.Unlock()
328	s.mu.Lock()
329	services, ok := s.availableTestHelpers[name]
330	return services, ok
331}
332
333// DefaultHTTPClient returns the session's default HTTP client.
334func (s *Session) DefaultHTTPClient() *http.Client {
335	return &http.Client{Transport: s.httpDefaultTransport}
336}
337
338// FetchTorTargets fetches tor targets from the API.
339func (s *Session) FetchTorTargets(
340	ctx context.Context, cc string) (map[string]model.TorTarget, error) {
341	clnt, err := s.NewOrchestraClient(ctx)
342	if err != nil {
343		return nil, err
344	}
345	return clnt.FetchTorTargets(ctx, cc)
346}
347
348// FetchURLList fetches the URL list from the API.
349func (s *Session) FetchURLList(
350	ctx context.Context, config model.URLListConfig) ([]model.URLInfo, error) {
351	clnt, err := s.NewOrchestraClient(ctx)
352	if err != nil {
353		return nil, err
354	}
355	return clnt.FetchURLList(ctx, config)
356}
357
358// KeyValueStore returns the configured key-value store.
359func (s *Session) KeyValueStore() model.KeyValueStore {
360	return s.kvStore
361}
362
363// Logger returns the logger used by the session.
364func (s *Session) Logger() model.Logger {
365	return s.logger
366}
367
368// MaybeLookupLocation is a caching location lookup call.
369func (s *Session) MaybeLookupLocation() error {
370	return s.MaybeLookupLocationContext(context.Background())
371}
372
373// MaybeLookupBackends is a caching OONI backends lookup call.
374func (s *Session) MaybeLookupBackends() error {
375	return s.MaybeLookupBackendsContext(context.Background())
376}
377
378// ErrAlreadyUsingProxy indicates that we cannot create a tunnel with
379// a specific name because we already configured a proxy.
380var ErrAlreadyUsingProxy = errors.New(
381	"session: cannot create a new tunnel of this kind: we are already using a proxy",
382)
383
384// NewExperimentBuilder returns a new experiment builder
385// for the experiment with the given name, or an error if
386// there's no such experiment with the given name
387func (s *Session) NewExperimentBuilder(name string) (*ExperimentBuilder, error) {
388	return newExperimentBuilder(s, name)
389}
390
391// NewProbeServicesClient creates a new client for talking with the
392// OONI probe services. This function will benchmark the available
393// probe services, and select the fastest. In case all probe services
394// seem to be down, we try again applying circumvention tactics.
395// This function will fail IMMEDIATELY if given a cancelled context.
396func (s *Session) NewProbeServicesClient(ctx context.Context) (*probeservices.Client, error) {
397	if ctx.Err() != nil {
398		return nil, ctx.Err() // helps with testing
399	}
400	if err := s.maybeLookupBackendsContext(ctx); err != nil {
401		return nil, err
402	}
403	if err := s.maybeLookupLocationContext(ctx); err != nil {
404		return nil, err
405	}
406	if s.selectedProbeServiceHook != nil {
407		s.selectedProbeServiceHook(s.selectedProbeService)
408	}
409	return probeservices.NewClient(s, *s.selectedProbeService)
410}
411
412// NewSubmitter creates a new submitter instance.
413func (s *Session) NewSubmitter(ctx context.Context) (Submitter, error) {
414	psc, err := s.NewProbeServicesClient(ctx)
415	if err != nil {
416		return nil, err
417	}
418	return probeservices.NewSubmitter(psc, s.Logger()), nil
419}
420
421// NewOrchestraClient creates a new orchestra client. This client is registered
422// and logged in with the OONI orchestra. An error is returned on failure.
423//
424// This function is DEPRECATED. New code SHOULD NOT use it. It will eventually
425// be made private or entirely removed from the codebase.
426func (s *Session) NewOrchestraClient(ctx context.Context) (*probeservices.Client, error) {
427	clnt, err := s.NewProbeServicesClient(ctx)
428	if err != nil {
429		return nil, err
430	}
431	return s.initOrchestraClient(ctx, clnt, clnt.MaybeLogin)
432}
433
434// Platform returns the current platform. The platform is one of:
435//
436// - android
437// - ios
438// - linux
439// - macos
440// - windows
441// - unknown
442//
443// When running on the iOS simulator, the returned platform is
444// macos rather than ios if CGO is disabled. This is a known issue,
445// that however should have a very limited impact.
446func (s *Session) Platform() string {
447	return platform.Name()
448}
449
450// ProbeASNString returns the probe ASN as a string.
451func (s *Session) ProbeASNString() string {
452	return fmt.Sprintf("AS%d", s.ProbeASN())
453}
454
455// ProbeASN returns the probe ASN as an integer.
456func (s *Session) ProbeASN() uint {
457	defer s.mu.Unlock()
458	s.mu.Lock()
459	asn := geolocate.DefaultProbeASN
460	if s.location != nil {
461		asn = s.location.ASN
462	}
463	return asn
464}
465
466// ProbeCC returns the probe CC.
467func (s *Session) ProbeCC() string {
468	defer s.mu.Unlock()
469	s.mu.Lock()
470	cc := geolocate.DefaultProbeCC
471	if s.location != nil {
472		cc = s.location.CountryCode
473	}
474	return cc
475}
476
477// ProbeNetworkName returns the probe network name.
478func (s *Session) ProbeNetworkName() string {
479	defer s.mu.Unlock()
480	s.mu.Lock()
481	nn := geolocate.DefaultProbeNetworkName
482	if s.location != nil {
483		nn = s.location.NetworkName
484	}
485	return nn
486}
487
488// ProbeIP returns the probe IP.
489func (s *Session) ProbeIP() string {
490	defer s.mu.Unlock()
491	s.mu.Lock()
492	ip := geolocate.DefaultProbeIP
493	if s.location != nil {
494		ip = s.location.ProbeIP
495	}
496	return ip
497}
498
499// ProxyURL returns the Proxy URL, or nil if not set
500func (s *Session) ProxyURL() *url.URL {
501	return s.proxyURL
502}
503
504// ResolverASNString returns the resolver ASN as a string
505func (s *Session) ResolverASNString() string {
506	return fmt.Sprintf("AS%d", s.ResolverASN())
507}
508
509// ResolverASN returns the resolver ASN
510func (s *Session) ResolverASN() uint {
511	defer s.mu.Unlock()
512	s.mu.Lock()
513	asn := geolocate.DefaultResolverASN
514	if s.location != nil {
515		asn = s.location.ResolverASN
516	}
517	return asn
518}
519
520// ResolverIP returns the resolver IP
521func (s *Session) ResolverIP() string {
522	defer s.mu.Unlock()
523	s.mu.Lock()
524	ip := geolocate.DefaultResolverIP
525	if s.location != nil {
526		ip = s.location.ResolverIP
527	}
528	return ip
529}
530
531// ResolverNetworkName returns the resolver network name.
532func (s *Session) ResolverNetworkName() string {
533	defer s.mu.Unlock()
534	s.mu.Lock()
535	nn := geolocate.DefaultResolverNetworkName
536	if s.location != nil {
537		nn = s.location.ResolverNetworkName
538	}
539	return nn
540}
541
542// SoftwareName returns the application name.
543func (s *Session) SoftwareName() string {
544	return s.softwareName
545}
546
547// SoftwareVersion returns the application version.
548func (s *Session) SoftwareVersion() string {
549	return s.softwareVersion
550}
551
552// TempDir returns the temporary directory.
553func (s *Session) TempDir() string {
554	return s.tempDir
555}
556
557// TorArgs returns the configured extra args for the tor binary. If not set
558// we will not pass in any extra arg. Applies to `-OTunnel=tor` mainly.
559func (s *Session) TorArgs() []string {
560	return s.torArgs
561}
562
563// TorBinary returns the configured path to the tor binary. If not set
564// we will attempt to use "tor". Applies to `-OTunnel=tor` mainly.
565func (s *Session) TorBinary() string {
566	return s.torBinary
567}
568
569// UserAgent constructs the user agent to be used in this session.
570func (s *Session) UserAgent() (useragent string) {
571	useragent += s.softwareName + "/" + s.softwareVersion
572	useragent += " ooniprobe-engine/" + version.Version
573	return
574}
575
576// getAvailableProbeServicesUnlocked returns the available probe
577// services. This function WILL NOT acquire the mu mutex, therefore,
578// you MUST ensure you are using it from a locked context.
579func (s *Session) getAvailableProbeServicesUnlocked() []model.Service {
580	if len(s.availableProbeServices) > 0 {
581		return s.availableProbeServices
582	}
583	return probeservices.Default()
584}
585
586func (s *Session) initOrchestraClient(
587	ctx context.Context, clnt *probeservices.Client,
588	maybeLogin func(ctx context.Context) error,
589) (*probeservices.Client, error) {
590	// The original implementation has as its only use case that we
591	// were registering and logging in for sending an update regarding
592	// the probe whereabouts. Yet here in probe-engine, the orchestra
593	// is currently only used to fetch inputs. For this purpose, we don't
594	// need to communicate any specific information. The code that will
595	// perform an update used to be responsible of doing that. Now, we
596	// are not using orchestra for this purpose anymore.
597	meta := probeservices.Metadata{
598		Platform:        "miniooni",
599		ProbeASN:        "AS0",
600		ProbeCC:         "ZZ",
601		SoftwareName:    "miniooni",
602		SoftwareVersion: "0.1.0-dev",
603		SupportedTests:  []string{"web_connectivity"},
604	}
605	if err := clnt.MaybeRegister(ctx, meta); err != nil {
606		return nil, err
607	}
608	if err := maybeLogin(ctx); err != nil {
609		return nil, err
610	}
611	return clnt, nil
612}
613
614// ErrAllProbeServicesFailed indicates all probe services failed.
615var ErrAllProbeServicesFailed = errors.New("all available probe services failed")
616
617// maybeLookupBackendsContext uses testMaybeLookupBackendsContext if
618// not nil, otherwise it calls MaybeLookupBackendsContext.
619func (s *Session) maybeLookupBackendsContext(ctx context.Context) error {
620	if s.testMaybeLookupBackendsContext != nil {
621		return s.testMaybeLookupBackendsContext(ctx)
622	}
623	return s.MaybeLookupBackendsContext(ctx)
624}
625
626// MaybeLookupBackendsContext is like MaybeLookupBackends but with context.
627func (s *Session) MaybeLookupBackendsContext(ctx context.Context) error {
628	defer s.mu.Unlock()
629	s.mu.Lock()
630	if s.selectedProbeService != nil {
631		return nil
632	}
633	s.queryProbeServicesCount.Add(1)
634	candidates := probeservices.TryAll(ctx, s, s.getAvailableProbeServicesUnlocked())
635	selected := probeservices.SelectBest(candidates)
636	if selected == nil {
637		return ErrAllProbeServicesFailed
638	}
639	s.logger.Infof("session: using probe services: %+v", selected.Endpoint)
640	s.selectedProbeService = &selected.Endpoint
641	s.availableTestHelpers = selected.TestHelpers
642	return nil
643}
644
645// LookupLocationContext performs a location lookup. If you want memoisation
646// of the results, you should use MaybeLookupLocationContext.
647func (s *Session) LookupLocationContext(ctx context.Context) (*geolocate.Results, error) {
648	task := geolocate.Must(geolocate.NewTask(geolocate.Config{
649		Logger:    s.Logger(),
650		Resolver:  s.resolver,
651		UserAgent: s.UserAgent(),
652	}))
653	return task.Run(ctx)
654}
655
656// lookupLocationContext calls testLookupLocationContext if set and
657// otherwise calls LookupLocationContext.
658func (s *Session) lookupLocationContext(ctx context.Context) (*geolocate.Results, error) {
659	if s.testLookupLocationContext != nil {
660		return s.testLookupLocationContext(ctx)
661	}
662	return s.LookupLocationContext(ctx)
663}
664
665// MaybeLookupLocationContext is like MaybeLookupLocation but with a context
666// that can be used to interrupt this long running operation. This function
667// will fail IMMEDIATELY if given a cancelled context.
668func (s *Session) MaybeLookupLocationContext(ctx context.Context) error {
669	if ctx.Err() != nil {
670		return ctx.Err() // helps with testing
671	}
672	defer s.mu.Unlock()
673	s.mu.Lock()
674	if s.location == nil {
675		location, err := s.lookupLocationContext(ctx)
676		if err != nil {
677			return err
678		}
679		s.location = location
680	}
681	return nil
682}
683
684var _ model.ExperimentSession = &Session{}
685