Skip to main content

netget/protocol/
dual.rs

1//! Dual-protocol detection: which server protocols have a client counterpart.
2//!
3//! The server and client registries use divergent canonical names ("DoH" vs
4//! "DNS-over-HTTPS", "Tor Relay" vs "Tor", …), and their keyword matchers are
5//! not suitable for joining them (`ClientRegistry::parse_from_str` does greedy
6//! substring matching over an unordered map, so an ambiguous input is
7//! nondeterministic). This module joins the two canonical name tables
8//! deterministically: an explicit alias table first, then normalized-name
9//! equality (lowercase, all non-alphanumerics stripped), which absorbs the
10//! case- and punctuation-only differences (IGMP/igmp, ISIS/IS-IS,
11//! SOCKET_FILE/SocketFile, BLUETOOTH_BLE/"Bluetooth (BLE)", …).
12//!
13//! `tests/dual_protocol_test.rs` enforces alias-table completeness by diffing
14//! both tables through [`alias_table_problems`].
15
16use super::client_registry::{ALL_KNOWN_CLIENT_PROTOCOLS, CLIENT_REGISTRY};
17use super::server_registry::ALL_KNOWN_PROTOCOLS;
18
19/// Server-canonical → client-canonical, for names that differ beyond
20/// case/punctuation. Both sides must exist in their respective `ALL_KNOWN_*`
21/// tables; `alias_table_problems` (and the test built on it) fails otherwise.
22///
23/// The USB-* and BLUETOOTH_BLE_* profile servers are deliberately absent: the
24/// generic "USB" / "Bluetooth (BLE)" clients speak the base transport, not a
25/// specific profile, so pairing a profile server with them would promise an
26/// exchange the client cannot hold up. Bitcoin P2P is likewise absent: the Bitcoin
27/// client speaks Bitcoin Core JSON-RPC over HTTP, not the peer-to-peer wire protocol.
28const SERVER_TO_CLIENT_ALIASES: &[(&str, &str)] = &[
29    ("DoH", "DNS-over-HTTPS"),
30    ("Proxy", "HTTP Proxy"),
31    ("Tor Relay", "Tor"),
32    ("OpenID", "OpenIDConnect"),
33    ("SamlIdp", "SAML"),
34    ("SamlSp", "SAML"),
35    ("WebRTC Signaling", "WebRTC"),
36    ("Torrent-Tracker", "BitTorrent Tracker"),
37    ("Torrent-DHT", "BitTorrent DHT"),
38    ("Torrent-Peer", "BitTorrent Peer Wire"),
39];
40
41/// Lowercase with every non-alphanumeric character stripped, so that
42/// "SSH Agent" == "ssh-agent" == "SshAgent".
43fn normalize(name: &str) -> String {
44    name.chars()
45        .filter(|c| c.is_ascii_alphanumeric())
46        .collect::<String>()
47        .to_ascii_lowercase()
48}
49
50/// The client-registry canonical name for a server protocol, if any client
51/// implementation exists in the codebase (regardless of whether it is compiled
52/// into this build). Deterministic; never consults keyword matching.
53pub fn client_protocol_for_server(server_name: &str) -> Option<&'static str> {
54    if let Some((_, client)) = SERVER_TO_CLIENT_ALIASES
55        .iter()
56        .find(|(server, _)| server.eq_ignore_ascii_case(server_name))
57    {
58        return Some(client);
59    }
60    let wanted = normalize(server_name);
61    ALL_KNOWN_CLIENT_PROTOCOLS
62        .iter()
63        .map(|(name, _feature)| *name)
64        .find(|name| normalize(name) == wanted)
65}
66
67/// Like [`client_protocol_for_server`], but `Some` only when the client
68/// protocol is actually compiled into this build (present in the runtime
69/// client registry), so the UI can offer "open a client against this server"
70/// truthfully.
71pub fn compiled_client_protocol_for_server(server_name: &str) -> Option<String> {
72    // A known canonical name keeps its identity even when its server is not compiled.
73    // The keyword resolver deliberately falls back from an unavailable FTP to TCP;
74    // that CLI convenience must not change which client a protocol is paired with.
75    let wanted = normalize(server_name);
76    let known_canonical = ALL_KNOWN_PROTOCOLS
77        .iter()
78        .map(|(name, _)| *name)
79        .find(|name| normalize(name) == wanted);
80    // Instances preserve the operator's spelling (e.g. `bitcoin`), which may be a
81    // registry keyword rather than the canonical server name (`Bitcoin P2P`). Resolve
82    // those aliases before joining: the Bitcoin keyword would match the RPC client.
83    let canonical = known_canonical.or_else(|| {
84        super::server_registry::registry()
85            .resolve(server_name)
86            .ok()
87            .map(|server| server.protocol_name())
88    });
89    let client_name = client_protocol_for_server(canonical.unwrap_or(server_name))?;
90    // The join already returned the client's canonical name. Keyword matching here
91    // could substitute another compiled client for an unavailable implementation.
92    CLIENT_REGISTRY
93        .get(client_name)
94        .map(|_| client_name.to_string())
95}
96
97/// Every (server canonical name, client canonical name) pair derivable from
98/// the two registry tables. Codebase-wide, not restricted to this build.
99pub fn all_dual_protocols() -> Vec<(&'static str, &'static str)> {
100    ALL_KNOWN_PROTOCOLS
101        .iter()
102        .filter_map(|(server, _feature)| {
103            client_protocol_for_server(server).map(|client| (*server, client))
104        })
105        .collect()
106}
107
108/// Consistency problems in the alias table, for the completeness test:
109/// an alias whose server side is not a known server protocol, or whose client
110/// side is not a known client protocol, or that duplicates what normalization
111/// already achieves (dead weight that will rot).
112pub fn alias_table_problems() -> Vec<String> {
113    let mut problems = Vec::new();
114    for (server, client) in SERVER_TO_CLIENT_ALIASES {
115        if !ALL_KNOWN_PROTOCOLS.iter().any(|(name, _)| name == server) {
116            problems.push(format!(
117                "alias server side {server:?} is not in ALL_KNOWN_PROTOCOLS"
118            ));
119        }
120        if !ALL_KNOWN_CLIENT_PROTOCOLS
121            .iter()
122            .any(|(name, _)| name == client)
123        {
124            problems.push(format!(
125                "alias client side {client:?} is not in ALL_KNOWN_CLIENT_PROTOCOLS"
126            ));
127        }
128        if normalize(server) == normalize(client) {
129            problems.push(format!(
130                "alias {server:?} -> {client:?} is redundant: normalization already matches them"
131            ));
132        }
133    }
134    problems
135}