1
//! Utility to return a random hostname.
2

            
3
use crate::RngExt as _;
4
use rand::{Rng, seq::IndexedRandom as _};
5

            
6
/// The prefixes we put at the front of every random hostname, with terminating `.`.
7
const PREFIXES: &[&str] = &["www."];
8

            
9
/// The suffixes that we use when picking a random hostname, with preceding `.`.
10
const 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).
16
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
17

            
18
/// Lowest permissible hostname length.
19
const MIN_LEN: usize = 16;
20
/// Highest permissible hostname length.
21
const MAX_LEN: usize = 32;
22

            
23
/// Return a somewhat random-looking hostname.
24
///
25
/// The specific format of the hostname is not guaranteed.
26
210
pub 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
210
    let prefix = PREFIXES.choose(rng).expect("TLDS was empty!?");
30
210
    let suffix = SUFFIXES.choose(rng).expect("TLDS was empty!?");
31

            
32
210
    let length: usize = rng
33
210
        .gen_range_checked(MIN_LEN..=MAX_LEN)
34
210
        .expect("Somehow MIN..=MAX wasn't a valid range?");
35
210
    let center_length = length
36
210
        .checked_sub(prefix.len() + suffix.len())
37
210
        .expect("prefix and suffix exceeded MIN_LEN");
38

            
39
210
    let mut output = String::from(*prefix);
40
3512
    for _ in 0..center_length {
41
3512
        output.push(*CHARSET.choose(rng).expect("CHARSET was empty!?") as char);
42
3512
    }
43
210
    output.push_str(suffix);
44

            
45
210
    assert_eq!(length, output.len());
46
210
    output
47
210
}
48

            
49
#[cfg(test)]
50
mod 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
}