Skip to content

Commit

Permalink
Add a torrent announce subcommand (#510)
Browse files Browse the repository at this point in the history
This command makes use of a partially-implemented UDP tracker client to
announce an infohash and list the response.

type: added
  • Loading branch information
atomgardner authored and casey committed Aug 20, 2023
1 parent 660c631 commit 50d5a93
Show file tree
Hide file tree
Showing 17 changed files with 1,288 additions and 7 deletions.
7 changes: 2 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ default-run = "imdl"

[features]
default = []
bench = ["rand"]
bench = []

[dependencies]
ansi_term = "0.12.0"
Expand All @@ -29,6 +29,7 @@ lexiclean = "0.0.1"
libc = "0.2.0"
log = "0.4.8"
md5 = "0.7.0"
rand = "0.7.3"
open = "1.4.0"
pretty_assertions = "0.6.0"
pretty_env_logger = "0.4.0"
Expand Down Expand Up @@ -65,10 +66,6 @@ features = ["default", "wrap_help"]
version = "2.1.1"
features = ["serde"]

[dependencies.rand]
version = "0.7.3"
optional = true

[dev-dependencies]
criterion = "0.3.0"
temptree = "0.0.0"
Expand Down
4 changes: 4 additions & 0 deletions bin/gen/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ examples:
text: "BitTorrent metainfo related functionality is under the `torrent` subcommand:"
code: "imdl torrent --help"

- command: imdl torrent announce
text: "Announce the infohash to all trackers in the supplied `.torrent` file, and print the peer lists that come back:"
code: "imdl torrent announce --input foo.torrent"

- command: imdl torrent create
text: "Intermodal can be used to create `.torrent` files:"
code: "imdl torrent create --input foo"
Expand Down
6 changes: 4 additions & 2 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ pub(crate) use std::{
hash::Hash,
io::{self, BufRead, BufReader, Cursor, Read, Write},
iter::{self, Sum},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, UdpSocket},
num::{ParseFloatError, ParseIntError, TryFromIntError},
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
path::{self, Path, PathBuf},
process::ExitStatus,
str::{self, FromStr},
string::FromUtf8Error,
sync::Once,
time::{SystemTime, SystemTimeError},
time::{Duration, SystemTime, SystemTimeError},
usize,
};

Expand All @@ -31,6 +32,7 @@ pub(crate) use ignore::WalkBuilder;
pub(crate) use indicatif::{ProgressBar, ProgressStyle};
pub(crate) use lexiclean::Lexiclean;
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use rand::Rng;
pub(crate) use regex::{Regex, RegexSet};
pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
pub(crate) use serde_hex::SerHex;
Expand All @@ -52,7 +54,7 @@ pub(crate) use url::{Host, Url};
pub(crate) use log::trace;

// modules
pub(crate) use crate::{consts, error, host_port_parse_error, magnet_link_parse_error};
pub(crate) use crate::{consts, error, host_port_parse_error, magnet_link_parse_error, tracker};

// functions
pub(crate) use crate::xor_args::xor_args;
Expand Down
39 changes: 39 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub(crate) enum Error {
source: bendy::serde::Error,
input: InputTarget,
},
#[snafu(display("Torrent metainfo does not specify any usable trackers"))]
MetainfoMissingTrackers,
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
MetainfoSerialize { source: bendy::serde::Error },
#[snafu(display("Failed to decode metainfo bencode from {}: {}", input, error))]
Expand Down Expand Up @@ -136,6 +138,43 @@ pub(crate) enum Error {
SymlinkRoot { root: PathBuf },
#[snafu(display("Failed to retrieve system time: {}", source))]
SystemTime { source: SystemTimeError },
#[snafu(display("Compact peer list is not the expected length"))]
TrackerCompactPeerList,
#[snafu(display("Tracker exchange to `udp://{}` timed out.", tracker_addr))]
TrackerExchange { tracker_addr: SocketAddr },
#[snafu(display(
"Cannot connect to tracker `{}`: URL does not specify a valid host port",
tracker_url
))]
TrackerHostPort {
source: HostPortParseError,
tracker_url: Url,
},
#[snafu(display("Tracker client cannot announce without a connection id"))]
TrackerNoConnectionId,
#[snafu(display("Tracker resolved to no useable addresses"))]
TrackerNoHosts,
#[snafu(display("Malformed response from tracker"))]
TrackerResponse,
#[snafu(display("Response from tracker has wrong length: got {}; want {}", got, want))]
TrackerResponseLength { want: usize, got: usize },
#[snafu(display("Tracker failed to send datagram: {}", source))]
TrackerSend { source: io::Error },
#[snafu(display("Failed to resolve socket addrs: {}", source))]
TrackerSocketAddrs { source: io::Error },
#[snafu(display(
"Cannot connect to tracker `{}`: only UDP trackers are supported",
tracker_url
))]
TrackerUdpOnly { tracker_url: Url },
#[snafu(display("Failed to bind to UDP socket: {}", source))]
UdpSocketBind { source: io::Error },
#[snafu(display("Failed to connect to `udp://{}`: {}", addr, source))]
UdpSocketConnect { addr: SocketAddr, source: io::Error },
#[snafu(display("Failed to get local UDP socket address: {}", source))]
UdpSocketLocalAddress { source: io::Error },
#[snafu(display("Failed to set read timeout: {}", source))]
UdpSocketReadTimeout { source: io::Error },
#[snafu(display(
"Feature `{}` cannot be used without passing the `--unstable` flag",
feature
Expand Down
50 changes: 50 additions & 0 deletions src/host_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ impl Display for HostPort {
}
}

