318 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package ooni
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	_ "embed" // because we embed a file
 | 
						|
	"io/ioutil"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"os/signal"
 | 
						|
	"syscall"
 | 
						|
 | 
						|
	"github.com/apex/log"
 | 
						|
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/config"
 | 
						|
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/utils"
 | 
						|
	"github.com/ooni/probe-cli/v3/internal/atomicx"
 | 
						|
	"github.com/ooni/probe-cli/v3/internal/database"
 | 
						|
	"github.com/ooni/probe-cli/v3/internal/engine"
 | 
						|
	"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/pkg/errors"
 | 
						|
)
 | 
						|
 | 
						|
// DefaultSoftwareName is the default software name.
 | 
						|
const DefaultSoftwareName = "ooniprobe-cli"
 | 
						|
 | 
						|
// logger is the logger used by the engine.
 | 
						|
var logger = log.WithFields(log.Fields{
 | 
						|
	"type": "engine",
 | 
						|
})
 | 
						|
 | 
						|
// ProbeCLI is the OONI Probe CLI context.
 | 
						|
type ProbeCLI interface {
 | 
						|
	Config() *config.Config
 | 
						|
	DB() *database.Database
 | 
						|
	IsBatch() bool
 | 
						|
	Home() string
 | 
						|
	TempDir() string
 | 
						|
	NewProbeEngine(ctx context.Context, runType model.RunType) (ProbeEngine, error)
 | 
						|
}
 | 
						|
 | 
						|
// ProbeEngine is an instance of the OONI Probe engine.
 | 
						|
type ProbeEngine interface {
 | 
						|
	Close() error
 | 
						|
	MaybeLookupLocation() error
 | 
						|
	ProbeASNString() string
 | 
						|
	ProbeCC() string
 | 
						|
	ProbeIP() string
 | 
						|
	ProbeNetworkName() string
 | 
						|
}
 | 
						|
 | 
						|
// Probe contains the ooniprobe CLI context.
 | 
						|
type Probe struct {
 | 
						|
	config  *config.Config
 | 
						|
	db      *database.Database
 | 
						|
	isBatch bool
 | 
						|
 | 
						|
	home      string
 | 
						|
	tempDir   string
 | 
						|
	tunnelDir string
 | 
						|
 | 
						|
	dbPath     string
 | 
						|
	configPath string
 | 
						|
 | 
						|
	isTerminated *atomicx.Int64
 | 
						|
 | 
						|
	softwareName    string
 | 
						|
	softwareVersion string
 | 
						|
	proxyURL        *url.URL
 | 
						|
}
 | 
						|
 | 
						|
// SetIsBatch sets the value of isBatch.
 | 
						|
func (p *Probe) SetIsBatch(v bool) {
 | 
						|
	p.isBatch = v
 | 
						|
}
 | 
						|
 | 
						|
// IsBatch returns whether we're running in batch mode.
 | 
						|
func (p *Probe) IsBatch() bool {
 | 
						|
	return p.isBatch
 | 
						|
}
 | 
						|
 | 
						|
// Config returns the configuration
 | 
						|
func (p *Probe) Config() *config.Config {
 | 
						|
	return p.config
 | 
						|
}
 | 
						|
 | 
						|
// DB returns the database we're using
 | 
						|
func (p *Probe) DB() *database.Database {
 | 
						|
	return p.db
 | 
						|
}
 | 
						|
 | 
						|
// Home returns the home directory.
 | 
						|
func (p *Probe) Home() string {
 | 
						|
	return p.home
 | 
						|
}
 | 
						|
 | 
						|
// TempDir returns the temporary directory.
 | 
						|
func (p *Probe) TempDir() string {
 | 
						|
	return p.tempDir
 | 
						|
}
 | 
						|
 | 
						|
// IsTerminated checks to see if the isTerminatedAtomicInt is set to a non zero
 | 
						|
// value and therefore we have received the signal to shutdown the running test
 | 
						|
