1#!/usr/bin/env python3
2#
3# Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
4#
5# Licensed under the Apache License 2.0 (the "License").  You may not use
6# this file except in compliance with the License.  You can obtain a copy
7# in the file LICENSE in the source distribution or at
8# https://www.openssl.org/source/license.html
9import sys, os, os.path, glob, json, re
10
11re_version = re.compile(r'''^OpenSSL/[0-9]+\.[0-9]\.[0-9](-[^ ]+)? ([^)]+)''')
12
13class Unexpected(Exception):
14    def __init__(self, filename, msg):
15        Exception.__init__(self, f"file {repr(filename)}: {msg}")
16
17class Malformed(Exception):
18    def __init__(self, line, msg):
19        Exception.__init__(self, f"{line}: {msg}")
20
21event_type_counts = {}
22frame_type_counts = {}
23
24def load_file(filename):
25    objs = []
26    with open(filename, 'r') as fi:
27        for line in fi:
28            if line[0] != '\x1e':
29                raise Unexpected(filename, "expected JSON-SEQ leader")
30
31            line = line[1:]
32            try:
33                objs.append(json.loads(line))
34            except:
35                fi.seek(0)
36                fdata = fi.read()
37                print(fdata)
38                raise Malformed(line, "Malformed json input")
39    return objs
40
41def check_header(filename, hdr):
42    if not 'qlog_format' in hdr:
43        raise Unexpected(filename, "must have qlog_format in header line")
44
45    if not 'qlog_version' in hdr:
46        raise Unexpected(filename, "must have qlog_version in header line")
47
48    if not 'trace' in hdr:
49        raise Unexpected(filename, "must have trace in header line")
50
51    hdr_trace = hdr["trace"]
52    if not 'common_fields' in hdr_trace:
53        raise Unexpected(filename, "must have common_fields in header line")
54
55    if not 'vantage_point' in hdr_trace:
56        raise Unexpected(filename, "must have vantage_point in header line")
57
58    if hdr_trace["vantage_point"].get('type') not in ('client', 'server'):
59        raise Unexpected(filename, "unexpected vantage_point")
60
61    vp_name = hdr_trace["vantage_point"].get('name')
62    if type(vp_name) != str:
63        raise Unexpected(filename, "expected vantage_point name")
64
65    if not re_version.match(vp_name):
66        raise Unexpected(filename, "expected correct vantage_point format")
67
68    hdr_common_fields = hdr_trace["common_fields"]
69    if hdr_common_fields.get("time_format") != "delta":
70        raise Unexpected(filename, "must have expected time_format")
71
72    if hdr_common_fields.get("protocol_type") != ["QUIC"]:
73        raise Unexpected(filename, "must have expected protocol_type")
74
75    if hdr["qlog_format"] != "JSON-SEQ":
76        raise Unexpected(filename, "unexpected qlog_format")
77
78    if hdr["qlog_version"] != "0.3":
79        raise Unexpected(filename, "unexpected qlog_version")
80
81def check_event(filename, event):
82    name = event.get("name")
83
84    if type(name) != str:
85        raise Unexpected(filename, "expected event to have name")
86
87    event_type_counts.setdefault(name, 0)
88    event_type_counts[name] += 1
89
90    if type(event.get("time")) != int:
91        raise Unexpected(filename, "expected event to have time")
92
93    data = event.get('data')
94    if type(data) != dict:
95        raise Unexpected(filename, "expected event to have data")
96
97    if "qlog_format" in event:
98        raise Unexpected(filename, "event must not be header line")
99
100    if name in ('transport:packet_sent', 'transport:packet_received'):
101        check_packet_header(filename, event, data.get('header'))
102
103        datagram_id = data.get('datagram_id')
104        if type(datagram_id) != int:
105            raise Unexpected(filename, "datagram ID must be integer")
106
107        for frame in data.get('frames', []):
108            check_frame(filename, event, frame)
109
110def check_packet_header(filename, event, header):
111    if type(header) != dict:
112        raise Unexpected(filename, "expected object for packet header")
113
114    # packet type -> has frames?
115    packet_types = {
116            'version_negotiation': False,
117            'retry': False,
118            'initial': True,
119            'handshake': True,
120            '0RTT': True,
121            '1RTT': True,
122    }
123
124    data = event['data']
125    packet_type = header.get('packet_type')
126    if packet_type not in packet_types:
127        raise Unexpected(filename, f"unexpected packet type: {packet_type}")
128
129    if type(header.get('dcid')) != str:
130        raise Unexpected(filename, "expected packet event to have DCID")
131    if packet_type != '1RTT' and type(header.get('scid')) != str:
132        raise Unexpected(filename, "expected packet event to have SCID")
133
134    if type(data.get('datagram_id')) != int:
135        raise Unexpected(filename, "expected packet event to have datagram ID")
136
137    if packet_types[packet_type]:
138        if type(header.get('packet_number')) != int:
139            raise Unexpected(filename, f"expected packet event to have packet number")
140        if type(data.get('frames')) != list:
141            raise Unexpected(filename, "expected packet event to have frames")
142
143def check_frame(filename, event, frame):
144    frame_type = frame.get('frame_type')
145    if type(frame_type) != str:
146        raise Unexpected(filename, "frame must have frame_type field")
147
148    frame_type_counts.setdefault(event['name'], {})
149    counts = frame_type_counts[event['name']]
150
151    counts.setdefault(frame_type, 0)
152    counts[frame_type] += 1
153
154def check_file(filename):
155    objs = load_file(filename)
156    if len(objs) < 2:
157        raise Unexpected(filename, "must have at least two objects")
158
159    check_header(filename, objs[0])
160    for event in objs[1:]:
161        check_event(filename, event)
162
163def run():
164    num_files = 0
165
166    # Check each file for validity.
167    qlogdir = os.environ['QLOGDIR']
168    for filename in glob.glob(os.path.join(qlogdir, '*.sqlog')):
169        check_file(filename)
170        num_files += 1
171
172    # Check that all supported events were generated.
173    required_events = (
174        "transport:parameters_set",
175        "connectivity:connection_state_updated",
176        "connectivity:connection_started",
177        "transport:packet_sent",
178        "transport:packet_received",
179        "connectivity:connection_closed"
180    )
181
182    if num_files < 300:
183        raise Unexpected(qlogdir, f"unexpectedly few output files: {num_files}")
184
185    for required_event in required_events:
186        count = event_type_counts.get(required_event, 0)
187        if count < 100:
188            raise Unexpected(qlogdir, f"unexpectedly low count of event '{required_event}': got {count}")
189
190    # For each direction, ensure that at least one of the tests we run generated
191    # a given frame type.
192    required_frame_types = (
193        "padding",
194        "ping",
195        "ack",
196
197        "crypto",
198        "handshake_done",
199        "connection_close",
200
201        "path_challenge",
202        "path_response",
203
204        "stream",
205        "reset_stream",
206        "stop_sending",
207
208        "new_connection_id",
209        "retire_connection_id",
210
211        "max_streams",
212        "streams_blocked",
213
214        "max_stream_data",
215        "stream_data_blocked",
216
217        "max_data",
218        "data_blocked",
219
220        "new_token",
221    )
222
223    for required_frame_type in required_frame_types:
224        sent_count = frame_type_counts.get('transport:packet_sent', {}).get(required_frame_type, 0)
225        if sent_count < 1:
226            raise Unexpected(qlogdir, f"unexpectedly did not send any '{required_frame_type}' frames")
227
228        received_count = frame_type_counts.get('transport:packet_received', {}).get(required_frame_type, 0)
229        if received_count < 1:
230            raise Unexpected(qlogdir, f"unexpectedly did not receive any '{required_frame_type}' frames")
231
232    return 0
233
234if __name__ == '__main__':
235    sys.exit(run())
236