Skip to content

Commit

Permalink
Merge pull request #181 from ikatson/torrent-bytes
Browse files Browse the repository at this point in the history
Add an HTTP endopoint to resolve magnet URL to bytes (address #177)
  • Loading branch information
ikatson authored Aug 13, 2024
2 parents 98b8fa1 + 1c1200c commit 3cc9e44
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 62 deletions.
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
1 change: 1 addition & 0 deletions crates/librqbit/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ impl Api {
only_files,
seen_peers,
output_folder,
..
}) => ApiAddTorrentResponse {
id: None,
output_folder: output_folder.to_string_lossy().into_owned(),
Expand Down
3 changes: 2 additions & 1 deletion crates/librqbit/src/dht_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use librqbit_core::hash_id::Id20;
pub enum ReadMetainfoResult<Rx> {
Found {
info: TorrentMetaV1Info<ByteBufOwned>,
info_bytes: ByteBufOwned,
rx: Rx,
seen: HashSet<SocketAddr>,
},
Expand Down Expand Up @@ -80,7 +81,7 @@ pub async fn read_metainfo_from_peer_receiver<A: Stream<Item = SocketAddr> + Unp
},
done = unordered.next(), if !unordered.is_empty() => {
match done {
Some(Ok(info)) => return ReadMetainfoResult::Found { info, seen, rx: addrs },
Some(Ok((info, info_bytes))) => return ReadMetainfoResult::Found { info, info_bytes, seen, rx: addrs },
Some(Err(e)) => {
debug!("{:#}", e);
},
Expand Down
52 changes: 51 additions & 1 deletion crates/librqbit/src/http_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::torrent_state::peer::stats::snapshot::PeerStatsFilter;
type ApiState = Api;

use crate::api::Result;
use crate::{ApiError, ManagedTorrent};
use crate::{ApiError, ListOnlyResponse, ManagedTorrent};

/// An HTTP server for the API.
pub struct HttpApi {
Expand Down Expand Up @@ -188,6 +188,55 @@ impl HttpApi {
)
}

async fn resolve_magnet(
State(state): State<ApiState>,
url: String,
) -> Result<impl IntoResponse> {
let added = state
.session()
.add_torrent(
AddTorrent::from_url(&url),
Some(AddTorrentOptions {
list_only: true,
..Default::default()
}),
)
.await?;
let (info, content) = match added {
crate::AddTorrentResponse::AlreadyManaged(_, handle) => (
handle.info().info.clone(),
handle.info().torrent_bytes.clone(),
),
crate::AddTorrentResponse::ListOnly(ListOnlyResponse {
info,
torrent_bytes,
..
}) => (info, torrent_bytes),
crate::AddTorrentResponse::Added(_, _) => {
return Err(ApiError::new_from_text(
StatusCode::INTERNAL_SERVER_ERROR,
"bug: torrent was added to session, but shouldn't have been",
))
}
};
let mut headers = HeaderMap::new();
headers.insert(
"Content-Type",
HeaderValue::from_static("application/x-bittorrent"),
);

if let Some(name) = info.name.as_ref() {
if let Ok(name) = std::str::from_utf8(name) {
if let Ok(h) =
HeaderValue::from_str(&format!("attachment; filename=\"{}.torrent\"", name))
{
headers.insert("Content-Disposition", h);
}
}
}
Ok((headers, content))
}

async fn torrent_playlist(
State(state): State<ApiState>,
headers: HeaderMap,
Expand Down Expand Up @@ -388,6 +437,7 @@ impl HttpApi {
.route("/torrents/:id/stream/:file_id", get(torrent_stream_file))
.route("/torrents/:id/playlist", get(torrent_playlist))
.route("/torrents/playlist", get(global_playlist))
.route("/torrents/resolve_magnet", post(resolve_magnet))
.route(
"/torrents/:id/stream/:file_id/*filename",
get(torrent_stream_file),
Expand Down
14 changes: 8 additions & 6 deletions crates/librqbit/src/peer_info_reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ pub(crate) async fn read_metainfo_from_peer(
peer_connection_options: Option<PeerConnectionOptions>,
spawner: BlockingSpawner,
connector: Arc<StreamConnector>,
) -> anyhow::Result<TorrentMetaV1Info<ByteBufOwned>> {
let (result_tx, result_rx) =
tokio::sync::oneshot::channel::<anyhow::Result<TorrentMetaV1Info<ByteBufOwned>>>();
) -> anyhow::Result<TorrentAndInfoBytes> {
let (result_tx, result_rx) = tokio::sync::oneshot::channel::<
anyhow::Result<(TorrentMetaV1Info<ByteBufOwned>, ByteBufOwned)>,
>();
let (writer_tx, writer_rx) = tokio::sync::mpsc::unbounded_channel::<WriterRequest>();
let handler = Handler {
addr,
Expand Down Expand Up @@ -135,13 +136,13 @@ impl HandlerLocked {
}
}

pub type TorrentAndInfoBytes = (TorrentMetaV1Info<ByteBufOwned>, ByteBufOwned);

struct Handler {
addr: SocketAddr,
info_hash: Id20,
writer_tx: UnboundedSender<WriterRequest>,
result_tx: Mutex<
Option<tokio::sync::oneshot::Sender<anyhow::Result<TorrentMetaV1Info<ByteBufOwned>>>>,
>,
result_tx: Mutex<Option<tokio::sync::oneshot::Sender<anyhow::Result<TorrentAndInfoBytes>>>>,
locked: RwLock<Option<HandlerLocked>>,
}

Expand Down Expand Up @@ -179,6 +180,7 @@ impl PeerConnectionHandler for Handler {
if piece_ready {
let buf = self.locked.write().take().unwrap().buffer;
let info = from_bytes::<TorrentMetaV1Info<ByteBufOwned>>(&buf);
let info = info.map(|i| (i, ByteBufOwned(buf.into_boxed_slice())));
self.result_tx
.lock()
.take()
Expand Down
Loading

0 comments on commit 3cc9e44

Please sign in to comment.