use super::{ BtPeerInfo, BtRuntimeState, DhtMessageModel, DhtNodeModel, Digest, DownloadId, RequestGroup, RpcError, rpc_bt_info_hash, }; /// Builds the outbound DHT `get_peers` message and selected target node. pub(in crate::dispatcher) fn build_dht_get_peers_request( group: &RequestGroup, ) -> Result<(DhtNodeModel, DhtMessageModel), RpcError> { let bt = group .bt() .ok_or_else(|| RpcError::unsupported("dht get_peers requires bt runtime state"))?; let node = pick_bt_dht_node(bt.dht_nodes())?; let info_hash = resolve_bt_info_hash(group, bt)?; Ok(( node, DhtMessageModel::get_peers_query( b"gp".to_vec(), rpc_bt_local_node_id(group.gid()), info_hash, ), )) } /// Builds the outbound DHT `ping` message and selected target node. pub(in crate::dispatcher) fn build_dht_ping_request( group: &RequestGroup, ) -> Result<(DhtNodeModel, DhtMessageModel), RpcError> { let bt = group .bt() .ok_or_else(|| RpcError::unsupported("dht ping requires bt runtime state"))?; let node = pick_bt_dht_node(bt.dht_nodes())?; Ok(( node, DhtMessageModel::ping_query(b"pi".to_vec(), rpc_bt_local_node_id(group.gid())), )) } /// Builds the outbound DHT `find_node` message and selected target node. pub(in crate::dispatcher) fn build_dht_find_node_request( group: &RequestGroup, ) -> Result<(DhtNodeModel, DhtMessageModel), RpcError> { let bt = group .bt() .ok_or_else(|| RpcError::unsupported("dht find_node requires bt runtime state"))?; let node = pick_bt_dht_node(bt.dht_nodes())?; let target = resolve_bt_info_hash(group, bt)?; Ok(( node, DhtMessageModel::find_node_query(b"fn".to_vec(), rpc_bt_local_node_id(group.gid()), target), )) } /// Builds the outbound DHT `announce_peer` message and selected target node. pub(in crate::dispatcher) fn build_dht_announce_peer_request( group: &RequestGroup, ) -> Result<(DhtNodeModel, DhtMessageModel), RpcError> { let bt = group .bt() .ok_or_else(|| RpcError::unsupported("dht announce_peer requires bt runtime state"))?; let node = pick_bt_dht_node(bt.dht_nodes())?; let token = group .dht_token() .map(|token| token.to_vec()) .ok_or_else(|| { RpcError::unsupported("dht announce_peer requires token from prior get_peers") })?; let info_hash = resolve_bt_info_hash(group, bt)?; Ok(( node, DhtMessageModel::announce_peer_query( b"ap".to_vec(), rpc_bt_local_node_id(group.gid()), info_hash, 6881, token, false, ), )) } /// Chooses the first parseable DHT node entry from runtime state. pub(in crate::dispatcher) fn pick_bt_dht_node(nodes: &[String]) -> Result { if nodes.is_empty() { return Err(RpcError::unsupported( "dht get_peers requires at least one dht node", )); } let mut last_error = None; for node in nodes { match parse_dht_node_spec(node) { Ok(parsed) => return Ok(parsed), Err(error) => last_error = Some(error.message), } } Err(RpcError::unsupported(&format!( "dht get_peers found no valid dht nodes in runtime state{}", last_error .map(|message| format!(": {message}")) .unwrap_or_default() ))) } /// Resolves the BitTorrent info hash bytes required by DHT and peer-wire requests. pub(in crate::dispatcher) fn resolve_bt_info_hash( group: &RequestGroup, bt: &BtRuntimeState, ) -> Result, RpcError> { let info_hash = if !bt.info_hash.is_empty() { bt.info_hash.clone() } else { rpc_bt_info_hash(group.uri()).unwrap_or_default() }; decode_hex_string_exact(&info_hash, 20, "dht get_peers info hash") .map_err(|error| RpcError::unsupported(&error)) } /// Parses a `host:port` DHT node spec into a transport model. pub(in crate::dispatcher) fn parse_dht_node_spec(raw: &str) -> Result { let (address, port_raw) = raw .rsplit_once(':') .ok_or_else(|| RpcError::unsupported("dht node entry must use host:port format"))?; let port = port_raw .parse::() .map_err(|_| RpcError::unsupported("dht node port must be a valid u16"))?; Ok(DhtNodeModel { node_id: String::new(), address: address.to_owned(), port, }) } /// Derives a deterministic local DHT node ID from a download GID. pub(in crate::dispatcher) fn rpc_bt_local_node_id(gid: DownloadId) -> Vec { let gid_hex = format!("{:040x}", gid.as_u64()); decode_hex_string_exact(&gid_hex, 20, "local dht node id").unwrap_or_else(|_| vec![0_u8; 20]) } /// Decodes a fixed-width hexadecimal string into raw bytes. pub(in crate::dispatcher) fn decode_hex_string_exact( raw: &str, expected_len: usize, label: &str, ) -> Result, String> { if raw.len() != expected_len * 2 { return Err(format!( "{label} must be {} hex characters", expected_len * 2 )); } let mut bytes = Vec::with_capacity(expected_len); for pair in raw.as_bytes().chunks_exact(2) { let hi = decode_hex_nibble(pair[0]) .ok_or_else(|| format!("{label} contains non-hex characters"))?; let lo = decode_hex_nibble(pair[1]) .ok_or_else(|| format!("{label} contains non-hex characters"))?; bytes.push((hi << 4) | lo); } Ok(bytes) } /// Decodes a single ASCII hex nibble. pub(in crate::dispatcher) fn decode_hex_nibble(byte: u8) -> Option { match byte { b'0'..=b'9' => Some(byte - b'0'), b'a'..=b'f' => Some(byte - b'a' + 10), b'A'..=b'F' => Some(byte - b'A' + 10), _ => None, } } /// Parses compact peer payloads returned by DHT `get_peers`. pub(in crate::dispatcher) fn parse_dht_compact_peers( values: &[Vec], ) -> Result, String> { let mut peers = Vec::new(); for value in values { if value.len() % 6 != 0 { return Err("compact peer list length must be a multiple of 6".to_owned()); } for chunk in value.chunks_exact(6) { peers.push(BtPeerInfo { peer_id: None, ip: format!("{}.{}.{}.{}", chunk[0], chunk[1], chunk[2], chunk[3]), port: u16::from_be_bytes([chunk[4], chunk[5]]), client_name: None, interested: false, choked: false, download_speed: 0, upload_speed: 0, seeder: false, }); } } Ok(peers) } /// Parses compact DHT node payload bytes into `host:port` strings. pub(in crate::dispatcher) fn parse_dht_compact_nodes( raw: Option<&[u8]>, ) -> Result, String> { let Some(raw) = raw else { return Ok(Vec::new()); }; if raw.len() % 26 != 0 { return Err("compact dht node list length must be a multiple of 26".to_owned()); } let mut nodes = Vec::new(); for chunk in raw.chunks_exact(26) { let ip = format!("{}.{}.{}.{}", chunk[20], chunk[21], chunk[22], chunk[23]); let port = u16::from_be_bytes([chunk[24], chunk[25]]); nodes.push(format!("{ip}:{port}")); } Ok(nodes) } /// Appends newly discovered DHT nodes while preserving existing order. pub(in crate::dispatcher) fn merge_bt_dht_nodes(existing: &mut Vec, discovered: I) where I: IntoIterator, { for node in discovered { if !existing.iter().any(|current| current == &node) { existing.push(node); } } } /// Moves a successfully used DHT node to the front of the runtime node list. pub(in crate::dispatcher) fn promote_bt_dht_node(existing: &mut Vec, node: &DhtNodeModel) { let entry = format!("{}:{}", node.address, node.port); if let Some(index) = existing.iter().position(|current| current == &entry) { if index > 0 { let value = existing.remove(index); existing.insert(0, value); } } else { existing.insert(0, entry); } }