Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an HTTP endopoint to resolve magnet URL to bytes (address #177) #181

Merged
merged 11 commits into from
Aug 13, 2024
Prev Previous commit
Next Next commit
Make the /resolve_magnet HTTP endpoint return an actual torrent file,…
… not info
  • Loading branch information
ikatson committed Aug 13, 2024
commit d54b67d2dc7e34298c8394961ad5c95697125241
1 change: 1 addition & 0 deletions crates/bencode/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod bencode_value;
pub mod raw_value;
mod serde_bencode_de;
mod serde_bencode_ser;

Expand Down
28 changes: 28 additions & 0 deletions crates/bencode/src/raw_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use serde::Serialize;

pub struct RawValue<T>(pub T);

pub(crate) const TAG: &str = "::librqbit_bencode::RawValue";

impl<T> Serialize for RawValue<T>
where
T: AsRef<[u8]>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
struct Wrapper<'a>(&'a [u8]);

impl<'a> Serialize for Wrapper<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(self.0)
}
}

serializer.serialize_newtype_struct(TAG, &Wrapper(self.0.as_ref()))
}
}
8 changes: 6 additions & 2 deletions crates/bencode/src/serde_bencode_de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct BencodeDeserializer<'de> {
// This is a f**ing hack
pub is_torrent_info: bool,
pub torrent_info_digest: Option<[u8; 20]>,
pub torrent_info_bytes: Option<&'de [u8]>,
}

