66fd1569b8
The main issue I see inside tracex at the moment is that we construct the HTTP measurement from separate events. This is fragile because we cannot be sure that these events belong to the same round trip. (Currently, they _are_ part of the same round trip, but this is a fragile assumption and it would be much more robust to dispose of it.) To prepare for emitting a single event, it's imperative to have two distinct fields for HTTP request and response headers, which is the main contribution in this commit. Then, we have a bunch of smaller changes including: 1. correctly naming 'response' the DNS response (instead of 'reply') 2. ensure we always use pointer receivers Reference issue: https://github.com/ooni/probe/issues/2121
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
package tracex
|
|
|
|
//
|
|
// HTTP
|
|
//
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
)
|
|
|
|
// SaverMetadataHTTPTransport is a RoundTripper that saves
|
|
// events related to HTTP request and response metadata
|
|
type SaverMetadataHTTPTransport struct {
|
|
model.HTTPTransport
|
|
Saver *Saver
|
|
}
|
|
|
|
// RoundTrip implements RoundTripper.RoundTrip
|
|
func (txp *SaverMetadataHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
txp.Saver.Write(&EventHTTPRequestMetadata{&EventValue{
|
|
HTTPRequestHeaders: httpCloneRequestHeaders(req),
|
|
HTTPMethod: req.Method,
|
|
HTTPURL: req.URL.String(),
|
|
Transport: txp.HTTPTransport.Network(),
|
|
Time: time.Now(),
|
|
}})
|
|
resp, err := txp.HTTPTransport.RoundTrip(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txp.Saver.Write(&EventHTTPResponseMetadata{&EventValue{
|
|
HTTPResponseHeaders: resp.Header,
|
|
HTTPStatusCode: resp.StatusCode,
|
|
Time: time.Now(),
|
|
}})
|
|
return resp, err
|
|
}
|
|
|
|
// httpCloneRequestHeaders returns a clone of the headers where we have
|
|
// also set the host header, which normally is not set by
|
|
// golang until it serializes the request itself.
|
|
func httpCloneRequestHeaders(req *http.Request) http.Header {
|
|
header := req.Header.Clone()
|
|
if req.Host != "" {
|
|
header.Set("Host", req.Host)
|
|
} else {
|
|
header.Set("Host", req.URL.Host)
|
|
}
|
|
return header
|
|
}
|
|
|
|
// SaverTransactionHTTPTransport is a RoundTripper that saves
|
|
// events related to the HTTP transaction
|
|
type SaverTransactionHTTPTransport struct {
|
|
model.HTTPTransport
|
|
Saver *Saver
|
|
}
|
|
|
|
// RoundTrip implements RoundTripper.RoundTrip
|
|
func (txp *SaverTransactionHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
txp.Saver.Write(&EventHTTPTransactionStart{&EventValue{
|
|
Time: time.Now(),
|
|
}})
|
|
resp, err := txp.HTTPTransport.RoundTrip(req)
|
|
txp.Saver.Write(&EventHTTPTransactionDone{&EventValue{
|
|
Err: err,
|
|
Time: time.Now(),
|
|
}})
|
|
return resp, err
|
|
}
|
|
|
|
// SaverBodyHTTPTransport is a RoundTripper that saves
|
|
// body events occurring during the round trip
|
|
type SaverBodyHTTPTransport struct {
|
|
model.HTTPTransport
|
|
Saver *Saver
|
|
SnapshotSize int
|
|
}
|
|
|
|
// RoundTrip implements RoundTripper.RoundTrip
|
|
func (txp *SaverBodyHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
const defaultSnapSize = 1 << 17
|
|
snapsize := defaultSnapSize
|
|
if txp.SnapshotSize != 0 {
|
|
snapsize = txp.SnapshotSize
|
|
}
|
|
if req.Body != nil {
|
|
data, err := httpSaverSnapRead(req.Context(), req.Body, snapsize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Body = httpSaverCompose(data, req.Body)
|
|
txp.Saver.Write(&EventHTTPRequestBodySnapshot{&EventValue{
|
|
DataIsTruncated: len(data) >= snapsize,
|
|
Data: data,
|
|
Time: time.Now(),
|
|
}})
|
|
}
|
|
resp, err := txp.HTTPTransport.RoundTrip(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := httpSaverSnapRead(req.Context(), resp.Body, snapsize)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return nil, err
|
|
}
|
|
resp.Body = httpSaverCompose(data, resp.Body)
|
|
txp.Saver.Write(&EventHTTPResponseBodySnapshot{&EventValue{
|
|
DataIsTruncated: len(data) >= snapsize,
|
|
Data: data,
|
|
Time: time.Now(),
|
|
}})
|
|
return resp, nil
|
|
}
|
|
|
|
func httpSaverSnapRead(ctx context.Context, r io.ReadCloser, snapsize int) ([]byte, error) {
|
|
return netxlite.ReadAllContext(ctx, io.LimitReader(r, int64(snapsize)))
|
|
}
|
|
|
|
func httpSaverCompose(data []byte, r io.ReadCloser) io.ReadCloser {
|
|
return httpSaverReadCloser{Closer: r, Reader: io.MultiReader(bytes.NewReader(data), r)}
|
|
}
|
|
|
|
type httpSaverReadCloser struct {
|
|
io.Closer
|
|
io.Reader
|
|
}
|
|
|
|
var _ model.HTTPTransport = &SaverMetadataHTTPTransport{}
|
|
var _ model.HTTPTransport = &SaverBodyHTTPTransport{}
|
|
var _ model.HTTPTransport = &SaverTransactionHTTPTransport{}
|