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
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