impl<'de> BencodeDeserializer<'de> {
Expand All @@ -20,6 +21,7 @@ impl<'de> BencodeDeserializer<'de> {
parsing_key: false,
is_torrent_info: false,
torrent_info_digest: None,
torrent_info_bytes: None,
}
}
pub fn into_remaining(self) -> &'de [u8] {
Expand Down Expand Up @@ -542,9 +544,11 @@ impl<'a, 'de> serde::de::MapAccess<'de> for MapAccess<'a, 'de> {
if self.de.is_torrent_info && self.de.field_context.as_slice() == [ByteBuf(b"info")] {
let len = self.de.buf.as_ptr() as usize - buf_before.as_ptr() as usize;
let mut hash = Sha1::new();
hash.update(&buf_before[..len]);
let torrent_info_bytes = &buf_before[..len];
hash.update(torrent_info_bytes);
let digest = hash.finish();
self.de.torrent_info_digest = Some(digest)
self.de.torrent_info_digest = Some(digest);
self.de.torrent_info_bytes = Some(torrent_info_bytes);
}
self.de.field_context.pop();
Ok(value)
Expand Down
10 changes: 8 additions & 2 deletions crates/bencode/src/serde_bencode_ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,18 @@ impl<'ser, W: std::io::Write> Serializer for &'ser mut BencodeSerializer<W> {

fn serialize_newtype_struct<T>(
self,
_name: &'static str,
_value: &T,
name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + serde::Serialize,
{
if name == crate::raw_value::TAG {
self.hack_no_bytestring_prefix = true;
value.serialize(&mut *self)?;
self.hack_no_bytestring_prefix = false;
return Ok(());
}
Err(SerError::custom_with_ser(
"bencode doesn't support newtype structs",
self,
Expand Down
73 changes: 62 additions & 11 deletions crates/librqbit/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,13 +502,23 @@ async fn create_tcp_listener(
bail!("no free TCP ports in range {port_range:?}");
}

fn torrent_file_from_info_and_bytes(
info: &TorrentMetaV1Info<ByteBufOwned>,
info_hash: &Id20,
info_bytes: &[u8],
trackers: &[String],
) -> Bytes {
todo!()
fn torrent_file_from_info_bytes(info_bytes: &[u8], trackers: &[String]) -> anyhow::Result<Bytes> {
#[derive(Serialize)]
struct Tmp<'a> {
announce: &'a str,
#[serde(rename = "announce-list")]
announce_list: &'a [&'a [String]],
info: bencode::raw_value::RawValue<&'a [u8]>,
}

let mut w = Vec::new();
let v = Tmp {
info: bencode::raw_value::RawValue(info_bytes),
announce: trackers.first().map(|s| s.as_str()).unwrap_or(""),
announce_list: &[trackers],
};
bencode_serialize_to_writer(&v, &mut w)?;
Ok(w.into())
}

pub(crate) struct CheckedIncomingConnection {
Expand Down Expand Up @@ -1003,12 +1013,10 @@ impl Session {
let trackers = magnet.trackers.into_iter().unique().collect_vec();
InternalAddResult {
info_hash,
torrent_bytes: torrent_file_from_info_and_bytes(
&info,
&info_hash,
torrent_bytes: torrent_file_from_info_bytes(
&info_bytes,
&trackers,
),
)?,
info,
trackers,
peer_rx: Some(rx),
Expand Down Expand Up @@ -1427,3 +1435,46 @@ impl tracker_comms::TorrentStatsProvider for PeerRxTorrentInfo {
}
}
}

#[cfg(test)]
mod tests {
use std::io::Write;

use buffers::ByteBuf;
use itertools::Itertools;
use librqbit_core::torrent_metainfo::{torrent_from_bytes_ext, TorrentMetaV1};

use super::torrent_file_from_info_bytes;

#[test]
fn test_torrent_file_from_info_and_bytes() {
fn get_trackers(info: &TorrentMetaV1<ByteBuf>) -> Vec<String> {
info.iter_announce()
.filter_map(|t| std::str::from_utf8(t.as_ref()).ok().map(|t| t.to_owned()))
.collect_vec()
}

let orig_full_torrent =
include_bytes!("../resources/ubuntu-21.04-desktop-amd64.iso.torrent");
let parsed = torrent_from_bytes_ext::<ByteBuf>(&orig_full_torrent[..]).unwrap();
let parsed_trackers = get_trackers(&parsed.meta);

let generated_torrent =
torrent_file_from_info_bytes(parsed.info_bytes.as_ref(), &parsed_trackers).unwrap();
{
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open("/tmp/generated")
.unwrap();
f.write_all(&generated_torrent).unwrap();
}
let generated_parsed =
torrent_from_bytes_ext::<ByteBuf>(generated_torrent.as_ref()).unwrap();
assert_eq!(parsed.meta.info_hash, generated_parsed.meta.info_hash);
assert_eq!(parsed.meta.info, generated_parsed.meta.info);
assert_eq!(parsed.info_bytes, generated_parsed.info_bytes);
assert_eq!(parsed_trackers, get_trackers(&generated_parsed.meta));
}
}
35 changes: 27 additions & 8 deletions crates/librqbit_core/src/torrent_metainfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,37 @@ use crate::{hash_id::Id20, lengths::Lengths};
pub type TorrentMetaV1Borrowed<'a> = TorrentMetaV1<ByteBuf<'a>>;
pub type TorrentMetaV1Owned = TorrentMetaV1<ByteBufOwned>;

/// Parse torrent metainfo from bytes.
pub fn torrent_from_bytes<'de, BufType: Deserialize<'de>>(
pub struct ParsedTorrent<BufType> {
/// The parsed torrent.
pub meta: TorrentMetaV1<BufType>,

/// The raw bytes of the torrent's "info" dict.
pub info_bytes: BufType,
}

/// Parse torrent metainfo from bytes (includes additional fields).
pub fn torrent_from_bytes_ext<'de, BufType: Deserialize<'de> + From<&'de [u8]>>(
buf: &'de [u8],
) -> anyhow::Result<TorrentMetaV1<BufType>> {
) -> anyhow::Result<ParsedTorrent<BufType>> {
let mut de = BencodeDeserializer::new_from_buf(buf);
de.is_torrent_info = true;
let mut t = TorrentMetaV1::deserialize(&mut de)?;
t.info_hash = Id20::new(
de.torrent_info_digest
.ok_or_else(|| anyhow::anyhow!("programming error"))?,
);
Ok(t)
let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) {
(Some(digest), Some(info_bytes)) => (digest, info_bytes),
_ => anyhow::bail!("programming error"),
};
t.info_hash = Id20::new(digest);
Ok(ParsedTorrent {
meta: t,
info_bytes: BufType::from(info_bytes),
})
}

/// Parse torrent metainfo from bytes.
pub fn torrent_from_bytes<'de, BufType: Deserialize<'de> + From<&'de [u8]>>(
buf: &'de [u8],
) -> anyhow::Result<TorrentMetaV1<BufType>> {
torrent_from_bytes_ext(buf).map(|r| r.meta)
}

/// A parsed .torrent file.
Expand Down
Loading