Merge pull request #299 from ikatson/private-torrents
[feature] Initial support for private torrents
This commit is contained in:
commit
6fea795899
8 changed files with 97 additions and 47 deletions
|
|
@ -218,14 +218,26 @@ impl<'de> serde::de::Deserializer<'de> for &mut BencodeDeserializer<'de> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_bool<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
|
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
where
|
where
|
||||||
V: serde::de::Visitor<'de>,
|
V: serde::de::Visitor<'de>,
|
||||||
{
|
{
|
||||||
Err(
|
if !self.buf.starts_with(b"i") {
|
||||||
Error::new_from_kind(ErrorKind::NotSupported("bencode doesn't support booleans"))
|
return Err(Error::custom_with_de(
|
||||||
.set_context(self),
|
"expected bencode int to represent bool",
|
||||||
)
|
self,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let value = self.parse_integer()?;
|
||||||
|
if value > 1 {
|
||||||
|
return Err(Error::custom_with_de(
|
||||||
|
format!("expected 0 or 1 for boolean, but got {value}"),
|
||||||
|
self,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
visitor
|
||||||
|
.visit_bool(value == 1)
|
||||||
|
.map_err(|e: Self::Error| e.set_context(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
|
|
||||||
|
|
@ -218,11 +218,8 @@ impl<'ser, W: std::io::Write> Serializer for &'ser mut BencodeSerializer<W> {
|
||||||
type SerializeStruct = SerializeStruct<'ser, W>;
|
type SerializeStruct = SerializeStruct<'ser, W>;
|
||||||
type SerializeStructVariant = Impossible<(), SerError>;
|
type SerializeStructVariant = Impossible<(), SerError>;
|
||||||
|
|
||||||
fn serialize_bool(self, _: bool) -> Result<Self::Ok, Self::Error> {
|
fn serialize_bool(self, value: bool) -> Result<Self::Ok, Self::Error> {
|
||||||
Err(SerError::custom_with_ser(
|
self.write_number(if value { 1 } else { 0 })
|
||||||
"bencode doesn't support booleans",
|
|
||||||
self,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
|
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ async fn create_torrent_raw<'a>(
|
||||||
attr: None,
|
attr: None,
|
||||||
sha1: None,
|
sha1: None,
|
||||||
symlink_path: None,
|
symlink_path: None,
|
||||||
|
private: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,8 @@ impl Session {
|
||||||
name,
|
name,
|
||||||
} = add_res;
|
} = add_res;
|
||||||
|
|
||||||
|
let private = metadata.as_ref().map_or(false, |m| m.info.private);
|
||||||
|
|
||||||
let make_peer_rx = || {
|
let make_peer_rx = || {
|
||||||
self.make_peer_rx(
|
self.make_peer_rx(
|
||||||
info_hash,
|
info_hash,
|
||||||
|
|
@ -1015,6 +1017,7 @@ impl Session {
|
||||||
!opts.paused && !opts.list_only,
|
!opts.paused && !opts.list_only,
|
||||||
opts.force_tracker_interval,
|
opts.force_tracker_interval,
|
||||||
opts.initial_peers.clone().unwrap_or_default(),
|
opts.initial_peers.clone().unwrap_or_default(),
|
||||||
|
private,
|
||||||
)
|
)
|
||||||
.context("error creating peer stream")
|
.context("error creating peer stream")
|
||||||
};
|
};
|
||||||
|
|
@ -1284,12 +1287,14 @@ impl Session {
|
||||||
t: &Arc<ManagedTorrent>,
|
t: &Arc<ManagedTorrent>,
|
||||||
announce: bool,
|
announce: bool,
|
||||||
) -> anyhow::Result<PeerStream> {
|
) -> anyhow::Result<PeerStream> {
|
||||||
|
let is_private = t.with_metadata(|m| m.info.private).unwrap_or(false);
|
||||||
self.make_peer_rx(
|
self.make_peer_rx(
|
||||||
t.info_hash(),
|
t.info_hash(),
|
||||||
t.shared().trackers.iter().cloned().collect(),
|
t.shared().trackers.iter().cloned().collect(),
|
||||||
announce,
|
announce,
|
||||||
t.shared().options.force_tracker_interval,
|
t.shared().options.force_tracker_interval,
|
||||||
t.shared().options.initial_peers.clone(),
|
t.shared().options.initial_peers.clone(),
|
||||||
|
is_private,
|
||||||
)?
|
)?
|
||||||
.context("no peer source")
|
.context("no peer source")
|
||||||
}
|
}
|
||||||
|
|
@ -1298,17 +1303,26 @@ impl Session {
|
||||||
fn make_peer_rx(
|
fn make_peer_rx(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
info_hash: Id20,
|
info_hash: Id20,
|
||||||
trackers: Vec<String>,
|
mut trackers: Vec<String>,
|
||||||
announce: bool,
|
announce: bool,
|
||||||
force_tracker_interval: Option<Duration>,
|
force_tracker_interval: Option<Duration>,
|
||||||
initial_peers: Vec<SocketAddr>,
|
initial_peers: Vec<SocketAddr>,
|
||||||
|
is_private: bool,
|
||||||
) -> anyhow::Result<Option<PeerStream>> {
|
) -> anyhow::Result<Option<PeerStream>> {
|
||||||
let announce_port = if announce { self.tcp_listen_port } else { None };
|
let announce_port = if announce { self.tcp_listen_port } else { None };
|
||||||
let dht_rx = self
|
let dht_rx = if is_private {
|
||||||
.dht
|
None
|
||||||
.as_ref()
|
} else {
|
||||||
.map(|dht| dht.get_peers(info_hash, announce_port))
|
self.dht
|
||||||
.transpose()?;
|
.as_ref()
|
||||||
|
.map(|dht| dht.get_peers(info_hash, announce_port))
|
||||||
|
.transpose()?
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_private && trackers.len() > 1 {
|
||||||
|
warn!("private trackers are not fully implemented, so using only the first tracker");
|
||||||
|
trackers.resize_with(1, Default::default);
|
||||||
|
}
|
||||||
|
|
||||||
let tracker_rx_stats = PeerRxTorrentInfo {
|
let tracker_rx_stats = PeerRxTorrentInfo {
|
||||||
info_hash,
|
info_hash,
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ async fn debug_server() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn spawn_debug_server() -> tokio::task::JoinHandle<anyhow::Result<()>> {
|
pub fn spawn_debug_server() -> tokio::task::JoinHandle<anyhow::Result<()>> {
|
||||||
tokio::spawn(debug_server())
|
tokio::spawn(debug_server())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,7 @@ mod tests {
|
||||||
attr: None,
|
attr: None,
|
||||||
sha1: None,
|
sha1: None,
|
||||||
symlink_path: None,
|
symlink_path: None,
|
||||||
|
private: false,
|
||||||
},
|
},
|
||||||
comment: None,
|
comment: None,
|
||||||
created_by: None,
|
created_by: None,
|
||||||
|
|
|
||||||
1
crates/librqbit_core/src/resources/test/private.torrent
Normal file
1
crates/librqbit_core/src/resources/test/private.torrent
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
d10:created by20:qBittorrent v4.4.3.113:creation datei1736784212e4:infod6:lengthi2e4:name7:private12:piece lengthi16384e6:pieces20:åúDò³µS¶s`Ð}]‘ÿ^7:privatei1eee
|
||||||
|
|
@ -50,6 +50,10 @@ pub fn torrent_from_bytes<'de, BufType: Deserialize<'de> + From<&'de [u8]>>(
|
||||||
torrent_from_bytes_ext(buf).map(|r| r.meta)
|
torrent_from_bytes_ext(buf).map(|r| r.meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_false(b: &bool) -> bool {
|
||||||
|
!*b
|
||||||
|
}
|
||||||
|
|
||||||
/// A parsed .torrent file.
|
/// A parsed .torrent file.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct TorrentMetaV1<BufType> {
|
pub struct TorrentMetaV1<BufType> {
|
||||||
|
|
@ -89,7 +93,7 @@ impl<BufType> TorrentMetaV1<BufType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main torrent information, shared by .torrent files and magnet link contents.
|
/// Main torrent information, shared by .torrent files and magnet link contents.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TorrentMetaV1Info<BufType> {
|
pub struct TorrentMetaV1Info<BufType> {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<BufType>,
|
pub name: Option<BufType>,
|
||||||
|
|
@ -117,6 +121,9 @@ pub struct TorrentMetaV1Info<BufType> {
|
||||||
// Multi-file mode
|
// Multi-file mode
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub files: Option<Vec<TorrentMetaV1File<BufType>>>,
|
pub files: Option<Vec<TorrentMetaV1File<BufType>>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "is_false", default)]
|
||||||
|
pub private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
@ -377,6 +384,7 @@ where
|
||||||
attr: self.attr.clone_to_owned(within_buffer),
|
attr: self.attr.clone_to_owned(within_buffer),
|
||||||
sha1: self.sha1.clone_to_owned(within_buffer),
|
sha1: self.sha1.clone_to_owned(within_buffer),
|
||||||
symlink_path: self.symlink_path.clone_to_owned(within_buffer),
|
symlink_path: self.symlink_path.clone_to_owned(within_buffer),
|
||||||
|
private: self.private,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,45 +413,28 @@ where
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::Read;
|
use bencode::BencodeValue;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const TORRENT_FILENAME: &str = "../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent";
|
const TORRENT_BYTES: &[u8] =
|
||||||
|
include_bytes!("../../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_torrent_owned() {
|
fn test_deserialize_torrent_owned() {
|
||||||
let mut buf = Vec::new();
|
let torrent: TorrentMetaV1Owned = torrent_from_bytes(TORRENT_BYTES).unwrap();
|
||||||
std::fs::File::open(TORRENT_FILENAME)
|
|
||||||
.unwrap()
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let torrent: TorrentMetaV1Owned = torrent_from_bytes(&buf).unwrap();
|
|
||||||
dbg!(torrent);
|
dbg!(torrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_torrent_borrowed() {
|
fn test_deserialize_torrent_borrowed() {
|
||||||
let mut buf = Vec::new();
|
let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap();
|
||||||
std::fs::File::open(TORRENT_FILENAME)
|
|
||||||
.unwrap()
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(&buf).unwrap();
|
|
||||||
dbg!(torrent);
|
dbg!(torrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_torrent_with_info_hash() {
|
fn test_deserialize_torrent_with_info_hash() {
|
||||||
let mut buf = Vec::new();
|
let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap();
|
||||||
std::fs::File::open(TORRENT_FILENAME)
|
|
||||||
.unwrap()
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(&buf).unwrap();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
torrent.info_hash.as_string(),
|
torrent.info_hash.as_string(),
|
||||||
"64a980abe6e448226bb930ba061592e44c3781a1"
|
"64a980abe6e448226bb930ba061592e44c3781a1"
|
||||||
|
|
@ -452,13 +443,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_then_deserialize_bencode() {
|
fn test_serialize_then_deserialize_bencode() {
|
||||||
let mut buf = Vec::new();
|
let torrent: TorrentMetaV1Info<ByteBuf> = torrent_from_bytes(TORRENT_BYTES).unwrap().info;
|
||||||
std::fs::File::open(TORRENT_FILENAME)
|
|
||||||
.unwrap()
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let torrent: TorrentMetaV1Info<ByteBuf> = torrent_from_bytes(&buf).unwrap().info;
|
|
||||||
let mut writer = Vec::new();
|
let mut writer = Vec::new();
|
||||||
bencode::bencode_serialize_to_writer(&torrent, &mut writer).unwrap();
|
bencode::bencode_serialize_to_writer(&torrent, &mut writer).unwrap();
|
||||||
let deserialized = TorrentMetaV1Info::<ByteBuf>::deserialize(
|
let deserialized = TorrentMetaV1Info::<ByteBuf>::deserialize(
|
||||||
|
|
@ -468,4 +453,42 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(torrent, deserialized);
|
assert_eq!(torrent, deserialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_private_serialize_deserialize() {
|
||||||
|
for private in [false, true] {
|
||||||
|
let info: TorrentMetaV1Info<ByteBufOwned> = TorrentMetaV1Info {
|
||||||
|
private,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
bencode::bencode_serialize_to_writer(&info, &mut buf).unwrap();
|
||||||
|
|
||||||
|
let deserialized = TorrentMetaV1Info::<ByteBuf>::deserialize(
|
||||||
|
&mut BencodeDeserializer::new_from_buf(&buf),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(info.private, deserialized.private);
|
||||||
|
|
||||||
|
let deserialized_dyn = ::bencode::dyn_from_bytes::<ByteBuf>(&buf).unwrap();
|
||||||
|
let hm = match deserialized_dyn {
|
||||||
|
bencode::BencodeValue::Dict(hm) => hm,
|
||||||
|
_ => panic!("expected dict"),
|
||||||
|
};
|
||||||
|
match (private, hm.get(&ByteBuf(b"private"))) {
|
||||||
|
(true, Some(BencodeValue::Integer(1))) => {}
|
||||||
|
(false, None) => {}
|
||||||
|
(_, v) => {
|
||||||
|
panic!("unexpected value for \"private\": {v:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_private_real_torrent() {
|
||||||
|
let buf = include_bytes!("resources/test/private.torrent");
|
||||||
|
let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(buf).unwrap();
|
||||||
|
assert!(torrent.info.private);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue