use zerocopy in udp protocol, easy running transfer CI locally

This commit is contained in:
Joakim Frostegård 2023-12-02 12:24:41 +01:00
parent af16a9e682
commit 0e12dd1b13
24 changed files with 783 additions and 652 deletions

View file

@ -1,10 +0,0 @@
# Not used by Github action, but can be used to run test locally:
# 1. docker build -t aquatic ./path/to/Dockerfile
# 2. docker run aquatic
# 3. On failure, run `docker rmi aquatic -f` and go back to step 1
FROM rust:bullseye
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View file

@ -68,7 +68,10 @@ tls_private_key_path = './key.pk8'
" > tls.toml " > tls.toml
./target/debug/aquatic http -c tls.toml > "$HOME/tls.log" 2>&1 & ./target/debug/aquatic http -c tls.toml > "$HOME/tls.log" 2>&1 &
echo "[network] echo "
log_level = 'trace'
[network]
address = '127.0.0.1:3000'" > udp.toml address = '127.0.0.1:3000'" > udp.toml
./target/debug/aquatic udp -c udp.toml > "$HOME/udp.log" 2>&1 & ./target/debug/aquatic udp -c udp.toml > "$HOME/udp.log" 2>&1 &

26
Cargo.lock generated
View file

@ -199,6 +199,7 @@ dependencies = [
"quickcheck", "quickcheck",
"regex", "regex",
"serde", "serde",
"zerocopy",
] ]
[[package]] [[package]]
@ -303,8 +304,10 @@ dependencies = [
"aquatic_peer_id", "aquatic_peer_id",
"byteorder", "byteorder",
"either", "either",
"pretty_assertions",
"quickcheck", "quickcheck",
"quickcheck_macros", "quickcheck_macros",
"zerocopy",
] ]
[[package]] [[package]]
@ -875,6 +878,12 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -2039,6 +2048,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]] [[package]]
name = "privdrop" name = "privdrop"
version = "0.5.4" version = "0.5.4"
@ -3135,12 +3154,19 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.26" version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
dependencies = [ dependencies = [
"byteorder",
"zerocopy-derive", "zerocopy-derive",
] ]

View file

@ -20,4 +20,5 @@ compact_str = "0.7"
hex = "0.4" hex = "0.4"
regex = "1" regex = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
quickcheck = { version = "1", optional = true } quickcheck = { version = "1", optional = true }
zerocopy = { version = "0.7", features = ["derive"] }

View file

@ -3,8 +3,24 @@ use std::{borrow::Cow, fmt::Display, sync::OnceLock};
use compact_str::{format_compact, CompactString}; use compact_str::{format_compact, CompactString};
use regex::bytes::Regex; use regex::bytes::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zerocopy::{AsBytes, FromBytes, FromZeroes};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
AsBytes,
FromBytes,
FromZeroes,
)]
#[repr(transparent)]
pub struct PeerId(pub [u8; 20]); pub struct PeerId(pub [u8; 20]);
impl PeerId { impl PeerId {

View file

@ -1,6 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::hash::Hash; use std::hash::Hash;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
@ -35,8 +34,8 @@ pub enum ConnectedRequest {
#[derive(Debug)] #[derive(Debug)]
pub enum ConnectedResponse { pub enum ConnectedResponse {
AnnounceIpv4(AnnounceResponse<Ipv4Addr>), AnnounceIpv4(AnnounceResponse<Ipv4AddrBytes>),
AnnounceIpv6(AnnounceResponse<Ipv6Addr>), AnnounceIpv6(AnnounceResponse<Ipv6AddrBytes>),
Scrape(PendingScrapeResponse), Scrape(PendingScrapeResponse),
} }
@ -125,7 +124,7 @@ impl PeerStatus {
pub fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: NumberOfBytes) -> Self { pub fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: NumberOfBytes) -> Self {
if event == AnnounceEvent::Stopped { if event == AnnounceEvent::Stopped {
Self::Stopped Self::Stopped
} else if bytes_left.0 == 0 { } else if bytes_left.0.get() == 0 {
Self::Seeding Self::Seeding
} else { } else {
Self::Leeching Self::Leeching
@ -207,17 +206,17 @@ mod tests {
let f = PeerStatus::from_event_and_bytes_left; let f = PeerStatus::from_event_and_bytes_left;
assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes(0))); assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(0)));
assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes(1))); assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(1)));
assert_eq!(Seeding, f(AnnounceEvent::Started, NumberOfBytes(0))); assert_eq!(Seeding, f(AnnounceEvent::Started, NumberOfBytes::new(0)));
assert_eq!(Leeching, f(AnnounceEvent::Started, NumberOfBytes(1))); assert_eq!(Leeching, f(AnnounceEvent::Started, NumberOfBytes::new(1)));
assert_eq!(Seeding, f(AnnounceEvent::Completed, NumberOfBytes(0))); assert_eq!(Seeding, f(AnnounceEvent::Completed, NumberOfBytes::new(0)));
assert_eq!(Leeching, f(AnnounceEvent::Completed, NumberOfBytes(1))); assert_eq!(Leeching, f(AnnounceEvent::Completed, NumberOfBytes::new(1)));
assert_eq!(Seeding, f(AnnounceEvent::None, NumberOfBytes(0))); assert_eq!(Seeding, f(AnnounceEvent::None, NumberOfBytes::new(0)));
assert_eq!(Leeching, f(AnnounceEvent::None, NumberOfBytes(1))); assert_eq!(Leeching, f(AnnounceEvent::None, NumberOfBytes::new(1)));
} }
// Assumes that announce response with maximum amount of ipv6 peers will // Assumes that announce response with maximum amount of ipv6 peers will
@ -229,17 +228,19 @@ mod tests {
let config = Config::default(); let config = Config::default();
let peers = ::std::iter::repeat(ResponsePeer { let peers = ::std::iter::repeat(ResponsePeer {
ip_address: Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1), ip_address: Ipv6AddrBytes(Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1).octets()),
port: Port(1), port: Port::new(1),
}) })
.take(config.protocol.max_response_peers) .take(config.protocol.max_response_peers)
.collect(); .collect();
let response = Response::AnnounceIpv6(AnnounceResponse { let response = Response::AnnounceIpv6(AnnounceResponse {
transaction_id: TransactionId(1), fixed: AnnounceResponseFixedData {
announce_interval: AnnounceInterval(1), transaction_id: TransactionId::new(1),
seeders: NumberOfPeers(1), announce_interval: AnnounceInterval::new(1),
leechers: NumberOfPeers(1), seeders: NumberOfPeers::new(1),
leechers: NumberOfPeers::new(1),
},
peers, peers,
}); });

View file

