108 lines
3.1 KiB
Go
108 lines
3.1 KiB
Go
|
package archival
|
||
|
|
||
|
//
|
||
|
// Saves HTTP events
|
||
|
//
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"time"
|
||
|
|
||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||
|
)
|
||
|
|
||
|
// HTTPRoundTripEvent contains an HTTP round trip.
|
||
|
type HTTPRoundTripEvent struct {
|
||
|
Failure error
|
||
|
Finished time.Time
|
||
|
Method string
|
||
|
RequestHeaders http.Header
|
||
|
ResponseBody []byte
|
||
|
ResponseBodyIsTruncated bool
|
||
|
ResponseBodyLength int64
|
||
|
ResponseHeaders http.Header
|
||
|
Started time.Time
|
||
|
StatusCode int64
|
||
|
Transport string
|
||
|
URL string
|
||
|
}
|
||
|
|
||
|
// HTTPRoundTrip performs the round trip with the given transport and
|
||
|
// the given arguments and saves the results into the saver.
|
||
|
//
|
||
|
// The maxBodySnapshotSize argument controls the maximum size of the
|
||
|
// body snapshot that we collect along with the HTTP round trip.
|
||
|
func (s *Saver) HTTPRoundTrip(
|
||
|
txp model.HTTPTransport, maxBodySnapshotSize int64,
|
||
|
req *http.Request) (*http.Response, error) {
|
||
|
started := time.Now()
|
||
|
resp, err := txp.RoundTrip(req)
|
||
|
rt := &HTTPRoundTripEvent{
|
||
|
Failure: nil, // set later
|
||
|
Finished: time.Time{}, // set later
|
||
|
Method: req.Method,
|
||
|
RequestHeaders: s.cloneRequestHeaders(req),
|
||
|
ResponseBody: nil, // set later
|
||
|
ResponseBodyIsTruncated: false,
|
||
|
ResponseBodyLength: 0,
|
||
|
ResponseHeaders: nil, // set later
|
||
|
Started: started,
|
||
|
StatusCode: 0, // set later
|
||
|
Transport: txp.Network(),
|
||
|
URL: req.URL.String(),
|
||
|
}
|
||
|
if err != nil {
|
||
|
rt.Finished = time.Now()
|
||
|
rt.Failure = err
|
||
|
s.appendHTTPRoundTripEvent(rt)
|
||
|
return nil, err
|
||
|
}
|
||
|
rt.StatusCode = int64(resp.StatusCode)
|
||
|
rt.ResponseHeaders = resp.Header.Clone()
|
||
|
r := io.LimitReader(resp.Body, maxBodySnapshotSize)
|
||
|
body, err := netxlite.ReadAllContext(req.Context(), r)
|
||
|
if err != nil {
|
||
|
rt.Finished = time.Now()
|
||
|
rt.Failure = err
|
||
|
s.appendHTTPRoundTripEvent(rt)
|
||
|
return nil, err
|
||
|
}
|
||
|
resp.Body = &archivalHTTPTransportBody{ // allow for reading again the whole body
|
||
|
Reader: io.MultiReader(bytes.NewReader(body), resp.Body),
|
||
|
Closer: resp.Body,
|
||
|
}
|
||
|
rt.ResponseBody = body
|
||
|
rt.ResponseBodyLength = int64(len(body))
|
||
|
rt.ResponseBodyIsTruncated = int64(len(body)) >= maxBodySnapshotSize
|
||
|
rt.Finished = time.Now()
|
||
|
s.appendHTTPRoundTripEvent(rt)
|
||
|
return resp, nil
|
||
|
}
|
||
|
|
||
|
// cloneRequestHeaders ensure we include the Host header among the saved
|
||
|
// headers, which is what OONI should do, even though the Go transport is
|
||
|
// such that this header is added later when we're sending the request.
|
||
|
func (s *Saver) cloneRequestHeaders(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
|
||
|
}
|
||
|
|
||
|
type archivalHTTPTransportBody struct {
|
||
|
io.Reader
|
||
|
io.Closer
|
||
|
}
|
||
|
|
||
|
func (s *Saver) appendHTTPRoundTripEvent(ev *HTTPRoundTripEvent) {
|
||
|
s.mu.Lock()
|
||
|
s.trace.HTTPRoundTrip = append(s.trace.HTTPRoundTrip, ev)
|
||
|
s.mu.Unlock()
|
||
|
}
|