From 6ddebec575aa8c9c5a357c673cbd8e8a5cc5769d Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 21:43:24 +0100 Subject: v2.3.2: group hosts by certificate fingerprint --- Cargo.toml | 2 +- docs/TESTING.md | 5 ++++- src/client/ui.rs | 28 ++++++++++++++++++++++++++-- tests/client_ui.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43b696b..c064cc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zagan-bet" -version = "2.3.1" +version = "2.3.2" edition = "2024" [dependencies] diff --git a/docs/TESTING.md b/docs/TESTING.md index 60bc4ac..15b060c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -64,7 +64,10 @@ Use an 80-column terminal because several output lines are width-sensitive. 8. `q` clears the board. Ctrl+C in a game clears the board and exits. 9. `info`, `ping`, `send hello`, and `hosts` print readable output; the host fingerprint fits 80 columns. -10. Windows: repeat 2-9 and confirm keys are not duplicated or printed twice. +10. Connect to the same server through two addresses (for example `127.0.0.1` + and the machine's LAN IP). `hosts` must list both addresses on one line + under a single fingerprint, because the certificate key is the same. +11. Windows: repeat 2-10 and confirm keys are not duplicated or printed twice. ## Manual roulette checklist diff --git a/src/client/ui.rs b/src/client/ui.rs index 1a35db1..0fe78ae 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -247,14 +247,38 @@ pub fn first_contact_lines(addr: &str, fingerprint: &str) -> Vec { /// Format remembered hosts so a 64-character fingerprint does not wrap on an /// 80-column terminal. +/// Render remembered hosts. +/// +/// Two addresses that present the same certificate key are the same server, so +/// they are grouped under one fingerprint: `127.0.0.1:11525` and +/// `10.0.0.100:11525` reached through different networks still show as one +/// entry. Hosts without a fingerprint are never merged, since nothing proves +/// they are related. pub fn host_lines(hosts: &[(String, Option)]) -> Vec { if hosts.is_empty() { return vec!["No remembered hosts.".to_string()]; } - let mut lines = vec!["Remembered hosts:".to_string()]; + let mut groups: Vec<(Option<&str>, Vec<&str>)> = Vec::new(); for (host, fingerprint) in hosts { - lines.push(format!(" {host}")); + match fingerprint { + Some(fingerprint) => { + match groups + .iter_mut() + .find(|(key, _)| *key == Some(fingerprint.as_str())) + { + Some((_, addresses)) => addresses.push(host), + None => groups.push((Some(fingerprint), vec![host])), + } + } + // An unknown key proves nothing about identity. + None => groups.push((None, vec![host])), + } + } + + let mut lines = vec!["Remembered hosts:".to_string()]; + for (fingerprint, addresses) in groups { + lines.push(format!(" {}", addresses.join(", "))); match fingerprint { Some(fingerprint) => lines.push(format!(" fingerprint: {fingerprint}")), None => lines.push(" (no fingerprint)".to_string()), diff --git a/tests/client_ui.rs b/tests/client_ui.rs index f5e3c0e..7a1b675 100644 --- a/tests/client_ui.rs +++ b/tests/client_ui.rs @@ -226,3 +226,45 @@ fn host_lines_fit_an_80_column_terminal() { ); } } + +#[test] +fn hosts_sharing_a_fingerprint_are_shown_as_one_server() { + let key = "5a15c712230831cb7270a36f5841bc5a52325db683073c845654a956d074f8ef"; + let lines = host_lines(&[ + ("10.0.0.100:11525".to_string(), Some(key.to_string())), + ("127.0.0.1:11525".to_string(), Some(key.to_string())), + ( + "other.example:11525".to_string(), + Some("ff".repeat(32)), + ), + ]); + + assert_eq!( + lines, + vec![ + "Remembered hosts:", + " 10.0.0.100:11525, 127.0.0.1:11525", + &format!(" fingerprint: {key}"), + " other.example:11525", + &format!(" fingerprint: {}", "ff".repeat(32)), + ] + ); + + // The shared key is printed once, not once per address. + assert_eq!(lines.iter().filter(|line| line.contains(key)).count(), 1); +} + +#[test] +fn hosts_without_a_fingerprint_are_never_merged() { + let lines = host_lines(&[ + ("a.example:443".to_string(), None), + ("b.example:443".to_string(), None), + ]); + + assert!(lines.contains(&" a.example:443".to_string())); + assert!(lines.contains(&" b.example:443".to_string())); + assert_eq!( + lines.iter().filter(|line| line.contains("no fingerprint")).count(), + 2 + ); +} -- cgit v1.2.3