241 lines
8.1 KiB
Rust
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);
|
|
}
|
|
}
|