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
|
||
|
}
|