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