func (p *Probe) IsTerminated() bool {
 | 
						|
	return p.isTerminated.Load() != 0
 | 
						|
}
 | 
						|
 | 
						|
// Terminate interrupts the running context
 | 
						|
func (p *Probe) Terminate() {
 | 
						|
	p.isTerminated.Add(1)
 | 
						|
}
 | 
						|
 | 
						|
// ListenForSignals will listen for SIGINT and SIGTERM. When it receives those
 | 
						|
// signals it will set isTerminatedAtomicInt to non-zero, which will cleanly
 | 
						|
// shutdown the test logic.
 | 
						|
//
 | 
						|
// TODO refactor this to use a cancellable context.Context instead of a bool
 | 
						|
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
 | 
						|
func (p *Probe) ListenForSignals() {
 | 
						|
	s := make(chan os.Signal, 1)
 | 
						|
	signal.Notify(s, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 | 
						|
	go func() {
 | 
						|
		<-s
 | 
						|
		log.Info("caught a stop signal, shutting down cleanly")
 | 
						|
		p.Terminate()
 | 
						|
	}()
 | 
						|
}
 | 
						|
 | 
						|
// MaybeListenForStdinClosed will treat any error on stdin just
 | 
						|
// like SIGTERM if and only if
 | 
						|
//
 | 
						|
//	os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") == "true"
 | 
						|
//
 | 
						|
// When this feature is enabled, a collateral effect is that we swallow
 | 
						|
// whatever is passed to us on the standard input.
 | 
						|
//
 | 
						|
// See https://github.com/ooni/probe-cli/pull/111 for more info
 | 
						|
// regarding the design of this functionality.
 | 
						|
//
 | 
						|
// TODO refactor this to use a cancellable context.Context instead of a bool
 | 
						|
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
 | 
						|
func (p *Probe) MaybeListenForStdinClosed() {
 | 
						|
	if os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") != "true" {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	go func() {
 | 
						|
		defer p.Terminate()
 | 
						|
		defer log.Info("stdin closed, shutting down cleanly")
 | 
						|
		b := make([]byte, 1<<10)
 | 
						|
		for {
 | 
						|
			if _, err := os.Stdin.Read(b); err != nil {
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
}
 | 
						|
 | 
						|
// Init the OONI manager
 | 
						|
func (p *Probe) Init(softwareName, softwareVersion, proxy string) error {
 | 
						|
	var err error
 | 
						|
 | 
						|
	if err = MaybeInitializeHome(p.home); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if p.configPath != "" {
 | 
						|
		log.Debugf("Reading config file from %s", p.configPath)
 | 
						|
		p.config, err = config.ReadConfig(p.configPath)
 | 
						|
	} else {
 | 
						|
		log.Debug("Reading default config file")
 | 
						|
		p.config, err = InitDefaultConfig(p.home)
 | 
						|
	}
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if err = p.config.MaybeMigrate(); err != nil {
 | 
						|
		return errors.Wrap(err, "migrating config")
 | 
						|
	}
 | 
						|
 | 
						|
	p.dbPath = utils.DBDir(p.home, "main")
 | 
						|
	log.Debugf("Connecting to database sqlite3://%s", p.dbPath)
 | 
						|
	db, err := database.Open(p.dbPath)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	p.db = db
 | 
						|
 | 
						|
	// 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(utils.AssetsDir(p.home))
 | 
						|
 | 
						|
	tempDir, err := ioutil.TempDir("", "ooni")
 | 
						|
	if err != nil {
 | 
						|
		return errors.Wrap(err, "creating TempDir")
 | 
						|
	}
 | 
						|
	p.tempDir = tempDir
 | 
						|
	p.tunnelDir = utils.TunnelDir(p.home)
 | 
						|
 | 
						|
	p.softwareName = softwareName
 | 
						|
	p.softwareVersion = softwareVersion
 | 
						|
	if proxy != "" {
 | 
						|
		p.proxyURL, err = url.Parse(proxy)
 | 
						|
		if err != nil {
 | 
						|
			return errors.Wrap(err, "invalid proxy URL")
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// NewSession creates a new ooni/probe-engine session using the
 | 
						|
// current configuration inside the context. The caller must close
 | 
						|
// the session when done using it, by calling sess.Close().
 | 
						|
func (p *Probe) NewSession(ctx context.Context, runType model.RunType) (*engine.Session, error) {
 | 
						|
	kvstore, err := kvstore.NewFS(
 | 
						|
		utils.EngineDir(p.home),
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(err, "creating engine's kvstore")
 | 
						|
	}
 | 
						|
	if err := os.MkdirAll(p.tunnelDir, 0700); err != nil {
 | 
						|
		return nil, errors.Wrap(err, "creating tunnel dir")
 | 
						|
	}
 | 
						|
	// When the software name is the default software name and we're running
 | 
						|
	// in unattended mode, adjust the software name accordingly.
 | 
						|
	//
 | 
						|
	// See https://github.com/ooni/probe/issues/2081.
 | 
						|
	softwareName := p.softwareName
 | 
						|
	if runType == model.RunTypeTimed && softwareName == DefaultSoftwareName {
 | 
						|
		softwareName = DefaultSoftwareName + "-unattended"
 | 
						|
	}
 | 
						|
	return engine.NewSession(ctx, engine.SessionConfig{
 | 
						|
		KVStore:         kvstore,
 | 
						|
		Logger:          logger,
 | 
						|
		SoftwareName:    softwareName,
 | 
						|
		SoftwareVersion: p.softwareVersion,
 | 
						|
		TempDir:         p.tempDir,
 | 
						|
		TunnelDir:       p.tunnelDir,
 | 
						|
		ProxyURL:        p.proxyURL,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// NewProbeEngine creates a new ProbeEngine instance.
 | 
						|
func (p *Probe) NewProbeEngine(ctx context.Context, runType model.RunType) (ProbeEngine, error) {
 | 
						|
	sess, err := p.NewSession(ctx, runType)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return sess, nil
 | 
						|
}
 | 
						|
 | 
						|
// NewProbe creates a new probe instance.
 | 
						|
func NewProbe(configPath string, homePath string) *Probe {
 | 
						|
	return &Probe{
 | 
						|
		home:         homePath,
 | 
						|
		config:       &config.Config{},
 | 
						|
		configPath:   configPath,
 | 
						|
		isTerminated: &atomicx.Int64{},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// MaybeInitializeHome does the setup for a new OONI Home
 | 
						|
func MaybeInitializeHome(home string) error {
 | 
						|
	for _, d := range utils.RequiredDirs(home) {
 | 
						|
		if _, e := os.Stat(d); e != nil {
 | 
						|
			if err := os.MkdirAll(d, 0700); err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
//go:embed default-config.json
 | 
						|
var defaultConfig []byte
 | 
						|
 | 
						|
// InitDefaultConfig reads the config from common locations or creates it if
 | 
						|
// missing.
 | 
						|
func InitDefaultConfig(home string) (*config.Config, error) {
 | 
						|
	var (
 | 
						|
		err        error
 | 
						|
		c          *config.Config
 | 
						|
		configPath = utils.ConfigPath(home)
 | 
						|
	)
 | 
						|
 | 
						|
	c, err = config.ReadConfig(configPath)
 | 
						|
	if err != nil {
 | 
						|
		if os.IsNotExist(err) {
 | 
						|
			log.Debugf("writing default config to %s", configPath)
 | 
						|
			if err = os.WriteFile(configPath, defaultConfig, 0644); err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			// If the user did the informed consent procedure in
 | 
						|
			// probe-legacy, migrate it over.
 | 
						|
			if utils.DidLegacyInformedConsent() {
 | 
						|
				c, err := config.ReadConfig(configPath)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
				c.Lock()
 | 
						|
				c.InformedConsent = true
 | 
						|
				c.Unlock()
 | 
						|
				if err := c.Write(); err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			return InitDefaultConfig(home)
 | 
						|
		}
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return c, nil
 | 
						|
}
 |