Skip to content

Commit

Permalink
Magnet links
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanjermakov committed Nov 7, 2023
1 parent 4640011 commit f6e9237
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 25 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ BitTorrent client written in Rust
| --- | --- | --- |
| BitTorrent Protocol | [BEP-3](https://www.bittorrent.org/beps/bep_0003.html) |[^1] |
| DHT Protocol | [BEP-5](https://www.bittorrent.org/beps/bep_0005.html) |[^2] |
| Metadata from peers and magnet URLs | [BEP-9](https://www.bittorrent.org/beps/bep_0009.html) | 🚧 |
| Metadata from peers and magnet URLs | [BEP-9](https://www.bittorrent.org/beps/bep_0009.html) | [^3][^4][^5] |
| Extension Protocol | [BEP-10](https://www.bittorrent.org/beps/bep_0010.html) ||
| UDP Tracker Protocol | [BEP-15](https://www.bittorrent.org/beps/bep_0015.html) ||
| Holepunch extension | [BEP-55](https://www.bittorrent.org/beps/bep_0055.html) | 🚧 |

[^1]: no seeding, requesting only
[^2]: no routing, `find_peers` only
[^3]: no metadata seeding
[^4]: only reading `info_hash` from magnet
[^5]: v1 magnets only

## Reference

Expand Down
7 changes: 7 additions & 0 deletions src/hex.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
pub fn hex(str: &[u8]) -> String {
str.iter().map(|c| format!("{:02x}", c)).collect::<String>()
}

pub fn from_hex(str: &str) -> Vec<u8> {
(0..str.len())
.step_by(2)
.map(|i| u8::from_str_radix(&str[i..i + 2], 16).unwrap())
.collect()
}
37 changes: 31 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
#[macro_use]
extern crate log;

use anyhow::{Error, Result};
use anyhow::{Context, Error, Result};
use expanduser::expanduser;
use reqwest::Url;
use std::{collections::BTreeSet, env, path::PathBuf, process, sync::Arc, time::Duration};
use tokio::sync::Mutex;

use crate::{
config::Config, peer::generate_peer_id, persist::PersistState, torrent::download_torrent,
config::Config,
hex::from_hex,
peer::generate_peer_id,
persist::PersistState,
torrent::{download_torrent, metainfo_from_path},
};

mod abort;
Expand Down Expand Up @@ -42,9 +47,9 @@ async fn try_main() -> Result<()> {
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);

let path = match env::args().nth(1) {
Some(arg) => PathBuf::from(arg),
_ => return Err(Error::msg("no torrent file specified")),
let arg = match env::args().nth(1) {
Some(arg) => arg,
_ => return Err(Error::msg("no torrent file/magnet specified")),
};

let config = Config {
Expand All @@ -70,7 +75,27 @@ async fn try_main() -> Result<()> {
debug!("read persist state from file: {:?}", p_state);
let p_state = Arc::new(Mutex::new(p_state));

download_torrent(&path, &config, p_state).await?;
if arg.starts_with("magnet:") {
debug!("parsing magnet: {}", arg);
let uri = Url::parse(&arg).context("magnet uri parsing error")?;
let xt = uri
.query_pairs()
.find(|(k, _)| k == "xt")
.context("no `info_hash` query param")?
.1
.to_string();
trace!("xt: {}", xt);
let info_hash = xt
.split("urn:btih:")
.last()
.context("invalid magnet")?
.to_lowercase();
info!("magnet info hash: {}", info_hash);
download_torrent(from_hex(&info_hash), None, &config, p_state).await?;
} else {
let (info_hash, metainfo) = metainfo_from_path(&PathBuf::from(arg))?;
download_torrent(info_hash, Some(metainfo), &config, p_state).await?;
}

Ok(())
}
44 changes: 26 additions & 18 deletions src/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
use tokio::{spawn, sync::Mutex};

use crate::hex::hex;
use crate::peer_metainfo::MetainfoState;
use crate::state::init_pieces;
use crate::types::ByteString;
use crate::{
abort::EnsureAbort,
Expand All @@ -23,22 +25,13 @@ use crate::{
tracker::tracker_loop,
};

pub fn get_info_hash(value: &BencodeValue) -> Result<ByteString> {
if let BencodeValue::Dict(d) = value {
let str = d.get("info").context("no 'info' key")?.encode();
Ok(sha1::encode(str))
} else {
Err(Error::msg("value is not a dict"))
}
}

pub async fn download_torrent(
path: &Path,
info_hash: ByteString,
metainfo: Option<Metainfo>,
config: &Config,
p_state: Arc<Mutex<PersistState>>,
) -> Result<()> {
let started = Instant::now();
let (_, info_hash) = metainfo_from_path(path)?;
let (dht_peers, peer_id) = {
let p_state = p_state.lock().await;
(
Expand All @@ -56,18 +49,24 @@ pub async fn download_torrent(
.await?;
info!("discovered {} dht peers", peers.len());

let pieces = metainfo.as_ref().map(|m| init_pieces(&m.info));
let status = if metainfo.is_some() {
TorrentStatus::Downloading
} else {
TorrentStatus::Metainfo
};
let state = State {
config: config.clone(),
metainfo: Err(MetainfoState::default()),
metainfo: metainfo.ok_or(MetainfoState::default()),
tracker_response: None,
info_hash,
peer_id: p_state.lock().await.peer_id.to_vec(),
pieces: None,
pieces,
peers: peers
.into_iter()
.map(|p| (p.clone(), Peer::new(p)))
.collect(),
status: TorrentStatus::Metainfo,
status,
};
let state = Arc::new(Mutex::new(state));
trace!("init state: {:?}", state);
Expand Down Expand Up @@ -168,24 +167,33 @@ pub async fn write_piece(piece_idx: u32, state: Arc<Mutex<State>>) -> Result<()>
Ok(())
}

pub fn metainfo_from_path(path: &Path) -> Result<(Metainfo, ByteString)> {
pub fn metainfo_from_path(path: &Path) -> Result<(ByteString, Metainfo)> {
debug!("reading torrent file: {:?}", path);
let bencoded = fs::read(path).context("no metadata file")?;
metainfo_from_str(bencoded)
}

pub fn metainfo_from_str(bencoded: ByteString) -> Result<(Metainfo, ByteString)> {
pub fn metainfo_from_str(bencoded: ByteString) -> Result<(ByteString, Metainfo)> {
let metainfo_dict = match parse_bencoded(bencoded) {
(Some(metadata), left) if left.is_empty() => metadata,
_ => return Err(Error::msg("metadata file parsing error")),
};
debug!("metainfo dict: {metainfo_dict:?}");
let info_hash = get_info_hash(&metainfo_dict)?;
info!("info hash: {info_hash:?}");
info!("info hash: {}", hex(&info_hash));
let metainfo = match Metainfo::try_from(metainfo_dict) {
Ok(info) => info,
Err(e) => return Err(Error::msg(e).context("metadata file structure error")),
};
info!("metainfo: {metainfo:?}");
Ok((metainfo, info_hash))
Ok((info_hash, metainfo))
}

pub fn get_info_hash(value: &BencodeValue) -> Result<ByteString> {
if let BencodeValue::Dict(d) = value {
let str = d.get("info").context("no 'info' key")?.encode();
Ok(sha1::encode(str))
} else {
Err(Error::msg("value is not a dict"))
}
}

0 comments on commit f6e9237

Please sign in to comment.