Skip to content

Commit

Permalink
Add support for BTv2 magnet links
Browse files Browse the repository at this point in the history
  • Loading branch information
jabedude committed Dec 25, 2023
1 parent 1157866 commit 011ec98
Show file tree
Hide file tree
Showing 19 changed files with 164 additions and 21 deletions.
2 changes: 1 addition & 1 deletion crates/dht/src/bprotocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{

use bencode::{ByteBuf, ByteString};
use clone_to_owned::CloneToOwned;
use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use serde::{
de::{IgnoredAny, Unexpected},
Deserialize, Deserializer, Serialize,
Expand Down
2 changes: 1 addition & 1 deletion crates/dht/src/dht.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use futures::{stream::FuturesUnordered, Stream, StreamExt, TryFutureExt};

use leaky_bucket::RateLimiter;
use librqbit_core::{
id20::Id20,
hash_id::Id20,
peer_id::generate_peer_id,
spawn_utils::{spawn, spawn_with_cancel},
};
Expand Down
2 changes: 1 addition & 1 deletion crates/dht/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::time::Duration;

pub use crate::dht::DhtStats;
pub use crate::dht::{DhtConfig, DhtState, RequestPeersStream};
pub use librqbit_core::id20::Id20;
pub use librqbit_core::hash_id::Id20;
pub use persistence::{PersistentDht, PersistentDhtConfig};

pub type Dht = Arc<DhtState>;
Expand Down
2 changes: 1 addition & 1 deletion crates/dht/src/peer_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{

use bencode::ByteString;
use chrono::{DateTime, Utc};
use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use parking_lot::RwLock;
use rand::RngCore;
use serde::{
Expand Down
4 changes: 2 additions & 2 deletions crates/dht/src/routing_table.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{net::SocketAddr, time::Instant};

use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use rand::RngCore;
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
use tracing::{debug, trace};
Expand Down Expand Up @@ -583,7 +583,7 @@ mod tests {
str::FromStr,
};

use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use rand::Rng;

use crate::routing_table::compute_split_start_end;
Expand Down
2 changes: 1 addition & 1 deletion crates/dht/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use serde::Serializer;

pub fn serialize_id20<S>(id: &Id20, ser: S) -> Result<S::Ok, S::Error>
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/dht_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tracing::debug;
use crate::{
peer_connection::PeerConnectionOptions, peer_info_reader, spawn_utils::BlockingSpawner,
};
use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;

#[derive(Debug)]
pub enum ReadMetainfoResult<Rx> {
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/peer_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use anyhow::{bail, Context};
use buffers::{ByteBuf, ByteString};
use clone_to_owned::CloneToOwned;
use librqbit_core::{id20::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id};
use librqbit_core::{hash_id::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id};
use parking_lot::RwLock;
use peer_binary_protocol::{
extended::{handshake::ExtendedHandshake, ExtendedMessage},
Expand Down
4 changes: 2 additions & 2 deletions crates/librqbit/src/peer_info_reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bencode::from_bytes;
use buffers::{ByteBuf, ByteString};
use librqbit_core::{
constants::CHUNK_SIZE,
id20::Id20,
hash_id::Id20,
lengths::{ceil_div_u64, last_element_size_u64, ChunkInfo},
torrent_metainfo::TorrentMetaV1Info,
};
Expand Down Expand Up @@ -226,7 +226,7 @@ impl PeerConnectionHandler for Handler {
mod tests {
use std::{net::SocketAddr, str::FromStr, sync::Once};

use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use librqbit_core::peer_id::generate_peer_id;

use crate::spawn_utils::BlockingSpawner;
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/torrent_state/live/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ use clone_to_owned::CloneToOwned;
use futures::{stream::FuturesUnordered, StreamExt};
use itertools::Itertools;
use librqbit_core::{
id20::Id20,
hash_id::Id20,
lengths::{ChunkInfo, Lengths, ValidPieceIndex},
spawn_utils::spawn_with_cancel,
speed_estimator::SpeedEstimator,
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/torrent_state/live/peer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub mod stats;

use std::collections::HashSet;

use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use librqbit_core::lengths::{ChunkInfo, ValidPieceIndex};

use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/torrent_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use anyhow::bail;
use anyhow::Context;
use buffers::ByteString;
use dht::RequestPeersStream;
use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;
use librqbit_core::lengths::Lengths;
use librqbit_core::peer_id::generate_peer_id;

Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit/src/tracker_comms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
str::FromStr,
};

use librqbit_core::id20::Id20;
use librqbit_core::hash_id::Id20;

#[derive(Clone, Copy)]
pub enum TrackerRequestEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,42 @@ impl PartialOrd<Id20> for Id20 {
}
}

/// A 32-byte hash used in Bittorrent V2, for torrent info hashes, piece hashing, etc.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Id32(pub [u8; 32]);

impl FromStr for Id32 {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut out = [0u8; 32];
if s.len() != 64 {
anyhow::bail!("expected a hex string of length 64")
};
hex::decode_to_slice(s, &mut out)?;
Ok(Id32(out))
}
}

impl Id32 {
pub fn as_string(&self) -> String {
hex::encode(self.0)
}
}

impl std::fmt::Debug for Id32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in self.0 {
write!(f, "{byte:02x?}")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::Id32;
use super::Id20;

#[test]
Expand All @@ -155,4 +189,11 @@ mod tests {
Id20([0, 127, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
)
}
}

#[test]
fn test_id32_from_str() {
let str = "06f04cc728bef957a658876ef807f0514e4d715392969998efef584d2c3e435e";
let _ih = Id32::from_str(str).unwrap();
}

}
2 changes: 1 addition & 1 deletion crates/librqbit_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod constants;
pub mod directories;
pub mod id20;
pub mod hash_id;
pub mod lengths;
pub mod magnet;
pub mod peer_id;
Expand Down
104 changes: 103 additions & 1 deletion crates/librqbit_core/src/magnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,30 @@ use std::str::FromStr;

use anyhow::Context;

use crate::id20::Id20;
use crate::hash_id::{Id20, Id32};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum InfoHash {
V1(Id20),
V2(Id32),
}

impl Default for InfoHash {
fn default() -> Self { InfoHash::V1(Id20([0; 20])) }
}

impl InfoHash {
pub fn as_string(&self) -> String {
match self {
InfoHash::V1(ih) => {
ih.as_string()
},
InfoHash::V2(ih) => {
ih.as_string()
}
}
}
}

/// A parsed magnet link.
pub struct Magnet {
Expand Down Expand Up @@ -54,11 +77,90 @@ impl std::fmt::Display for Magnet {
}
}

/// A parsed magnet link, supporting both v1 and v2 links.
pub struct MagnetBEP52 {
pub info_hash: InfoHash,
pub trackers: Vec<String>,
}

impl MagnetBEP52 {
/// Parse a magnet link.
pub fn parse(url: &str) -> anyhow::Result<MagnetBEP52> {
let url = url::Url::parse(url).context("magnet link must be a valid URL")?;
if url.scheme() != "magnet" {
anyhow::bail!("expected scheme magnet");
}
let mut info_hash: Option<InfoHash> = None;
let mut trackers = Vec::<String>::new();
for (key, value) in url.query_pairs() {
match key.as_ref() {
"xt" => {
if let Some(ih) = value.as_ref().strip_prefix("urn:btih:") {
let i: InfoHash = InfoHash::V1(Id20::from_str(ih)?);
info_hash.replace(i);
} else if let Some(ih) = value.as_ref().strip_prefix("urn:btmh:1220") {
let i: InfoHash = InfoHash::V2(Id32::from_str(ih)?);
info_hash.replace(i);
} else {
anyhow::bail!("expected xt to start with btih or btmh");
}
},
"tr" => trackers.push(value.into()),
_ => {}
}
}
match info_hash {
Some(info_hash) => Ok(MagnetBEP52 {
info_hash,
trackers,
}),
None => {
anyhow::bail!("did not find infohash")
}
}
}
}

impl std::fmt::Display for MagnetBEP52 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.info_hash {
InfoHash::V1(ih) => {
write!(
f,
"magnet:?xt=urn:btih:{}&tr={}",
ih.as_string(),
self.trackers.join("&tr=")
)
},
InfoHash::V2(ih) => {
write!(
f,
"magnet:?xt=urn:btmh:1220{}&tr={}",
ih.as_string(),
self.trackers.join("&tr=")
)
},
}
}
}

#[cfg(test)]
mod tests {
#[test]
fn test_parse_magnet_as_url() {
let magnet = "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&dn=rutor.info_%D0%92%D0%BE%D0%B9%D0%BD%D0%B0+%D0%B1%D1%83%D0%B4%D1%83%D1%89%D0%B5%D0%B3%D0%BE+%2F+The+Tomorrow+War+%282021%29+WEB-DLRip+%D0%BE%D1%82+MegaPeer+%7C+P+%7C+NewComers&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce";
dbg!(url::Url::parse(magnet).unwrap());
}

#[test]
fn test_parse_magnet_v2() {
use super::{InfoHash, MagnetBEP52};
use crate::magnet::Id32;
use std::str::FromStr;
let magnet = "magnet:?xt=urn:btmh:1220caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e&dn=bittorrent-v2-test
";
let info_hash = Id32::from_str("caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e").unwrap();
let m = MagnetBEP52::parse(&magnet).unwrap();
assert!(m.info_hash == InfoHash::V2(info_hash));
}
}
2 changes: 1 addition & 1 deletion crates/librqbit_core/src/peer_id.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::id20::Id20;
use crate::hash_id::Id20;

#[derive(Debug)]
pub enum AzureusStyleKind {
Expand Down
2 changes: 1 addition & 1 deletion crates/librqbit_core/src/torrent_metainfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use clone_to_owned::CloneToOwned;
use itertools::Either;
use serde::{Deserialize, Serialize};

use crate::id20::Id20;
use crate::hash_id::Id20;

pub type TorrentMetaV1Borrowed<'a> = TorrentMetaV1<ByteBuf<'a>>;
pub type TorrentMetaV1Owned = TorrentMetaV1<ByteString>;
Expand Down
2 changes: 1 addition & 1 deletion crates/peer_binary_protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bincode::Options;
use buffers::{ByteBuf, ByteString};
use byteorder::{ByteOrder, BE};
use clone_to_owned::CloneToOwned;
use librqbit_core::{constants::CHUNK_SIZE, id20::Id20, lengths::ChunkInfo};
use librqbit_core::{constants::CHUNK_SIZE, hash_id::Id20, lengths::ChunkInfo};
use serde::{Deserialize, Serialize};

use self::extended::ExtendedMessage;
Expand Down

0 comments on commit 011ec98

Please sign in to comment.