Files
aria2-rust-pro/crates/aria2-rust-pro-rpc/src/dispatcher/compat_support/dht.rs
T

241 lines
8.1 KiB
Rust

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<DhtNodeModel, RpcError> {
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<Vec<u8>, 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<DhtNodeModel, RpcError> {
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::<u16>()
.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<u8> {
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<Vec<u8>, 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<u8> {
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<u8>],
) -> Result<Vec<BtPeerInfo>, 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<Vec<String>, 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<I>(existing: &mut Vec<String>, discovered: I)
where
I: IntoIterator<Item = String>,
{
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<String>, 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);
}
}