diff --git a/Cargo.lock b/Cargo.lock index ebf35cb..ebd66c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.10.3" @@ -848,6 +854,15 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -861,6 +876,29 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1098,6 +1136,25 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1231,6 +1288,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1239,6 +1297,39 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1247,14 +1338,24 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1441,6 +1542,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1471,6 +1588,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1576,9 +1717,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "manyhow" @@ -1799,21 +1940,25 @@ dependencies = [ "chrono", "clap", "derive_more", + "env_logger", "html-escaper", "indexmap 2.10.0", "lettre", + "log", "neptune-cash", "proptest", "proptest-arbitrary-interop", "rand 0.9.2", "readonly", + "regex", + "reqwest", "serde", "serde_json", "tarpc", "test-strategy", "thiserror 1.0.69", "tokio", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "url", @@ -2517,13 +2662,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.11", "regex-syntax 0.8.5", ] @@ -2538,9 +2683,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -2559,6 +2704,60 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http 0.6.6", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rs-leveldb" version = "0.1.5" @@ -2594,6 +2793,39 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -2915,6 +3147,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2941,6 +3176,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "systemstat" version = "0.2.5" @@ -3229,6 +3485,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-serde" version = "0.8.0" @@ -3301,6 +3567,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3507,6 +3791,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "twenty-first" version = "0.50.0" @@ -3589,6 +3879,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -3639,6 +3935,15 @@ dependencies = [ "libc", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3680,6 +3985,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -3712,6 +4030,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3819,6 +4147,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result 0.3.4", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 1c222c4..1ef5b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,20 @@ name = "neptune-explorer" version = "0.1.0" edition = "2021" +default-run = "neptune-explorer" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "high_rate_attack" +path = "src/bin/high_rate_attack.rs" +required-features = ["attacks"] + +[[bin]] +name = "scraper" +path = "src/bin/scraper.rs" +required-features = ["attacks"] + [dependencies] axum = { version = "0.7.9", features = ["macros"] } serde = { version = "1.0.197", features = ["derive"] } @@ -40,6 +51,11 @@ indexmap = "2.7.0" blake3 = { version = "1.8.2", optional = true } rand = { version = "0.9.2", optional = true } +reqwest = { version = "0.12.23", optional = true } +log = {version = "0.4.28", optional = true} +env_logger = {version = "0.11.8", optional = true} +regex = {version = "1.11.3", optional = true } + #[dev-dependencies] test-strategy = "0.4.3" @@ -52,3 +68,4 @@ neptune-cash = { git = "https://github.com/Neptune-Crypto/neptune-core.git", bra [features] mock = ["dep:blake3", "dep:rand"] +attacks = ["reqwest", "log", "env_logger", "regex", "dep:rand"] diff --git a/src/bin/high_rate_attack.rs b/src/bin/high_rate_attack.rs new file mode 100644 index 0000000..185a036 --- /dev/null +++ b/src/bin/high_rate_attack.rs @@ -0,0 +1,27 @@ +use reqwest::Client; +use tokio::sync::futures; +use tokio::time::Instant; + +#[tokio::main] +async fn main() { + let client = Client::new(); + let url = "http://127.0.0.1:3000/your_endpoint"; + let num_requests = 200; // adjust as needed + let concurrency = 20; // parallel tasks + + let start = Instant::now(); + let futures = (0..num_requests).map(|_| { + let client = &client; + async move { + let _ = client.get(url).send().await; + } + }); + + futures::future::join_all(futures).await; + + let elapsed = start.elapsed(); + println!( + "Sent {} requests in {:.2?} (concurrency {})", + num_requests, elapsed, concurrency + ); +} diff --git a/src/bin/scraper.rs b/src/bin/scraper.rs new file mode 100644 index 0000000..bf5e2b6 --- /dev/null +++ b/src/bin/scraper.rs @@ -0,0 +1,103 @@ +use env_logger; +use log::LevelFilter; +use log::{error, info, warn}; +use rand::seq::IteratorRandom; +use regex::Regex; +use reqwest::Client; +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::{signal, time}; +use url::Url; + +/// Scrape the explorer website when running locally. +/// +/// This program maintains a dictionary of URLs, which is initially populated +/// with 'http://localhost:3000'. It fetches a random URL from the dictionary in +/// each iteration, logs positive messages if successful, extracts new URLs from +/// the response body to add to the dictionary, logs warnings or errors for +/// request failures or timeouts, sleeps for a bit, and continues until Ctrl-C +/// is pressed. +/// +/// Run with: +/// `> cargo run --bin scraper` +#[tokio::main] +async fn main() { + // Initialize logger + env_logger::builder().filter_level(LevelFilter::Info).init(); + + let client = Client::builder() + .timeout(Duration::from_millis(300)) + .build() + .expect("Failed to build HTTP client"); + + let root_url = "http://localhost:3000".to_string(); + let urls = Arc::new(Mutex::new(HashSet::from([root_url.clone()]))); + + let href_regex = Regex::new(r#"]*?\s+)?href=['\"](.*?)['\"]"#).unwrap(); + + info!("Starting fetch loop. Press Ctrl-C to stop."); + + let urls_clone = Arc::clone(&urls); + let fetch_loop = async move { + loop { + // Pick a random URL safely + let url_opt = { + let urls_guard = urls_clone.lock().unwrap(); + urls_guard.iter().choose(&mut rand::rng()).cloned() + }; + + if let Some(url) = url_opt { + match client.get(&url).send().await { + Ok(resp) => { + if resp.status().is_success() { + match resp.text().await { + Ok(text) => { + info!("Success fetching {}", url); + let mut urls_guard = urls_clone.lock().unwrap(); + for cap in href_regex.captures_iter(&text) { + let href = &cap[1]; + if let Ok(parsed_url) = + Url::parse(&[&root_url.clone(), href].concat()) + { + let normalized = parsed_url.as_str(); + if urls_guard.insert(normalized.to_owned()) { + info!( + "Added new URL to dictionary: {}", + normalized + ); + } + } + } + } + Err(e) => { + warn!("Failed to read response body from {}: {}", url, e); + } + } + } else { + warn!("Non-success status {} from {}", resp.status(), url); + } + } + Err(err) => { + if err.is_timeout() { + warn!("Timeout fetching {}", url); + } else { + error!("Error fetching {}: {}", url, err); + } + } + } + } else { + warn!("URL dictionary is empty, no URL to fetch"); + } + + time::sleep(Duration::from_millis(500)).await; + } + }; + + tokio::select! { + _ = fetch_loop => {}, // This runs indefinitely unless stopped + _ = signal::ctrl_c() => { + info!("Ctrl-C received, stopping..."); + } + } +}