chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user