Skip to content

Commit

Permalink
Support adding DHT bootstrap nodes to created torrents
Browse files Browse the repository at this point in the history
The --dht-node flag can be used to add DHT bootstrap nodes to new torrents.

This is the only piece of metainfo-related functionality in BEP 5, so we can mark BEP
5 as implemented.

type: added
  • Loading branch information
casey committed Apr 8, 2020
1 parent 6549850 commit 165a7ea
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 63 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ at any time.
| [02](http://bittorrent.org/beps/bep_0002.html) | :heavy_minus_sign: | Sample reStructured Text BEP Template |
| [03](http://bittorrent.org/beps/bep_0003.html) | :white_check_mark: | The BitTorrent Protocol Specification |
| [04](http://bittorrent.org/beps/bep_0004.html) | :heavy_minus_sign: | Assigned Numbers |
| [05](http://bittorrent.org/beps/bep_0005.html) | [:x:](https://github.com/casey/intermodal/issues/90) | DHT Protocol |
| [05](http://bittorrent.org/beps/bep_0005.html) | :white_check_mark: | DHT Protocol |
| [06](http://bittorrent.org/beps/bep_0006.html) | :heavy_minus_sign: | Fast Extension |
| [07](http://bittorrent.org/beps/bep_0007.html) | :heavy_minus_sign: | IPv6 Tracker Extension |
| [08](http://bittorrent.org/beps/bep_0008.html) | :heavy_minus_sign: | Tracker Peer Obfuscation |
Expand Down
8 changes: 4 additions & 4 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub(crate) use std::{
hash::Hash,
io::{self, Read, Write},
iter::{self, Sum},
num::{ParseFloatError, TryFromIntError},
num::{ParseFloatError, ParseIntError, TryFromIntError},
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign},
path::{self, Path, PathBuf},
process::{self, Command, ExitStatus},
Expand All @@ -26,7 +26,7 @@ pub(crate) use chrono::{TimeZone, Utc};
pub(crate) use globset::{Glob, GlobMatcher};
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use regex::{Regex, RegexSet};
pub(crate) use serde::{Deserialize, Serialize};
pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
pub(crate) use serde_hex::SerHex;
pub(crate) use serde_with::rust::unwrap_or_skip;
pub(crate) use sha1::Sha1;
Expand All @@ -37,7 +37,7 @@ pub(crate) use structopt::{
StructOpt,
};
pub(crate) use unicode_width::UnicodeWidthStr;
pub(crate) use url::Url;
pub(crate) use url::{Host, Url};
pub(crate) use walkdir::WalkDir;

// modules
Expand All @@ -53,7 +53,7 @@ pub(crate) use crate::{
pub(crate) use crate::{
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath,
file_status::FileStatus, files::Files, hasher::Hasher, info::Info, lint::Lint, linter::Linter,
md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, opt::Opt,
md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, node::Node, opt::Opt,
piece_length_picker::PieceLengthPicker, platform::Platform, status::Status, style::Style,
table::Table, target::Target, torrent_summary::TorrentSummary, use_color::UseColor,
verifier::Verifier, walker::Walker,
Expand Down
17 changes: 13 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,25 @@ pub(crate) enum Error {
CommandInvoke { command: String, source: io::Error },
#[snafu(display("Command `{}` returned bad exit status: {}", command, status))]
CommandStatus { command: String, status: ExitStatus },
#[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))]
FilenameDecode { filename: OsString },
#[snafu(display("Filename was not valid unicode: {}", filename.display()))]
FilenameDecode { filename: PathBuf },
#[snafu(display("Path had no file name: {}", path.display()))]
FilenameExtract { path: PathBuf },
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
Filesystem { source: io::Error, path: PathBuf },
#[snafu(display("Invalid glob: {}", source))]
GlobParse { source: globset::Error },
#[snafu(display("Unknown lint: {}", text))]
LintUnknown { text: String },
#[snafu(display("DHT node port missing: {}", text))]
NodeParsePortMissing { text: String },
#[snafu(display("Failed to parse DHT node host `{}`: {}", text, source))]
NodeParseHost {
text: String,
source: url::ParseError,
},
#[snafu(display("Failed to parse DHT node port `{}`: {}", text, source))]
NodeParsePort { text: String, source: ParseIntError },
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
OpenerMissing { tried: &'static [&'static str] },
#[snafu(display(
Expand Down Expand Up @@ -104,8 +115,6 @@ pub(crate) enum Error {
feature
))]
Unstable { feature: &'static str },
#[snafu(display("Unknown lint: {}", text))]
LintUnknown { text: String },
#[snafu(display("Torrent verification failed: {}", status))]
Verify { status: Status },
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mod linter;
mod md5_digest;
mod metainfo;
mod mode;
mod node;
mod opt;
mod path_ext;
mod piece_length_picker;
Expand Down
14 changes: 11 additions & 3 deletions src/metainfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::common::*;
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub(crate) struct Metainfo {
pub(crate) announce: String,
#[serde(rename = "announce-list")]
#[serde(
rename = "announce-list",
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
Expand All @@ -16,15 +16,15 @@ pub(crate) struct Metainfo {
with = "unwrap_or_skip"
)]
pub(crate) comment: Option<String>,
#[serde(rename = "created by")]
#[serde(
rename = "created by",
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) created_by: Option<String>,
#[serde(rename = "creation date")]
#[serde(
rename = "creation date",
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
Expand All @@ -37,6 +37,12 @@ pub(crate) struct Metainfo {
)]
pub(crate) encoding: Option<String>,
pub(crate) info: Info,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) nodes: Option<Vec<Node>>,
}

impl Metainfo {
Expand Down Expand Up @@ -103,6 +109,7 @@ mod tests {
created_by: Some("created by".into()),
creation_date: Some(1),
encoding: Some("UTF-8".into()),
nodes: Some(vec!["x:12".parse().unwrap(), "1.1.1.1:16".parse().unwrap()]),
info: Info {
private: Some(true),
piece_length: Bytes(16 * 1024),
Expand Down Expand Up @@ -130,6 +137,7 @@ mod tests {
let value = Metainfo {
announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
nodes: Some(vec!["x:12".parse().unwrap(), "1.1.1.1:16".parse().unwrap()]),
comment: Some("comment".into()),
created_by: Some("created by".into()),
creation_date: Some(1),
Expand Down
148 changes: 148 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use crate::common::*;

#[derive(Debug, PartialEq, Clone)]
pub(crate) struct Node {
host: Host,
port: u16,
}

impl FromStr for Node {
type Err = Error;

fn from_str(text: &str) -> Result<Self, Self::Err> {
let socket_address_re = Regex::new(
r"(?x)
^
(?P<host>.*?)
:
(?P<port>\d+?)
$
",
)
.unwrap();

if let Some(captures) = socket_address_re.captures(text) {
let host_text = captures.name("host").unwrap().as_str();
let port_text = captures.name("port").unwrap().as_str();

let host = Host::parse(&host_text).context(error::NodeParseHost {
text: text.to_owned(),
})?;

let port = port_text.parse::<u16>().context(error::NodeParsePort {
text: text.to_owned(),
})?;

Ok(Self { host, port })
} else {
Err(Error::NodeParsePortMissing {
text: text.to_owned(),
})
}
}
}

impl Display for Node {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}:{}", self.host, self.port)
}
}

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

impl From<&Node> for Tuple {
fn from(node: &Node) -> Self {
let host = match &node.host {
Host::Domain(domain) => domain.to_string(),
Host::Ipv4(ipv4) => ipv4.to_string(),
Host::Ipv6(ipv6) => ipv6.to_string(),
};
Self(host, node.port)
}
}

impl Serialize for Node {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Tuple::from(self).serialize(serializer)
}
}

impl<'de> Deserialize<'de> for Node {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let tuple = Tuple::deserialize(deserializer)?;

let host = if tuple.0.contains(':') {
Host::parse(&format!("[{}]", tuple.0))
} else {
Host::parse(&tuple.0)
}
.map_err(|error| D::Error::custom(format!("Failed to parse node host: {}", error)))?;

Ok(Node {
host,
port: tuple.1,
})
}
}

#[cfg(test)]
mod tests {
use super::*;

use std::net::{Ipv4Addr, Ipv6Addr};

fn case(host: Host, port: u16, text: &str, bencode: &str) {
let node = Node { host, port };
let parsed: Node = text.parse().expect(&format!("Failed to parse {}", text));
assert_eq!(parsed, node);
let ser = bendy::serde::to_bytes(&node).unwrap();
assert_eq!(
ser,
bencode.as_bytes(),
"Unexpected serialization: {} != {}",
String::from_utf8_lossy(&ser),
bencode,
);
let de = bendy::serde::from_bytes::<Node>(&ser).unwrap();
assert_eq!(de, node);
}

#[test]
fn test_domain() {
case(
Host::Domain("imdl.com".to_owned()),
12,
"imdl.com:12",
"l8:imdl.comi12ee",
);
}

#[test]
fn test_ipv4() {
case(
Host::Ipv4(Ipv4Addr::new(1, 2, 3, 4)),
100,
"1.2.3.4:100",
"l7:1.2.3.4i100ee",
);
}

#[test]
fn test_ipv6() {
case(
Host::Ipv6(Ipv6Addr::new(
0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1234, 0x5678, 0x9ABC, 0xDEF0,
)),
65000,
"[1234:5678:9abc:def0:1234:5678:9abc:def0]:65000",
"l39:1234:5678:9abc:def0:1234:5678:9abc:def0i65000ee",
);
}
}
Loading

0 comments on commit 165a7ea

Please sign in to comment.