use rand::rngs::StdRng;
use rand::RngExt;
const WORDS: &[&str] = &[
"wrapper",
"inner",
"content",
"item",
"node",
"list",
"entry",
"field",
"block",
"panel",
"row",
"col",
"data",
"meta",
"label",
"value",
"group",
"section",
"module",
"unit",
"container",
"holder",
"box",
"cell",
];
const PATHS: &[&str] = &[
"track", "redirect", "collect", "api", "internal", "gateway", "sink", "trap", "beacon", "pixel", "log", "event",
"session", "ref",
];
pub struct Honeypots {
marker: String,
}
impl Honeypots {
pub fn new(rng: &mut StdRng) -> Self {
let mut marker = String::from("data-");
for _ in 0..6 {
marker.push((b'a' + rng.random_range(0..26)) as char);
}
Self { marker }
}
pub fn generate(&self, count: usize, rng: &mut StdRng) -> String {
let mut out = String::with_capacity(count * 80);
for _ in 0..count {
match rng.random_range(0u8..3) {
0 => self.decoy_link(&mut out, rng),
1 => self.decoy_field(&mut out, rng),
_ => self.decoy_block(&mut out, rng),
}
}
out
}
pub fn removal_script(&self) -> String {
format!(
"<script>document.querySelectorAll('[{m}]').forEach(function(e){{e.remove();}});</script>",
m = self.marker
)
}
fn hidden(&self) -> String {
format!(
r#"style="display:none!important" aria-hidden="true" tabindex="-1" {}"#,
self.marker
)
}
fn decoy_link(&self, out: &mut String, rng: &mut StdRng) {
let path = PATHS[rng.random_range(0..PATHS.len())];
let token = rand_token(rng, 8);
let text = format!("{} {}", word(rng), word(rng));
out.push_str(&format!(
r#"<a href="/{path}/{token}" {h} rel="nofollow">{text}</a>"#,
h = self.hidden(),
));
}
fn decoy_field(&self, out: &mut String, rng: &mut StdRng) {
let name = format!("{}_{}", word(rng), word(rng));
out.push_str(&format!(
r#"<input type="text" name="{name}" {h} autocomplete="off" value="">"#,
h = self.hidden(),
));
}
fn decoy_block(&self, out: &mut String, rng: &mut StdRng) {
let cls = format!("{}-{}-{}", word(rng), word(rng), rand_token(rng, 3));
let val_len = rng.random_range(6..=14);
let val = rand_token(rng, val_len);
out.push_str(&format!(
r#"<div class="{cls}" {h}><span class="{c2}">{val}</span></div>"#,
h = self.hidden(),
c2 = word(rng),
));
}
}
fn word(rng: &mut StdRng) -> &'static str {
WORDS[rng.random_range(0..WORDS.len())]
}
fn rand_token(rng: &mut StdRng, len: usize) -> String {
const CH: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
(0..len).map(|_| CH[rng.random_range(0..CH.len())] as char).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
#[test]
fn generates_requested_count_of_nodes() {
let mut rng = StdRng::seed_from_u64(7);
let hp = Honeypots::new(&mut rng);
let frag = hp.generate(5, &mut rng);
assert_eq!(frag.matches("aria-hidden=\"true\"").count(), 5);
}
#[test]
fn decoys_are_hidden_and_inert() {
let mut rng = StdRng::seed_from_u64(1);
let hp = Honeypots::new(&mut rng);
let frag = hp.generate(10, &mut rng);
assert!(frag.contains("display:none"));
assert!(frag.contains("tabindex=\"-1\""));
}
#[test]
fn decoys_and_removal_share_marker() {
let mut rng = StdRng::seed_from_u64(3);
let hp = Honeypots::new(&mut rng);
let frag = hp.generate(5, &mut rng);
let script = hp.removal_script();
assert!(frag.contains(&hp.marker), "decoys must carry the marker");
assert!(script.contains(&hp.marker), "removal script must target the marker");
assert!(script.contains("e.remove()"));
}
#[test]
fn deterministic_with_seed() {
let mut a = StdRng::seed_from_u64(42);
let mut b = StdRng::seed_from_u64(42);
let ha = Honeypots::new(&mut a);
let hb = Honeypots::new(&mut b);
assert_eq!(ha.generate(4, &mut a), hb.generate(4, &mut b));
}
}