package measurex import ( "net" "net/http" "strconv" "strings" "time" ) // // Archival // // This file defines helpers to serialize to the OONI data format. // // // BinaryData // // ArchivalBinaryData is the archival format for binary data. type ArchivalBinaryData struct { Data []byte `json:"data"` Format string `json:"format"` } // NewArchivalBinaryData builds a new ArchivalBinaryData // from an array of bytes. If the array is nil, we return nil. func NewArchivalBinaryData(data []byte) (out *ArchivalBinaryData) { if len(data) > 0 { out = &ArchivalBinaryData{ Data: data, Format: "base64", } } return } // // NetworkEvent // // ArchivalNetworkEvent is the OONI data format representation // of a network event according to df-008-netevents. type ArchivalNetworkEvent struct { // JSON names compatible with df-008-netevents RemoteAddr string `json:"address"` Failure *string `json:"failure"` Count int `json:"num_bytes,omitempty"` Operation string `json:"operation"` Network string `json:"proto"` Finished float64 `json:"t"` Started float64 `json:"started"` // Names that are not part of the spec. Oddity Oddity `json:"oddity"` } // NewArchivalNetworkEvent converts a network event to its archival format. func NewArchivalNetworkEvent(in *NetworkEvent) *ArchivalNetworkEvent { return &ArchivalNetworkEvent{ RemoteAddr: in.RemoteAddr, Failure: in.Failure, Count: in.Count, Operation: in.Operation, Network: in.Network, Finished: in.Finished, Started: in.Started, Oddity: in.Oddity, } } // NewArchivalNetworkEventList converts a list of NetworkEvent // to a list of ArchivalNetworkEvent. func NewArchivalNetworkEventList(in []*NetworkEvent) (out []*ArchivalNetworkEvent) { for _, ev := range in { out = append(out, NewArchivalNetworkEvent(ev)) } return } // // DNSRoundTripEvent // // ArchivalDNSRoundTripEvent is the OONI data format representation // of a DNS round trip, which is currently not specified. // // We are trying to use names compatible with the names currently // used by other specifications we currently use. type ArchivalDNSRoundTripEvent struct { Network string `json:"engine"` Address string `json:"resolver_address"` Query *ArchivalBinaryData `json:"raw_query"` Started float64 `json:"started"` Finished float64 `json:"t"` Failure *string `json:"failure"` Reply *ArchivalBinaryData `json:"raw_reply"` } // NewArchivalDNSRoundTripEvent converts a DNSRoundTripEvent into is archival format. func NewArchivalDNSRoundTripEvent(in *DNSRoundTripEvent) *ArchivalDNSRoundTripEvent { return &ArchivalDNSRoundTripEvent{ Network: in.Network, Address: in.Address, Query: NewArchivalBinaryData(in.Query), Started: in.Started, Finished: in.Finished, Failure: in.Failure, Reply: NewArchivalBinaryData(in.Reply), } } // NewArchivalDNSRoundTripEventList converts a DNSRoundTripEvent // list to the corresponding archival format. func NewArchivalDNSRoundTripEventList(in []*DNSRoundTripEvent) (out []*ArchivalDNSRoundTripEvent) { for _, ev := range in { out = append(out, NewArchivalDNSRoundTripEvent(ev)) } return } // // HTTPRoundTrip // // ArchivalHTTPRequest is the archival format of an HTTP // request according to df-001-http.md. type ArchivalHTTPRequest struct { Method string `json:"method"` URL string `json:"url"` Headers ArchivalHeaders `json:"headers"` } // ArchivalHTTPResponse is the archival format of an HTTP // response according to df-001-http.md. type ArchivalHTTPResponse struct { // Names consistent with df-001-http.md Code int64 `json:"code"` Headers ArchivalHeaders `json:"headers"` Body *ArchivalBinaryData `json:"body"` BodyIsTruncated bool `json:"body_is_truncated"` // Fields not part of the spec BodyLength int64 `json:"x_body_length"` BodyIsUTF8 bool `json:"x_body_is_utf8"` } // ArchivalHTTPRoundTripEvent is the archival format of an // HTTP response according to df-001-http.md. type ArchivalHTTPRoundTripEvent struct { // JSON names following the df-001-httpt data format. Failure *string `json:"failure"` Request *HTTPRequest `json:"request"` Response *HTTPResponse `json:"response"` Finished float64 `json:"t"` Started float64 `json:"started"` // Names not in the specification Oddity Oddity `json:"oddity"` } // ArchivalHeaders is a list of HTTP headers. type ArchivalHeaders map[string]string // Get searches for the first header with the named key // and returns it. If not found, returns an empty string. func (headers ArchivalHeaders) Get(key string) string { return headers[strings.ToLower(key)] } // NewArchivalHeaders builds a new HeadersList from http.Header. func NewArchivalHeaders(in http.Header) (out ArchivalHeaders) { out = make(ArchivalHeaders) for k, vv := range in { for _, v := range vv { // It breaks my hearth a little bit to ignore // subsequent headers, but this does not happen // very frequently, and I know the pipeline // parses the map headers format only. out[strings.ToLower(k)] = v break } } return } // NewArchivalHTTPRoundTripEvent converts an HTTPRoundTrip to its archival format. func NewArchivalHTTPRoundTripEvent(in *HTTPRoundTripEvent) *ArchivalHTTPRoundTripEvent { return &ArchivalHTTPRoundTripEvent{ Failure: in.Failure, Request: &HTTPRequest{ Method: in.Method, URL: in.URL, Headers: NewArchivalHeaders(in.RequestHeaders), }, Response: &HTTPResponse{ Code: in.StatusCode, Headers: NewArchivalHeaders(in.ResponseHeaders), Body: NewArchivalBinaryData(in.ResponseBody), BodyLength: in.ResponseBodyLength, BodyIsTruncated: in.ResponseBodyIsTruncated, BodyIsUTF8: in.ResponseBodyIsUTF8, }, Finished: in.Finished, Started: in.Started, Oddity: in.Oddity, } } // NewArchivalHTTPRoundTripEventList converts a list of // HTTPRoundTripEvent to a list of ArchivalRoundTripEvent. func NewArchivalHTTPRoundTripEventList(in []*HTTPRoundTripEvent) (out []*ArchivalHTTPRoundTripEvent) { for _, ev := range in { out = append(out, NewArchivalHTTPRoundTripEvent(ev)) } return } // // QUICTLSHandshakeEvent // // ArchivalQUICTLSHandshakeEvent is the archival data format for a // QUIC or TLS handshake event according to df-006-tlshandshake. type ArchivalQUICTLSHandshakeEvent struct { // JSON names compatible with df-006-tlshandshake CipherSuite string `json:"cipher_suite"` Failure *string `json:"failure"` NegotiatedProto string `json:"negotiated_proto"` TLSVersion string `json:"tls_version"` PeerCerts []*ArchivalBinaryData `json:"peer_certificates"` Finished float64 `json:"t"` // JSON names that are consistent with the // spirit of the spec but are not in it RemoteAddr string `json:"address"` SNI string `json:"server_name"` // used in prod ALPN []string `json:"alpn"` SkipVerify bool `json:"no_tls_verify"` // used in prod Oddity Oddity `json:"oddity"` Network string `json:"proto"` Started float64 `json:"started"` } // NewArchivalTLSCertList builds a new []ArchivalBinaryData // from a list of raw x509 certificates data. func NewArchivalTLSCerts(in [][]byte) (out []*ArchivalBinaryData) { for _, cert := range in { out = append(out, &ArchivalBinaryData{ Data: cert, Format: "base64", }) } return } // NewArchivalQUICTLSHandshakeEvent converts a QUICTLSHandshakeEvent // to its archival data format. func NewArchivalQUICTLSHandshakeEvent(in *QUICTLSHandshakeEvent) *ArchivalQUICTLSHandshakeEvent { return &ArchivalQUICTLSHandshakeEvent{ CipherSuite: in.CipherSuite, Failure: in.Failure, NegotiatedProto: in.NegotiatedProto, TLSVersion: in.TLSVersion, PeerCerts: NewArchivalTLSCerts(in.PeerCerts), Finished: in.Finished, RemoteAddr: in.RemoteAddr, SNI: in.SNI, ALPN: in.ALPN, SkipVerify: in.SkipVerify, Oddity: in.Oddity, Network: in.Network, Started: in.Started, } } // NewArchivalQUICTLSHandshakeEventList converts a list of // QUICTLSHandshakeEvent to a list of ArchivalQUICTLSHandshakeEvent. func NewArchivalQUICTLSHandshakeEventList(in []*QUICTLSHandshakeEvent) (out []*ArchivalQUICTLSHandshakeEvent) { for _, ev := range in { out = append(out, NewArchivalQUICTLSHandshakeEvent(ev)) } return } // // DNSLookup // // ArchivalDNSLookupAnswer is the archival format of a // DNS lookup answer according to df-002-dnst. type ArchivalDNSLookupAnswer struct { // JSON names compatible with df-002-dnst's spec Type string `json:"answer_type"` IPv4 string `json:"ipv4,omitempty"` IPv6 string `json:"ivp6,omitempty"` // Names not part of the spec. ALPN string `json:"alpn,omitempty"` } // ArchivalDNSLookupEvent is the archival data format // of a DNS lookup according to df-002-dnst. type ArchivalDNSLookupEvent struct { // fields inside df-002-dnst Answers []ArchivalDNSLookupAnswer `json:"answers"` Network string `json:"engine"` Failure *string `json:"failure"` Domain string `json:"hostname"` QueryType string `json:"query_type"` Address string `json:"resolver_address"` Finished float64 `json:"t"` // Names not part of the spec. Started float64 `json:"started"` Oddity Oddity `json:"oddity"` } // NewArchivalDNSLookupAnswers creates a list of ArchivalDNSLookupAnswer. func NewArchivalDNSLookupAnswers(in *DNSLookupEvent) (out []ArchivalDNSLookupAnswer) { for _, ip := range in.A { out = append(out, ArchivalDNSLookupAnswer{ Type: "A", IPv4: ip, }) } for _, ip := range in.AAAA { out = append(out, ArchivalDNSLookupAnswer{ Type: "AAAA", IPv6: ip, }) } for _, alpn := range in.ALPN { out = append(out, ArchivalDNSLookupAnswer{ Type: "ALPN", ALPN: alpn, }) } return } // NewArchivalDNSLookupEvent converts a DNSLookupEvent // to its archival representation. func NewArchivalDNSLookupEvent(in *DNSLookupEvent) *ArchivalDNSLookupEvent { return &ArchivalDNSLookupEvent{ Answers: NewArchivalDNSLookupAnswers(in), Network: in.Network, Failure: in.Failure, Domain: in.Domain, QueryType: in.QueryType, Address: in.Address, Finished: in.Finished, Started: in.Started, Oddity: in.Oddity, } } // NewArchivalDNSLookupEventList converts a list of DNSLookupEvent // to a list of ArchivalDNSLookupEvent. func NewArchivalDNSLookupEventList(in []*DNSLookupEvent) (out []*ArchivalDNSLookupEvent) { for _, ev := range in { out = append(out, NewArchivalDNSLookupEvent(ev)) } return } // // TCPConnect // // ArchivalTCPConnect is the archival form of TCP connect // events in compliance with df-005-tcpconnect. type ArchivalTCPConnect struct { // Names part of the spec. IP string `json:"ip"` Port int64 `json:"port"` Finished float64 `json:"t"` Status *ArchivalTCPConnectStatus `json:"status"` // Names not part of the spec. Started float64 `json:"started"` Oddity Oddity `json:"oddity"` } // ArchivalTCPConnectStatus contains the status of a TCP connect. type ArchivalTCPConnectStatus struct { Blocked bool `json:"blocked"` Failure *string `json:"failure"` Success bool `json:"success"` } // NewArchivalTCPConnect converts a NetworkEvent to an ArchivalTCPConnect. func NewArchivalTCPConnect(in *NetworkEvent) *ArchivalTCPConnect { // We ignore errors because values come from Go code that // emits correct serialization of TCP/UDP addresses. addr, port, _ := net.SplitHostPort(in.RemoteAddr) portnum, _ := strconv.Atoi(port) return &ArchivalTCPConnect{ IP: addr, Port: int64(portnum), Finished: in.Finished, Status: &ArchivalTCPConnectStatus{ Blocked: in.Failure != nil, Failure: in.Failure, Success: in.Failure == nil, }, Started: in.Started, Oddity: in.Oddity, } } // NewArchivalTCPConnectList converts a list of NetworkEvent // to a list of ArchivalTCPConnect. In doing that, the code // only considers "connect" events using the TCP protocol. func NewArchivalTCPConnectList(in []*NetworkEvent) (out []*ArchivalTCPConnect) { for _, ev := range in { if ev.Operation != "connect" { continue } switch ev.Network { case "tcp", "tcp4", "tcp6": out = append(out, NewArchivalTCPConnect(ev)) default: // nothing } } return } // // URLMeasurement // // ArchivalURLMeasurement is the archival representation of URLMeasurement type ArchivalURLMeasurement struct { URL string `json:"url"` DNS []*ArchivalDNSMeasurement `json:"dns"` Endpoints []*ArchivalHTTPEndpointMeasurement `json:"endpoints"` TH *ArchivalTHMeasurement `json:"th"` TotalRuntime time.Duration `json:"x_total_runtime"` DNSRuntime time.Duration `json:"x_dns_runtime"` THRuntime time.Duration `json:"x_th_runtime"` EpntsRuntime time.Duration `json:"x_epnts_runtime"` } // NewArchivalURLMeasurement creates the archival representation // of an URLMeasurement data structure. func NewArchivalURLMeasurement(in *URLMeasurement) *ArchivalURLMeasurement { return &ArchivalURLMeasurement{ URL: in.URL, DNS: NewArchivalDNSMeasurementList(in.DNS), Endpoints: NewArchivalHTTPEndpointMeasurementList(in.Endpoints), TH: NewArchivalTHMeasurement(in.TH), TotalRuntime: in.TotalRuntime, DNSRuntime: in.DNSRuntime, THRuntime: in.THRuntime, EpntsRuntime: in.EpntsRuntime, } } // // EndpointMeasurement // // ArchivalEndpointMeasurement is the archival representation of EndpointMeasurement. type ArchivalEndpointMeasurement struct { // Network is the network of this endpoint. Network EndpointNetwork `json:"network"` // Address is the address of this endpoint. Address string `json:"address"` // An EndpointMeasurement is a Measurement. *ArchivalMeasurement } // NewArchivalEndpointMeasurement converts an EndpointMeasurement // to the corresponding archival data format. func NewArchivalEndpointMeasurement(in *EndpointMeasurement) *ArchivalEndpointMeasurement { return &ArchivalEndpointMeasurement{ Network: in.Network, Address: in.Address, ArchivalMeasurement: NewArchivalMeasurement(in.Measurement), } } // // THMeasurement // // ArchivalTHMeasurement is the archival representation of THMeasurement. type ArchivalTHMeasurement struct { DNS []*ArchivalDNSMeasurement `json:"dns"` Endpoints []*ArchivalHTTPEndpointMeasurement `json:"endpoints"` } // NewArchivalTHMeasurement creates the archival representation of THMeasurement. func NewArchivalTHMeasurement(in *THMeasurement) *ArchivalTHMeasurement { return &ArchivalTHMeasurement{ DNS: NewArchivalDNSMeasurementList(in.DNS), Endpoints: NewArchivalHTTPEndpointMeasurementList(in.Endpoints), } } // // DNSMeasurement // // ArchivalDNSMeasurement is the archival representation of DNSMeasurement. type ArchivalDNSMeasurement struct { Domain string `json:"domain"` *ArchivalMeasurement } // NewArchivalDNSMeasurement converts a DNSMeasurement to an ArchivalDNSMeasurement. func NewArchivalDNSMeasurement(in *DNSMeasurement) *ArchivalDNSMeasurement { return &ArchivalDNSMeasurement{ Domain: in.Domain, ArchivalMeasurement: NewArchivalMeasurement(in.Measurement), } } // NewArchivalDNSMeasurementList converts a list of DNSMeasurement // to a list of ArchivalDNSMeasurement. func NewArchivalDNSMeasurementList(in []*DNSMeasurement) (out []*ArchivalDNSMeasurement) { for _, m := range in { out = append(out, NewArchivalDNSMeasurement(m)) } return } // // HTTPEndpointMeasurement // // ArchivalHTTPEndpointMeasurement is the archival representation // of an HTTPEndpointMeasurement. type ArchivalHTTPEndpointMeasurement struct { URL string `json:"url"` Network EndpointNetwork `json:"network"` Address string `json:"address"` *ArchivalMeasurement } // NewArchivalHTTPEndpointMeasurement converts an HTTPEndpointMeasurement // to an ArchivalHTTPEndpointMeasurement. func NewArchivalHTTPEndpointMeasurement(in *HTTPEndpointMeasurement) *ArchivalHTTPEndpointMeasurement { return &ArchivalHTTPEndpointMeasurement{ URL: in.URL, Network: in.Network, Address: in.Address, ArchivalMeasurement: NewArchivalMeasurement(in.Measurement), } } // NewArchivalHTTPEndpointMeasurementList converts a list of HTTPEndpointMeasurement // to a list of ArchivalHTTPEndpointMeasurement. func NewArchivalHTTPEndpointMeasurementList(in []*HTTPEndpointMeasurement) (out []*ArchivalHTTPEndpointMeasurement) { for _, m := range in { out = append(out, NewArchivalHTTPEndpointMeasurement(m)) } return } // // Measurement // // ArchivalMeasurement is the archival representation of a Measurement. type ArchivalMeasurement struct { NetworkEvents []*ArchivalNetworkEvent `json:"network_events,omitempty"` DNSEvents []*ArchivalDNSRoundTripEvent `json:"dns_events,omitempty"` Queries []*ArchivalDNSLookupEvent `json:"queries,omitempty"` TCPConnect []*ArchivalTCPConnect `json:"tcp_connect,omitempty"` TLSHandshakes []*ArchivalQUICTLSHandshakeEvent `json:"tls_handshakes,omitempty"` QUICHandshakes []*ArchivalQUICTLSHandshakeEvent `json:"quic_handshakes,omitempty"` Requests []*ArchivalHTTPRoundTripEvent `json:"requests,omitempty"` } // NewArchivalMeasurement converts a Measurement to ArchivalMeasurement. func NewArchivalMeasurement(in *Measurement) *ArchivalMeasurement { out := &ArchivalMeasurement{ NetworkEvents: NewArchivalNetworkEventList(in.ReadWrite), DNSEvents: NewArchivalDNSRoundTripEventList(in.DNSRoundTrip), Queries: nil, // done below TCPConnect: NewArchivalTCPConnectList(in.Connect), TLSHandshakes: NewArchivalQUICTLSHandshakeEventList(in.TLSHandshake), QUICHandshakes: NewArchivalQUICTLSHandshakeEventList(in.QUICHandshake), Requests: NewArchivalHTTPRoundTripEventList(in.HTTPRoundTrip), } out.Queries = append(out.Queries, NewArchivalDNSLookupEventList(in.LookupHost)...) out.Queries = append(out.Queries, NewArchivalDNSLookupEventList(in.LookupHTTPSSvc)...) return out }