fake_torrent_client/
lib.rs

1use url::form_urlencoded::byte_serialize;
2
3mod algorithm;
4pub mod clients;
5
6const PEER_ID_LENGTH: usize = 20;
7const KEY_LENGTH: usize = 8;
8
9#[derive(Debug, Clone)]
10pub enum RefreshInterval {
11    Never,
12    TimedOrAfterStartedAnnounce,
13    TorrentVolatile,
14    TorrentPersistent,
15}
16
17#[derive(Debug, Clone)]
18pub struct Client {
19    pub name: String,
20    pub key: u32,
21    pub peer_id: String,
22    pub key_refresh_every: Option<u16>,
23    pub query: String,
24    //request_headers: HashMap<String, String>, //HashMap<&str, i32> = [("Norway", 100), ("Denmark", 50), ("Iceland", 10)]
25    pub user_agent: String,
26    pub accept: String,
27    pub accept_encoding: String,
28    pub accept_language: String, //for some version of µTorrent
29    pub connection: Option<String>,
30    /// Optional. Number of peers that the client would like to receive from the tracker. This value is permitted to be zero. If omitted, typically defaults to 50 peers.
31    pub num_want: u16,
32    pub num_want_on_stop: u16,
33
34    //Client configuration
35    //----------- algorithms
36    key_algorithm: algorithm::Algorithm, //length=8
37    //key_length: u8, //key algorithm, key length is always 8
38    key_pattern: String,
39    key_refresh_on: RefreshInterval,
40    key_uppercase: Option<bool>,
41    peer_url_encode: bool,
42    //----------- peer ID
43    peer_algorithm: algorithm::Algorithm,
44    ///for REGEX method, for RANDOM_POOL_WITH_CHECKSUM: list pf available chars, the base is the length of the string
45    peer_pattern: String,
46    /// for RANDOM_POOL_WITH_CHECKSUM
47    peer_prefix: String,
48    peer_refresh_on: RefreshInterval,
49    //----------- URL encoder
50    encoding_exclusion_pattern: String,
51    /// if the encoded hex string should be in upper case or no
52    uppercase_encoded_hex: bool,
53}
54
55impl Default for Client {
56    fn default() -> Self {
57        Client {
58        //client configuration
59        //key generator default values
60        key_algorithm: algorithm::Algorithm::Hash,
61        key_pattern:String::new(),
62        key_uppercase: None,
63        key_refresh_on: RefreshInterval::TimedOrAfterStartedAnnounce,
64        key_refresh_every: None,
65        //peer ID generator
66        peer_algorithm: algorithm::Algorithm::Regex,
67        peer_pattern: String::new(), peer_prefix:String::new(),
68        peer_refresh_on: RefreshInterval::Never,
69        peer_url_encode: false,
70        //URL encoder
71        encoding_exclusion_pattern: r"[A-Za-z0-9-]".to_owned(),
72        uppercase_encoded_hex: false,
73        //misc
74        num_want: 200,
75        num_want_on_stop: 0,
76        //query headers
77        query: "info_hash={infohash}&peer_id={peerid}&port={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&key={key}&event={event}&numwant={numwant}&compact=1&no_peer_id=1".to_owned(),
78        user_agent: String::with_capacity(64), //must be defined
79        accept: String::new(),
80        accept_encoding: String::from("gzip"),
81        accept_language: String::with_capacity(5),
82        connection: Some(String::from("Close")),
83        key: 0,
84        peer_id: String::new(),
85        name: String::from("INVALID"),
86    }
87    }
88}
89
90impl Client {
91    pub fn new() -> Client {
92        Client::default()
93    }
94
95    /// Returns the query to append to your announce URL. Variables are:
96    /// * `{infohash}`:
97    /// * `{peerid}`:
98    /// * `{port}`: torrent port
99    /// * `{uploaded}`: uploaded data in bytes
100    /// * `{downloaded}`: downloaded data in bytes
101    /// * `{left}`: remaining data to download in bytes
102    /// * `{key}`:
103    /// * `{event}`:
104    /// * `{numwant}`:
105    /// * `{os}` and `{java}` for Vuze
106    ///
107    /// Returns: (URL, Vec<(Header name, Header value)>)
108    pub fn get_query(&self) -> (String, Vec<(String, String)>) {
109        let mut headers: Vec<(String, String)> = Vec::with_capacity(4);
110        if !self.user_agent.is_empty() {
111            headers.push((String::from("User-Agent"), self.user_agent.clone()));
112        }
113        if !self.accept.is_empty() {
114            headers.push((String::from("Accept"), self.accept.clone()));
115        }
116        if !self.accept_encoding.is_empty() {
117            headers.push((
118                String::from("Accept-Encoding"),
119                self.accept_encoding.clone(),
120            ));
121        }
122        if !self.accept_language.is_empty() {
123            headers.push((
124                String::from("Accept-Language"),
125                self.accept_language.clone(),
126            ));
127        }
128        (self.query.clone(), headers)
129    }
130
131    /// Generate the client key, and encode it for HTTP request
132    pub fn generate_key(&mut self) {
133        let key = match &self.key_algorithm {
134            algorithm::Algorithm::Hash => algorithm::hash(false, self.key_uppercase),
135            algorithm::Algorithm::HashNoLeadingZero => algorithm::hash(true, self.key_uppercase),
136            algorithm::Algorithm::DigitRangeTransformedToHexWithoutLeadingZeroes => {
137                algorithm::digit_range_transformed_to_hex_without_leading_zero()
138            }
139            algorithm::Algorithm::Regex => byte_serialize(
140                &algorithm::regex(self.peer_pattern.clone()).as_bytes()[0..KEY_LENGTH],
141            )
142            .collect(),
143            _ => String::with_capacity(KEY_LENGTH),
144        };
145        if let Ok(key) = key.parse::<u32>() {
146            self.key = key;
147        }
148    }
149    /// Generate the peer ID and encode it for HTTP request
150    pub fn generate_peer_id(&mut self) {
151        let hash = match &self.peer_algorithm {
152            algorithm::Algorithm::Regex => algorithm::regex(self.peer_pattern.clone()), //replace \ otherwise the generator crashes
153            algorithm::Algorithm::RandomPoolWithChecksum => {
154                algorithm::random_pool_with_checksum(&self.peer_prefix, &self.peer_pattern)
155            }
156            _ => String::new(),
157        };
158        self.peer_id = byte_serialize(&hash.as_bytes()[0..PEER_ID_LENGTH]).collect();
159        //take the first 20 charsencode it because weird chars
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use crate::{clients::ClientVersion, Client};
166
167    const CLIENT_VERSIONS: [ClientVersion; 62] = [
168        ClientVersion::Bittorrent_7_10_1_43917,
169        ClientVersion::Bittorrent_7_10_3_44359,
170        ClientVersion::Bittorrent_7_10_3_44429,
171        ClientVersion::Deluge_1_3_13,
172        ClientVersion::Deluge_1_3_14,
173        ClientVersion::Deluge_1_3_15,
174        ClientVersion::Deluge_2_0_3,
175        ClientVersion::Leap_2_6_0_1,
176        ClientVersion::Rtorrent_0_9_6_0_13_6,
177        ClientVersion::Transmission_2_82_14160,
178        ClientVersion::Transmission_2_92_14714,
179        ClientVersion::Transmission_2_93,
180        ClientVersion::Transmission_2_94,
181        ClientVersion::Transmission_3_00,
182        ClientVersion::Utorrent_3_2_2_28500,
183        ClientVersion::Utorrent_3_5_0_43916,
184        ClientVersion::Utorrent_3_5_0_44090,
185        ClientVersion::Utorrent_3_5_0_44294,
186        ClientVersion::Utorrent_3_5_1_44332,
187        ClientVersion::Utorrent_3_5_3_44358,
188        ClientVersion::Utorrent_3_5_3_44428,
189        ClientVersion::Utorrent_3_5_4_44498,
190        ClientVersion::Vuze_5_7_5_0,
191        //QBittorrent
192        ClientVersion::Qbittorrent_3_3_1,
193        ClientVersion::Qbittorrent_3_3_13,
194        ClientVersion::Qbittorrent_3_3_14,
195        ClientVersion::Qbittorrent_3_3_15,
196        ClientVersion::Qbittorrent_3_3_16,
197        ClientVersion::Qbittorrent_3_3_7,
198        ClientVersion::Qbittorrent_4_0_0,
199        ClientVersion::Qbittorrent_4_0_1,
200        ClientVersion::Qbittorrent_4_0_2,
201        ClientVersion::Qbittorrent_4_0_3,
202        ClientVersion::Qbittorrent_4_0_4,
203        ClientVersion::Qbittorrent_4_1_0,
204        ClientVersion::Qbittorrent_4_1_1,
205        ClientVersion::Qbittorrent_4_1_2,
206        ClientVersion::Qbittorrent_4_1_3,
207        ClientVersion::Qbittorrent_4_1_4,
208        ClientVersion::Qbittorrent_4_1_5,
209        ClientVersion::Qbittorrent_4_1_6,
210        ClientVersion::Qbittorrent_4_1_7,
211        ClientVersion::Qbittorrent_4_1_8,
212        ClientVersion::Qbittorrent_4_1_9,
213        ClientVersion::Qbittorrent_4_2_0,
214        ClientVersion::Qbittorrent_4_2_1,
215        ClientVersion::Qbittorrent_4_2_2,
216        ClientVersion::Qbittorrent_4_2_3,
217        ClientVersion::Qbittorrent_4_2_4,
218        ClientVersion::Qbittorrent_4_2_5,
219        ClientVersion::Qbittorrent_4_3_0_1,
220        ClientVersion::Qbittorrent_4_3_0,
221        ClientVersion::Qbittorrent_4_3_1,
222        ClientVersion::Qbittorrent_4_3_2,
223        ClientVersion::Qbittorrent_4_3_3,
224        ClientVersion::Qbittorrent_4_3_4_1,
225        ClientVersion::Qbittorrent_4_3_5,
226        ClientVersion::Qbittorrent_4_3_6,
227        ClientVersion::Qbittorrent_4_3_8,
228        ClientVersion::Qbittorrent_4_3_9,
229        ClientVersion::Qbittorrent_4_4_2,
230        ClientVersion::Qbittorrent_4_4_3_1,
231    ];
232
233    #[test]
234    fn check_queries() {
235        for cv in crate::tests::CLIENT_VERSIONS {
236            let mut c = Client::new();
237            c.build(cv);
238            // println!("CLient: {} {}", c.name, c.peer_pattern);
239            let q = c.query;
240            assert!(q.contains("info_hash={infohash}"));
241            assert!(q.contains("peer_id={peerid}"));
242            assert!(q.contains("uploaded={uploaded}"));
243            assert!(q.contains("downloaded={downloaded}"));
244            assert!(q.contains("left={left}"));
245            assert!(q.contains("key={key}"));
246            assert!(q.contains("event={event}"));
247            if !c.name.starts_with("rtorrent") {
248                assert!(q.contains("numwant={numwant}"));
249            }
250            if q.contains("ipv6=") || q.contains("{ipv6}") {
251                assert!(q.contains("ipv6={ipv6}"));
252            }
253            if q.contains("ip=") || q.contains("{ip}") {
254                assert!(q.contains("ip={ip}"));
255            }
256            assert!(!q.contains("&&"));
257            assert!(!q.starts_with('&'));
258            assert!(!q.ends_with('&'));
259        }
260    }
261}