refactor: sync messages with spec draft (#435)

Work part of: https://github.com/ooni/probe/issues/1733

Spec draft: https://github.com/ooni/spec/pull/219
This commit is contained in:
Simone Basso 2021-08-17 11:56:36 +02:00 committed by GitHub
parent ce854e8ae1
commit f2b6a5972f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 128 deletions

View File

@ -114,7 +114,7 @@ func (g *DefaultGenerator) GenerateHTTPEndpoint(ctx context.Context, rt *RoundTr
Endpoint: endpoint, Endpoint: endpoint,
Resolver: g.resolver, Resolver: g.resolver,
}) })
currentEndpoint.TCPConnectMeasurement = &TCPConnectMeasurement{ currentEndpoint.TCPConnect = &TCPConnectMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
if err != nil { if err != nil {
@ -123,7 +123,7 @@ func (g *DefaultGenerator) GenerateHTTPEndpoint(ctx context.Context, rt *RoundTr
defer tcpConn.Close() defer tcpConn.Close()
// prepare HTTPRoundTripMeasurement of this endpoint // prepare HTTPRoundTripMeasurement of this endpoint
currentEndpoint.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ currentEndpoint.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: rt.Request.Header, Headers: rt.Request.Header,
Method: "GET", Method: "GET",
@ -137,13 +137,13 @@ func (g *DefaultGenerator) GenerateHTTPEndpoint(ctx context.Context, rt *RoundTr
resp, body, err := HTTPDo(rt.Request, transport) resp, body, err := HTTPDo(rt.Request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
return currentEndpoint return currentEndpoint
} }
// successful Response // successful Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,
@ -168,7 +168,7 @@ func (g *DefaultGenerator) GenerateHTTPSEndpoint(ctx context.Context, rt *RoundT
Endpoint: endpoint, Endpoint: endpoint,
Resolver: g.resolver, Resolver: g.resolver,
}) })
currentEndpoint.TCPConnectMeasurement = &TCPConnectMeasurement{ currentEndpoint.TCPConnect = &TCPConnectMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
if err != nil { if err != nil {
@ -177,7 +177,7 @@ func (g *DefaultGenerator) GenerateHTTPSEndpoint(ctx context.Context, rt *RoundT
defer tcpConn.Close() defer tcpConn.Close()
tlsConn, err = TLSDo(tcpConn, rt.Request.URL.Hostname()) tlsConn, err = TLSDo(tcpConn, rt.Request.URL.Hostname())
currentEndpoint.TLSHandshakeMeasurement = &TLSHandshakeMeasurement{ currentEndpoint.TLSHandshake = &TLSHandshakeMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
if err != nil { if err != nil {
@ -186,7 +186,7 @@ func (g *DefaultGenerator) GenerateHTTPSEndpoint(ctx context.Context, rt *RoundT
defer tlsConn.Close() defer tlsConn.Close()
// prepare HTTPRoundTripMeasurement of this endpoint // prepare HTTPRoundTripMeasurement of this endpoint
currentEndpoint.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ currentEndpoint.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: rt.Request.Header, Headers: rt.Request.Header,
Method: "GET", Method: "GET",
@ -200,13 +200,13 @@ func (g *DefaultGenerator) GenerateHTTPSEndpoint(ctx context.Context, rt *RoundT
resp, body, err := HTTPDo(rt.Request, transport) resp, body, err := HTTPDo(rt.Request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
return currentEndpoint return currentEndpoint
} }
// successful Response // successful Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,
@ -234,14 +234,14 @@ func (g *DefaultGenerator) GenerateH3Endpoint(ctx context.Context, rt *RoundTrip
TLSConf: tlsConf, TLSConf: tlsConf,
Resolver: g.resolver, Resolver: g.resolver,
}) })
currentEndpoint.QUICHandshakeMeasurement = &TLSHandshakeMeasurement{ currentEndpoint.QUICHandshake = &TLSHandshakeMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
if err != nil { if err != nil {
return currentEndpoint return currentEndpoint
} }
// prepare HTTPRoundTripMeasurement of this endpoint // prepare HTTPRoundTripMeasurement of this endpoint
currentEndpoint.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ currentEndpoint.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: rt.Request.Header, Headers: rt.Request.Header,
Method: "GET", Method: "GET",
@ -255,13 +255,13 @@ func (g *DefaultGenerator) GenerateH3Endpoint(ctx context.Context, rt *RoundTrip
resp, body, err := HTTPDo(rt.Request, transport) resp, body, err := HTTPDo(rt.Request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: newfailure(err), Failure: newfailure(err),
} }
return currentEndpoint return currentEndpoint
} }
// successful Response // successful Response
currentEndpoint.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ currentEndpoint.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,

View File

@ -220,10 +220,10 @@ func TestGenerateHTTP(t *testing.T) {
if endpointMeasurement == nil { if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement") t.Fatal("unexpected nil urlMeasurement")
} }
if endpointMeasurement.TCPConnectMeasurement == nil { if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.HTTPRoundTripMeasurement == nil { if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil") t.Fatal("HTTPRoundTripMeasurement should not be nil")
} }
} }
@ -248,13 +248,13 @@ func TestGenerateHTTPS(t *testing.T) {
if endpointMeasurement == nil { if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement") t.Fatal("unexpected nil urlMeasurement")
} }
if endpointMeasurement.TCPConnectMeasurement == nil { if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.TLSHandshakeMeasurement == nil { if endpointMeasurement.TLSHandshake == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.HTTPRoundTripMeasurement == nil { if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil") t.Fatal("HTTPRoundTripMeasurement should not be nil")
} }
} }
@ -279,13 +279,13 @@ func TestGenerateHTTPSTLSFailure(t *testing.T) {
if endpointMeasurement == nil { if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement") t.Fatal("unexpected nil urlMeasurement")
} }
if endpointMeasurement.TCPConnectMeasurement == nil { if endpointMeasurement.TCPConnect == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.TLSHandshakeMeasurement == nil { if endpointMeasurement.TLSHandshake == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.HTTPRoundTripMeasurement != nil { if endpointMeasurement.HTTPRoundTrip != nil {
t.Fatal("HTTPRoundTripMeasurement should be nil") t.Fatal("HTTPRoundTripMeasurement should be nil")
} }
} }
@ -310,10 +310,10 @@ func TestGenerateH3(t *testing.T) {
if endpointMeasurement == nil { if endpointMeasurement == nil {
t.Fatal("unexpected nil urlMeasurement") t.Fatal("unexpected nil urlMeasurement")
} }
if endpointMeasurement.QUICHandshakeMeasurement == nil { if endpointMeasurement.QUICHandshake == nil {
t.Fatal("TCPConnectMeasurement should not be nil") t.Fatal("TCPConnectMeasurement should not be nil")
} }
if endpointMeasurement.HTTPRoundTripMeasurement == nil { if endpointMeasurement.HTTPRoundTrip == nil {
t.Fatal("HTTPRoundTripMeasurement should not be nil") t.Fatal("HTTPRoundTripMeasurement should not be nil")
} }
} }
@ -340,13 +340,13 @@ func TestGenerateTCPDoFails(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("unexpected err") t.Fatal("unexpected err")
} }
if endpointMeasurement.TCPConnectMeasurement == nil { if endpointMeasurement.TCPConnect == nil {
t.Fatal("QUIC handshake should not be nil") t.Fatal("QUIC handshake should not be nil")
} }
if endpointMeasurement.TCPConnectMeasurement.Failure == nil { if endpointMeasurement.TCPConnect.Failure == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
if *endpointMeasurement.TCPConnectMeasurement.Failure != *newfailure(expected) { if *endpointMeasurement.TCPConnect.Failure != *newfailure(expected) {
t.Fatal("unexpected error type") t.Fatal("unexpected error type")
} }
} }
@ -373,13 +373,13 @@ func TestGenerateQUICDoFails(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("unexpected err") t.Fatal("unexpected err")
} }
if endpointMeasurement.QUICHandshakeMeasurement == nil { if endpointMeasurement.QUICHandshake == nil {
t.Fatal("QUIC handshake should not be nil") t.Fatal("QUIC handshake should not be nil")
} }
if endpointMeasurement.QUICHandshakeMeasurement.Failure == nil { if endpointMeasurement.QUICHandshake.Failure == nil {
t.Fatal("expected an error here") t.Fatal("expected an error here")
} }
if *endpointMeasurement.QUICHandshakeMeasurement.Failure != *newfailure(expected) { if *endpointMeasurement.QUICHandshake.Failure != *newfailure(expected) {
t.Fatal("unexpected error type") t.Fatal("unexpected error type")
} }
} }
@ -441,22 +441,22 @@ func TestGenerateHTTPDoFails(t *testing.T) {
t.Fatal("unexpected number of endpoints", len(u.Endpoints)) t.Fatal("unexpected number of endpoints", len(u.Endpoints))
} }
// this can occur when the network is unreachable, but it is irrelevant for checking HTTP behavior // this can occur when the network is unreachable, but it is irrelevant for checking HTTP behavior
if u.Endpoints[0].TCPConnectMeasurement != nil && u.Endpoints[0].TCPConnectMeasurement.Failure != nil { if u.Endpoints[0].TCPConnect != nil && u.Endpoints[0].TCPConnect.Failure != nil {
continue continue
} }
if u.Endpoints[0].QUICHandshakeMeasurement != nil && u.Endpoints[0].QUICHandshakeMeasurement.Failure != nil { if u.Endpoints[0].QUICHandshake != nil && u.Endpoints[0].QUICHandshake.Failure != nil {
continue continue
} }
if u.Endpoints[0].HTTPRoundTripMeasurement == nil { if u.Endpoints[0].HTTPRoundTrip == nil {
t.Fatal("roundtrip should not be nil", u.Endpoints[0].TCPConnectMeasurement.Failure, "jaaaa") t.Fatal("roundtrip should not be nil", u.Endpoints[0].TCPConnect.Failure, "jaaaa")
} }
if u.Endpoints[0].HTTPRoundTripMeasurement.Response == nil { if u.Endpoints[0].HTTPRoundTrip.Response == nil {
t.Fatal("roundtrip response should not be nil") t.Fatal("roundtrip response should not be nil")
} }
if u.Endpoints[0].HTTPRoundTripMeasurement.Response.Failure == nil { if u.Endpoints[0].HTTPRoundTrip.Response.Failure == nil {
t.Fatal("expected an HTTP error") t.Fatal("expected an HTTP error")
} }
if !strings.HasSuffix(*u.Endpoints[0].HTTPRoundTripMeasurement.Response.Failure, expected.Error()) { if !strings.HasSuffix(*u.Endpoints[0].HTTPRoundTrip.Response.Failure, expected.Error()) {
t.Fatal("unexpected failure type") t.Fatal("unexpected failure type")
} }
} }