@ -195,6 +195,8 @@ impl SocketWorker {
let src = CanonicalSocketAddr::new(src); let src = CanonicalSocketAddr::new(src);
::log::trace!("received request bytes: {}", hex_slice(&self.buffer[..bytes_read]));
let request_parsable = match Request::from_bytes( let request_parsable = match Request::from_bytes(
&self.buffer[..bytes_read], &self.buffer[..bytes_read],
self.config.protocol.max_scrape_torrents, self.config.protocol.max_scrape_torrents,
@ -221,7 +223,7 @@ impl SocketWorker {
if self.validator.connection_id_valid(src, connection_id) { if self.validator.connection_id_valid(src, connection_id) {
let response = ErrorResponse { let response = ErrorResponse {
transaction_id, transaction_id,
message: err.right_or("Parse error").into(), message: err.into(),
}; };
local_responses.push((response.into(), src)); local_responses.push((response.into(), src));
@ -285,6 +287,8 @@ impl SocketWorker {
match request { match request {
Request::Connect(request) => { Request::Connect(request) => {
::log::trace!("received {:?} from {:?}", request, src);
let connection_id = self.validator.create_connection_id(src); let connection_id = self.validator.create_connection_id(src);
let response = Response::Connect(ConnectResponse { let response = Response::Connect(ConnectResponse {
@ -295,6 +299,8 @@ impl SocketWorker {
local_responses.push((response, src)) local_responses.push((response, src))
} }
Request::Announce(request) => { Request::Announce(request) => {
::log::trace!("received {:?} from {:?}", request, src);
if self if self
.validator .validator
.connection_id_valid(src, request.connection_id) .connection_id_valid(src, request.connection_id)
@ -323,6 +329,8 @@ impl SocketWorker {
} }
} }
Request::Scrape(request) => { Request::Scrape(request) => {
::log::trace!("received {:?} from {:?}", request, src);
if self if self
.validator .validator
.connection_id_valid(src, request.connection_id) .connection_id_valid(src, request.connection_id)
@ -372,6 +380,8 @@ impl SocketWorker {
canonical_addr.get_ipv6_mapped() canonical_addr.get_ipv6_mapped()
}; };
::log::trace!("sending {:?} to {}, bytes: {}", response, addr, hex_slice(&cursor.get_ref()[..bytes_written]));
match socket.send_to(&cursor.get_ref()[..bytes_written], addr) { match socket.send_to(&cursor.get_ref()[..bytes_written], addr) {
Ok(amt) if config.statistics.active() => { Ok(amt) if config.statistics.active() => {
let stats = if canonical_addr.is_ipv4() { let stats = if canonical_addr.is_ipv4() {
@ -430,3 +440,14 @@ impl SocketWorker {
} }
} }
} }
fn hex_slice(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len() * 3);
for chunk in bytes.chunks(4) {
output.push_str(&hex::encode(chunk));
output.push(' ');
}
output
}

View file

@ -156,8 +156,8 @@ mod tests {
} }
let request = ScrapeRequest { let request = ScrapeRequest {
transaction_id: TransactionId(t), transaction_id: TransactionId::new(t),
connection_id: ConnectionId(c), connection_id: ConnectionId::new(c),
info_hashes, info_hashes,
}; };
@ -192,9 +192,9 @@ mod tests {
( (
i, i,
TorrentScrapeStatistics { TorrentScrapeStatistics {
seeders: NumberOfPeers((info_hash.0[0]) as i32), seeders: NumberOfPeers::new((info_hash.0[0]) as i32),
leechers: NumberOfPeers(0), leechers: NumberOfPeers::new(0),
completed: NumberOfDownloads(0), completed: NumberOfDownloads::new(0),
}, },
) )
}) })

View file

@ -393,7 +393,7 @@ impl SocketWorker {
if self.validator.connection_id_valid(addr, connection_id) { if self.validator.connection_id_valid(addr, connection_id) {
let response = ErrorResponse { let response = ErrorResponse {
transaction_id, transaction_id,
message: err.right_or("Parse error").into(), message: err.into(),
}; };
self.local_responses.push_back((response.into(), addr)); self.local_responses.push_back((response.into(), addr));

View file

@ -59,7 +59,7 @@ impl ConnectionValidator {
(&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until);
(&mut connection_id_bytes[4..]).copy_from_slice(&hash); (&mut connection_id_bytes[4..]).copy_from_slice(&hash);
ConnectionId(i64::from_ne_bytes(connection_id_bytes)) ConnectionId::new(i64::from_ne_bytes(connection_id_bytes))
} }
pub fn connection_id_valid( pub fn connection_id_valid(
@ -67,7 +67,7 @@ impl ConnectionValidator {
source_addr: CanonicalSocketAddr, source_addr: CanonicalSocketAddr,
connection_id: ConnectionId, connection_id: ConnectionId,
) -> bool { ) -> bool {
let bytes = connection_id.0.to_ne_bytes(); let bytes = connection_id.0.get().to_ne_bytes();
let (valid_until, hash) = bytes.split_at(4); let (valid_until, hash) = bytes.split_at(4);
let valid_until: [u8; 4] = valid_until.try_into().unwrap(); let valid_until: [u8; 4] = valid_until.try_into().unwrap();

View file

@ -53,7 +53,7 @@ pub fn run_swarm_worker(
&statistics_sender, &statistics_sender,
&mut torrents.ipv4, &mut torrents.ipv4,
request, request,
ip, ip.into(),
peer_valid_until, peer_valid_until,
); );
@ -66,7 +66,7 @@ pub fn run_swarm_worker(
&statistics_sender, &statistics_sender,
&mut torrents.ipv6, &mut torrents.ipv6,
request, request,
ip, ip.into(),
peer_valid_until, peer_valid_until,
); );
@ -126,18 +126,19 @@ fn handle_announce_request<I: Ip>(
peer_ip: I, peer_ip: I,
peer_valid_until: ValidUntil, peer_valid_until: ValidUntil,
) -> AnnounceResponse<I> { ) -> AnnounceResponse<I> {
let max_num_peers_to_take: usize = if request.peers_wanted.0 <= 0 { let max_num_peers_to_take: usize = if request.peers_wanted.0.get() <= 0 {
config.protocol.max_response_peers config.protocol.max_response_peers
} else { } else {
::std::cmp::min( ::std::cmp::min(
config.protocol.max_response_peers, config.protocol.max_response_peers,
request.peers_wanted.0.try_into().unwrap(), request.peers_wanted.0.get().try_into().unwrap(),
) )
}; };
let torrent_data = torrents.0.entry(request.info_hash).or_default(); let torrent_data = torrents.0.entry(request.info_hash).or_default();
let peer_status = PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left); let peer_status =
PeerStatus::from_event_and_bytes_left(request.event.into(), request.bytes_left);
torrent_data.update_peer( torrent_data.update_peer(
config, config,
@ -156,10 +157,14 @@ fn handle_announce_request<I: Ip>(
}; };
AnnounceResponse { AnnounceResponse {
transaction_id: request.transaction_id, fixed: AnnounceResponseFixedData {
announce_interval: AnnounceInterval(config.protocol.peer_announce_interval), transaction_id: request.transaction_id,
leechers: NumberOfPeers(torrent_data.num_leechers().try_into().unwrap_or(i32::MAX)), announce_interval: AnnounceInterval::new(config.protocol.peer_announce_interval),
seeders: NumberOfPeers(torrent_data.num_seeders().try_into().unwrap_or(i32::MAX)), leechers: NumberOfPeers::new(
torrent_data.num_leechers().try_into().unwrap_or(i32::MAX),
),
seeders: NumberOfPeers::new(torrent_data.num_seeders().try_into().unwrap_or(i32::MAX)),
},
peers: response_peers, peers: response_peers,
} }
} }
@ -168,8 +173,6 @@ fn handle_scrape_request<I: Ip>(
torrents: &mut TorrentMap<I>, torrents: &mut TorrentMap<I>,
request: PendingScrapeRequest, request: PendingScrapeRequest,
) -> PendingScrapeResponse { ) -> PendingScrapeResponse {
const EMPTY_STATS: TorrentScrapeStatistics = create_torrent_scrape_statistics(0, 0);
let torrent_stats = request let torrent_stats = request
.info_hashes .info_hashes
.into_iter() .into_iter()
@ -178,7 +181,7 @@ fn handle_scrape_request<I: Ip>(
.0 .0
.get(&info_hash) .get(&info_hash)
.map(|torrent_data| torrent_data.scrape_statistics()) .map(|torrent_data| torrent_data.scrape_statistics())
.unwrap_or(EMPTY_STATS); .unwrap_or_else(|| create_torrent_scrape_statistics(0, 0));
(i, stats) (i, stats)
}) })
@ -191,10 +194,10 @@ fn handle_scrape_request<I: Ip>(
} }
#[inline(always)] #[inline(always)]
const fn create_torrent_scrape_statistics(seeders: i32, leechers: i32) -> TorrentScrapeStatistics { fn create_torrent_scrape_statistics(seeders: i32, leechers: i32) -> TorrentScrapeStatistics {
TorrentScrapeStatistics { TorrentScrapeStatistics {
seeders: NumberOfPeers(seeders), seeders: NumberOfPeers::new(seeders),
completed: NumberOfDownloads(0), // No implementation planned completed: NumberOfDownloads::new(0), // No implementation planned
leechers: NumberOfPeers(leechers), leechers: NumberOfPeers::new(leechers),
} }
} }

View file

@ -1,5 +1,3 @@
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
@ -256,8 +254,8 @@ impl<I: Ip> TorrentMap<I> {
} }
pub struct TorrentMaps { pub struct TorrentMaps {
pub ipv4: TorrentMap<Ipv4Addr>, pub ipv4: TorrentMap<Ipv4AddrBytes>,
pub ipv6: TorrentMap<Ipv6Addr>, pub ipv6: TorrentMap<Ipv6AddrBytes>,
} }
impl Default for TorrentMaps { impl Default for TorrentMaps {
@ -312,7 +310,6 @@ impl TorrentMaps {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashSet; use std::collections::HashSet;
use std::net::Ipv4Addr;
use quickcheck::{quickcheck, TestResult}; use quickcheck::{quickcheck, TestResult};
use rand::thread_rng; use rand::thread_rng;
@ -326,10 +323,10 @@ mod tests {
peer_id peer_id
} }
fn gen_peer(i: u32) -> Peer<Ipv4Addr> { fn gen_peer(i: u32) -> Peer<Ipv4AddrBytes> {
Peer { Peer {
ip_address: Ipv4Addr::from(i.to_be_bytes()), ip_address: Ipv4AddrBytes(i.to_be_bytes()),
port: Port(1), port: Port::new(1),
is_seeder: false, is_seeder: false,
valid_until: ValidUntil::new(ServerStartInstant::new(), 0), valid_until: ValidUntil::new(ServerStartInstant::new(), 0),
} }
@ -341,7 +338,7 @@ mod tests {
let gen_num_peers = data.0 as u32; let gen_num_peers = data.0 as u32;
let req_num_peers = data.1 as usize; let req_num_peers = data.1 as usize;
let mut peer_map: PeerMap<Ipv4Addr> = Default::default(); let mut peer_map: PeerMap<Ipv4AddrBytes> = Default::default();
let mut opt_sender_key = None; let mut opt_sender_key = None;
let mut opt_sender_peer = None; let mut opt_sender_peer = None;

View file

@ -10,8 +10,8 @@ use anyhow::Context;
use aquatic_udp::{common::BUFFER_SIZE, config::Config}; use aquatic_udp::{common::BUFFER_SIZE, config::Config};
use aquatic_udp_protocol::{ use aquatic_udp_protocol::{
common::PeerId, AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, InfoHash, common::PeerId, AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, InfoHash,
NumberOfBytes, NumberOfPeers, PeerKey, Port, Request, Response, ScrapeRequest, ScrapeResponse, Ipv4AddrBytes, NumberOfBytes, NumberOfPeers, PeerKey, Port, Request, Response, ScrapeRequest,
TransactionId, ScrapeResponse, TransactionId,
}; };
// FIXME: should ideally try different ports and use sync primitives to find // FIXME: should ideally try different ports and use sync primitives to find
@ -26,7 +26,7 @@ pub fn run_tracker(config: Config) {
pub fn connect(socket: &UdpSocket, tracker_addr: SocketAddr) -> anyhow::Result<ConnectionId> { pub fn connect(socket: &UdpSocket, tracker_addr: SocketAddr) -> anyhow::Result<ConnectionId> {
let request = Request::Connect(ConnectRequest { let request = Request::Connect(ConnectRequest {
transaction_id: TransactionId(0), transaction_id: TransactionId::new(0),
}); });
let response = request_and_response(&socket, tracker_addr, request)?; let response = request_and_response(&socket, tracker_addr, request)?;
@ -55,17 +55,18 @@ pub fn announce(
let request = Request::Announce(AnnounceRequest { let request = Request::Announce(AnnounceRequest {
connection_id, connection_id,
transaction_id: TransactionId(0), action_placeholder: Default::default(),
transaction_id: TransactionId::new(0),
info_hash, info_hash,
peer_id, peer_id,
bytes_downloaded: NumberOfBytes(0), bytes_downloaded: NumberOfBytes::new(0),
bytes_uploaded: NumberOfBytes(0), bytes_uploaded: NumberOfBytes::new(0),
bytes_left: NumberOfBytes(if seeder { 0 } else { 1 }), bytes_left: NumberOfBytes::new(if seeder { 0 } else { 1 }),
event: AnnounceEvent::Started, event: AnnounceEvent::Started.into(),
ip_address: None, ip_address: Ipv4AddrBytes([0; 4]),
key: PeerKey(0), key: PeerKey::new(0),
peers_wanted: NumberOfPeers(peers_wanted as i32), peers_wanted: NumberOfPeers::new(peers_wanted as i32),
port: Port(peer_port), port: Port::new(peer_port),
}); });
Ok(request_and_response(&socket, tracker_addr, request)?) Ok(request_and_response(&socket, tracker_addr, request)?)
@ -79,7 +80,7 @@ pub fn scrape(
) -> anyhow::Result<ScrapeResponse> { ) -> anyhow::Result<ScrapeResponse> {
let request = Request::Scrape(ScrapeRequest { let request = Request::Scrape(ScrapeRequest {
connection_id, connection_id,
transaction_id: TransactionId(0), transaction_id: TransactionId::new(0),
info_hashes, info_hashes,
}); });

View file

@ -11,8 +11,8 @@ use std::{
use anyhow::Context; use anyhow::Context;
use aquatic_udp::{common::BUFFER_SIZE, config::Config}; use aquatic_udp::{common::BUFFER_SIZE, config::Config};
use aquatic_udp_protocol::{ use aquatic_udp_protocol::{
common::PeerId, AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, common::PeerId, AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, Ipv4AddrBytes,
NumberOfPeers, PeerKey, Port, Request, ScrapeRequest, TransactionId, NumberOfBytes, NumberOfPeers, PeerKey, Port, Request, ScrapeRequest, TransactionId,
}; };
#[test] #[test]
@ -40,22 +40,23 @@ fn test_invalid_connection_id() -> anyhow::Result<()> {
let announce_request = Request::Announce(AnnounceRequest { let announce_request = Request::Announce(AnnounceRequest {
connection_id: invalid_connection_id, connection_id: invalid_connection_id,
transaction_id: TransactionId(0), action_placeholder: Default::default(),
transaction_id: TransactionId::new(0),
info_hash: InfoHash([0; 20]), info_hash: InfoHash([0; 20]),
peer_id: PeerId([0; 20]), peer_id: PeerId([0; 20]),
bytes_downloaded: NumberOfBytes(0), bytes_downloaded: NumberOfBytes::new(0),
bytes_uploaded: NumberOfBytes(0), bytes_uploaded: NumberOfBytes::new(0),
bytes_left: NumberOfBytes(0), bytes_left: NumberOfBytes::new(0),
event: AnnounceEvent::Started, event: AnnounceEvent::Started.into(),
ip_address: None, ip_address: Ipv4AddrBytes([0; 4]),
key: PeerKey(0), key: PeerKey::new(0),
peers_wanted: NumberOfPeers(10), peers_wanted: NumberOfPeers::new(10),
port: Port(1), port: Port::new(1),
}); });
let scrape_request = Request::Scrape(ScrapeRequest { let scrape_request = Request::Scrape(ScrapeRequest {
connection_id: invalid_connection_id, connection_id: invalid_connection_id,
transaction_id: TransactionId(0), transaction_id: TransactionId::new(0),
info_hashes: vec![InfoHash([0; 20])], info_hashes: vec![InfoHash([0; 20])],
}); });

View file

@ -67,11 +67,11 @@ fn test_multiple_connect_announce_scrape() -> anyhow::Result<()> {
assert_eq!(announce_response.peers.len(), i.min(PEERS_WANTED)); assert_eq!(announce_response.peers.len(), i.min(PEERS_WANTED));
assert_eq!(announce_response.seeders.0, num_seeders); assert_eq!(announce_response.fixed.seeders.0.get(), num_seeders);
assert_eq!(announce_response.leechers.0, num_leechers); assert_eq!(announce_response.fixed.leechers.0.get(), num_leechers);
let response_peer_ports: HashSet<u16, RandomState> = let response_peer_ports: HashSet<u16, RandomState> =
HashSet::from_iter(announce_response.peers.iter().map(|p| p.port.0)); HashSet::from_iter(announce_response.peers.iter().map(|p| p.port.0.get()));
let expected_peer_ports: HashSet<u16, RandomState> = let expected_peer_ports: HashSet<u16, RandomState> =
HashSet::from_iter((0..i).map(|i| PEER_PORT_START + i as u16)); HashSet::from_iter((0..i).map(|i| PEER_PORT_START + i as u16));
@ -89,10 +89,16 @@ fn test_multiple_connect_announce_scrape() -> anyhow::Result<()> {
) )
.with_context(|| "scrape")?; .with_context(|| "scrape")?;
assert_eq!(scrape_response.torrent_stats[0].seeders.0, num_seeders); assert_eq!(
assert_eq!(scrape_response.torrent_stats[0].leechers.0, num_leechers); scrape_response.torrent_stats[0].seeders.0.get(),
assert_eq!(scrape_response.torrent_stats[1].seeders.0, 0); num_seeders
assert_eq!(scrape_response.torrent_stats[1].leechers.0, 0); );
assert_eq!(
scrape_response.torrent_stats[0].leechers.0.get(),
num_leechers
);
assert_eq!(scrape_response.torrent_stats[1].seeders.0.get(), 0);
assert_eq!(scrape_response.torrent_stats[1].leechers.0.get(), 0);
} }
Ok(()) Ok(())

View file

@ -49,7 +49,7 @@ pub fn bench_announce_handler(
num_responses += 1; num_responses += 1;
if let Some(last_peer) = r.peers.last() { if let Some(last_peer) = r.peers.last() {
dummy ^= last_peer.port.0; dummy ^= last_peer.port.0.get();
} }
} }
} }
@ -61,7 +61,7 @@ pub fn bench_announce_handler(
num_responses += 1; num_responses += 1;
if let Some(last_peer) = r.peers.last() { if let Some(last_peer) = r.peers.last() {
dummy ^= last_peer.port.0; dummy ^= last_peer.port.0.get();
} }
} }
} }
@ -91,18 +91,19 @@ pub fn create_requests(
let info_hash_index = gamma_usize(rng, gamma, max_index); let info_hash_index = gamma_usize(rng, gamma, max_index);
let request = AnnounceRequest { let request = AnnounceRequest {
connection_id: ConnectionId(0), connection_id: ConnectionId::new(0),
transaction_id: TransactionId(rng.gen()), action_placeholder: Default::default(),
transaction_id: TransactionId::new(rng.gen()),
info_hash: info_hashes[info_hash_index], info_hash: info_hashes[info_hash_index],
peer_id: PeerId(rng.gen()), peer_id: PeerId(rng.gen()),
bytes_downloaded: NumberOfBytes(rng.gen()), bytes_downloaded: NumberOfBytes::new(rng.gen()),
bytes_uploaded: NumberOfBytes(rng.gen()), bytes_uploaded: NumberOfBytes::new(rng.gen()),
bytes_left: NumberOfBytes(rng.gen()), bytes_left: NumberOfBytes::new(rng.gen()),
event: AnnounceEvent::Started, event: AnnounceEvent::Started.into(),
ip_address: None, ip_address: Ipv4AddrBytes([0; 4]),
key: PeerKey(rng.gen()), key: PeerKey::new(rng.gen()),
peers_wanted: NumberOfPeers(rng.gen()), peers_wanted: NumberOfPeers::new(rng.gen()),
port: Port(rng.gen()), port: Port::new(rng.gen()),
}; };
requests.push(( requests.push((

View file

@ -60,7 +60,7 @@ pub fn bench_scrape_handler(
num_responses += 1; num_responses += 1;
if let Some(stat) = response.torrent_stats.values().last() { if let Some(stat) = response.torrent_stats.values().last() {
dummy ^= stat.leechers.0; dummy ^= stat.leechers.0.get();
} }
} }
} }
@ -72,7 +72,7 @@ pub fn bench_scrape_handler(
num_responses += 1; num_responses += 1;
if let Some(stat) = response.torrent_stats.values().last() { if let Some(stat) = response.torrent_stats.values().last() {
dummy ^= stat.leechers.0; dummy ^= stat.leechers.0.get();
} }
} }
} }
@ -108,8 +108,8 @@ pub fn create_requests(
} }
let request = ScrapeRequest { let request = ScrapeRequest {
connection_id: ConnectionId(0), connection_id: ConnectionId::new(0),
transaction_id: TransactionId(rng.gen()), transaction_id: TransactionId::new(rng.gen()),
info_hashes: request_info_hashes, info_hashes: request_info_hashes,
}; };

View file

@ -19,7 +19,7 @@ pub fn generate_info_hash() -> InfoHash {
} }
pub fn generate_transaction_id(rng: &mut impl Rng) -> TransactionId { pub fn generate_transaction_id(rng: &mut impl Rng) -> TransactionId {
TransactionId(rng.gen()) TransactionId::new(rng.gen())
} }
pub fn create_connect_request(transaction_id: TransactionId) -> Request { pub fn create_connect_request(transaction_id: TransactionId) -> Request {

View file

@ -49,14 +49,14 @@ pub fn process_response(
rng, rng,
info_hashes, info_hashes,
torrent_peers, torrent_peers,
r.transaction_id, r.fixed.transaction_id,
), ),
Response::AnnounceIpv6(r) => if_torrent_peer_move_and_create_random_request( Response::AnnounceIpv6(r) => if_torrent_peer_move_and_create_random_request(
config, config,
rng, rng,
info_hashes, info_hashes,
torrent_peers, torrent_peers,
r.transaction_id, r.fixed.transaction_id,
), ),
Response::Scrape(r) => if_torrent_peer_move_and_create_random_request( Response::Scrape(r) => if_torrent_peer_move_and_create_random_request(
config, config,
@ -143,24 +143,25 @@ fn create_announce_request(
) -> Request { ) -> Request {
let (event, bytes_left) = { let (event, bytes_left) = {
if rng.gen_bool(config.requests.peer_seeder_probability) { if rng.gen_bool(config.requests.peer_seeder_probability) {
(AnnounceEvent::Completed, NumberOfBytes(0)) (AnnounceEvent::Completed, NumberOfBytes::new(0))
} else { } else {
(AnnounceEvent::Started, NumberOfBytes(50)) (AnnounceEvent::Started, NumberOfBytes::new(50))
} }
}; };
(AnnounceRequest { (AnnounceRequest {
connection_id: torrent_peer.connection_id, connection_id: torrent_peer.connection_id,
action_placeholder: Default::default(),
transaction_id, transaction_id,
info_hash: torrent_peer.info_hash, info_hash: torrent_peer.info_hash,
peer_id: torrent_peer.peer_id, peer_id: torrent_peer.peer_id,
bytes_downloaded: NumberOfBytes(50), bytes_downloaded: NumberOfBytes::new(50),
bytes_uploaded: NumberOfBytes(50), bytes_uploaded: NumberOfBytes::new(50),
bytes_left, bytes_left,
event, event: event.into(),
ip_address: None, ip_address: Ipv4AddrBytes([0; 4]),
key: PeerKey(12345), key: PeerKey::new(12345),
peers_wanted: NumberOfPeers(100), peers_wanted: NumberOfPeers::new(100),
port: torrent_peer.port, port: torrent_peer.port,
}) })
.into() .into()
@ -209,7 +210,7 @@ fn create_torrent_peer(
scrape_hash_indeces, scrape_hash_indeces,
connection_id, connection_id,
peer_id: generate_peer_id(), peer_id: generate_peer_id(),
port: Port(rng.gen()), port: Port::new(rng.gen()),
} }
} }

View file

@ -15,7 +15,9 @@ aquatic_peer_id.workspace = true
byteorder = "1" byteorder = "1"
either = "1" either = "1"
zerocopy = { version = "0.7", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1"
quickcheck = "1" quickcheck = "1"
quickcheck_macros = "1" quickcheck_macros = "1"

View file

@ -2,45 +2,174 @@ use std::fmt::Debug;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
pub use aquatic_peer_id::{PeerClient, PeerId}; pub use aquatic_peer_id::{PeerClient, PeerId};
use zerocopy::network_endian::{I32, I64, U16, U32};
use zerocopy::{AsBytes, FromBytes, FromZeroes};
pub trait Ip: Clone + Copy + Debug + PartialEq + Eq {} pub trait Ip: Clone + Copy + Debug + PartialEq + Eq + AsBytes {}
impl Ip for Ipv4Addr {} #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
impl Ip for Ipv6Addr {} #[repr(transparent)]
pub struct AnnounceInterval(pub I32);
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] impl AnnounceInterval {
pub struct AnnounceInterval(pub i32); pub fn new(v: i32) -> Self {
Self(I32::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct InfoHash(pub [u8; 20]); pub struct InfoHash(pub [u8; 20]);
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
pub struct ConnectionId(pub i64); #[repr(transparent)]
pub struct ConnectionId(pub I64);
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] impl ConnectionId {
pub struct TransactionId(pub i32); pub fn new(v: i64) -> Self {
Self(I64::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
pub struct NumberOfBytes(pub i64); #[repr(transparent)]
pub struct TransactionId(pub I32);
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] impl TransactionId {
pub struct NumberOfPeers(pub i32); pub fn new(v: i32) -> Self {
Self(I32::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
pub struct NumberOfDownloads(pub i32); #[repr(transparent)]
pub struct NumberOfBytes(pub I64);
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] impl NumberOfBytes {
pub struct Port(pub u16); pub fn new(v: i64) -> Self {
Self(I64::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
pub struct PeerKey(pub u32); #[repr(transparent)]
pub struct NumberOfPeers(pub I32);
#[derive(PartialEq, Eq, Clone, Debug)] impl NumberOfPeers {
pub fn new(v: i32) -> Self {
Self(I32::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct NumberOfDownloads(pub I32);
impl NumberOfDownloads {
pub fn new(v: i32) -> Self {
Self(I32::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct Port(pub U16);
impl Port {
pub fn new(v: u16) -> Self {
Self(U16::new(v))
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct PeerKey(pub I32);
impl PeerKey {
pub fn new(v: i32) -> Self {
Self(I32::new(v))
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(C, packed)]
pub struct ResponsePeer<I: Ip> { pub struct ResponsePeer<I: Ip> {
pub ip_address: I, pub ip_address: I,
pub port: Port, pub port: Port,
} }
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct Ipv4AddrBytes(pub [u8; 4]);
impl Ip for Ipv4AddrBytes {}
impl Into<Ipv4Addr> for Ipv4AddrBytes {
fn into(self) -> Ipv4Addr {
Ipv4Addr::from(self.0)
}
}
impl Into<Ipv4AddrBytes> for Ipv4Addr {
fn into(self) -> Ipv4AddrBytes {
Ipv4AddrBytes(self.octets())
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct Ipv6AddrBytes(pub [u8; 16]);
impl Ip for Ipv6AddrBytes {}
impl Into<Ipv6Addr> for Ipv6AddrBytes {
fn into(self) -> Ipv6Addr {
Ipv6Addr::from(self.0)
}
}
impl Into<Ipv6AddrBytes> for Ipv6Addr {
fn into(self) -> Ipv6AddrBytes {
Ipv6AddrBytes(self.octets())
}
}
pub fn read_i32_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result<I32> {
let mut tmp = [0u8; 4];
bytes.read_exact(&mut tmp)?;
Ok(I32::from_bytes(tmp))
}
pub fn read_i64_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result<I64> {
let mut tmp = [0u8; 8];
bytes.read_exact(&mut tmp)?;
Ok(I64::from_bytes(tmp))
}
pub fn read_u16_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result<U16> {
let mut tmp = [0u8; 2];
bytes.read_exact(&mut tmp)?;
Ok(U16::from_bytes(tmp))
}
pub fn read_u32_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result<U32> {
let mut tmp = [0u8; 4];
bytes.read_exact(&mut tmp)?;
Ok(U32::from_bytes(tmp))
}
pub fn invalid_data() -> ::std::io::Error {
::std::io::Error::new(::std::io::ErrorKind::InvalidData, "invalid data")
}
#[cfg(test)] #[cfg(test)]
impl quickcheck::Arbitrary for InfoHash { impl quickcheck::Arbitrary for InfoHash {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {

View file

@ -1,9 +1,9 @@
use std::convert::TryInto; use std::io::{self, Cursor, Write};
use std::io::{self, Cursor, Read, Write};
use std::net::Ipv4Addr;
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{NetworkEndian, WriteBytesExt};
use either::Either; use either::Either;
use zerocopy::FromZeroes;
use zerocopy::{byteorder::network_endian::I32, AsBytes, FromBytes};
use aquatic_peer_id::PeerId; use aquatic_peer_id::PeerId;
@ -11,103 +11,6 @@ use super::common::*;
const PROTOCOL_IDENTIFIER: i64 = 4_497_486_125_440; const PROTOCOL_IDENTIFIER: i64 = 4_497_486_125_440;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum AnnounceEvent {
Started,
Stopped,
Completed,
None,
}
impl AnnounceEvent {
#[inline]
pub fn from_i32(i: i32) -> Self {
match i {
1 => Self::Completed,
2 => Self::Started,
3 => Self::Stopped,
_ => Self::None,
}
}
#[inline]
pub fn to_i32(&self) -> i32 {
match self {
AnnounceEvent::None => 0,
AnnounceEvent::Completed => 1,
AnnounceEvent::Started => 2,
AnnounceEvent::Stopped => 3,
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ConnectRequest {
pub transaction_id: TransactionId,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct AnnounceRequest {
pub connection_id: ConnectionId,
pub transaction_id: TransactionId,
pub info_hash: InfoHash,
pub peer_id: PeerId,
pub bytes_downloaded: NumberOfBytes,
pub bytes_uploaded: NumberOfBytes,
pub bytes_left: NumberOfBytes,
pub event: AnnounceEvent,
pub ip_address: Option<Ipv4Addr>,
pub key: PeerKey,
pub peers_wanted: NumberOfPeers,
pub port: Port,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ScrapeRequest {
pub connection_id: ConnectionId,
pub transaction_id: TransactionId,
pub info_hashes: Vec<InfoHash>,
}
#[derive(Debug)]
pub enum RequestParseError {
Sendable {
connection_id: ConnectionId,
transaction_id: TransactionId,
err: Either<io::Error, &'static str>,
},
Unsendable {
err: Either<io::Error, &'static str>,
},
}
impl RequestParseError {
pub fn sendable_io(err: io::Error, connection_id: i64, transaction_id: i32) -> Self {
Self::Sendable {
connection_id: ConnectionId(connection_id),
transaction_id: TransactionId(transaction_id),
err: Either::Left(err),
}
}
pub fn sendable_text(text: &'static str, connection_id: i64, transaction_id: i32) -> Self {
Self::Sendable {
connection_id: ConnectionId(connection_id),
transaction_id: TransactionId(transaction_id),
err: Either::Right(text),
}
}
pub fn unsendable_io(err: io::Error) -> Self {
Self::Unsendable {
err: Either::Left(err),
}
}
pub fn unsendable_text(text: &'static str) -> Self {
Self::Unsendable {
err: Either::Right(text),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
pub enum Request { pub enum Request {
Connect(ConnectRequest), Connect(ConnectRequest),
@ -115,6 +18,115 @@ pub enum Request {
Scrape(ScrapeRequest), Scrape(ScrapeRequest),
} }
impl Request {
pub fn write(self, bytes: &mut impl Write) -> Result<(), io::Error> {
match self {
Request::Connect(r) => {
bytes.write_i64::<NetworkEndian>(PROTOCOL_IDENTIFIER)?;
bytes.write_i32::<NetworkEndian>(0)?;
bytes.write_all(r.transaction_id.as_bytes())?;
}
Request::Announce(r) => {
bytes.write_all(r.as_bytes())?;
}
Request::Scrape(r) => {
bytes.write_all(r.connection_id.as_bytes())?;
bytes.write_i32::<NetworkEndian>(2)?;
bytes.write_all(r.transaction_id.as_bytes())?;
bytes.write_all((*r.info_hashes.as_slice()).as_bytes())?;
}
}
Ok(())
}
pub fn from_bytes(bytes: &[u8], max_scrape_torrents: u8) -> Result<Self, RequestParseError> {
let action = bytes
.get(8..12)
.map(|bytes| I32::from_bytes(bytes.try_into().unwrap()))
.ok_or_else(|| RequestParseError::unsendable_text("Couldn't parse action"))?;
match action.get() {
// Connect
0 => {
let mut bytes = Cursor::new(bytes);
let protocol_identifier =
read_i64_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?;
let _action = read_i32_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?;
let transaction_id = read_i32_ne(&mut bytes)
.map(TransactionId)
.map_err(RequestParseError::unsendable_io)?;
if protocol_identifier.get() == PROTOCOL_IDENTIFIER {
Ok((ConnectRequest { transaction_id }).into())
} else {
Err(RequestParseError::unsendable_text(
"Protocol identifier missing",
))
}
}
// Announce
1 => {
let request = AnnounceRequest::read_from_prefix(bytes)
.ok_or_else(|| RequestParseError::unsendable_text("invalid data"))?;
// Make sure not to create AnnounceEventBytes with invalid value
if matches!(request.event.0.get(), (0..=3)) {
Ok(Request::Announce(request))
} else {
Err(RequestParseError::sendable_text(
"Invalid announce event",
request.connection_id,
request.transaction_id,
))
}
}
// Scrape
2 => {
let mut bytes = Cursor::new(bytes);
let connection_id = read_i64_ne(&mut bytes)
.map(ConnectionId)
.map_err(RequestParseError::unsendable_io)?;
let _action = read_i32_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?;
let transaction_id = read_i32_ne(&mut bytes)
.map(TransactionId)
.map_err(RequestParseError::unsendable_io)?;
let remaining_bytes = {
let position = bytes.position() as usize;
let inner = bytes.into_inner();
&inner[position..]
};
let info_hashes = FromBytes::slice_from(remaining_bytes).ok_or_else(|| {
RequestParseError::sendable_text(
"Invalid info hash list. Note that full scrapes are not allowed",
connection_id,
transaction_id,
)
})?;
let info_hashes = Vec::from(
&info_hashes[..(max_scrape_torrents as usize).min(info_hashes.len())],
);
Ok((ScrapeRequest {
connection_id,
transaction_id,
info_hashes,
})
.into())
}
_ => Err(RequestParseError::unsendable_text("Invalid action")),
}
}
}
impl From<ConnectRequest> for Request { impl From<ConnectRequest> for Request {
fn from(r: ConnectRequest) -> Self { fn from(r: ConnectRequest) -> Self {
Self::Connect(r) Self::Connect(r)
@ -133,173 +145,116 @@ impl From<ScrapeRequest> for Request {
} }
} }
impl Request { #[derive(PartialEq, Eq, Clone, Debug)]
pub fn write(self, bytes: &mut impl Write) -> Result<(), io::Error> { pub struct ConnectRequest {
match self { pub transaction_id: TransactionId,
Request::Connect(r) => { }
bytes.write_i64::<NetworkEndian>(PROTOCOL_IDENTIFIER)?;
bytes.write_i32::<NetworkEndian>(0)?;
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
}
Request::Announce(r) => { #[derive(PartialEq, Eq, Clone, Debug, AsBytes, FromBytes, FromZeroes)]
bytes.write_i64::<NetworkEndian>(r.connection_id.0)?; #[repr(C, packed)]
bytes.write_i32::<NetworkEndian>(1)?; pub struct AnnounceRequest {
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?; pub connection_id: ConnectionId,
/// This field is only present to enable zero-copy serialization and
/// deserialization.
pub action_placeholder: AnnounceActionPlaceholder,
pub transaction_id: TransactionId,
pub info_hash: InfoHash,
pub peer_id: PeerId,
pub bytes_downloaded: NumberOfBytes,
pub bytes_left: NumberOfBytes,
pub bytes_uploaded: NumberOfBytes,
pub event: AnnounceEventBytes,
pub ip_address: Ipv4AddrBytes,
pub key: PeerKey,
pub peers_wanted: NumberOfPeers,
pub port: Port,
}
bytes.write_all(&r.info_hash.0)?; /// Note: Request::from_bytes only creates this struct with value 1
bytes.write_all(&r.peer_id.0)?; #[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct AnnounceActionPlaceholder(I32);
bytes.write_i64::<NetworkEndian>(r.bytes_downloaded.0)?; impl Default for AnnounceActionPlaceholder {
bytes.write_i64::<NetworkEndian>(r.bytes_left.0)?; fn default() -> Self {
bytes.write_i64::<NetworkEndian>(r.bytes_uploaded.0)?; Self(I32::new(1))
bytes.write_i32::<NetworkEndian>(r.event.to_i32())?;
bytes.write_all(&r.ip_address.map_or([0; 4], |ip| ip.octets()))?;
bytes.write_u32::<NetworkEndian>(r.key.0)?;
bytes.write_i32::<NetworkEndian>(r.peers_wanted.0)?;
bytes.write_u16::<NetworkEndian>(r.port.0)?;
}
Request::Scrape(r) => {
bytes.write_i64::<NetworkEndian>(r.connection_id.0)?;
bytes.write_i32::<NetworkEndian>(2)?;
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
for info_hash in r.info_hashes {
bytes.write_all(&info_hash.0)?;
}
}
}
Ok(())
} }
}
pub fn from_bytes(bytes: &[u8], max_scrape_torrents: u8) -> Result<Self, RequestParseError> { /// Note: Request::from_bytes only creates this struct with values 0..=3
let mut cursor = Cursor::new(bytes); #[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)]
#[repr(transparent)]
pub struct AnnounceEventBytes(I32);
let connection_id = cursor impl From<AnnounceEvent> for AnnounceEventBytes {
.read_i64::<NetworkEndian>() fn from(value: AnnounceEvent) -> Self {
.map_err(RequestParseError::unsendable_io)?; Self(I32::new(match value {
let action = cursor AnnounceEvent::None => 0,
.read_i32::<NetworkEndian>() AnnounceEvent::Completed => 1,
.map_err(RequestParseError::unsendable_io)?; AnnounceEvent::Started => 2,
let transaction_id = cursor AnnounceEvent::Stopped => 3,
.read_i32::<NetworkEndian>() }))
.map_err(RequestParseError::unsendable_io)?; }
}
match action { #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
// Connect pub enum AnnounceEvent {
0 => { Started,
if connection_id == PROTOCOL_IDENTIFIER { Stopped,
Ok((ConnectRequest { Completed,
transaction_id: TransactionId(transaction_id), None,
}) }
.into())
} else {
Err(RequestParseError::unsendable_text(
"Protocol identifier missing",
))
}
}
// Announce impl From<AnnounceEventBytes> for AnnounceEvent {
1 => { fn from(value: AnnounceEventBytes) -> Self {
let mut info_hash = [0; 20]; match value.0.get() {
let mut peer_id = [0; 20]; 1 => Self::Completed,
let mut ip = [0; 4]; 2 => Self::Started,
3 => Self::Stopped,
_ => Self::None,
}
}
}
cursor.read_exact(&mut info_hash).map_err(|err| { #[derive(PartialEq, Eq, Clone, Debug)]
RequestParseError::sendable_io(err, connection_id, transaction_id) pub struct ScrapeRequest {
})?; pub connection_id: ConnectionId,
cursor.read_exact(&mut peer_id).map_err(|err| { pub transaction_id: TransactionId,
RequestParseError::sendable_io(err, connection_id, transaction_id) pub info_hashes: Vec<InfoHash>,
})?; }
let bytes_downloaded = cursor.read_i64::<NetworkEndian>().map_err(|err| { #[derive(Debug)]
RequestParseError::sendable_io(err, connection_id, transaction_id) pub enum RequestParseError {
})?; Sendable {
let bytes_left = cursor.read_i64::<NetworkEndian>().map_err(|err| { connection_id: ConnectionId,
RequestParseError::sendable_io(err, connection_id, transaction_id) transaction_id: TransactionId,
})?; err: &'static str,
let bytes_uploaded = cursor.read_i64::<NetworkEndian>().map_err(|err| { },
RequestParseError::sendable_io(err, connection_id, transaction_id) Unsendable {
})?; err: Either<io::Error, &'static str>,
let event = cursor.read_i32::<NetworkEndian>().map_err(|err| { },
RequestParseError::sendable_io(err, connection_id, transaction_id) }
})?;
cursor.read_exact(&mut ip).map_err(|err| { impl RequestParseError {
RequestParseError::sendable_io(err, connection_id, transaction_id) pub fn sendable_text(
})?; text: &'static str,
connection_id: ConnectionId,
let key = cursor.read_u32::<NetworkEndian>().map_err(|err| { transaction_id: TransactionId,
RequestParseError::sendable_io(err, connection_id, transaction_id) ) -> Self {
})?; Self::Sendable {
let peers_wanted = cursor.read_i32::<NetworkEndian>().map_err(|err| { connection_id,
RequestParseError::sendable_io(err, connection_id, transaction_id) transaction_id,
})?; err: text,
let port = cursor.read_u16::<NetworkEndian>().map_err(|err| { }
RequestParseError::sendable_io(err, connection_id, transaction_id) }
})?; pub fn unsendable_io(err: io::Error) -> Self {
Self::Unsendable {
let opt_ip = if ip == [0; 4] { err: Either::Left(err),
None }
} else { }
Some(Ipv4Addr::from(ip)) pub fn unsendable_text(text: &'static str) -> Self {
}; Self::Unsendable {
err: Either::Right(text),
Ok((AnnounceRequest {
connection_id: ConnectionId(connection_id),
transaction_id: TransactionId(transaction_id),
info_hash: InfoHash(info_hash),
peer_id: PeerId(peer_id),
bytes_downloaded: NumberOfBytes(bytes_downloaded),
bytes_uploaded: NumberOfBytes(bytes_uploaded),
bytes_left: NumberOfBytes(bytes_left),
event: AnnounceEvent::from_i32(event),
ip_address: opt_ip,
key: PeerKey(key),
peers_wanted: NumberOfPeers(peers_wanted),
port: Port(port),
})
.into())
}
// Scrape
2 => {
let position = cursor.position() as usize;
let inner = cursor.into_inner();
let info_hashes: Vec<InfoHash> = (&inner[position..])
.chunks_exact(20)
.take(max_scrape_torrents as usize)
.map(|chunk| InfoHash(chunk.try_into().unwrap()))
.collect();
if info_hashes.is_empty() {
Err(RequestParseError::sendable_text(
"Full scrapes are not allowed",
connection_id,
transaction_id,
))
} else {
Ok((ScrapeRequest {
connection_id: ConnectionId(connection_id),
transaction_id: TransactionId(transaction_id),
info_hashes,
})
.into())
}
}
_ => Err(RequestParseError::sendable_text(
"Invalid action",
connection_id,
transaction_id,
)),
} }
} }
} }
@ -308,6 +263,7 @@ impl Request {
mod tests { mod tests {
use quickcheck::TestResult; use quickcheck::TestResult;
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use zerocopy::network_endian::{I32, I64, U16};
use super::*; use super::*;
@ -325,7 +281,7 @@ mod tests {
impl quickcheck::Arbitrary for ConnectRequest { impl quickcheck::Arbitrary for ConnectRequest {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self { Self {
transaction_id: TransactionId(i32::arbitrary(g)), transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
} }
} }
} }
@ -333,18 +289,19 @@ mod tests {
impl quickcheck::Arbitrary for AnnounceRequest { impl quickcheck::Arbitrary for AnnounceRequest {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self { Self {
connection_id: ConnectionId(i64::arbitrary(g)), connection_id: ConnectionId(I64::new(i64::arbitrary(g))),
transaction_id: TransactionId(i32::arbitrary(g)), action_placeholder: AnnounceActionPlaceholder::default(),
transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
info_hash: InfoHash::arbitrary(g), info_hash: InfoHash::arbitrary(g),
peer_id: PeerId::arbitrary(g), peer_id: PeerId::arbitrary(g),
bytes_downloaded: NumberOfBytes(i64::arbitrary(g)), bytes_downloaded: NumberOfBytes(I64::new(i64::arbitrary(g))),
bytes_uploaded: NumberOfBytes(i64::arbitrary(g)), bytes_uploaded: NumberOfBytes(I64::new(i64::arbitrary(g))),
bytes_left: NumberOfBytes(i64::arbitrary(g)), bytes_left: NumberOfBytes(I64::new(i64::arbitrary(g))),
event: AnnounceEvent::arbitrary(g), event: AnnounceEvent::arbitrary(g).into(),
ip_address: None, ip_address: Ipv4AddrBytes::arbitrary(g),
key: PeerKey(u32::arbitrary(g)), key: PeerKey::new(i32::arbitrary(g)),
peers_wanted: NumberOfPeers(i32::arbitrary(g)), peers_wanted: NumberOfPeers(I32::new(i32::arbitrary(g))),
port: Port(u16::arbitrary(g)), port: Port(U16::new(u16::arbitrary(g))),
} }
} }
} }
@ -356,8 +313,8 @@ mod tests {
.collect(); .collect();
Self { Self {
connection_id: ConnectionId(i64::arbitrary(g)), connection_id: ConnectionId(I64::new(i64::arbitrary(g))),
transaction_id: TransactionId(i32::arbitrary(g)), transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
info_hashes, info_hashes,
} }
} }
@ -372,7 +329,7 @@ mod tests {
let success = request == r2; let success = request == r2;
if !success { if !success {
println!("before: {:#?}\nafter: {:#?}", request, r2); ::pretty_assertions::assert_eq!(request, r2);
} }
success success

View file

@ -1,69 +1,138 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::TryInto; use std::io::{self, Write};
use std::io::{self, Cursor, Write}; use std::mem::size_of;
use std::net::{Ipv4Addr, Ipv6Addr};
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{NetworkEndian, WriteBytesExt};
use zerocopy::{AsBytes, FromBytes, FromZeroes};
use super::common::*; use super::common::*;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct TorrentScrapeStatistics {
pub seeders: NumberOfPeers,
pub completed: NumberOfDownloads,
pub leechers: NumberOfPeers,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ConnectResponse {
pub connection_id: ConnectionId,
pub transaction_id: TransactionId,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct AnnounceResponse<I: Ip> {
pub transaction_id: TransactionId,
pub announce_interval: AnnounceInterval,
pub leechers: NumberOfPeers,
pub seeders: NumberOfPeers,
pub peers: Vec<ResponsePeer<I>>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ScrapeResponse {
pub transaction_id: TransactionId,
pub torrent_stats: Vec<TorrentScrapeStatistics>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ErrorResponse {
pub transaction_id: TransactionId,
pub message: Cow<'static, str>,
}
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
pub enum Response { pub enum Response {
Connect(ConnectResponse), Connect(ConnectResponse),
AnnounceIpv4(AnnounceResponse<Ipv4Addr>), AnnounceIpv4(AnnounceResponse<Ipv4AddrBytes>),
AnnounceIpv6(AnnounceResponse<Ipv6Addr>), AnnounceIpv6(AnnounceResponse<Ipv6AddrBytes>),
Scrape(ScrapeResponse), Scrape(ScrapeResponse),
Error(ErrorResponse), Error(ErrorResponse),
} }
impl Response {
#[inline]
pub fn write(&self, bytes: &mut impl Write) -> Result<(), io::Error> {
match self {
Response::Connect(r) => {
bytes.write_i32::<NetworkEndian>(0)?;
bytes.write_all(r.as_bytes())?;
}
Response::AnnounceIpv4(r) => {
bytes.write_i32::<NetworkEndian>(1)?;
bytes.write_all(r.fixed.as_bytes())?;
bytes.write_all((*r.peers.as_slice()).as_bytes())?;
}
Response::AnnounceIpv6(r) => {
bytes.write_i32::<NetworkEndian>(1)?;
bytes.write_all(r.fixed.as_bytes())?;
bytes.write_all((*r.peers.as_slice()).as_bytes())?;
}
Response::Scrape(r) => {
bytes.write_i32::<NetworkEndian>(2)?;
bytes.write_all(r.transaction_id.as_bytes())?;
bytes.write_all((*r.torrent_stats.as_slice()).as_bytes())?;
}
Response::Error(r) => {
bytes.write_i32::<NetworkEndian>(3)?;
bytes.write_all(r.transaction_id.as_bytes())?;
bytes.write_all(r.message.as_bytes())?;
}
}
Ok(())
}
#[inline]
pub fn from_bytes(mut bytes: &[u8], ipv4: bool) -> Result<Self, io::Error> {
let action = read_i32_ne(&mut bytes)?;
match action.get() {
// Connect
0 => Ok(Response::Connect(
ConnectResponse::read_from_prefix(bytes).ok_or_else(invalid_data)?,
)),
// Announce
1 if ipv4 => {
let fixed =
AnnounceResponseFixedData::read_from_prefix(bytes).ok_or_else(invalid_data)?;
let peers = if let Some(bytes) = bytes.get(size_of::<AnnounceResponseFixedData>()..)
{
Vec::from(
ResponsePeer::<Ipv4AddrBytes>::slice_from(bytes)
.ok_or_else(invalid_data)?,
)
} else {
Vec::new()
};
Ok(Response::AnnounceIpv4(AnnounceResponse { fixed, peers }))
}
1 if !ipv4 => {
let fixed =
AnnounceResponseFixedData::read_from_prefix(bytes).ok_or_else(invalid_data)?;
let peers = if let Some(bytes) = bytes.get(size_of::<AnnounceResponseFixedData>()..)
{
Vec::from(
ResponsePeer::<Ipv6AddrBytes>::slice_from(bytes)
.ok_or_else(invalid_data)?,
)
} else {
Vec::new()
};
Ok(Response::AnnounceIpv6(AnnounceResponse { fixed, peers }))
}
// Scrape
2 => {
let transaction_id = read_i32_ne(&mut bytes).map(TransactionId)?;
let torrent_stats =
Vec::from(TorrentScrapeStatistics::slice_from(bytes).ok_or_else(invalid_data)?);
Ok((ScrapeResponse {
transaction_id,
torrent_stats,
})
.into())
}
// Error
3 => {
let transaction_id = read_i32_ne(&mut bytes).map(TransactionId)?;
let message = String::from_utf8_lossy(&bytes).into_owned().into();
Ok((ErrorResponse {
transaction_id,
message,
})
.into())
}
_ => Err(invalid_data()),
}
}
}
impl From<ConnectResponse> for Response { impl From<ConnectResponse> for Response {
fn from(r: ConnectResponse) -> Self { fn from(r: ConnectResponse) -> Self {
Self::Connect(r) Self::Connect(r)
} }
} }
impl From<AnnounceResponse<Ipv4Addr>> for Response { impl From<AnnounceResponse<Ipv4AddrBytes>> for Response {
fn from(r: AnnounceResponse<Ipv4Addr>) -> Self { fn from(r: AnnounceResponse<Ipv4AddrBytes>) -> Self {
Self::AnnounceIpv4(r) Self::AnnounceIpv4(r)
} }
} }
impl From<AnnounceResponse<Ipv6Addr>> for Response { impl From<AnnounceResponse<Ipv6AddrBytes>> for Response {
fn from(r: AnnounceResponse<Ipv6Addr>) -> Self { fn from(r: AnnounceResponse<Ipv6AddrBytes>) -> Self {
Self::AnnounceIpv6(r) Self::AnnounceIpv6(r)
} }
} }
@ -80,203 +149,85 @@ impl From<ErrorResponse> for Response {
} }
} }
impl Response { #[derive(PartialEq, Eq, Clone, Debug, AsBytes, FromBytes, FromZeroes)]
#[inline] #[repr(C, packed)]
pub fn write(&self, bytes: &mut impl Write) -> Result<(), io::Error> { pub struct ConnectResponse {
match self { pub transaction_id: TransactionId,
Response::Connect(r) => { pub connection_id: ConnectionId,
bytes.write_i32::<NetworkEndian>(0)?; }
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
bytes.write_i64::<NetworkEndian>(r.connection_id.0)?;
}
Response::AnnounceIpv4(r) => {
bytes.write_i32::<NetworkEndian>(1)?;
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
bytes.write_i32::<NetworkEndian>(r.announce_interval.0)?;
bytes.write_i32::<NetworkEndian>(r.leechers.0)?;
bytes.write_i32::<NetworkEndian>(r.seeders.0)?;
for peer in r.peers.iter() { #[derive(PartialEq, Eq, Clone, Debug)]
bytes.write_all(&peer.ip_address.octets())?; pub struct AnnounceResponse<I: Ip> {
bytes.write_u16::<NetworkEndian>(peer.port.0)?; pub fixed: AnnounceResponseFixedData,
} pub peers: Vec<ResponsePeer<I>>,
} }
Response::AnnounceIpv6(r) => {
bytes.write_i32::<NetworkEndian>(1)?;
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
bytes.write_i32::<NetworkEndian>(r.announce_interval.0)?;
bytes.write_i32::<NetworkEndian>(r.leechers.0)?;
bytes.write_i32::<NetworkEndian>(r.seeders.0)?;
for peer in r.peers.iter() { #[derive(PartialEq, Eq, Clone, Debug, AsBytes, FromBytes, FromZeroes)]
bytes.write_all(&peer.ip_address.octets())?; #[repr(C, packed)]
bytes.write_u16::<NetworkEndian>(peer.port.0)?; pub struct AnnounceResponseFixedData {
} pub transaction_id: TransactionId,
} pub announce_interval: AnnounceInterval,
Response::Scrape(r) => { pub leechers: NumberOfPeers,
bytes.write_i32::<NetworkEndian>(2)?; pub seeders: NumberOfPeers,
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?; }
for torrent_stat in r.torrent_stats.iter() { #[derive(PartialEq, Eq, Clone, Debug)]
bytes.write_i32::<NetworkEndian>(torrent_stat.seeders.0)?; pub struct ScrapeResponse {
bytes.write_i32::<NetworkEndian>(torrent_stat.completed.0)?; pub transaction_id: TransactionId,
bytes.write_i32::<NetworkEndian>(torrent_stat.leechers.0)?; pub torrent_stats: Vec<TorrentScrapeStatistics>,
} }
}
Response::Error(r) => {
bytes.write_i32::<NetworkEndian>(3)?;
bytes.write_i32::<NetworkEndian>(r.transaction_id.0)?;
bytes.write_all(r.message.as_bytes())?; #[derive(PartialEq, Eq, Debug, Copy, Clone, AsBytes, FromBytes, FromZeroes)]
} #[repr(C, packed)]
} pub struct TorrentScrapeStatistics {
pub seeders: NumberOfPeers,
pub completed: NumberOfDownloads,
pub leechers: NumberOfPeers,
}
Ok(()) #[derive(PartialEq, Eq, Clone, Debug)]
} pub struct ErrorResponse {
pub transaction_id: TransactionId,
#[inline] pub message: Cow<'static, str>,
pub fn from_bytes(bytes: &[u8], ipv4: bool) -> Result<Self, io::Error> {
let mut cursor = Cursor::new(bytes);
let action = cursor.read_i32::<NetworkEndian>()?;
let transaction_id = cursor.read_i32::<NetworkEndian>()?;
match action {
// Connect
0 => {
let connection_id = cursor.read_i64::<NetworkEndian>()?;
Ok((ConnectResponse {
connection_id: ConnectionId(connection_id),
transaction_id: TransactionId(transaction_id),
})
.into())
}
// Announce
1 if ipv4 => {
let announce_interval = cursor.read_i32::<NetworkEndian>()?;
let leechers = cursor.read_i32::<NetworkEndian>()?;
let seeders = cursor.read_i32::<NetworkEndian>()?;
let position = cursor.position() as usize;
let inner = cursor.into_inner();
let peers = inner[position..]
.chunks_exact(6)
.map(|chunk| {
let ip_bytes: [u8; 4] = (&chunk[..4]).try_into().unwrap();
let ip_address = Ipv4Addr::from(ip_bytes);
let port = (&chunk[4..]).read_u16::<NetworkEndian>().unwrap();
ResponsePeer {
ip_address,
port: Port(port),
}
})
.collect();
Ok((AnnounceResponse {
transaction_id: TransactionId(transaction_id),
announce_interval: AnnounceInterval(announce_interval),
leechers: NumberOfPeers(leechers),
seeders: NumberOfPeers(seeders),
peers,
})
.into())
}
1 if !ipv4 => {
let announce_interval = cursor.read_i32::<NetworkEndian>()?;
let leechers = cursor.read_i32::<NetworkEndian>()?;
let seeders = cursor.read_i32::<NetworkEndian>()?;
let position = cursor.position() as usize;
let inner = cursor.into_inner();
let peers = inner[position..]
.chunks_exact(18)
.map(|chunk| {
let ip_bytes: [u8; 16] = (&chunk[..16]).try_into().unwrap();
let ip_address = Ipv6Addr::from(ip_bytes);
let port = (&chunk[16..]).read_u16::<NetworkEndian>().unwrap();
ResponsePeer {
ip_address,
port: Port(port),
}
})
.collect();
Ok((AnnounceResponse {
transaction_id: TransactionId(transaction_id),
announce_interval: AnnounceInterval(announce_interval),
leechers: NumberOfPeers(leechers),
seeders: NumberOfPeers(seeders),
peers,
})
.into())
}
// Scrape
2 => {
let position = cursor.position() as usize;
let inner = cursor.into_inner();
let stats = inner[position..]
.chunks_exact(12)
.map(|chunk| {
let mut cursor: Cursor<&[u8]> = Cursor::new(&chunk[..]);
let seeders = cursor.read_i32::<NetworkEndian>().unwrap();
let downloads = cursor.read_i32::<NetworkEndian>().unwrap();
let leechers = cursor.read_i32::<NetworkEndian>().unwrap();
TorrentScrapeStatistics {
seeders: NumberOfPeers(seeders),
completed: NumberOfDownloads(downloads),
leechers: NumberOfPeers(leechers),
}
})
.collect();
Ok((ScrapeResponse {
transaction_id: TransactionId(transaction_id),
torrent_stats: stats,
})
.into())
}
// Error
3 => {
let position = cursor.position() as usize;
let inner = cursor.into_inner();
Ok((ErrorResponse {
transaction_id: TransactionId(transaction_id),
message: String::from_utf8_lossy(&inner[position..])
.into_owned()
.into(),
})
.into())
}
_ => Ok((ErrorResponse {
transaction_id: TransactionId(transaction_id),
message: "Invalid action".into(),
})
.into()),
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use zerocopy::network_endian::I32;
use zerocopy::network_endian::I64;
use super::*; use super::*;
impl quickcheck::Arbitrary for Ipv4AddrBytes {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self([
u8::arbitrary(g),
u8::arbitrary(g),
u8::arbitrary(g),
u8::arbitrary(g),
])
}
}
impl quickcheck::Arbitrary for Ipv6AddrBytes {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut bytes = [0; 16];
for byte in bytes.iter_mut() {
*byte = u8::arbitrary(g)
}
Self(bytes)
}
}
impl quickcheck::Arbitrary for TorrentScrapeStatistics { impl quickcheck::Arbitrary for TorrentScrapeStatistics {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self { Self {
seeders: NumberOfPeers(i32::arbitrary(g)), seeders: NumberOfPeers(I32::new(i32::arbitrary(g))),
completed: NumberOfDownloads(i32::arbitrary(g)), completed: NumberOfDownloads(I32::new(i32::arbitrary(g))),
leechers: NumberOfPeers(i32::arbitrary(g)), leechers: NumberOfPeers(I32::new(i32::arbitrary(g))),
} }
} }
} }
@ -284,8 +235,8 @@ mod tests {
impl quickcheck::Arbitrary for ConnectResponse { impl quickcheck::Arbitrary for ConnectResponse {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self { Self {
connection_id: ConnectionId(i64::arbitrary(g)), connection_id: ConnectionId(I64::new(i64::arbitrary(g))),
transaction_id: TransactionId(i32::arbitrary(g)), transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
} }
} }
} }
@ -297,10 +248,12 @@ mod tests {
.collect(); .collect();
Self { Self {
transaction_id: TransactionId(i32::arbitrary(g)), fixed: AnnounceResponseFixedData {
announce_interval: AnnounceInterval(i32::arbitrary(g)), transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
leechers: NumberOfPeers(i32::arbitrary(g)), announce_interval: AnnounceInterval(I32::new(i32::arbitrary(g))),
seeders: NumberOfPeers(i32::arbitrary(g)), leechers: NumberOfPeers(I32::new(i32::arbitrary(g))),
seeders: NumberOfPeers(I32::new(i32::arbitrary(g))),
},
peers, peers,
} }
} }
@ -313,7 +266,7 @@ mod tests {
.collect(); .collect();
Self { Self {
transaction_id: TransactionId(i32::arbitrary(g)), transaction_id: TransactionId(I32::new(i32::arbitrary(g))),
torrent_stats, torrent_stats,
} }
} }
@ -328,7 +281,7 @@ mod tests {
let success = response == r2; let success = response == r2;
if !success { if !success {
println!("before: {:#?}\nafter: {:#?}", response, r2); ::pretty_assertions::assert_eq!(response, r2);
} }
success success
@ -340,12 +293,16 @@ mod tests {
} }
#[quickcheck] #[quickcheck]
fn test_announce_response_ipv4_convert_identity(response: AnnounceResponse<Ipv4Addr>) -> bool { fn test_announce_response_ipv4_convert_identity(
response: AnnounceResponse<Ipv4AddrBytes>,
) -> bool {
same_after_conversion(response.into(), true) same_after_conversion(response.into(), true)
} }
#[quickcheck] #[quickcheck]
fn test_announce_response_ipv6_convert_identity(response: AnnounceResponse<Ipv6Addr>) -> bool { fn test_announce_response_ipv6_convert_identity(
response: AnnounceResponse<Ipv6AddrBytes>,
) -> bool {
same_after_conversion(response.into(), false) same_after_conversion(response.into(), false)
} }

18
docker/ci.Dockerfile Normal file
View file

@ -0,0 +1,18 @@
# Can be used to run file transfer CI test locally. Usage:
# 1. docker build -t aquatic -f ./docker/ci.Dockerfile .
# 2. docker run aquatic
# 3. On failure, run `docker rmi aquatic -f` and go back to step 1
FROM rust:bullseye
RUN mkdir "/opt/aquatic"
ENV "GITHUB_WORKSPACE" "/opt/aquatic"
WORKDIR "/opt/aquatic"
COPY ./.github/actions/test-file-transfers/entrypoint.sh entrypoint.sh
COPY Cargo.toml Cargo.lock ./
COPY crates crates
ENTRYPOINT ["./entrypoint.sh"]