9354191b85
By just storing the raw certificate we simplify the internal data structure we use. In turn, this enables us to write better unit tests using github.com/google/go-cmp where we can construct the expected result and compare with that. (Yeah, in principle we could also construct the full certificate but I'm not sure it's worth the effort since we basically only care about the raw certificate.) The general idea here is to make tracex more tested. Once it's more tested, I will create separate structs for each event, which is something that measurex also does. Once that is done, we can start ensuring that the code in measurex and the code in tracex do the same thing in terms of storing observations. When also this is done, we can then rewrite measurex to use tracex directly. The overall goal is https://github.com/ooni/probe/issues/2035.
311 lines
9.1 KiB
Go
311 lines
9.1 KiB
Go
package tracex
|
|
|
|
//
|
|
// Code to generate the OONI archival data format from events
|
|
//
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/geolocate"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
)
|
|
|
|
// Compatibility types. Most experiments still use these names.
|
|
type (
|
|
ExtSpec = model.ArchivalExtSpec
|
|
TCPConnectEntry = model.ArchivalTCPConnectResult
|
|
TCPConnectStatus = model.ArchivalTCPConnectStatus
|
|
MaybeBinaryValue = model.ArchivalMaybeBinaryData
|
|
DNSQueryEntry = model.ArchivalDNSLookupResult
|
|
DNSAnswerEntry = model.ArchivalDNSAnswer
|
|
TLSHandshake = model.ArchivalTLSOrQUICHandshakeResult
|
|
HTTPBody = model.ArchivalHTTPBody
|
|
HTTPHeader = model.ArchivalHTTPHeader
|
|
RequestEntry = model.ArchivalHTTPRequestResult
|
|
HTTPRequest = model.ArchivalHTTPRequest
|
|
HTTPResponse = model.ArchivalHTTPResponse
|
|
NetworkEvent = model.ArchivalNetworkEvent
|
|
)
|
|
|
|
// Compatibility variables. Most experiments still use these names.
|
|
var (
|
|
ExtDNS = model.ArchivalExtDNS
|
|
ExtNetevents = model.ArchivalExtNetevents
|
|
ExtHTTP = model.ArchivalExtHTTP
|
|
ExtTCPConnect = model.ArchivalExtTCPConnect
|
|
ExtTLSHandshake = model.ArchivalExtTLSHandshake
|
|
ExtTunnel = model.ArchivalExtTunnel
|
|
)
|
|
|
|
// NewTCPConnectList creates a new TCPConnectList
|
|
func NewTCPConnectList(begin time.Time, events []Event) (out []TCPConnectEntry) {
|
|
for _, wrapper := range events {
|
|
if _, ok := wrapper.(*EventConnectOperation); !ok {
|
|
continue
|
|
}
|
|
event := wrapper.Value()
|
|
if event.Proto != "tcp" {
|
|
continue
|
|
}
|
|
// We assume Go is passing us legit data structures
|
|
ip, sport, _ := net.SplitHostPort(event.Address)
|
|
iport, _ := strconv.Atoi(sport)
|
|
out = append(out, TCPConnectEntry{
|
|
IP: ip,
|
|
Port: iport,
|
|
Status: TCPConnectStatus{
|
|
Blocked: nil, // only used by Web Connectivity
|
|
Failure: event.Err.ToFailure(),
|
|
Success: event.Err.IsNil(),
|
|
},
|
|
T: event.Time.Sub(begin).Seconds(),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewFailure creates a failure nullable string from the given error. This function
|
|
// is equivalent to NewFailureStr(err).ToFailure().
|
|
func NewFailure(err error) *string {
|
|
return NewFailureStr(err).ToFailure()
|
|
}
|
|
|
|
// NewFailedOperation creates a failed operation string from the given error.
|
|
func NewFailedOperation(err error) *string {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var (
|
|
errWrapper *netxlite.ErrWrapper
|
|
s = netxlite.UnknownOperation
|
|
)
|
|
if errors.As(err, &errWrapper) && errWrapper.Operation != "" {
|
|
s = errWrapper.Operation
|
|
}
|
|
return &s
|
|
}
|
|
|
|
// httpAddHeaders adds the headers inside source into destList and destMap.
|
|
func httpAddHeaders(source http.Header, destList *[]HTTPHeader,
|
|
destMap *map[string]MaybeBinaryValue) {
|
|
*destList = []HTTPHeader{}
|
|
*destMap = make(map[string]model.ArchivalMaybeBinaryData)
|
|
for key, values := range source {
|
|
for index, value := range values {
|
|
value := MaybeBinaryValue{Value: value}
|
|
// With the map representation we can only represent a single
|
|
// value for every key. Hence the list representation.
|
|
if index == 0 {
|
|
(*destMap)[key] = value
|
|
}
|
|
*destList = append(*destList, HTTPHeader{
|
|
Key: key,
|
|
Value: value,
|
|
})
|
|
}
|
|
}
|
|
// Sorting helps with unit testing (map keys are unordered)
|
|
sort.Slice(*destList, func(i, j int) bool {
|
|
return (*destList)[i].Key < (*destList)[j].Key
|
|
})
|
|
}
|
|
|
|
// NewRequestList returns the list for "requests"
|
|
func NewRequestList(begin time.Time, events []Event) (out []RequestEntry) {
|
|
// OONI wants the last request to appear first
|
|
tmp := newRequestList(begin, events)
|
|
for i := len(tmp) - 1; i >= 0; i-- {
|
|
out = append(out, tmp[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
func newRequestList(begin time.Time, events []Event) (out []RequestEntry) {
|
|
for _, wrapper := range events {
|
|
ev := wrapper.Value()
|
|
switch wrapper.(type) {
|
|
case *EventHTTPTransactionDone:
|
|
entry := RequestEntry{}
|
|
entry.T = ev.Time.Sub(begin).Seconds()
|
|
httpAddHeaders(
|
|
ev.HTTPRequestHeaders, &entry.Request.HeadersList, &entry.Request.Headers)
|
|
entry.Request.Method = ev.HTTPMethod
|
|
entry.Request.URL = ev.HTTPURL
|
|
entry.Request.Transport = ev.Transport
|
|
httpAddHeaders(
|
|
ev.HTTPResponseHeaders, &entry.Response.HeadersList, &entry.Response.Headers)
|
|
entry.Response.Code = int64(ev.HTTPStatusCode)
|
|
entry.Response.Locations = ev.HTTPResponseHeaders.Values("Location")
|
|
entry.Response.Body.Value = string(ev.HTTPResponseBody)
|
|
entry.Response.BodyIsTruncated = ev.HTTPResponseBodyIsTruncated
|
|
entry.Failure = ev.Err.ToFailure()
|
|
out = append(out, entry)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
type dnsQueryType string
|
|
|
|
// NewDNSQueriesList returns a list of DNS queries.
|
|
func NewDNSQueriesList(begin time.Time, events []Event) (out []DNSQueryEntry) {
|
|
// TODO(bassosimone): add support for CNAME lookups.
|
|
for _, wrapper := range events {
|
|
if _, ok := wrapper.(*EventResolveDone); !ok {
|
|
continue
|
|
}
|
|
ev := wrapper.Value()
|
|
for _, qtype := range []dnsQueryType{"A", "AAAA"} {
|
|
entry := qtype.makeQueryEntry(begin, ev)
|
|
for _, addr := range ev.Addresses {
|
|
if qtype.ipOfType(addr) {
|
|
entry.Answers = append(
|
|
entry.Answers, qtype.makeAnswerEntry(addr))
|
|
}
|
|
}
|
|
if len(entry.Answers) <= 0 && ev.Err.IsNil() {
|
|
// This allows us to skip cases where the server does not have
|
|
// an IPv6 address but has an IPv4 address. Instead, when we
|
|
// receive an error, we want to track its existence. The main
|
|
// issue here is that we are cheating, because we are creating
|
|
// entries representing queries, but we don't know what the
|
|
// resolver actually did, especially the system resolver. So,
|
|
// this output is just our best guess.
|
|
continue
|
|
}
|
|
out = append(out, entry)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (qtype dnsQueryType) ipOfType(addr string) bool {
|
|
switch qtype {
|
|
case "A":
|
|
return !strings.Contains(addr, ":")
|
|
case "AAAA":
|
|
return strings.Contains(addr, ":")
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (qtype dnsQueryType) makeAnswerEntry(addr string) DNSAnswerEntry {
|
|
answer := DNSAnswerEntry{AnswerType: string(qtype)}
|
|
// Figuring out the ASN and the org here is not just a service to whoever
|
|
// is reading a JSON: Web Connectivity also depends on it!
|
|
asn, org, _ := geolocate.LookupASN(addr)
|
|
answer.ASN = int64(asn)
|
|
answer.ASOrgName = org
|
|
switch qtype {
|
|
case "A":
|
|
answer.IPv4 = addr
|
|
case "AAAA":
|
|
answer.IPv6 = addr
|
|
}
|
|
return answer
|
|
}
|
|
|
|
func (qtype dnsQueryType) makeQueryEntry(begin time.Time, ev *EventValue) DNSQueryEntry {
|
|
return DNSQueryEntry{
|
|
Engine: ev.Proto,
|
|
Failure: ev.Err.ToFailure(),
|
|
Hostname: ev.Hostname,
|
|
QueryType: string(qtype),
|
|
ResolverAddress: ev.Address,
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
}
|
|
}
|
|
|
|
// NewNetworkEventsList returns a list of network events.
|
|
func NewNetworkEventsList(begin time.Time, events []Event) (out []NetworkEvent) {
|
|
for _, wrapper := range events {
|
|
ev := wrapper.Value()
|
|
switch wrapper.(type) {
|
|
case *EventConnectOperation:
|
|
out = append(out, NetworkEvent{
|
|
Address: ev.Address,
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
Proto: ev.Proto,
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
case *EventReadOperation:
|
|
out = append(out, NetworkEvent{
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
NumBytes: int64(ev.NumBytes),
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
case *EventWriteOperation:
|
|
out = append(out, NetworkEvent{
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
NumBytes: int64(ev.NumBytes),
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
case *EventReadFromOperation:
|
|
out = append(out, NetworkEvent{
|
|
Address: ev.Address,
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
NumBytes: int64(ev.NumBytes),
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
case *EventWriteToOperation:
|
|
out = append(out, NetworkEvent{
|
|
Address: ev.Address,
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
NumBytes: int64(ev.NumBytes),
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
default: // For example, "tls_handshake_done" (used in data analysis!)
|
|
out = append(out, NetworkEvent{
|
|
Failure: ev.Err.ToFailure(),
|
|
Operation: wrapper.Name(),
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
})
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewTLSHandshakesList creates a new TLSHandshakesList
|
|
func NewTLSHandshakesList(begin time.Time, events []Event) (out []TLSHandshake) {
|
|
for _, wrapper := range events {
|
|
switch wrapper.(type) {
|
|
case *EventQUICHandshakeDone, *EventTLSHandshakeDone: // interested
|
|
default:
|
|
continue // not interested
|
|
}
|
|
ev := wrapper.Value()
|
|
out = append(out, TLSHandshake{
|
|
Address: ev.Address,
|
|
CipherSuite: ev.TLSCipherSuite,
|
|
Failure: ev.Err.ToFailure(),
|
|
NegotiatedProtocol: ev.TLSNegotiatedProto,
|
|
NoTLSVerify: ev.NoTLSVerify,
|
|
PeerCertificates: tlsMakePeerCerts(ev.TLSPeerCerts),
|
|
ServerName: ev.TLSServerName,
|
|
T: ev.Time.Sub(begin).Seconds(),
|
|
TLSVersion: ev.TLSVersion,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
func tlsMakePeerCerts(in [][]byte) (out []MaybeBinaryValue) {
|
|
for _, entry := range in {
|
|
out = append(out, MaybeBinaryValue{Value: string(entry)})
|
|
}
|
|
return
|
|
}
|