View File

@ -14,7 +14,7 @@ import (
type ( type (
CtrlRequest = websteps.CtrlRequest CtrlRequest = websteps.CtrlRequest
ControlResponse = websteps.ControlResponse ControlResponse = websteps.CtrlResponse
) )
var ErrInternalServer = errors.New("internal server error") var ErrInternalServer = errors.New("internal server error")
@ -45,11 +45,11 @@ func Measure(ctx context.Context, creq *CtrlRequest, config *Config) (*ControlRe
if checker == nil { if checker == nil {
checker = &DefaultInitChecker{resolver: resolver} checker = &DefaultInitChecker{resolver: resolver}
} }
URL, err = checker.InitialChecks(creq.HTTPRequest) URL, err = checker.InitialChecks(creq.URL)
if err != nil { if err != nil {
// return a valid response in case of NXDOMAIN so the probe can compare the failure // return a valid response in case of NXDOMAIN so the probe can compare the failure
if err == ErrNoSuchHost { if err == ErrNoSuchHost {
return newDNSFailedResponse(err, creq.HTTPRequest), nil return newDNSFailedResponse(err, creq.URL), nil
} }
return nil, err return nil, err
} }
@ -57,7 +57,7 @@ func Measure(ctx context.Context, creq *CtrlRequest, config *Config) (*ControlRe
if explorer == nil { if explorer == nil {
explorer = &DefaultExplorer{resolver: resolver} explorer = &DefaultExplorer{resolver: resolver}
} }
rts, err := explorer.Explore(URL, creq.HTTPRequestHeaders) rts, err := explorer.Explore(URL, creq.Headers)
if err != nil { if err != nil {
return nil, ErrInternalServer return nil, ErrInternalServer
} }
@ -69,7 +69,7 @@ func Measure(ctx context.Context, creq *CtrlRequest, config *Config) (*ControlRe
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ControlResponse{URLMeasurements: meas}, nil return &ControlResponse{URLs: meas}, nil
} }
// newDNSFailedResponse creates a new response with one URLMeasurement entry // newDNSFailedResponse creates a new response with one URLMeasurement entry
@ -82,7 +82,7 @@ func newDNSFailedResponse(err error, URL string) *ControlResponse {
Failure: newfailure(err), Failure: newfailure(err),
}, },
} }
resp.URLMeasurements = append(resp.URLMeasurements, m) resp.URLs = append(resp.URLs, m)
return resp return resp
} }

