feat: create tunnel inside NewSession (#286)

* feat: create tunnel inside NewSession

We want to create the tunnel when we create the session. This change
allows us to nicely ignore the problem of creating a tunnel when we
already have a proxy, as well as the problem of locking. Everything is
happening, in fact, inside of the NewSession factory.

Modify miniooni such that --tunnel is just syntactic sugar for
--proxy, at least for now. We want, in the future, to teach the
tunnel to possibly use a socks5 proxy.

Because starting a tunnel is a slow operation, we need a context in
NewSession. This causes a bunch of places to change. Not really a big
deal except we need to propagate the changes.

Make sure that the mobile code can create a new session using a
proxy for all the APIs we support.

Make sure all tests are still green and we don't loose coverage of
the various ways in which this code could be used.

This change is part of https://github.com/ooni/probe/issues/985.

* changes after merge

* fix: only keep tests that can hopefully work

While there, identify other places where we should add more
tests or fix integration tests.

Part of https://github.com/ooni/probe/issues/985
This commit is contained in:
Simone Basso 2021-04-05 15:28:13 +02:00 committed by GitHub
parent a849213b59
commit c5ad5eedeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 135 additions and 223 deletions

View File

@ -1,6 +1,8 @@
package geoip package geoip
import ( import (
"context"
"github.com/alecthomas/kingpin" "github.com/alecthomas/kingpin"
"github.com/apex/log" "github.com/apex/log"
"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/cli/root" "github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/cli/root"
@ -34,7 +36,7 @@ func dogeoip(config dogeoipconfig) error {
return err return err
} }
engine, err := probeCLI.NewProbeEngine() engine, err := probeCLI.NewProbeEngine(context.Background())
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,6 +1,7 @@
package nettests package nettests
import ( import (
"context"
"io/ioutil" "io/ioutil"
"path" "path"
"testing" "testing"
@ -34,7 +35,7 @@ func TestCreateContext(t *testing.T) {
func TestRun(t *testing.T) { func TestRun(t *testing.T) {
probe := newOONIProbe(t) probe := newOONIProbe(t)
sess, err := probe.NewSession() sess, err := probe.NewSession(context.Background())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package nettests package nettests
import ( import (
"context"
"sync" "sync"
"time" "time"
@ -57,7 +58,7 @@ func RunGroup(config RunGroupConfig) error {
return nil return nil
} }
sess, err := config.Probe.NewSession() sess, err := config.Probe.NewSession(context.Background())
if err != nil { if err != nil {
log.WithError(err).Error("Failed to create a measurement session") log.WithError(err).Error("Failed to create a measurement session")
return err return err

View File

@ -1,6 +1,7 @@
package ooni package ooni
import ( import (
"context"
_ "embed" // because we embed a file _ "embed" // because we embed a file
"io/ioutil" "io/ioutil"
"os" "os"
@ -26,7 +27,7 @@ type ProbeCLI interface {
IsBatch() bool IsBatch() bool
Home() string Home() string
TempDir() string TempDir() string
NewProbeEngine() (ProbeEngine, error) NewProbeEngine(ctx context.Context) (ProbeEngine, error)
} }
// ProbeEngine is an instance of the OONI Probe engine. // ProbeEngine is an instance of the OONI Probe engine.
@ -201,7 +202,7 @@ func (p *Probe) Init(softwareName, softwareVersion string) error {
// NewSession creates a new ooni/probe-engine session using the // NewSession creates a new ooni/probe-engine session using the
// current configuration inside the context. The caller must close // current configuration inside the context. The caller must close
// the session when done using it, by calling sess.Close(). // the session when done using it, by calling sess.Close().
func (p *Probe) NewSession() (*engine.Session, error) { func (p *Probe) NewSession(ctx context.Context) (*engine.Session, error) {
kvstore, err := engine.NewFileSystemKVStore( kvstore, err := engine.NewFileSystemKVStore(
utils.EngineDir(p.home), utils.EngineDir(p.home),
) )
@ -211,7 +212,7 @@ func (p *Probe) NewSession() (*engine.Session, error) {
if err := os.MkdirAll(utils.TunnelDir(p.home), 0700); err != nil { if err := os.MkdirAll(utils.TunnelDir(p.home), 0700); err != nil {
return nil, errors.Wrap(err, "creating tunnel dir") return nil, errors.Wrap(err, "creating tunnel dir")
} }
return engine.NewSession(engine.SessionConfig{ return engine.NewSession(ctx, engine.SessionConfig{
KVStore: kvstore, KVStore: kvstore,
Logger: enginex.Logger, Logger: enginex.Logger,
SoftwareName: p.softwareName, SoftwareName: p.softwareName,
@ -222,8 +223,8 @@ func (p *Probe) NewSession() (*engine.Session, error) {
} }
// NewProbeEngine creates a new ProbeEngine instance. // NewProbeEngine creates a new ProbeEngine instance.
func (p *Probe) NewProbeEngine() (ProbeEngine, error) { func (p *Probe) NewProbeEngine(ctx context.Context) (ProbeEngine, error) {
sess, err := p.NewSession() sess, err := p.NewSession(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,6 +2,7 @@
package oonitest package oonitest
import ( import (
"context"
"sync" "sync"
"github.com/apex/log" "github.com/apex/log"
@ -60,7 +61,7 @@ func (cli *FakeProbeCLI) TempDir() string {
} }
// NewProbeEngine implements ProbeCLI.NewProbeEngine // NewProbeEngine implements ProbeCLI.NewProbeEngine
func (cli *FakeProbeCLI) NewProbeEngine() (ooni.ProbeEngine, error) { func (cli *FakeProbeCLI) NewProbeEngine(ctx context.Context) (ooni.ProbeEngine, error) {
return cli.FakeProbeEnginePtr, cli.FakeProbeEngineErr return cli.FakeProbeEnginePtr, cli.FakeProbeEngineErr
} }

View File

@ -140,6 +140,10 @@ func fatalIfFalse(cond bool, msg string) {
} }
} }
func fatalIfTrue(cond bool, msg string) {
fatalIfFalse(!cond, msg)
}
// Main is the main function of miniooni. This function parses the command line // Main is the main function of miniooni. This function parses the command line
// options and uses a global state. Use MainWithConfiguration if you want to avoid // options and uses a global state. Use MainWithConfiguration if you want to avoid
// using any global state and relying on command line options. // using any global state and relying on command line options.
@ -273,6 +277,15 @@ of seconds after which to stop running Web Connectivity.
This error message will be removed after 2021-11-01. This error message will be removed after 2021-11-01.
` `
// tunnelAndProxy is the text printed when the user specifies
// both the --tunnel and the --proxy options
const tunnelAndProxy = `USAGE ERROR: The --tunnel option and the --proxy
option cannot be specified at the same time. The --tunnel option is actually
just syntactic sugar for --proxy. Setting --tunnel=psiphon is currently the
equivalent of setting --proxy=psiphon:///. This MAY change in a future version
of miniooni, when we will allow a tunnel to use a proxy.
`
// MainWithConfiguration is the miniooni main with a specific configuration // MainWithConfiguration is the miniooni main with a specific configuration
// represented by the experiment name and the current options. // represented by the experiment name and the current options.
// //
@ -280,6 +293,11 @@ This error message will be removed after 2021-11-01.
// integrate this function to either handle the panic of ignore it. // integrate this function to either handle the panic of ignore it.
func MainWithConfiguration(experimentName string, currentOptions Options) { func MainWithConfiguration(experimentName string, currentOptions Options) {
fatalIfFalse(currentOptions.Limit == 0, limitRemoved) fatalIfFalse(currentOptions.Limit == 0, limitRemoved)
fatalIfTrue(currentOptions.Proxy != "" && currentOptions.Tunnel != "",
tunnelAndProxy)
if currentOptions.Tunnel != "" {
currentOptions.Proxy = fmt.Sprintf("%s:///", currentOptions.Tunnel)
}
ctx := context.Background() ctx := context.Background()
@ -354,7 +372,7 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
}} }}
} }
sess, err := engine.NewSession(config) sess, err := engine.NewSession(ctx, config)
fatalOnError(err, "cannot create measurement session") fatalOnError(err, "cannot create measurement session")
defer func() { defer func() {
sess.Close() sess.Close()
@ -365,9 +383,6 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
}() }()
log.Debugf("miniooni temporary directory: %s", sess.TempDir()) log.Debugf("miniooni temporary directory: %s", sess.TempDir())
err = sess.MaybeStartTunnel(context.Background(), currentOptions.Tunnel)
fatalOnError(err, "cannot start session tunnel")
log.Info("Looking up OONI backends; please be patient...") log.Info("Looking up OONI backends; please be patient...")
err = sess.MaybeLookupBackends() err = sess.MaybeLookupBackends()
fatalOnError(err, "cannot lookup OONI backends") fatalOnError(err, "cannot lookup OONI backends")

View File

@ -221,7 +221,7 @@ func TestComputeEndpointStatsDNSIsLying(t *testing.T) {
} }
func newsession(t *testing.T) model.ExperimentSession { func newsession(t *testing.T) model.ExperimentSession {
sess, err := engine.NewSession(engine.SessionConfig{ sess, err := engine.NewSession(context.Background(), engine.SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org", Address: "https://ams-pg-test.ooni.org",
Type: "https", Type: "https",

View File

@ -558,7 +558,7 @@ func TestTransactCannotReadBody(t *testing.T) {
} }
func newsession(t *testing.T) model.ExperimentSession { func newsession(t *testing.T) model.ExperimentSession {
sess, err := engine.NewSession(engine.SessionConfig{ sess, err := engine.NewSession(context.Background(), engine.SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org", Address: "https://ams-pg-test.ooni.org",
Type: "https", Type: "https",

View File

@ -82,6 +82,10 @@ func (g Getter) Get(ctx context.Context) (TestKeys, error) {
return tk, err return tk, err
} }
// TODO(bassosimone): this mechanism where we count breaks tests
// because now tests are not idempotent anymore. Therefore, we
// SHOULD be creating a temporary directory instead.
var ( var (
// tunnelDirCount counts the number of tunnels started by // tunnelDirCount counts the number of tunnels started by
// the urlgetter package so far. // the urlgetter package so far.

View File

@ -654,6 +654,9 @@ func TestGetterIntegrationHTTPSWithTunnel(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
// TODO(bassosimone): this test is broken. It now requires a
// real Session to work as intended. We didn't notice until now
// because integration tests do not run for every PR.
ctx := context.Background() ctx := context.Background()
g := urlgetter.Getter{ g := urlgetter.Getter{
Config: urlgetter.Config{ Config: urlgetter.Config{

View File

@ -201,7 +201,7 @@ func TestMeasureWithNoAvailableTestHelpers(t *testing.T) {
} }
func newsession(t *testing.T, lookupBackends bool) model.ExperimentSession { func newsession(t *testing.T, lookupBackends bool) model.ExperimentSession {
sess, err := engine.NewSession(engine.SessionConfig{ sess, err := engine.NewSession(context.Background(), engine.SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org", Address: "https://ams-pg-test.ooni.org",
Type: "https", Type: "https",

View File

@ -14,7 +14,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInput(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := engine.NewSession(engine.SessionConfig{ sess, err := engine.NewSession(context.Background(), engine.SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org/", Address: "https://ams-pg-test.ooni.org/",
Type: "https", Type: "https",

View File

@ -236,7 +236,7 @@ func TestInputLoaderInputOrQueryBackendWithInput(t *testing.T) {
} }
func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing.T) { func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing.T) {
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
KVStore: kvstore.NewMemoryKeyValueStore(), KVStore: kvstore.NewMemoryKeyValueStore(),
Logger: log.Log, Logger: log.Log,
SoftwareName: "miniooni", SoftwareName: "miniooni",

View File

@ -63,12 +63,6 @@ type Session struct {
softwareName string softwareName string
softwareVersion string softwareVersion string
tempDir string tempDir string
torArgs []string
torBinary string
tunnelDir string
tunnelMu sync.Mutex
tunnelName string
tunnel tunnel.Tunnel
// closeOnce allows us to call Close just once. // closeOnce allows us to call Close just once.
closeOnce sync.Once closeOnce sync.Once
@ -92,6 +86,18 @@ type Session struct {
// allowing us to mock NewProbeServicesClient when calling CheckIn. // allowing us to mock NewProbeServicesClient when calling CheckIn.
testNewProbeServicesClientForCheckIn func(ctx context.Context) ( testNewProbeServicesClientForCheckIn func(ctx context.Context) (
sessionProbeServicesClientForCheckIn, error) sessionProbeServicesClientForCheckIn, error)
// torArgs contains the optional arguments for tor that we may need
// to pass to urlgetter when it uses a tor tunnel.
torArgs []string
// torBinary contains the optional path to the tor binary that we
// may need to pass to urlgetter when it uses a tor tunnel.
torBinary string
// tunnel is the optional tunnel that we may be using. It is created
// by NewSession and it is cleaned up by Close.
tunnel tunnel.Tunnel
} }
// sessionProbeServicesClientForCheckIn returns the probe services // sessionProbeServicesClientForCheckIn returns the probe services
@ -100,8 +106,32 @@ type sessionProbeServicesClientForCheckIn interface {
CheckIn(ctx context.Context, config model.CheckInConfig) (*model.CheckInInfo, error) CheckIn(ctx context.Context, config model.CheckInConfig) (*model.CheckInInfo, error)
} }
// NewSession creates a new session or returns an error // NewSession creates a new session. This factory function will
func NewSession(config SessionConfig) (*Session, error) { // execute the following steps:
//
// 1. Make sure the config is sane, apply reasonable defaults
// where possible, otherwise return an error.
//
// 2. Create a temporary directory.
//
// 3. Create an instance of the session.
//
// 4. If the user requested for a proxy that entails a tunnel (at the
// moment of writing this note, either psiphon or tor), then start the
// requested tunnel and configure it as our proxy.
//
// 5. Create a compound resolver for the session that will attempt
// to use a bunch of DoT/DoH servers before falling back to the system
// resolver if nothing else works (see the sessionresolver pkg). This
// sessionresolver will be using the configured proxy, if any.
//
// 6. Create the default HTTP transport that we should be using when
// we communicate with the OONI backends. This transport will be
// using the configured proxy, if any.
//
// If any of these steps fails, then we cannot create a measurement
// session and we return an error.
func NewSession(ctx context.Context, config SessionConfig) (*Session, error) {
if config.Logger == nil { if config.Logger == nil {
return nil, errors.New("Logger is empty") return nil, errors.New("Logger is empty")
} }
@ -127,26 +157,43 @@ func NewSession(config SessionConfig) (*Session, error) {
byteCounter: bytecounter.New(), byteCounter: bytecounter.New(),
kvStore: config.KVStore, kvStore: config.KVStore,
logger: config.Logger, logger: config.Logger,
proxyURL: config.ProxyURL,
queryProbeServicesCount: atomicx.NewInt64(), queryProbeServicesCount: atomicx.NewInt64(),
softwareName: config.SoftwareName, softwareName: config.SoftwareName,
softwareVersion: config.SoftwareVersion, softwareVersion: config.SoftwareVersion,
tempDir: tempDir, tempDir: tempDir,
torArgs: config.TorArgs, torArgs: config.TorArgs,
torBinary: config.TorBinary, torBinary: config.TorBinary,
tunnelDir: config.TunnelDir,
} }
proxyURL := config.ProxyURL
if proxyURL != nil {
switch proxyURL.Scheme {
case "psiphon", "tor":
tunnel, err := tunnel.Start(ctx, &tunnel.Config{
Name: proxyURL.Scheme,
Session: &sessionTunnelEarlySession{},
TorArgs: config.TorArgs,
TorBinary: config.TorBinary,
TunnelDir: config.TunnelDir,
})
if err != nil {
return nil, err
}
sess.tunnel = tunnel
proxyURL = tunnel.SOCKS5ProxyURL()
}
}
sess.proxyURL = proxyURL
httpConfig := netx.Config{ httpConfig := netx.Config{
ByteCounter: sess.byteCounter, ByteCounter: sess.byteCounter,
BogonIsError: true, BogonIsError: true,
Logger: sess.logger, Logger: sess.logger,
ProxyURL: config.ProxyURL, ProxyURL: proxyURL,
} }
sess.resolver = &sessionresolver.Resolver{ sess.resolver = &sessionresolver.Resolver{
ByteCounter: sess.byteCounter, ByteCounter: sess.byteCounter,
KVStore: config.KVStore, KVStore: config.KVStore,
Logger: sess.logger, Logger: sess.logger,
ProxyURL: config.ProxyURL, ProxyURL: proxyURL,
} }
httpConfig.FullResolver = sess.resolver httpConfig.FullResolver = sess.resolver
sess.httpDefaultTransport = netx.NewHTTPTransport(httpConfig) sess.httpDefaultTransport = netx.NewHTTPTransport(httpConfig)
@ -330,63 +377,6 @@ var ErrAlreadyUsingProxy = errors.New(
"session: cannot create a new tunnel of this kind: we are already using a proxy", "session: cannot create a new tunnel of this kind: we are already using a proxy",
) )
// MaybeStartTunnel starts the requested tunnel.
//
// This function silently succeeds if we're already using a tunnel with
// the same name or if the requested tunnel name is the empty string. This
// function fails, tho, when we already have a proxy or a tunnel with
// another name and we try to open a tunnel. This function of course also
// fails if we cannot start the requested tunnel. All in all, if you request
// for a tunnel name that is not the empty string and you get a nil error,
// you can be confident that session.ProxyURL() gives you the tunnel URL.
//
// The tunnel will be closed by session.Close().
func (s *Session) MaybeStartTunnel(ctx context.Context, name string) error {
// TODO(bassosimone): see if we can unify tunnelMu and mu.
s.tunnelMu.Lock()
defer s.tunnelMu.Unlock()
if name == "" {
// There is no point in continuing if we know
// we don't need to do anything.
return nil
}
if s.tunnel != nil && s.tunnelName == name {
// We've been asked more than once to start the same tunnel.
return nil
}
if s.proxyURL != nil && name == "" {
// The user configured a proxy and here we're not actually trying
// to start any tunnel since `name` is empty.
// TODO(bassosimone): this if branch is probably useless now
// because we stop above when name is "".
return nil
}
if s.proxyURL != nil || s.tunnel != nil {
// We already have a proxy or we have a different tunnel. Because a tunnel
// sets a proxy, the second check for s.tunnel is for robustness.
return ErrAlreadyUsingProxy
}
s.logger.Infof("starting '%s' tunnel; please be patient...", name)
tunnel, err := tunnel.Start(ctx, &tunnel.Config{
Name: name,
Session: &sessionTunnelEarlySession{},
TorArgs: s.TorArgs(),
TorBinary: s.TorBinary(),
TunnelDir: s.tunnelDir,
})
if err != nil {
return err
}
// Implementation note: tunnel _may_ be NIL here if name is ""
if tunnel == nil {
return nil
}
s.tunnelName = name
s.tunnel = tunnel
s.proxyURL = tunnel.SOCKS5ProxyURL()
return nil
}
// NewExperimentBuilder returns a new experiment builder // NewExperimentBuilder returns a new experiment builder
// for the experiment with the given name, or an error if // for the experiment with the given name, or an error if
// there's no such experiment with the given name // there's no such experiment with the given name

View File

@ -75,7 +75,7 @@ func TestNewSessionBuilderGood(t *testing.T) {
} }
func newSessionMustFail(t *testing.T, config SessionConfig) { func newSessionMustFail(t *testing.T, config SessionConfig) {
sess, err := NewSession(config) sess, err := NewSession(context.Background(), config)
if err == nil { if err == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
@ -88,7 +88,7 @@ func TestSessionTorArgsTorBinary(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org", Address: "https://ams-pg-test.ooni.org",
Type: "https", Type: "https",
@ -120,7 +120,7 @@ func TestSessionTorArgsTorBinary(t *testing.T) {
} }
func newSessionForTestingNoLookupsWithProxyURL(t *testing.T, URL *url.URL) *Session { func newSessionForTestingNoLookupsWithProxyURL(t *testing.T, URL *url.URL) *Session {
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
AvailableProbeServices: []model.Service{{ AvailableProbeServices: []model.Service{{
Address: "https://ams-pg-test.ooni.org", Address: "https://ams-pg-test.ooni.org",
Type: "https", Type: "https",
@ -336,7 +336,7 @@ func TestGetAvailableProbeServices(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
SoftwareName: "ooniprobe-engine", SoftwareName: "ooniprobe-engine",
SoftwareVersion: "0.0.1", SoftwareVersion: "0.0.1",
@ -356,7 +356,7 @@ func TestMaybeLookupBackendsFailure(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
SoftwareName: "ooniprobe-engine", SoftwareName: "ooniprobe-engine",
SoftwareVersion: "0.0.1", SoftwareVersion: "0.0.1",
@ -377,7 +377,7 @@ func TestMaybeLookupTestHelpersIdempotent(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
SoftwareName: "ooniprobe-engine", SoftwareName: "ooniprobe-engine",
SoftwareVersion: "0.0.1", SoftwareVersion: "0.0.1",
@ -402,7 +402,7 @@ func TestAllProbeServicesUnsupported(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := NewSession(SessionConfig{ sess, err := NewSession(context.Background(), SessionConfig{
Logger: model.DiscardLogger, Logger: model.DiscardLogger,
SoftwareName: "ooniprobe-engine", SoftwareName: "ooniprobe-engine",
SoftwareVersion: "0.0.1", SoftwareVersion: "0.0.1",
@ -421,127 +421,8 @@ func TestAllProbeServicesUnsupported(t *testing.T) {
} }
} }
func TestStartTunnelGood(t *testing.T) { // TODO(bassosimone): we should write unit/integration tests
if testing.Short() { // for the new way in which tunnels work.
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx := context.Background()
if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil {
t.Fatal(err)
}
if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil {
t.Fatal(err) // check twice, must be idempotent
}
if sess.ProxyURL() == nil {
t.Fatal("expected non-nil ProxyURL")
}
}
func TestStartTunnelNonexistent(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx := context.Background()
if err := sess.MaybeStartTunnel(ctx, "antani"); err.Error() != "unsupported tunnel" {
t.Fatal("not the error we expected")
}
if sess.ProxyURL() != nil {
t.Fatal("expected nil ProxyURL")
}
}
func TestStartTunnelEmptyString(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx := context.Background()
if sess.MaybeStartTunnel(ctx, "") != nil {
t.Fatal("expected no error here")
}
if sess.ProxyURL() != nil {
t.Fatal("expected nil ProxyURL")
}
}
func TestStartTunnelEmptyStringWithProxy(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
proxyURL := &url.URL{Scheme: "socks5", Host: "127.0.0.1:9050"}
sess := newSessionForTestingNoLookups(t)
sess.proxyURL = proxyURL
defer sess.Close()
ctx := context.Background()
if sess.MaybeStartTunnel(ctx, "") != nil {
t.Fatal("expected no error here")
}
diff := cmp.Diff(proxyURL, sess.ProxyURL())
if diff != "" {
t.Fatal(diff)
}
}
func TestStartTunnelWithAlreadyExistingTunnel(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx := context.Background()
if sess.MaybeStartTunnel(ctx, "psiphon") != nil {
t.Fatal("expected no error here")
}
prev := sess.ProxyURL()
err := sess.MaybeStartTunnel(ctx, "tor")
if !errors.Is(err, ErrAlreadyUsingProxy) {
t.Fatal("expected another error here")
}
cur := sess.ProxyURL()
diff := cmp.Diff(prev, cur)
if diff != "" {
t.Fatal(diff)
}
}
func TestStartTunnelWithAlreadyExistingProxy(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx := context.Background()
orig := &url.URL{Scheme: "socks5", Host: "[::1]:9050"}
sess.proxyURL = orig
err := sess.MaybeStartTunnel(ctx, "psiphon")
if !errors.Is(err, ErrAlreadyUsingProxy) {
t.Fatal("expected another error here")
}
cur := sess.ProxyURL()
diff := cmp.Diff(orig, cur)
if diff != "" {
t.Fatal(diff)
}
}
func TestStartTunnelCanceledContext(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel
err := sess.MaybeStartTunnel(ctx, "psiphon")
if !errors.Is(err, context.Canceled) {
t.Fatal("not the error we expected")
}
}
func TestUserAgentNoProxy(t *testing.T) { func TestUserAgentNoProxy(t *testing.T) {
if testing.Short() { if testing.Short() {

View File

@ -15,7 +15,7 @@ func TestPsiphonStartWithCancelledContext(t *testing.T) {
// can move it inside of the internal tests. // can move it inside of the internal tests.
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately cancel() // fail immediately
sess, err := engine.NewSession(engine.SessionConfig{ sess, err := engine.NewSession(ctx, engine.SessionConfig{
Logger: log.Log, Logger: log.Log,
SoftwareName: "miniooni", SoftwareName: "miniooni",
SoftwareVersion: "0.1.0-dev", SoftwareVersion: "0.1.0-dev",
@ -41,7 +41,8 @@ func TestPsiphonStartStop(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skip test in short mode") t.Skip("skip test in short mode")
} }
sess, err := engine.NewSession(engine.SessionConfig{ ctx := context.Background()
sess, err := engine.NewSession(ctx, engine.SessionConfig{
Logger: log.Log, Logger: log.Log,
SoftwareName: "ooniprobe-engine", SoftwareName: "ooniprobe-engine",
SoftwareVersion: "0.0.1", SoftwareVersion: "0.0.1",

View File

@ -69,7 +69,7 @@ func (r *Runner) hasUnsupportedSettings(logger *ChanLogger) bool {
return false return false
} }
func (r *Runner) newsession(logger *ChanLogger) (*engine.Session, error) { func (r *Runner) newsession(ctx context.Context, logger *ChanLogger) (*engine.Session, error) {
kvstore, err := engine.NewFileSystemKVStore(r.settings.StateDir) kvstore, err := engine.NewFileSystemKVStore(r.settings.StateDir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -88,7 +88,7 @@ func (r *Runner) newsession(logger *ChanLogger) (*engine.Session, error) {
Address: r.settings.Options.ProbeServicesBaseURL, Address: r.settings.Options.ProbeServicesBaseURL,
}} }}
} }
return engine.NewSession(config) return engine.NewSession(ctx, config)
} }
func (r *Runner) contextForExperiment( func (r *Runner) contextForExperiment(
@ -121,7 +121,7 @@ func (r *Runner) Run(ctx context.Context) {
return return
} }
r.emitter.Emit(statusStarted, eventEmpty{}) r.emitter.Emit(statusStarted, eventEmpty{})
sess, err := r.newsession(logger) sess, err := r.newsession(ctx, logger)
if err != nil { if err != nil {
r.emitter.EmitFailureStartup(err.Error()) r.emitter.EmitFailureStartup(err.Error())
return return

View File

@ -116,11 +116,23 @@ type Session struct {
sessp *engine.Session sessp *engine.Session
} }
// NewSession creates a new session. You should use a session for running // 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 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 // 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. // the Session code is not specifically designed for this use case.
func NewSession(config *SessionConfig) (*Session, error) { 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 := engine.NewFileSystemKVStore(config.StateDir) kvstore, err := engine.NewFileSystemKVStore(config.StateDir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -150,7 +162,7 @@ func NewSession(config *SessionConfig) (*Session, error) {
TempDir: config.TempDir, TempDir: config.TempDir,
TunnelDir: config.TunnelDir, TunnelDir: config.TunnelDir,
} }
sessp, err := engine.NewSession(engineConfig) sessp, err := engine.NewSession(ctx, engineConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }