torrust_tracker/servers/http/v1/responses/
announce.rs

1//! `Announce` response for the HTTP tracker [`announce`](crate::servers::http::v1::requests::announce::Announce) request.
2//!
3//! Data structures and logic to build the `announce` response.
4use std::io::Write;
5use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
6
7use axum::http::StatusCode;
8use derive_more::{AsRef, Constructor, From};
9use torrust_tracker_contrib_bencode::{ben_bytes, ben_int, ben_list, ben_map, BMutAccess, BencodeMut};
10use torrust_tracker_primitives::peer;
11
12use super::Response;
13use crate::core::AnnounceData;
14use crate::servers::http::v1::responses;
15
16/// An [`Announce`] response, that can be anything that is convertible from [`AnnounceData`].
17///
18/// The [`Announce`] can built from any data that implements: [`From<AnnounceData>`] and [`Into<Vec<u8>>`].
19///
20/// The two standard forms of an announce response are: [`Normal`] and [`Compact`].
21///
22///
23/// _"To reduce the size of tracker responses and to reduce memory and
24/// computational requirements in trackers, trackers may return peers as a
25/// packed string rather than as a bencoded list."_
26///
27/// Refer to the official BEPs for more information:
28///
29/// - [BEP 03: The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
30/// - [BEP 23: Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html)
31/// - [BEP 07: IPv6 Tracker Extension](https://www.bittorrent.org/beps/bep_0007.html)
32
33#[derive(Debug, AsRef, PartialEq, Constructor)]
34pub struct Announce<E>
35where
36    E: From<AnnounceData> + Into<Vec<u8>>,
37{
38    data: E,
39}
40
41/// Build any [`Announce`] from an [`AnnounceData`].
42impl<E: From<AnnounceData> + Into<Vec<u8>>> From<AnnounceData> for Announce<E> {
43    fn from(data: AnnounceData) -> Self {
44        Self::new(data.into())
45    }
46}
47
48/// Convert any Announce [`Announce`] into a [`axum::response::Response`]
49impl<E: From<AnnounceData> + Into<Vec<u8>>> axum::response::IntoResponse for Announce<E>
50where
51    Announce<E>: Response,
52{
53    fn into_response(self) -> axum::response::Response {
54        axum::response::IntoResponse::into_response(self.body().map(|bytes| (StatusCode::OK, bytes)))
55    }
56}
57
58/// Implement the [`Response`] for the [`Announce`].
59///
60impl<E: From<AnnounceData> + Into<Vec<u8>>> Response for Announce<E> {
61    fn body(self) -> Result<Vec<u8>, responses::error::Error> {
62        Ok(self.data.into())
63    }
64}
65
66/// Format of the [`Normal`] (Non-Compact) Encoding
67pub struct Normal {
68    complete: i64,
69    incomplete: i64,
70    interval: i64,
71    min_interval: i64,
72    peers: Vec<NormalPeer>,
73}
74
75impl From<AnnounceData> for Normal {
76    fn from(data: AnnounceData) -> Self {
77        Self {
78            complete: data.stats.complete.into(),
79            incomplete: data.stats.incomplete.into(),
80            interval: data.policy.interval.into(),
81            min_interval: data.policy.interval_min.into(),
82            peers: data.peers.iter().map(AsRef::as_ref).copied().collect(),
83        }
84    }
85}
86
87#[allow(clippy::from_over_into)]
88impl Into<Vec<u8>> for Normal {
89    fn into(self) -> Vec<u8> {
90        let mut peers_list = ben_list!();
91        let peers_list_mut = peers_list.list_mut().unwrap();
92        for peer in &self.peers {
93            peers_list_mut.push(peer.into());
94        }
95
96        (ben_map! {
97            "complete" => ben_int!(self.complete),
98            "incomplete" => ben_int!(self.incomplete),
99            "interval" => ben_int!(self.interval),
100            "min interval" => ben_int!(self.min_interval),
101            "peers" => peers_list.clone()
102        })
103        .encode()
104    }
105}
106
107/// Format of the [`Compact`] Encoding
108pub struct Compact {
109    complete: i64,
110    incomplete: i64,
111    interval: i64,
112    min_interval: i64,
113    peers: Vec<u8>,
114    peers6: Vec<u8>,
115}
116
117impl From<AnnounceData> for Compact {
118    fn from(data: AnnounceData) -> Self {
119        let compact_peers: Vec<CompactPeer> = data.peers.iter().map(AsRef::as_ref).copied().collect();
120
121        let (peers, peers6): (Vec<CompactPeerData<Ipv4Addr>>, Vec<CompactPeerData<Ipv6Addr>>) =
122            compact_peers.into_iter().collect();
123
124        let peers_encoded: CompactPeersEncoded = peers.into_iter().collect();
125        let peers_encoded_6: CompactPeersEncoded = peers6.into_iter().collect();
126
127        Self {
128            complete: data.stats.complete.into(),
129            incomplete: data.stats.incomplete.into(),
130            interval: data.policy.interval.into(),
131            min_interval: data.policy.interval_min.into(),
132            peers: peers_encoded.0,
133            peers6: peers_encoded_6.0,
134        }
135    }
136}
137
138#[allow(clippy::from_over_into)]
139impl Into<Vec<u8>> for Compact {
140    fn into(self) -> Vec<u8> {
141        (ben_map! {
142            "complete" => ben_int!(self.complete),
143            "incomplete" => ben_int!(self.incomplete),
144            "interval" => ben_int!(self.interval),
145            "min interval" => ben_int!(self.min_interval),
146            "peers" => ben_bytes!(self.peers),
147            "peers6" => ben_bytes!(self.peers6)
148        })
149        .encode()
150    }
151}
152
153/// A [`NormalPeer`], for the [`Normal`] form.
154///
155/// ```rust
156/// use std::net::{IpAddr, Ipv4Addr};
157/// use torrust_tracker::servers::http::v1::responses::announce::{Normal, NormalPeer};
158///
159/// let peer = NormalPeer {
160///     peer_id: *b"-qB00000000000000001",
161///     ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105
162///     port: 0x7070,                                          // 28784
163/// };
164///
165///  ```
166#[derive(Debug, PartialEq)]
167pub struct NormalPeer {
168    /// The peer's ID.
169    pub peer_id: [u8; 20],
170    /// The peer's IP address.
171    pub ip: IpAddr,
172    /// The peer's port number.
173    pub port: u16,
174}
175
176impl peer::Encoding for NormalPeer {}
177
178impl From<peer::Peer> for NormalPeer {
179    fn from(peer: peer::Peer) -> Self {
180        NormalPeer {
181            peer_id: peer.peer_id.0,
182            ip: peer.peer_addr.ip(),
183            port: peer.peer_addr.port(),
184        }
185    }
186}
187
188impl From<&NormalPeer> for BencodeMut<'_> {
189    fn from(value: &NormalPeer) -> Self {
190        ben_map! {
191            "peer id" => ben_bytes!(value.peer_id.clone().to_vec()),
192            "ip" => ben_bytes!(value.ip.to_string()),
193            "port" => ben_int!(i64::from(value.port))
194        }
195    }
196}
197
198/// A [`CompactPeer`], for the [`Compact`] form.
199///
200///  _"To reduce the size of tracker responses and to reduce memory and
201/// computational requirements in trackers, trackers may return peers as a
202/// packed string rather than as a bencoded list."_
203///
204/// A part from reducing the size of the response, this format does not contain
205/// the peer's ID.
206///
207/// ```rust
208///  use std::net::{IpAddr, Ipv4Addr};
209///  use torrust_tracker::servers::http::v1::responses::announce::{Compact, CompactPeer, CompactPeerData};
210///
211///  let peer = CompactPeer::V4(CompactPeerData {
212///     ip: Ipv4Addr::new(0x69, 0x69, 0x69, 0x69), // 105.105.105.105
213///     port: 0x7070, // 28784
214/// });
215///
216///  ```
217///
218/// Refer to [BEP 23: Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html)
219/// for more information.
220#[derive(Clone, Debug, PartialEq)]
221pub enum CompactPeer {
222    /// The peer's IP address.
223    V4(CompactPeerData<Ipv4Addr>),
224    /// The peer's port number.
225    V6(CompactPeerData<Ipv6Addr>),
226}
227
228impl peer::Encoding for CompactPeer {}
229
230impl From<peer::Peer> for CompactPeer {
231    fn from(peer: peer::Peer) -> Self {
232        match (peer.peer_addr.ip(), peer.peer_addr.port()) {
233            (IpAddr::V4(ip), port) => Self::V4(CompactPeerData { ip, port }),
234            (IpAddr::V6(ip), port) => Self::V6(CompactPeerData { ip, port }),
235        }
236    }
237}
238
239/// The [`CompactPeerData`], that made with either a [`Ipv4Addr`], or [`Ipv6Addr`] along with a `port`.
240///
241#[derive(Clone, Debug, PartialEq)]
242pub struct CompactPeerData<V> {
243    /// The peer's IP address.
244    pub ip: V,
245    /// The peer's port number.
246    pub port: u16,
247}
248
249impl FromIterator<CompactPeer> for (Vec<CompactPeerData<Ipv4Addr>>, Vec<CompactPeerData<Ipv6Addr>>) {
250    fn from_iter<T: IntoIterator<Item = CompactPeer>>(iter: T) -> Self {
251        let mut peers_v4: Vec<CompactPeerData<Ipv4Addr>> = vec![];
252        let mut peers_v6: Vec<CompactPeerData<Ipv6Addr>> = vec![];
253
254        for peer in iter {
255            match peer {
256                CompactPeer::V4(peer) => peers_v4.push(peer),
257                CompactPeer::V6(peer6) => peers_v6.push(peer6),
258            }
259        }
260
261        (peers_v4, peers_v6)
262    }
263}
264
265#[derive(From, PartialEq)]
266struct CompactPeersEncoded(Vec<u8>);
267
268impl FromIterator<CompactPeerData<Ipv4Addr>> for CompactPeersEncoded {
269    fn from_iter<T: IntoIterator<Item = CompactPeerData<Ipv4Addr>>>(iter: T) -> Self {
270        let mut bytes: Vec<u8> = vec![];
271
272        for peer in iter {
273            bytes
274                .write_all(&u32::from(peer.ip).to_be_bytes())
275                .expect("it should write peer ip");
276            bytes.write_all(&peer.port.to_be_bytes()).expect("it should write peer port");
277        }
278
279        bytes.into()
280    }
281}
282
283impl FromIterator<CompactPeerData<Ipv6Addr>> for CompactPeersEncoded {
284    fn from_iter<T: IntoIterator<Item = CompactPeerData<Ipv6Addr>>>(iter: T) -> Self {
285        let mut bytes: Vec<u8> = Vec::new();
286
287        for peer in iter {
288            bytes
289                .write_all(&u128::from(peer.ip).to_be_bytes())
290                .expect("it should write peer ip");
291            bytes.write_all(&peer.port.to_be_bytes()).expect("it should write peer port");
292        }
293        bytes.into()
294    }
295}
296
297#[cfg(test)]
298mod tests {
299
300    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
301    use std::sync::Arc;
302
303    use aquatic_udp_protocol::PeerId;
304    use torrust_tracker_configuration::AnnouncePolicy;
305    use torrust_tracker_primitives::peer::fixture::PeerBuilder;
306    use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
307
308    use crate::core::AnnounceData;
309    use crate::servers::http::v1::responses::announce::{Announce, Compact, Normal, Response};
310
311    // Some ascii values used in tests:
312    //
313    // +-----------------+
314    // | Dec | Hex | Chr |
315    // +-----------------+
316    // | 105 | 69  | i   |
317    // | 112 | 70  | p   |
318    // +-----------------+
319    //
320    // IP addresses and port numbers used in tests are chosen so that their bencoded representation
321    // is also a valid string which makes asserts more readable.
322
323    fn setup_announce_data() -> AnnounceData {
324        let policy = AnnouncePolicy::new(111, 222);
325
326        let peer_ipv4 = PeerBuilder::default()
327            .with_peer_id(&PeerId(*b"-qB00000000000000001"))
328            .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 0x7070))
329            .build();
330
331        let peer_ipv6 = PeerBuilder::default()
332            .with_peer_id(&PeerId(*b"-qB00000000000000002"))
333            .with_peer_addr(&SocketAddr::new(
334                IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)),
335                0x7070,
336            ))
337            .build();
338
339        let peers = vec![Arc::new(peer_ipv4), Arc::new(peer_ipv6)];
340        let stats = SwarmMetadata::new(333, 333, 444);
341
342        AnnounceData::new(peers, stats, policy)
343    }
344
345    #[test]
346    fn non_compact_announce_response_can_be_bencoded() {
347        let response: Announce<Normal> = setup_announce_data().into();
348        let bytes = response.body().expect("it should encode the response");
349
350        // cspell:disable-next-line
351        let expected_bytes = b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer id20:-qB000000000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer id20:-qB000000000000000024:porti28784eeee";
352
353        assert_eq!(
354            String::from_utf8(bytes).unwrap(),
355            String::from_utf8(expected_bytes.to_vec()).unwrap()
356        );
357    }
358
359    #[test]
360    fn compact_announce_response_can_be_bencoded() {
361        let response: Announce<Compact> = setup_announce_data().into();
362        let bytes = response.body().expect("it should encode the response");
363
364        let expected_bytes =
365            // cspell:disable-next-line
366            b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peers6:iiiipp6:peers618:iiiiiiiiiiiiiiiippe";
367
368        assert_eq!(
369            String::from_utf8(bytes).unwrap(),
370            String::from_utf8(expected_bytes.to_vec()).unwrap()
371        );
372    }
373}