From 3ca21390dff1afd555cbc437e52832baac19171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sun, 4 Jun 2023 16:10:13 +0200 Subject: [PATCH] Add aquatic_peer_id crate for peer client parsing --- Cargo.lock | 9 ++ Cargo.toml | 2 + aquatic_peer_id/Cargo.toml | 18 ++++ aquatic_peer_id/src/lib.rs | 166 +++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 aquatic_peer_id/Cargo.toml create mode 100644 aquatic_peer_id/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 20f3262..b6d2301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,15 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "aquatic_peer_id" +version = "0.8.0" +dependencies = [ + "compact_str", + "regex", + "serde", +] + [[package]] name = "aquatic_toml_config" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 1bd11d7..1ba315c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "aquatic_http", "aquatic_http_load_test", "aquatic_http_protocol", + "aquatic_peer_id", "aquatic_toml_config", "aquatic_toml_config_derive", "aquatic_udp", @@ -29,6 +30,7 @@ rust-version = "1.64" aquatic_common = { version = "0.8.0", path = "./aquatic_common" } aquatic_http_protocol = { version = "0.8.0", path = "./aquatic_http_protocol" } aquatic_http = { version = "0.8.0", path = "./aquatic_http" } +aquatic_peer_id = { version = "0.8.0", path = "./aquatic_peer_id" } aquatic_toml_config = { version = "0.8.0", path = "./aquatic_toml_config" } aquatic_toml_config_derive = { version = "0.8.0", path = "./aquatic_toml_config_derive" } aquatic_udp_protocol = { version = "0.8.0", path = "./aquatic_udp_protocol" } diff --git a/aquatic_peer_id/Cargo.toml b/aquatic_peer_id/Cargo.toml new file mode 100644 index 0000000..3074e6b --- /dev/null +++ b/aquatic_peer_id/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "aquatic_peer_id" +description = "BitTorrent peer ID handling" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +rust-version.workspace = true + +[lib] +name = "aquatic_peer_id" + +[dependencies] +compact_str = "0.7" +regex = "1" +serde = { version = "1", features = ["derive"] } \ No newline at end of file diff --git a/aquatic_peer_id/src/lib.rs b/aquatic_peer_id/src/lib.rs new file mode 100644 index 0000000..0371e22 --- /dev/null +++ b/aquatic_peer_id/src/lib.rs @@ -0,0 +1,166 @@ +use std::{fmt::Display, sync::OnceLock}; + +use compact_str::CompactString; +use regex::bytes::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PeerId(pub [u8; 20]); + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PeerClient { + BitTorrent(CompactString), + Deluge(CompactString), + LibTorrentRakshasa(CompactString), + LibTorrentRasterbar(CompactString), + QBitTorrent(CompactString), + Transmission(CompactString), + UTorrent(CompactString), + UTorrentEmbedded(CompactString), + UTorrentMac(CompactString), + UTorrentWeb(CompactString), + Vuze(CompactString), + WebTorrent(CompactString), + WebTorrentDesktop(CompactString), + Mainline(CompactString), + OtherWithPrefixAndVersion { + prefix: CompactString, + version: CompactString, + }, + OtherWithPrefix(CompactString), + Other, +} + +impl PeerClient { + pub fn from_prefix_and_version(prefix: &[u8], version: &[u8]) -> Option { + let version = CompactString::from_utf8(version).ok()?; + + match prefix { + b"AZ" => Some(Self::Vuze(version)), + b"BT" => Some(Self::BitTorrent(version)), + b"DE" => Some(Self::Deluge(version)), + b"lt" => Some(Self::LibTorrentRakshasa(version)), + b"LT" => Some(Self::LibTorrentRasterbar(version)), + b"qB" => Some(Self::QBitTorrent(version)), + b"TR" => Some(Self::Transmission(version)), + b"UE" => Some(Self::UTorrentEmbedded(version)), + b"UM" => Some(Self::UTorrentMac(version)), + b"UT" => Some(Self::UTorrent(version)), + b"UW" => Some(Self::UTorrentWeb(version)), + b"WD" => Some(Self::WebTorrentDesktop(version)), + b"WW" => Some(Self::WebTorrent(version)), + b"M" => Some(Self::Mainline(version)), + name => Some(Self::OtherWithPrefixAndVersion { + prefix: CompactString::from_utf8(name).ok()?, + version, + }), + } + } + + pub fn from_peer_id(peer_id: PeerId) -> Self { + static AZ_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = AZ_RE + .get_or_init(|| { + Regex::new(r"^\-(?P[a-zA-Z]{2})(?P[0-9A-Z]{4})") + .expect("compile AZ_RE regex") + }) + .captures(&peer_id.0) + { + if let Some(client) = Self::from_prefix_and_version(&caps["name"], &caps["version"]) { + return client; + } + } + + static MAINLINE_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = MAINLINE_RE + .get_or_init(|| { + Regex::new(r"^(?P[a-zA-Z])(?P[0-9\-]{6})\-") + .expect("compile MAINLINE_RE regex") + }) + .captures(&peer_id.0) + { + if let Some(client) = Self::from_prefix_and_version(&caps["name"], &caps["version"]) { + return client; + } + } + + static PREFIX_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = PREFIX_RE + .get_or_init(|| { + Regex::new(r"^(?P[a-zA-Z0-9\-]*)\-").expect("compile PREFIX_RE regex") + }) + .captures(&peer_id.0) + { + if let Ok(prefix) = CompactString::from_utf8(&caps["prefix"]) { + return Self::OtherWithPrefix(prefix); + } + } + + Self::Other + } +} + +impl Display for PeerClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BitTorrent(v) => write!(f, "BitTorrent ({})", v.as_str()), + Self::Deluge(v) => write!(f, "Deluge ({})", v.as_str()), + Self::LibTorrentRakshasa(v) => write!(f, "libTorrent (rakshasa) ({})", v.as_str()), + Self::LibTorrentRasterbar(v) => write!(f, "libtorrent (rasterbar) ({})", v.as_str()), + Self::QBitTorrent(v) => write!(f, "QBitTorrent ({})", v.as_str()), + Self::Transmission(v) => write!(f, "Transmission ({})", v.as_str()), + Self::UTorrent(v) => write!(f, "uTorrent ({})", v.as_str()), + Self::UTorrentEmbedded(v) => write!(f, "uTorrent Embedded ({})", v.as_str()), + Self::UTorrentMac(v) => write!(f, "uTorrent Mac ({})", v.as_str()), + Self::UTorrentWeb(v) => write!(f, "uTorrent Web ({})", v.as_str()), + Self::Vuze(v) => write!(f, "Vuze ({})", v.as_str()), + Self::WebTorrent(v) => write!(f, "WebTorrent ({})", v.as_str()), + Self::WebTorrentDesktop(v) => write!(f, "WebTorrent Desktop ({})", v.as_str()), + Self::Mainline(v) => write!(f, "Mainline ({})", v.as_str()), + Self::OtherWithPrefixAndVersion { prefix, version } => { + write!(f, "Other ({}) ({})", prefix.as_str(), version.as_str()) + } + Self::OtherWithPrefix(prefix) => write!(f, "Other ({})", prefix.as_str()), + Self::Other => f.write_str("Other"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_peer_id(bytes: &[u8]) -> PeerId { + let mut peer_id = PeerId([0; 20]); + + let len = bytes.len(); + + (&mut peer_id.0[..len]).copy_from_slice(bytes); + + peer_id + } + + #[test] + fn test_client_from_peer_id() { + assert_eq!( + PeerClient::from_peer_id(create_peer_id(b"-lt1234-k/asdh3")), + PeerClient::LibTorrentRakshasa("1234".into()) + ); + assert_eq!( + PeerClient::from_peer_id(create_peer_id(b"M1-2-3--k/asdh3")), + PeerClient::Mainline("1-2-3-".into()) + ); + assert_eq!( + PeerClient::from_peer_id(create_peer_id(b"M1-23-4-k/asdh3")), + PeerClient::Mainline("1-23-4".into()) + ); + assert_eq!( + PeerClient::from_peer_id(create_peer_id(b"S3-k/asdh3")), + PeerClient::OtherWithPrefix("S3".into()) + ); + } +}