Skip to main content

tor_basic_utils/
rand_hostname.rs

1//! Utility to return a random hostname.
2
3use crate::RngExt as _;
4use rand::{Rng, seq::IndexedRandom as _};
5
6/// The prefixes we put at the front of every random hostname, with terminating `.`.
7const PREFIXES: &[&str] = &["www."];
8
9/// The suffixes that we use when picking a random hostname, with preceding `.`.
10const SUFFIXES: &[&str] = &[".com", ".net", ".org"];
11
12/// The characters that we use for the middle part of a hostname.
13// NOTE: Some characters have restrictions.
14// For example `-` isn't allowed as the first or last character of a subdomain,
15// so we don't include it here (see arti#2597).
16const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
17
18/// Lowest permissible hostname length.
19const MIN_LEN: usize = 16;
20/// Highest permissible hostname length.
21const MAX_LEN: usize = 32;
22
23/// Return a somewhat random-looking hostname.
24///
25/// The specific format of the hostname is not guaranteed.
26pub fn random_hostname<R: Rng>(rng: &mut R) -> String {
27    // TODO: This is, roughly, what C tor does.
28    // But that doesn't mean it's remotely clever.
29    let prefix = PREFIXES.choose(rng).expect("TLDS was empty!?");
30    let suffix = SUFFIXES.choose(rng).expect("TLDS was empty!?");
31
32    let length: usize = rng
33        .gen_range_checked(MIN_LEN..=MAX_LEN)
34        .expect("Somehow MIN..=MAX wasn't a valid range?");
35    let center_length = length
36        .checked_sub(prefix.len() + suffix.len())
37        .expect("prefix and suffix exceeded MIN_LEN");
38
39    let mut output = String::from(*prefix);
40    for _ in 0..center_length {
41        output.push(*CHARSET.choose(rng).expect("CHARSET was empty!?") as char);
42    }
43    output.push_str(suffix);
44
45    assert_eq!(length, output.len());
46    output
47}
48
49#[cfg(test)]
50mod test {
51    // @@ begin test lint list maintained by maint/add_warning @@
52    #![allow(clippy::bool_assert_comparison)]
53    #![allow(clippy::clone_on_copy)]
54    #![allow(clippy::dbg_macro)]
55    #![allow(clippy::mixed_attributes_style)]
56    #![allow(clippy::print_stderr)]
57    #![allow(clippy::print_stdout)]
58    #![allow(clippy::single_char_pattern)]
59    #![allow(clippy::unwrap_used)]
60    #![allow(clippy::unchecked_time_subtraction)]
61    #![allow(clippy::useless_vec)]
62    #![allow(clippy::needless_pass_by_value)]
63    #![allow(clippy::string_slice)] // See arti#2571
64    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
65
66    use super::*;
67    use crate::test_rng::testing_rng;
68
69    #[test]
70    fn generate_names() {
71        let mut rng = testing_rng();
72
73        for _ in 0..100 {
74            let name = random_hostname(&mut rng);
75            assert!(PREFIXES.iter().any(|tld| name.starts_with(tld)));
76            assert!(SUFFIXES.iter().any(|tld| name.ends_with(tld)));
77            assert!(name.len() >= MIN_LEN);
78            assert!(name.len() <= MAX_LEN);
79            for ch in name.chars() {
80                assert!(matches!(ch, '.' | '0'..='9' | 'a'..='z'));
81            }
82        }
83    }
84}