impl TryFrom<&Url> for HostPort {
type Error = HostPortParseError;

fn try_from(url: &Url) -> Result<Self, HostPortParseError> {
match (url.host(), url.port()) {
(Some(host), Some(port)) => Ok(HostPort {
host: host.to_owned(),
port,
}),
(Some(_), None) => Err(HostPortParseError::PortMissing {
text: url.as_str().to_owned(),
}),
(None, Some(_)) => Err(HostPortParseError::HostMissing {
text: url.as_str().to_owned(),
}),
(None, None) => Err(HostPortParseError::HostPortMissing {
text: url.as_str().to_owned(),
}),
}
}
}

impl ToSocketAddrs for HostPort {
type Iter = std::vec::IntoIter<SocketAddr>;

fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
let address = match &self.host {
Host::Domain(domain) => return (domain.clone(), self.port).to_socket_addrs(),
Host::Ipv4(address) => IpAddr::V4(*address),
Host::Ipv6(address) => IpAddr::V6(*address),
};

Ok(vec![SocketAddr::new(address, self.port)].into_iter())
}
}

#[derive(Serialize, Deserialize)]
struct Tuple(String, u16);

Expand Down Expand Up @@ -156,4 +192,18 @@ mod tests {
"l39:1234:5678:9abc:def0:1234:5678:9abc:def0i65000ee",
);
}

#[test]
fn test_from_url() {
let url = Url::parse("udp://imdl.io:12345").unwrap();
let host_port = HostPort::try_from(&url).unwrap();
assert_eq!(host_port.host, Host::Domain("imdl.io".into()));
assert_eq!(host_port.port, 12345);
}

#[test]
fn test_from_url_no_port() {
let url = Url::parse("udp://imdl.io").unwrap();
assert!(HostPort::try_from(&url).is_err());
}
}
4 changes: 4 additions & 0 deletions src/host_port_parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ pub(crate) enum HostPortParseError {
Port { text: String, source: ParseIntError },
#[snafu(display("Port missing: `{}`", text))]
PortMissing { text: String },
#[snafu(display("Host missing: `{}`", text))]
HostMissing { text: String },
#[snafu(display("Host and port missing: `{}`", text))]
HostPortMissing { text: String },
}
6 changes: 6 additions & 0 deletions src/infohash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ impl From<Sha1Digest> for Infohash {
}
}

impl From<Infohash> for [u8; 20] {
fn from(infohash: Infohash) -> Self {
infohash.inner.bytes()
}
}

impl From<Infohash> for Sha1Digest {
fn from(infohash: Infohash) -> Sha1Digest {
infohash.inner
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mod style;
mod subcommand;
mod table;
mod torrent_summary;
mod tracker;
mod use_color;
mod verifier;
mod walker;
Expand Down
3 changes: 3 additions & 0 deletions src/subcommand/torrent.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::common::*;

mod announce;
mod create;
mod link;
mod piece_length;
Expand All @@ -14,6 +15,7 @@ mod verify;
about("Subcommands related to the BitTorrent protocol.")
)]
pub(crate) enum Torrent {
Announce(announce::Announce),
Create(create::Create),
Link(link::Link),
#[structopt(alias = "piece-size")]
Expand All @@ -26,6 +28,7 @@ pub(crate) enum Torrent {
impl Torrent {
pub(crate) fn run(self, env: &mut Env, options: &Options) -> Result<(), Error> {
match self {
Self::Announce(announce) => announce.run(env),
Self::Create(create) => create.run(env, options),
Self::Link(link) => link.run(env),
Self::PieceLength(piece_length) => piece_length.run(env),
Expand Down
Loading

0 comments on commit 50d5a93

Please sign in to comment.