c31591f298
This is the extension of https://github.com/ooni/probe-cli/pull/431, and my final deliverable for GSoC 2021. The diff introduces: 1) The new `testhelper` which supports testing multiple IP endpoints per domain and introduces HTTP/3 control measurements. The specification of the `testhelper` can be found at https://github.com/ooni/spec/pull/219. The `testhelper` algorithm consists of three main steps: * `InitialChecks` verifies that the input URL can be parsed, has an expected scheme, and contains a valid domain name. * `Explore` enumerates all the URLs that it discovers by redirection from the original URL, or by detecting h3 support at the target host. * `Generate` performs a step-by-step measurement of each discovered URL. 2) A prototype of the corresponding new experiment `websteps` which uses the control measurement of the `testhelper` to know which URLs to measure, and what to expect. The prototype does not yet have: * unit and integration tests, * an analysis tool to compare the control and the probe measurement. This PR is my final deliverable as it is the outcome of the trials, considerations and efforts of my GSoC weeks at OONI. It fully integrates HTTP/3 (QUIC) support which has been only used in the `urlgetter` experiment until now. Related issues: https://github.com/ooni/probe/issues/1729 and https://github.com/ooni/probe/issues/1733.
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package websteps
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/lucas-clemente/quic-go"
|
|
"github.com/lucas-clemente/quic-go/http3"
|
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/quicdialer"
|
|
"github.com/ooni/probe-cli/v3/internal/errorsx"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
)
|
|
|
|
var ErrNoConnReuse = errors.New("cannot reuse connection")
|
|
|
|
func NewRequest(ctx context.Context, URL *url.URL, headers http.Header) *http.Request {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", URL.String(), nil)
|
|
runtimex.PanicOnError(err, "NewRequestWithContect failed")
|
|
for k, vs := range headers {
|
|
for _, v := range vs {
|
|
req.Header.Add(k, v)
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
// NewDialerResolver contructs a new dialer for TCP connections,
|
|
// with default, errorwrapping and resolve functionalities
|
|
func NewDialerResolver(resolver netxlite.Resolver) netxlite.Dialer {
|
|
var d netxlite.Dialer = netxlite.DefaultDialer
|
|
d = &errorsx.ErrorWrapperDialer{Dialer: d}
|
|
d = &netxlite.DialerResolver{Resolver: resolver, Dialer: d}
|
|
return d
|
|
}
|
|
|
|
// NewQUICDialerResolver creates a new QUICDialerResolver
|
|
// with default, errorwrapping and resolve functionalities
|
|
func NewQUICDialerResolver(resolver netxlite.Resolver) netxlite.QUICContextDialer {
|
|
var ql quicdialer.QUICListener = &netxlite.QUICListenerStdlib{}
|
|
ql = &errorsx.ErrorWrapperQUICListener{QUICListener: ql}
|
|
var dialer netxlite.QUICContextDialer = &netxlite.QUICDialerQUICGo{
|
|
QUICListener: ql,
|
|
}
|
|
dialer = &errorsx.ErrorWrapperQUICDialer{Dialer: dialer}
|
|
dialer = &netxlite.QUICDialerResolver{Resolver: resolver, Dialer: dialer}
|
|
return dialer
|
|
}
|
|
|
|
// NewSingleH3Transport creates an http3.RoundTripper
|
|
func NewSingleH3Transport(qsess quic.EarlySession, tlscfg *tls.Config, qcfg *quic.Config) *http3.RoundTripper {
|
|
transport := &http3.RoundTripper{
|
|
DisableCompression: true,
|
|
TLSClientConfig: tlscfg,
|
|
QuicConfig: qcfg,
|
|
Dial: (&SingleDialerH3{qsess: &qsess}).Dial,
|
|
}
|
|
return transport
|
|
}
|
|
|
|
// NewSingleTransport determines the appropriate HTTP Transport from the ALPN
|
|
func NewSingleTransport(conn net.Conn) (transport http.RoundTripper) {
|
|
singledialer := &SingleDialer{conn: &conn}
|
|
transport = http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.(*http.Transport).DialContext = singledialer.DialContext
|
|
transport.(*http.Transport).DialTLSContext = singledialer.DialContext
|
|
transport.(*http.Transport).DisableCompression = true
|
|
transport.(*http.Transport).MaxConnsPerHost = 1
|
|
|
|
transport = &netxlite.HTTPTransportLogger{Logger: log.Log, HTTPTransport: transport.(*http.Transport)}
|
|
return transport
|
|
}
|
|
|
|
type SingleDialer struct {
|
|
sync.Mutex
|
|
conn *net.Conn
|
|
}
|
|
|
|
func (s *SingleDialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
if s.conn == nil {
|
|
return nil, ErrNoConnReuse
|
|
}
|
|
c := s.conn
|
|
s.conn = nil
|
|
return *c, nil
|
|
}
|
|
|
|
type SingleDialerH3 struct {
|
|
sync.Mutex
|
|
qsess *quic.EarlySession
|
|
}
|
|
|
|
func (s *SingleDialerH3) Dial(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
if s.qsess == nil {
|
|
return nil, ErrNoConnReuse
|
|
}
|
|
qs := s.qsess
|
|
s.qsess = nil
|
|
return *qs, nil
|
|
}
|