use std::collections::HashSet; use std::future::Future; use std::net::SocketAddr; use std::ops::RangeInclusive; use std::pin::Pin; use std::process::ExitCode; use std::sync::Arc; use clap::Parser; use http_body_util::Full; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::Service; use hyper::{Request, Response, StatusCode}; use hyper_util::rt::TokioIo; use rand::prelude::*; use rand_pcg::Pcg64; use rand_seeder::Seeder; use tokio::net::TcpListener; /// Endless honeypot for webcrawlers #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// seed for deterministic page generation seed: String, /// port for serving http traffic #[arg(short, long, default_value_t = 3200)] port: u16, /// delay all responses (in milliseconds) #[arg(short, long, default_value_t = 0)] delay: u32, } #[tokio::main] async fn main() -> Result> { let args = Args::parse(); tracing_subscriber::fmt::fmt().init(); let svc = RandomPageService::new(&args); let addr = SocketAddr::from(([127, 0, 0, 1], args.port)); let listener = TcpListener::bind(addr).await?; loop { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); let svc = svc.clone(); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, svc).await { eprintln!("Error serving connection: {:?}", err); } }); } } #[derive(Debug, Clone)] struct RandomPageService { pub ctx: Arc, } impl RandomPageService { pub fn new(args: &Args) -> Self { let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); let dictionary_string: &'static str = std::str::from_utf8(dictionary_data).unwrap(); Self { ctx: Arc::new(PageGenerator { seed: args.seed.clone(), delay: args.delay, dict: dictionary_string.split_whitespace().collect(), dict_set: dictionary_string.split_whitespace().collect(), }), } } } impl Service> for RandomPageService { type Response = Response>; type Error = hyper::Error; type Future = Pin> + Send>>; fn call(&self, req: Request) -> Self::Future { fn mk_response( s: String, status_code: StatusCode, ) -> Result>, hyper::Error> { Ok( Response::builder() .status(status_code) .body(Full::new(Bytes::from(s))) .unwrap(), ) } let ctx = self.ctx.clone(); Box::pin(async move { let path = req.uri().path(); match ctx.build_page(path).await { Ok(body) => mk_response(body, StatusCode::OK), Err((err_message, code)) => mk_response(err_message.to_string(), code), } }) } } const MAX_ROUTE_SEGMENTS: usize = 6; const PARAGRAPH_WORDS: RangeInclusive = 10..=200; const N_LINKS: RangeInclusive = 3..=10; #[derive(Debug)] struct PageGenerator { seed: String, delay: u32, dict: Vec<&'static str>, dict_set: HashSet<&'static str>, } impl PageGenerator { async fn build_page( &self, route: &str, ) -> Result { if self.delay > 0 { tokio::time::sleep(tokio::time::Duration::from_millis(self.delay as u64)) .await; } for segment in route.split('/').filter(|v| v.len() > 0) { if !self.dict_set.contains(segment) { return Err(("not found", StatusCode::NOT_FOUND)); } } let mut rng: Pcg64 = Seeder::from(format!("{}---{}", self.seed, route)).make_rng(); let n_words = rng.gen_range(PARAGRAPH_WORDS); let n_links = rng.gen_range(N_LINKS); let random_paragraph = (0..n_words) .map(|_| random_word(&mut rng, &self.dict)) .collect::>() .join(" "); let random_links = (0..n_links) .map(|_| random_route_link(&mut rng, &self.dict)) .map(|link| format!("

{}

", link)) .collect::>() .join("\n"); tracing::info!(route); Ok(format!( r#"

{}

{} "#, random_paragraph, random_links, )) } } fn random_route_link(rng: &mut Pcg64, dictionary: &[&'static str]) -> String { let n_segments = rng.gen_range(1..=MAX_ROUTE_SEGMENTS); let random_route = (0..n_segments) .map(|_| random_word(rng, dictionary)) .collect::>() .join("/"); let label = random_word(rng, dictionary); format!("{}", random_route, label) } pub fn random_word( rng: &mut Pcg64, dictionary: &[&'static str], ) -> &'static str { let i = rng.gen_range(0..dictionary.len()); dictionary[i] } pub fn build_dict() -> Vec<&'static str> { let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); let dictionary_string: &'static str = std::str::from_utf8(dictionary_data).unwrap(); dictionary_string.split_whitespace().collect() }