ooni-probe-cli/internal/measurex/easy.go

274 lines
8.0 KiB
Go
Raw Normal View History

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
}