View File

@ -11,7 +11,7 @@ import (
func TestMeasureSuccess(t *testing.T) { func TestMeasureSuccess(t *testing.T) {
req := &CtrlRequest{ req := &CtrlRequest{
HTTPRequest: "https://example.com", URL: "https://example.com",
} }
resp, err := Measure(context.Background(), req, &Config{}) resp, err := Measure(context.Background(), req, &Config{})
if err != nil { if err != nil {
@ -48,7 +48,7 @@ var ErrExpectedGenerate error = errors.New("expected error generator")
func TestMeasureInitialChecksFail(t *testing.T) { func TestMeasureInitialChecksFail(t *testing.T) {
req := &CtrlRequest{ req := &CtrlRequest{
HTTPRequest: "https://example.com", URL: "https://example.com",
} }
resp, err := Measure(context.Background(), req, &Config{checker: &MockChecker{err: ErrExpectedCheck}}) resp, err := Measure(context.Background(), req, &Config{checker: &MockChecker{err: ErrExpectedCheck}})
if err == nil { if err == nil {
@ -64,7 +64,7 @@ func TestMeasureInitialChecksFail(t *testing.T) {
func TestMeasureInitialChecksFailWithNXDOMAIN(t *testing.T) { func TestMeasureInitialChecksFailWithNXDOMAIN(t *testing.T) {
req := &CtrlRequest{ req := &CtrlRequest{
HTTPRequest: "https://example.com", URL: "https://example.com",
} }
resp, err := Measure(context.Background(), req, &Config{checker: &MockChecker{err: ErrNoSuchHost}}) resp, err := Measure(context.Background(), req, &Config{checker: &MockChecker{err: ErrNoSuchHost}})
if err != nil { if err != nil {
@ -73,20 +73,20 @@ func TestMeasureInitialChecksFailWithNXDOMAIN(t *testing.T) {
if resp == nil { if resp == nil {
t.Fatal("resp should not be nil") t.Fatal("resp should not be nil")
} }
if len(resp.URLMeasurements) != 1 { if len(resp.URLs) != 1 {
t.Fatal("unexpected number of measurements") t.Fatal("unexpected number of measurements")
} }
if resp.URLMeasurements[0].DNS == nil { if resp.URLs[0].DNS == nil {
t.Fatal("DNS entry should not be nil") t.Fatal("DNS entry should not be nil")
} }
if *resp.URLMeasurements[0].DNS.Failure != errorsx.FailureDNSNXDOMAINError { if *resp.URLs[0].DNS.Failure != errorsx.FailureDNSNXDOMAINError {
t.Fatal("unexpected failure") t.Fatal("unexpected failure")
} }
} }
func TestMeasureExploreFails(t *testing.T) { func TestMeasureExploreFails(t *testing.T) {
req := &CtrlRequest{ req := &CtrlRequest{
HTTPRequest: "https://example.com", URL: "https://example.com",
} }
resp, err := Measure(context.Background(), req, &Config{explorer: &MockExplorer{}}) resp, err := Measure(context.Background(), req, &Config{explorer: &MockExplorer{}})
if err == nil { if err == nil {
@ -102,7 +102,7 @@ func TestMeasureExploreFails(t *testing.T) {
func TestMeasureGenerateFails(t *testing.T) { func TestMeasureGenerateFails(t *testing.T) {
req := &CtrlRequest{ req := &CtrlRequest{
HTTPRequest: "https://example.com", URL: "https://example.com",
} }
resp, err := Measure(context.Background(), req, &Config{generator: &MockGenerator{}}) resp, err := Measure(context.Background(), req, &Config{generator: &MockGenerator{}})
if err == nil { if err == nil {

View File

@ -13,5 +13,5 @@ type (
TLSHandshakeMeasurement = websteps.TLSHandshakeMeasurement TLSHandshakeMeasurement = websteps.TLSHandshakeMeasurement
HTTPRequestMeasurement = websteps.HTTPRequestMeasurement HTTPRequestMeasurement = websteps.HTTPRequestMeasurement
HTTPResponseMeasurement = websteps.HTTPResponseMeasurement HTTPResponseMeasurement = websteps.HTTPResponseMeasurement
RoundTrip = websteps.RoundTrip RoundTrip = websteps.RoundTripInfo
) )

View File

@ -8,22 +8,10 @@ import (
"github.com/ooni/probe-cli/v3/internal/errorsx" "github.com/ooni/probe-cli/v3/internal/errorsx"
) )
// CtrlRequest is the request sent by the probe
type CtrlRequest struct {
HTTPRequest string `json:"url"`
HTTPRequestHeaders map[string][]string `json:"headers"`
Addrs []string `json:"addrs"`
}
// ControlResponse is the response from the control service.
type ControlResponse struct {
URLMeasurements []*URLMeasurement `json:"urls"`
}
// Control performs the control request and returns the response. // Control performs the control request and returns the response.
func Control( func Control(
ctx context.Context, sess model.ExperimentSession, ctx context.Context, sess model.ExperimentSession,
thAddr string, creq CtrlRequest) (out ControlResponse, err error) { thAddr string, creq CtrlRequest) (out CtrlResponse, err error) {
clnt := httpx.Client{ clnt := httpx.Client{
BaseURL: thAddr, BaseURL: thAddr,
HTTPClient: sess.DefaultHTTPClient(), HTTPClient: sess.DefaultHTTPClient(),

View File

@ -2,70 +2,104 @@ package websteps
import "net/http" import "net/http"
// RoundTrip describes a specific round trip. // Websteps test helper spec messages:
type RoundTrip struct {
// proto is the protocol used, it can be "h2", "http/1.1", "h3", "h3-29" // CtrlRequest is the request sent by the probe to the test helper.
type CtrlRequest struct {
// URL is the mandatory URL to measure.
URL string `json:"url"`
// Headers contains optional headers.
Headers map[string][]string `json:"headers"`
// Addrs contains the optional IP addresses resolved by the
// probe for the domain inside URL.
Addrs []string `json:"addrs"`
}
// CtrlResponse is the response from the test helper.
type CtrlResponse struct {
// URLs contains the URLs we should measure. These URLs
// derive from CtrlRequest.URL.
URLs []*URLMeasurement `json:"urls"`
}
// URLMeasurement contains all the URLs measured by the test helper.
type URLMeasurement struct {
// URL is the URL to which this measurement refers.
URL string `json:"url"`
// DNS contains the domain names resolved by the test helper.
DNS *DNSMeasurement `json:"dns"`
// Endpoints contains endpoint measurements.
Endpoints []*EndpointMeasurement `json:"endpoints"`
// RoundTrip is the related round trip. This field MUST NOT be
// exported as JSON, since it's only used internally by the test
// helper and it's completely ignored by the probe.
RoundTrip *RoundTripInfo `json:"-"`
}
// RoundTripInfo contains info on a specific round trip. This data
// structure is not part of the test helper protocol. We use it
// _inside_ the test helper to describe the discovery phase where
// we gather all the URLs that can derive from a given URL.
type RoundTripInfo struct {
// Proto is the protocol used, it can be "h2", "http/1.1", "h3", "h3-29".
Proto string Proto string
// Request is the original HTTP request. The headers // Request is the original HTTP request. Headers also include cookies.
// also include cookies.
Request *http.Request Request *http.Request
// Response is the HTTP response. // Response is the HTTP response.
Response *http.Response Response *http.Response
// sortIndex is an internal field using for sorting. // SortIndex is the index using for sorting round trips.
SortIndex int SortIndex int
} }
// URLMeasurement is a measurement of a given URL that
// includes connectivity measurement for each endpoint
// implied by the given URL.
type URLMeasurement struct {
// URL is the URL we're using
URL string `json:"url"`
// DNS contains the domain names resolved by the helper.
DNS *DNSMeasurement `json:"dns"`
// RoundTrip is the related round trip.
RoundTrip *RoundTrip `json:"-"`
// Endpoints contains endpoint measurements.
Endpoints []*EndpointMeasurement `json:"endpoints"`
}
// DNSMeasurement is a DNS measurement. // DNSMeasurement is a DNS measurement.
type DNSMeasurement struct { type DNSMeasurement struct {
// Domain is the domain we wanted to resolve. // Domain is the domain we wanted to resolve.
Domain string `json:"domain"` Domain string `json:"domain"`
// Addrs contains the resolved addresses.
Addrs []string `json:"addrs"`
// Failure is the error that occurred. // Failure is the error that occurred.
Failure *string `json:"failure"` Failure *string `json:"failure"`
// Addrs contains the resolved addresses.
Addrs []string `json:"addrs"`
} }
// HTTPSEndpointMeasurement is the measurement of requesting a specific endpoint via HTTPS. // EndpointMeasurement is an HTTP measurement where we are using
// a specific TCP/TLS/QUIC endpoint to get the URL.
//
// The specification describes this data structure as the sum of
// three distinct types: HTTPEndpointMeasurement for "http",
// HTTPSEndpointMeasurement for "https", and H3EndpointMeasurement
// for "h3". We don't have sum types here, therefore we use the
// Protocol field to indicate which fields are meaningful.
type EndpointMeasurement struct { type EndpointMeasurement struct {
// Endpoint is the endpoint we're measuring. // Endpoint is the endpoint we're measuring.
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
// Protocol is the used protocol. It can be "http", "https", "h3", "h3-29" or other supported QUIC protocols // Protocol is one of "http", "https", "h3", and "h3-29".
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
// TCPConnectMeasurement is the related TCP connect measurement, if applicable (nil for h3 requests) // TCPConnect is the TCP connect measurement. This field
TCPConnectMeasurement *TCPConnectMeasurement `json:"tcp_connect"` // is only meaningful when protocol is "http" or "https."
TCPConnect *TCPConnectMeasurement `json:"tcp_connect"`
// QUICHandshakeMeasurement is the related QUIC(TLS 1.3) handshake measurement, if applicable (nil for http, https requests) // QUICHandshake is the QUIC handshake measurement. This field
QUICHandshakeMeasurement *TLSHandshakeMeasurement `json:"quic_handshake"` // is only meaningful when the protocol is one of "h3" and "h3-29".
QUICHandshake *QUICHandshakeMeasurement `json:"quic_handshake"`
// TLSHandshakeMeasurement is the related TLS handshake measurement, if applicable (nil for http, h3 requests) // TLSHandshake is the TLS handshake measurement. This field
TLSHandshakeMeasurement *TLSHandshakeMeasurement `json:"tls_handshake"` // is only meaningful when the protocol is "https".
TLSHandshake *TLSHandshakeMeasurement `json:"tls_handshake"`
// HTTPRoundTripMeasurement is the related HTTP GET measurement. // HTTPRoundTrip is the related HTTP GET measurement.
HTTPRoundTripMeasurement *HTTPRoundTripMeasurement `json:"http_round_trip"` HTTPRoundTrip *HTTPRoundTripMeasurement `json:"http_round_trip"`
} }
// TCPConnectMeasurement is a TCP connect measurement. // TCPConnectMeasurement is a TCP connect measurement.
@ -80,23 +114,42 @@ type TLSHandshakeMeasurement struct {
Failure *string `json:"failure"` Failure *string `json:"failure"`
} }
// HTTPRoundTripMeasurement contains a measured HTTP request and the corresponding response. // QUICHandshakeMeasurement is a QUIC handshake measurement.
type QUICHandshakeMeasurement = TLSHandshakeMeasurement
// HTTPRoundTripMeasurement contains a measured HTTP request and
// the corresponding response.
type HTTPRoundTripMeasurement struct { type HTTPRoundTripMeasurement struct {
// Request contains request data.
Request *HTTPRequestMeasurement `json:"request"` Request *HTTPRequestMeasurement `json:"request"`
// Response contains response data.
Response *HTTPResponseMeasurement `json:"response"` Response *HTTPResponseMeasurement `json:"response"`
} }
// HTTPRequestMeasurement contains the headers of the measured HTTP Get request. // HTTPRequestMeasurement contains request data.
type HTTPRequestMeasurement struct { type HTTPRequestMeasurement struct {
Headers http.Header `json:"headers"` // Method is the request method.
Method string `json:"method"` Method string `json:"method"`
// URL is the request URL.
URL string `json:"url"` URL string `json:"url"`
// Headers contains request headers.
Headers http.Header `json:"headers"`
} }
// HTTPResponseMeasurement contains the response of the measured HTTP Get request. // HTTPResponseMeasurement contains response data.
type HTTPResponseMeasurement struct { type HTTPResponseMeasurement struct {
// BodyLength contains the body length in bytes.
BodyLength int64 `json:"body_length"` BodyLength int64 `json:"body_length"`
// Failure is the error that occurred.
Failure *string `json:"failure"` Failure *string `json:"failure"`
// Headers contains response headers.
Headers http.Header `json:"headers"` Headers http.Header `json:"headers"`
// StatusCode is the response status code.
StatusCode int64 `json:"status_code"` StatusCode int64 `json:"status_code"`
} }

View File

@ -113,19 +113,19 @@ func (m Measurer) Run(
} }
// 4. Query the testhelper // 4. Query the testhelper
resp, err := Control(ctx, sess, testhelper.Address, CtrlRequest{ resp, err := Control(ctx, sess, testhelper.Address, CtrlRequest{
HTTPRequest: URL.String(), URL: URL.String(),
HTTPRequestHeaders: map[string][]string{ Headers: map[string][]string{
"Accept": {httpheader.Accept()}, "Accept": {httpheader.Accept()},
"Accept-Language": {httpheader.AcceptLanguage()}, "Accept-Language": {httpheader.AcceptLanguage()},
"User-Agent": {httpheader.UserAgent()}, "User-Agent": {httpheader.UserAgent()},
}, },
Addrs: endpoints, Addrs: endpoints,
}) })
if err != nil || resp.URLMeasurements == nil { if err != nil || resp.URLs == nil {
return errors.New("no control response") return errors.New("no control response")
} }
// 5. Go over the Control URL measurements and reproduce them without following redirects, one by one. // 5. Go over the Control URL measurements and reproduce them without following redirects, one by one.
for _, controlURLMeasurement := range resp.URLMeasurements { for _, controlURLMeasurement := range resp.URLs {
urlMeasurement := &URLMeasurement{ urlMeasurement := &URLMeasurement{
URL: controlURLMeasurement.URL, URL: controlURLMeasurement.URL,
Endpoints: []*EndpointMeasurement{}, Endpoints: []*EndpointMeasurement{},
@ -145,7 +145,7 @@ func (m Measurer) Run(
} }
// the testhelper tells us which endpoints to measure // the testhelper tells us which endpoints to measure
for _, controlEndpoint := range controlURLMeasurement.Endpoints { for _, controlEndpoint := range controlURLMeasurement.Endpoints {
rt := controlEndpoint.HTTPRoundTripMeasurement rt := controlEndpoint.HTTPRoundTrip
if rt == nil || rt.Request == nil { if rt == nil || rt.Request == nil {
continue continue
} }
@ -176,7 +176,7 @@ func (m *Measurer) measureEndpointHTTP(ctx context.Context, URL *url.URL, endpoi
} }
// TCP connect step // TCP connect step
conn, err := TCPDo(ctx, TCPConfig{Endpoint: endpoint}) conn, err := TCPDo(ctx, TCPConfig{Endpoint: endpoint})
endpointMeasurement.TCPConnectMeasurement = &TCPConnectMeasurement{ endpointMeasurement.TCPConnect = &TCPConnectMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
if err != nil { if err != nil {
@ -186,7 +186,7 @@ func (m *Measurer) measureEndpointHTTP(ctx context.Context, URL *url.URL, endpoi
// HTTP roundtrip step // HTTP roundtrip step
request := NewRequest(ctx, URL, headers) request := NewRequest(ctx, URL, headers)
endpointMeasurement.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ endpointMeasurement.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: request.Header, Headers: request.Header,
Method: "GET", Method: "GET",
@ -197,13 +197,13 @@ func (m *Measurer) measureEndpointHTTP(ctx context.Context, URL *url.URL, endpoi
resp, body, err := HTTPDo(request, transport) resp, body, err := HTTPDo(request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
return endpointMeasurement return endpointMeasurement
} }
// successful Response // successful Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,
@ -219,7 +219,7 @@ func (m *Measurer) measureEndpointHTTPS(ctx context.Context, URL *url.URL, endpo
} }
// TCP connect step // TCP connect step
conn, err := TCPDo(ctx, TCPConfig{Endpoint: endpoint}) conn, err := TCPDo(ctx, TCPConfig{Endpoint: endpoint})
endpointMeasurement.TCPConnectMeasurement = &TCPConnectMeasurement{ endpointMeasurement.TCPConnect = &TCPConnectMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
if err != nil { if err != nil {
@ -229,7 +229,7 @@ func (m *Measurer) measureEndpointHTTPS(ctx context.Context, URL *url.URL, endpo
// TLS handshake step // TLS handshake step
tlsconn, err := TLSDo(conn, URL.Hostname()) tlsconn, err := TLSDo(conn, URL.Hostname())
endpointMeasurement.TLSHandshakeMeasurement = &TLSHandshakeMeasurement{ endpointMeasurement.TLSHandshake = &TLSHandshakeMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
if err != nil { if err != nil {
@ -239,7 +239,7 @@ func (m *Measurer) measureEndpointHTTPS(ctx context.Context, URL *url.URL, endpo
// HTTP roundtrip step // HTTP roundtrip step
request := NewRequest(ctx, URL, headers) request := NewRequest(ctx, URL, headers)
endpointMeasurement.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ endpointMeasurement.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: request.Header, Headers: request.Header,
Method: "GET", Method: "GET",
@ -250,13 +250,13 @@ func (m *Measurer) measureEndpointHTTPS(ctx context.Context, URL *url.URL, endpo
resp, body, err := HTTPDo(request, transport) resp, body, err := HTTPDo(request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
return endpointMeasurement return endpointMeasurement
} }
// successful Response // successful Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,
@ -279,7 +279,7 @@ func (m *Measurer) measureEndpointH3(ctx context.Context, URL *url.URL, endpoint
Endpoint: endpoint, Endpoint: endpoint,
TLSConf: tlsConf, TLSConf: tlsConf,
}) })
endpointMeasurement.QUICHandshakeMeasurement = &TLSHandshakeMeasurement{ endpointMeasurement.QUICHandshake = &TLSHandshakeMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
if err != nil { if err != nil {
@ -287,7 +287,7 @@ func (m *Measurer) measureEndpointH3(ctx context.Context, URL *url.URL, endpoint
} }
// HTTP roundtrip step // HTTP roundtrip step
request := NewRequest(ctx, URL, headers) request := NewRequest(ctx, URL, headers)
endpointMeasurement.HTTPRoundTripMeasurement = &HTTPRoundTripMeasurement{ endpointMeasurement.HTTPRoundTrip = &HTTPRoundTripMeasurement{
Request: &HTTPRequestMeasurement{ Request: &HTTPRequestMeasurement{
Headers: request.Header, Headers: request.Header,
Method: "GET", Method: "GET",
@ -298,13 +298,13 @@ func (m *Measurer) measureEndpointH3(ctx context.Context, URL *url.URL, endpoint
resp, body, err := HTTPDo(request, transport) resp, body, err := HTTPDo(request, transport)
if err != nil { if err != nil {
// failed Response // failed Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
Failure: archival.NewFailure(err), Failure: archival.NewFailure(err),
} }
return endpointMeasurement return endpointMeasurement
} }
// successful Response // successful Response
endpointMeasurement.HTTPRoundTripMeasurement.Response = &HTTPResponseMeasurement{ endpointMeasurement.HTTPRoundTrip.Response = &HTTPResponseMeasurement{
BodyLength: int64(len(body)), BodyLength: int64(len(body)),
Failure: nil, Failure: nil,
Headers: resp.Header, Headers: resp.Header,