diff --git a/inject_secrets.py b/inject_secrets.py index 437b1fded3c8d9a044ed8db9fef8b09120e4b9bf..53a253843c815bb6a3c008130c0b06a0f54d4bc3 100755 --- a/inject_secrets.py +++ b/inject_secrets.py @@ -47,7 +47,7 @@ class SecretsInjector: if len(results) > 1: self.log( - f"[i] found more than one keylog file for {test_case_path}. Using the first one.", + f"⚒ found more than one keylog file for {test_case_path}. Using the first one.", color="yellow", ) @@ -56,7 +56,7 @@ class SecretsInjector: def inject(self, pcap_path: Path, pcap_ng_path: Path, keylog_file: Path): if not pcap_path.is_file(): self.log( - f"[!] Raw pcap file {pcap_path} not found. Skipping.", + f"⨯ Raw pcap file {pcap_path} not found. Skipping.", color="red", ) @@ -64,7 +64,7 @@ class SecretsInjector: if pcap_ng_path.is_file() and pcap_ng_path.stat().st_size > 0: self.log( - f"[i] {pcap_ng_path} already exists. Skipping.", + f"⚒ {pcap_ng_path} already exists. Skipping.", color="cyan", ) self.num_already_injected += 1 @@ -72,7 +72,7 @@ class SecretsInjector: return try: - self.log(f"[i] Injecting {keylog_file} into {pcap_path} -> {pcap_ng_path}.") + self.log(f"⚒ Injecting {keylog_file} into {pcap_path} -> {pcap_ng_path}.") subprocess.check_call( [ "editcap", @@ -85,7 +85,7 @@ class SecretsInjector: self.num_injected += 1 except subprocess.CalledProcessError: self.log( - f"[!] Failed to inject secrets {keylog_file} into {pcap_path}.", + f"⨯ Failed to inject secrets {keylog_file} into {pcap_path}.", color="red", ) self.num_failed += 1 @@ -99,7 +99,7 @@ class SecretsInjector: if not keylog_file: self.log( - f"[!] no keylog file found in {test_run_dir}", + f"⨯ no keylog file found in {test_run_dir}", color="red", ) self.num_no_secret_found += 1 @@ -163,7 +163,7 @@ class SecretsInjector: cprint(msg, **kwargs, end="\n") log_str = ( - f"[i] injected: {self.num_injected}, " + f"⚒ injected: {self.num_injected}, " f"failed: {self.num_failed}, " f"already injected: {self.num_already_injected}, " f"no keylog file found: {self.num_no_secret_found}" @@ -179,7 +179,7 @@ class SecretsInjector: else: self.inject_in_log_dir(spec) - self.log("[i] Done", color="green", attrs=["bold"]) + self.log("⚒ Done", color="green", attrs=["bold"]) def main(): diff --git a/plot_all.py b/plot_all.py index bb1fbf960a4929335ac7dbba327c3f24cfd1f38f..c27f1386b6090b76158ee65f1ff7d95f52df954d 100755 --- a/plot_all.py +++ b/plot_all.py @@ -55,7 +55,7 @@ def parse_args(): default=DEFAULT_TITLE, help=( f"The title for the diagram (default='{DEFAULT_TITLE}'). " - "WATCH OUT! THIS TTILE WILL BE FORMATTED -> WE TRUST THE FORMAT STRING!" + "WATCH OUT! THIS TTILE WILL BE FORMATTED → WE TRUST THE FORMAT STRING!" ), ) parser.add_argument( @@ -122,11 +122,11 @@ class PlotAllCli: self._current_log_dir: Optional[Path] = None self.debug = debug - def plot_in_testcase_dir( + def plot_in_meas_run_dir( self, measurement_result: ExtendedMeasurementResult, modes: list[PlotMode], - ) -> Optional[str]: + ) -> list[str]: """Generate plot for for this test case.""" assert self._current_log_dir test_case_dir = measurement_result.log_dir_for_test @@ -134,7 +134,7 @@ class PlotAllCli: if not measurement_result.succeeded and not self.include_failed: cprint( ( - "[i] Measurement " + "⚒ Measurement " f"{measurement_result.log_dir_for_test.relative_to(self._current_log_dir)} " "Failed. Skipping. Use --include-failed to include it anyway." ), @@ -142,19 +142,19 @@ class PlotAllCli: color="cyan", ) - return "testcase failed" + return ["testcase failed"] pcaps: list[Path] = [] - for iteration_dir in measurement_result.iteration_log_dirs: + for repetition_dir in measurement_result.repetition_log_dirs: pcapng = ( - iteration_dir + repetition_dir / "sim" / f"trace_node_{self.side.value}_with_secrets.pcapng" ) if not pcapng.is_file(): - print(f"[!] {pcapng} does not exist", file=sys.stderr) + print(f"⨯ {pcapng} does not exist", file=sys.stderr) continue @@ -162,12 +162,12 @@ class PlotAllCli: if not pcaps: cprint( - f"[!] no pcapng files found for {test_case_dir}. Skipping...", + f"⨯ no pcapng files found for {test_case_dir}. Skipping...", file=sys.stderr, color="red", ) - return "no traces files found" + return ["no traces files found"] cli = PlotCli( pcap_files=pcaps, @@ -176,25 +176,29 @@ class PlotAllCli: debug=self.debug, ) + rets = list[str]() + for mode in modes: output_file = Path(test_case_dir / f"time_{mode.value}_plot.{self.format}") if not self.force and output_file.is_file(): cprint( ( - f"[i] {output_file.relative_to(self._current_log_dir)} already exists. " + f"⚒ {output_file.relative_to(self._current_log_dir)} already exists. " "Skipping. Use --force to overwrite." ), file=sys.stderr, color="cyan", ) - return "already exists" + rets.append("already exists") + + continue cprint( ( - f"[i] Plotting in {create_relpath(test_case_dir)} " - f"({len(pcaps)} traces) -> {create_relpath(output_file)}" + f"⚒ Plotting in {create_relpath(test_case_dir)} " + f"({len(pcaps)} traces) → {create_relpath(output_file)}" ), attrs=["bold"], ) @@ -213,29 +217,31 @@ class PlotAllCli: except ParsingError as err: cprint( ( - f"[!] Could not parse {err.trace} in " + f"⨯ Could not parse {err.trace} in " f"{test_case_dir.relative_to(self._current_log_dir)}. " "Skipping..." ), file=sys.stderr, color="red", ) - cprint(f"[!] {err}", file=sys.stderr, color="red") + cprint(f"⨯ {err}", file=sys.stderr, color="red") - return str(err) + rets.append(str(err)) - return None + continue + + return rets def plot_in_log_dir(self, result: Result): """Generate plots for result file.""" cprint( - f"[i] Plotting results {result} (log dir: {result.log_dir})", + f"⚒ Plotting results {result} (log dir: {result.log_dir})", attrs=["bold"], ) self._current_log_dir = result.log_dir - plot_results = dict[ExtendedMeasurementResult, Optional[str]]() + plot_results = defaultdict[str, set[str]](set[str]) for tmp1 in result.measurement_results.values(): for tmp2 in tmp1.values(): @@ -247,45 +253,33 @@ class PlotAllCli: measurement_results = list(tmp2.values()) for measurement_result in measurement_results: - plot_result = self.plot_in_testcase_dir( + results = self.plot_in_meas_run_dir( measurement_result, modes=self.modes, ) - plot_results[measurement_result] = plot_result - - self.evaluate_plot_results(plot_results) - def evaluate_plot_results( - self, plot_results: dict[ExtendedMeasurementResult, Optional[str]] - ): - """Print a summary.""" - grouped = defaultdict(list) + for plot_result in results: + if plot_result == "already_exists": + plot_result = colored(plot_result, color="green") + plot_results[plot_result].add(measurement_result.combination) - for measurement_result, plot_result in plot_results.items(): - msg: str - - if not plot_result: - msg = colored("success", "green") - elif plot_result in ("already exists"): - msg = colored(plot_result, "green") - else: - msg = colored(plot_result, "red") - - grouped[msg].append(measurement_result) + if not plot_results: + plot_results[colored("success", color="green")].add( + measurement_result.combination + ) + # Print a summary. print() print("#### Results:") - - for msg, measurement_results in grouped.items(): - print(f" - {msg}: {len(measurement_results)}") - print() - for msg, measurement_results in grouped.items(): - print(f"- {msg}") + for msg, combinations in plot_results.items(): + print(f"- {msg}: {len(combinations)}") + + for combination in combinations: + print(f" - `{combination}`") - for measurement_result in measurement_results: - print(f" - `{measurement_result.combination}`") + print() def run(self): for result_file in self.result_files: @@ -313,7 +307,7 @@ def main(): except KeyboardInterrupt: sys.exit("\nQuit") - cprint("Done", color="green", attrs=["bold"]) + cprint("✔ Done", color="green", attrs=["bold"]) if __name__ == "__main__": diff --git a/plot_diagram.py b/plot_diagram.py index 22bd753e28acfe22f9c89f783694f3859fc2a355..10176fb94cbb365caf198a0fd243e91864d03877 100755 --- a/plot_diagram.py +++ b/plot_diagram.py @@ -255,10 +255,21 @@ class PlotCli: bbox=dict(fc="white", ec="none"), ) - def _annotate_time_plot(self, ax: plt.Axes, height: Union[float, int]): + def _annotate_time_plot( + self, ax: plt.Axes, height: Union[float, int], spinner: YaspinWrapper + ): if not self.annotate: return + if not self.traces[0].facts["is_http09"]: + spinner.write( + colored( + f"⨯ Can't annotate plot, because facts are missing.", color="red" + ) + ) + + return + ttfb = self.traces[0].facts["ttfb"] req_start = self.traces[0].facts["request_start"] pglt = self.traces[0].facts["plt"] @@ -423,10 +434,10 @@ class PlotCli: response_retrans_offsets[0], marker="o", linestyle="", - color=self._colors.ScarletRed, + color=self._colors.Orange, ) - self._annotate_time_plot(ax, height=max_offset) + self._annotate_time_plot(ax, height=max_offset, spinner=spinner) self._save(output_file, spinner) def plot_packet_number(self, output_file: Optional[Path]): @@ -501,17 +512,17 @@ class PlotCli: request_packet_numbers[0], marker="o", linestyle="", - color=self._colors.skyblue1, + color=self._colors.Plum, ) ax.plot( response_timestamps[0], response_packet_numbers[0], marker="o", linestyle="", - color=self._colors.plum1, + color=self._colors.SkyBlue, ) - self._annotate_time_plot(ax, height=max_packet_number) + self._annotate_time_plot(ax, height=max_packet_number, spinner=spinner) self._save(output_file, spinner) def plot_file_size(self, output_file: Optional[Path]): @@ -583,7 +594,7 @@ class PlotCli: color=self._colors.SkyBlue, ) - self._annotate_time_plot(ax, height=max_file_size) + self._annotate_time_plot(ax, height=max_file_size, spinner=spinner) self._save(output_file, spinner) def _process_packet_sizes(self): @@ -707,7 +718,7 @@ class PlotCli: ), ) - self._annotate_time_plot(ax, height=packet_stats.max) + self._annotate_time_plot(ax, height=packet_stats.max, spinner=spinner) self._save(output_file, spinner) diff --git a/tracer.py b/tracer.py index 7637ca9dceee44b49156a32faac1371a6869ead2..2112049aac75ff342804153cb2b57a759c0b992b 100644 --- a/tracer.py +++ b/tracer.py @@ -31,6 +31,18 @@ class ParsingError(Exception): self.trace = trace +class FinError(ParsingError): + """Error with closing streams detected.""" + + +class HTTP09Error(ParsingError): + """Error with HTTP/0.9 detected.""" + + +class CryptoError(ParsingError): + """Error with crypto detected.""" + + # def get_frame_prop_from_all_frames( # packet: "Packet", # prop_name: str, @@ -80,7 +92,7 @@ class Trace: if file.suffix not in (".pcap", ".pcapng"): cprint( - f"[!] Warning! Are you sure, that {file} is a pcap/pcapng file?", + f"⨯ Warning! Are you sure, that {file} is a pcap/pcapng file?", color="yellow", ) @@ -100,6 +112,10 @@ class Trace: self._request_stream_frames = list[QuicStreamLayer]() self._response_stream_frames = list[QuicStreamLayer]() self._parsed = False + self._error_cfg = { + HTTP09Error: "warning", + FinError: "warning", + } def __str__(self): trace_file_name = Path(self._cap.input_filename).name @@ -360,62 +376,81 @@ class Trace: trace=self, ) - self._facts["request_stream_fin_pkns"] = self.get_stream_fin_packet_number( - self._request_stream_frames - ) + try: + self._facts["request_stream_fin_pkns"] = self.get_stream_fin_packet_number( + self._request_stream_frames + ) + except FinError as err: + if self._error_cfg[FinError] == "error": + raise err + else: + cprint(f"⨯ Validation error: {err}", file=sys.stderr, color="red") - request_raw = self.follow_stream(self._request_stream_frames) try: - request = request_raw.decode("utf-8") - except UnicodeDecodeError as err: - raise ParsingError( - ( - "Request seems not to be a HTTP/0.9 GET request. Maybe it is HTTP/3? " - f"{request_raw[:16]!r}" - ), - trace=self, - ) from err + request_raw = self.follow_stream(self._request_stream_frames) + try: + request = request_raw.decode("utf-8") + except UnicodeDecodeError as err: + raise HTTP09Error( + ( + "Request seems not to be a HTTP/0.9 GET request. Maybe it is HTTP/3? " + f"{request_raw[:16]!r}" + ), + trace=self, + ) from err - if not request.startswith("GET /"): - raise ParsingError( - "First packet is not a HTTP/0.9 GET request.", - trace=self, - ) + if not request.startswith("GET /"): + raise HTTP09Error( + "First packet is not a HTTP/0.9 GET request.", + trace=self, + ) - self._facts["request"] = request - self._facts["is_http"] = True + self._facts["request"] = request + self._facts["is_http09"] = True - # check if all other packets are in direction from server to client: + # check if all other packets are in direction from server to client: - if float(self._request_stream_frames[-1].norm_time) >= float( - self._response_stream_frames[0].norm_time - ): - raise ParsingError( - "Request packets appear after first response packet. Is this really HTTP/0.9?", - trace=self, - ) + if float(self._request_stream_frames[-1].norm_time) >= float( + self._response_stream_frames[0].norm_time + ): + raise HTTP09Error( + "Request packets appear after first response packet. Is this really HTTP/0.9?", + trace=self, + ) - # calculate times - ttfb = self._response_stream_frames[0].norm_time - pglt = self._response_stream_frames[-1].norm_time - req_start = self._request_stream_frames[0].norm_time - resp_delay = ttfb - req_start - self._facts["ttfb"] = ttfb - self._facts["plt"] = pglt - self._facts["request_start"] = req_start - self._facts["response_delay"] = resp_delay - - # check fin bit - self._facts["response_stream_fin_pkns"] = self.get_stream_fin_packet_number( - self._response_stream_frames - ) + # calculate times + ttfb = self._response_stream_frames[0].norm_time + pglt = self._response_stream_frames[-1].norm_time + req_start = self._request_stream_frames[0].norm_time + resp_delay = ttfb - req_start + self._facts["ttfb"] = ttfb + self._facts["plt"] = pglt + self._facts["request_start"] = req_start + self._facts["response_delay"] = resp_delay + except HTTP09Error as err: + if self._error_cfg[HTTP09Error] == "error": + raise err + else: + self._facts["is_http09"] = False + cprint(f"⨯ Validation error: {err}", file=sys.stderr, color="red") + + try: + # check fin bit + self._facts["response_stream_fin_pkns"] = self.get_stream_fin_packet_number( + self._response_stream_frames + ) + except FinError as err: + if self._error_cfg[FinError] == "error": + raise err + else: + cprint(f"⨯ Validation error: {err}", file=sys.stderr, color="red") self._parsed = True def iter_stream_frames(self, packet: "Packet") -> Iterator: for quic_layer in packet.get_multiple_layers("quic"): if hasattr(quic_layer, "decryption_failed"): - raise ParsingError( + raise CryptoError( f"Decryption of QUIC crypto failed: {quic_layer.decryption_failed}", trace=self, ) @@ -469,7 +504,7 @@ class Trace: if not all(byte is not None for byte in buf): cprint( - "[!] Warning! Did not receive all bytes in follow_stream.", + "⨯ Warning! Did not receive all bytes in follow_stream.", color="yellow", ) @@ -545,11 +580,11 @@ class Trace: msg = "Last packet that contains a stream frame does not close stream." if warn_only: - cprint(f"[!] {msg}", color="red", file=sys.stderr) + cprint(f"⨯ {msg}", color="red", file=sys.stderr) return [] else: - raise ParsingError(msg, trace=self) + raise FinError(msg, trace=self) elif len(stream_fins) == 1: fin_pkn: int = stream_fins[0]["packet_number"] @@ -563,11 +598,11 @@ class Trace: ) if warn_only: - cprint(f"[!] {msg}", color="red", file=sys.stderr) + cprint(f"⨯ {msg}", color="red", file=sys.stderr) return [] else: - raise ParsingError( + raise FinError( msg, trace=self, ) @@ -582,11 +617,11 @@ class Trace: msg = "There are multiple stream ids in this list. Is it HTTP/3?" if warn_only: - cprint(f"[!] {msg}", color="red", file=sys.stderr) + cprint(f"⨯ {msg}", color="red", file=sys.stderr) return [] else: - raise ParsingError(msg, trace=self) + raise FinError(msg, trace=self) if not all( fin_pkg["offset"] == stream_fins[0]["offset"] for fin_pkg in stream_fins @@ -594,11 +629,11 @@ class Trace: msg = "Stream was closed multiple times." if warn_only: - cprint(f"[!] {msg}", color="red", file=sys.stderr) + cprint(f"⨯ {msg}", color="red", file=sys.stderr) return [] else: - raise ParsingError(msg, trace=self) + raise FinError(msg, trace=self) assert max_offset == stream_fins[0]["offset"]