dfa5e708fe
This diff rewrites the tor experiment to use measurex "easy" API. To this end, we need to introduce an "easy" measurex API, which basically performs easy measurements returning two pieces of data: 1. the resulting measurement, which is already using the OONI archival data format and is always non-nil 2. a failure (i.e., the pointer to an error string), which is nil on success and points to a string on failure With this change, we should now be able to completely dispose of the original netx API, which was only used by tor. Reference issue: https://github.com/ooni/probe/issues/1688.
274 lines
8.0 KiB
Go
274 lines
8.0 KiB
Go
package measurex
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/ptx"
|
|
)
|
|
|
|
//
|
|
// API for reducing boilerplate for simple measurements.
|
|
//
|
|
|
|
// EasyHTTPRoundTripGET performs a GET with the given URL
|
|
// and default headers. This function will perform just
|
|
// a single HTTP round trip (i.e., no redirections).
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - ctx is the context for deadline/timeout/cancellation;
|
|
//
|
|
// - timeout is the timeout for the whole operation;
|
|
//
|
|
// - URL is the URL to GET;
|
|
//
|
|
// Returns:
|
|
//
|
|
// - meas is a JSON serializable OONI measurement (this
|
|
// field will never be a nil pointer);
|
|
//
|
|
// - failure is either nil or a pointer to a OONI failure.
|
|
func (mx *Measurer) EasyHTTPRoundTripGET(ctx context.Context, timeout time.Duration,
|
|
URL string) (meas *ArchivalMeasurement, failure *string) {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout
|
|
defer cancel()
|
|
db := &MeasurementDB{}
|
|
req, err := NewHTTPRequestWithContext(ctx, "GET", URL, nil)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
txp := mx.NewTracingHTTPTransportWithDefaultSettings(db)
|
|
resp, err := txp.RoundTrip(req)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
resp.Body.Close()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), nil
|
|
}
|
|
|
|
// EasyTLSConfig helps you to generate a *tls.Config.
|
|
type EasyTLSConfig struct {
|
|
config *tls.Config
|
|
}
|
|
|
|
// NewEasyTLSConfig creates a new EasyTLSConfig instance.
|
|
func NewEasyTLSConfig() *EasyTLSConfig {
|
|
return &EasyTLSConfig{
|
|
config: &tls.Config{
|
|
RootCAs: netxlite.NewDefaultCertPool(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewEasyTLSConfigWithServerName creates a new EasyTLSConfig
|
|
// with an already configured value for ServerName.
|
|
func NewEasyTLSConfigWithServerName(serverName string) *EasyTLSConfig {
|
|
return NewEasyTLSConfig().ServerName(serverName)
|
|
}
|
|
|
|
// ServerName sets the SNI value.
|
|
func (easy *EasyTLSConfig) ServerName(v string) *EasyTLSConfig {
|
|
easy.config.ServerName = v
|
|
return easy
|
|
}
|
|
|
|
// InsecureSkipVerify disables TLS verification.
|
|
func (easy *EasyTLSConfig) InsecureSkipVerify(v bool) *EasyTLSConfig {
|
|
easy.config.InsecureSkipVerify = v
|
|
return easy
|
|
}
|
|
|
|
// RootCAs allows the set the CA pool.
|
|
func (easy *EasyTLSConfig) RootCAs(v *x509.CertPool) *EasyTLSConfig {
|
|
easy.config.RootCAs = v
|
|
return easy
|
|
}
|
|
|
|
// asTLSConfig converts an *EasyTLSConfig to a *tls.Config.
|
|
func (easy *EasyTLSConfig) asTLSConfig() *tls.Config {
|
|
if easy == nil || easy.config == nil {
|
|
return &tls.Config{}
|
|
}
|
|
return easy.config
|
|
}
|
|
|
|
// EasyTLSConnectAndHandshake performs a TCP connect to a TCP endpoint
|
|
// followed by a TLS handshake using the given config.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - ctx is the context for deadline/timeout/cancellation;
|
|
//
|
|
// - endpoint is the TCP endpoint to connect to (e.g.,
|
|
// 8.8.8.8:443 where the address part of the endpoint MUST
|
|
// be an IPv4 or IPv6 address and MUST NOT be a domain);
|
|
//
|
|
// - tlsConfig is the EasyTLSConfig to use (MUST NOT be nil).
|
|
//
|
|
// Returns:
|
|
//
|
|
// - meas is a JSON serializable OONI measurement (this
|
|
// field will never be a nil pointer);
|
|
//
|
|
// - failure is either nil or a pointer to a OONI failure.
|
|
//
|
|
// Note:
|
|
//
|
|
// - we use the Measurer's TCPConnectTimeout and TLSHandshakeTimeout.
|
|
func (mx *Measurer) EasyTLSConnectAndHandshake(ctx context.Context, endpoint string,
|
|
tlsConfig *EasyTLSConfig) (meas *ArchivalMeasurement, failure *string) {
|
|
// Note: TLSConnectAndHandshakeWithDB uses the timeout configured inside mx.
|
|
db := &MeasurementDB{}
|
|
conn, err := mx.TLSConnectAndHandshakeWithDB(ctx, db, endpoint, tlsConfig.asTLSConfig())
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
conn.Close()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), nil
|
|
}
|
|
|
|
// EasyTCPConnect performs a TCP connect to a TCP endpoint.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - ctx is the context for deadline/timeout/cancellation;
|
|
//
|
|
// - endpoint is the TCP endpoint to connect to (e.g.,
|
|
// 8.8.8.8:443 where the address part of the endpoint MUST
|
|
// be an IPv4 or IPv6 address and MUST NOT be a domain).
|
|
//
|
|
// Returns:
|
|
//
|
|
// - meas is a JSON serializable OONI measurement (this
|
|
// field will never be a nil pointer);
|
|
//
|
|
// - failure is either nil or a pointer to a OONI failure.
|
|
//
|
|
// Note:
|
|
//
|
|
// - we use the Measurer's TCPConnectTimeout.
|
|
func (mx *Measurer) EasyTCPConnect(ctx context.Context,
|
|
endpoint string) (meas *ArchivalMeasurement, failure *string) {
|
|
// Note: TCPConnectWithDB uses the timeout configured inside mx.
|
|
db := &MeasurementDB{}
|
|
conn, err := mx.TCPConnectWithDB(ctx, db, endpoint)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
conn.Close()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), nil
|
|
}
|
|
|
|
// easyOBFS4Params contains params for OBFS4.
|
|
type easyOBFS4Params struct {
|
|
// Cert contains the MANDATORY certificate parameter.
|
|
Cert string
|
|
|
|
// DataDir is the MANDATORY directory where to store obfs4 data.
|
|
DataDir string
|
|
|
|
// Fingerprint is the MANDATORY bridge fingerprint.
|
|
Fingerprint string
|
|
|
|
// IATMode contains the MANDATORY iat-mode parameter.
|
|
IATMode string
|
|
}
|
|
|
|
// newEasyOBFS4Params constructs an EasyOBFS4Params structure
|
|
// from the map[string][]string returned by the OONI API.
|
|
//
|
|
// This function will only fail when the rawParams contains
|
|
// more than one entry for each input key.
|
|
func newEasyOBFS4Params(dataDir string, rawParams map[string][]string) (*easyOBFS4Params, error) {
|
|
out := &easyOBFS4Params{DataDir: dataDir}
|
|
for key, values := range rawParams {
|
|
var field *string
|
|
switch key {
|
|
case "cert":
|
|
field = &out.Cert
|
|
case "fingerprint":
|
|
field = &out.Fingerprint
|
|
case "iat-mode":
|
|
field = &out.IATMode
|
|
default:
|
|
continue // not interested
|
|
}
|
|
if len(values) != 1 {
|
|
return nil, fmt.Errorf("obfs4: expected exactly one value for %s", key)
|
|
}
|
|
*field = values[0]
|
|
}
|
|
// Assume that the API knows what it's returning, so don't bother
|
|
// checking whether some fields are missing. If this happens, it
|
|
// will be the obfs4 library task to tell us about that.
|
|
return out, nil
|
|
}
|
|
|
|
// EasyOBFS4ConnectAndHandshake performs a TCP connect to a TCP endpoint
|
|
// followed by an OBFS4 handshake. This function is designed to receive
|
|
// in input the Tor bridges from the OONI API.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - ctx is the context for deadline/timeout/cancellation;
|
|
//
|
|
// - timeout is the timeout for the whole operation;
|
|
//
|
|
// - endpoint is the TCP endpoint to connect to (e.g.,
|
|
// 8.8.8.8:443 where the address part of the endpoint MUST
|
|
// be an IPv4 or IPv6 address and MUST NOT be a domain);
|
|
//
|
|
// - dataDir is the data directory to use for obfs4;
|
|
//
|
|
// - rawParams contains raw obfs4 params from the OONI API.
|
|
//
|
|
// Returns:
|
|
//
|
|
// - meas is a JSON serializable OONI measurement (this
|
|
// field will never be a nil pointer);
|
|
//
|
|
// - failure is either nil or a pointer to a OONI failure.
|
|
func (mx *Measurer) EasyOBFS4ConnectAndHandshake(ctx context.Context,
|
|
timeout time.Duration, endpoint string, dataDir string,
|
|
rawParams map[string][]string) (meas *ArchivalMeasurement, failure *string) {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout
|
|
defer cancel()
|
|
db := &MeasurementDB{}
|
|
params, err := newEasyOBFS4Params(dataDir, rawParams)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
conn, err := mx.TCPConnectWithDB(ctx, db, endpoint)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
defer conn.Close()
|
|
dialer := netxlite.NewSingleUseDialer(conn)
|
|
obfs4 := ptx.OBFS4Dialer{
|
|
Address: endpoint,
|
|
Cert: params.Cert,
|
|
DataDir: params.DataDir,
|
|
Fingerprint: params.Fingerprint,
|
|
IATMode: params.IATMode,
|
|
UnderlyingDialer: dialer,
|
|
}
|
|
o4conn, err := obfs4.DialContext(ctx)
|
|
if err != nil {
|
|
failure := err.Error()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), &failure
|
|
}
|
|
o4conn.Close()
|
|
return NewArchivalMeasurement(db.AsMeasurement()), nil
|
|
}
|