Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68424b5a30 | |||
| 76cda0d88b | |||
| b49c8404b8 | |||
| e5f81110d0 | |||
| 6831fdd97a | |||
| 9b8d2b213e | |||
| d27628929f | |||
| 36b2c008e0 | |||
| d0e72fb145 |
13 changed files with 617 additions and 108 deletions
285
Cargo.lock
generated
285
Cargo.lock
generated
|
|
@ -8,10 +8,26 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bombai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deborrow",
|
||||
"flate2",
|
||||
"horrorhttp",
|
||||
|
|
@ -20,12 +36,47 @@ dependencies = [
|
|||
"readformat",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
|
|
@ -51,10 +102,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "551a13c0871ba8964b30d2407fdfd4c9b8e5f289950c152ff3d0d8de5be6b948"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.5"
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
|
|
@ -62,13 +119,59 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "horrorhttp"
|
||||
version = "0.2.1"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9940b42c31d4b7bc31b55b7aaa8c3bfb08ab9ffc5142aa7f882ff7a59719fd1a"
|
||||
checksum = "6e7217c84d64ff6e6ee8dc1057a8255b74bf2c5a2bbf7b0dea1dd2c4b7f05af1"
|
||||
dependencies = [
|
||||
"readformat",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "microlock"
|
||||
version = "0.3.1"
|
||||
|
|
@ -99,13 +202,179 @@ version = "0.2.1"
|
|||
source = "git+https://github.com/tudbut/nanoserde#fc010f51957432aec80dba0a70af0dadc3cbe38f"
|
||||
|
||||
[[package]]
|
||||
name = "readformat"
|
||||
version = "1.0.3"
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cc7f16cea0fc473653b54865015941baf47c6f2b796b54c518a5d0e8e631a98"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "readformat"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94c3a263091233283319d916f89668dac0ee49ffefa7cb7537c03810c7693674"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.42"
|
||||
deborrow = "0.3.1"
|
||||
flate2 = "1.1.5"
|
||||
horrorhttp = "0.2.1"
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -18,6 +18,7 @@ expect breakage resulting in crashes if you track main.
|
|||
- **zip bombs**
|
||||
- traps (like iocaine but muuuch simpler)
|
||||
- redirecting to iocaine :)
|
||||
- metrics
|
||||
|
||||
# more detail
|
||||
|
||||
|
|
@ -55,6 +56,11 @@ default config is automatically dropped to disk and can also be found at src/bom
|
|||
|
||||
it contains a lot of documentation
|
||||
|
||||
# metrics
|
||||
|
||||
dashboard base url can be set in config; metrics available at /bombai/metrics (assuming
|
||||
dashboard is at /bombai).
|
||||
|
||||
# how to
|
||||
|
||||
add to caddyfile as per the caddyfile in this repo. the iocaine part is not required.
|
||||
|
|
@ -84,6 +90,11 @@ i dont like big dependency trees. so this one is small.
|
|||
```
|
||||
tudbut@Tud-NixX260 ~/g/bombai (main)> cargo tree
|
||||
bombai v0.1.0 (/home/tudbut/gitshit/bombai)
|
||||
├── chrono v0.4.42
|
||||
│ ├── iana-time-zone v0.1.64
|
||||
│ └── num-traits v0.2.19
|
||||
│ [build-dependencies]
|
||||
│ └── autocfg v1.5.0
|
||||
├── deborrow v0.3.1
|
||||
│ └── deborrow-macro v0.2.0 (proc-macro)
|
||||
├── flate2 v1.1.5
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ by_ua.only_if_contains = [
|
|||
]
|
||||
by_ua.timeout = true
|
||||
|
||||
# time until IP and UA entries are removed from memory
|
||||
cleanup_time = 180 # minutes
|
||||
|
||||
[dashboard]
|
||||
enable = true
|
||||
path = "/bombai"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::{collections::HashSet, net::IpAddr, time::Duration};
|
||||
use std::{collections::HashMap, net::IpAddr, time::Duration};
|
||||
|
||||
use horrorhttp::Connection;
|
||||
use readformat::readf;
|
||||
|
||||
use crate::{CONFIG, ClientID, Counter, Directive};
|
||||
use crate::{CONFIG, ClientID, Counter, Directive, log, timed::Timeout};
|
||||
|
||||
pub enum CheckResponse {
|
||||
Okay,
|
||||
|
|
@ -120,7 +120,7 @@ pub fn request_is_okay(
|
|||
connection: &mut Connection,
|
||||
discrim_ip: IpAddr,
|
||||
transformed_ua: &str,
|
||||
known: &HashSet<ClientID>,
|
||||
known: &HashMap<ClientID, Timeout<()>>,
|
||||
directives: &[Directive],
|
||||
) -> CheckResponse {
|
||||
if CONFIG["fail_response.continuous_failure.enable"].boolean()
|
||||
|
|
@ -128,26 +128,26 @@ pub fn request_is_okay(
|
|||
.path
|
||||
.starts_with(CONFIG["fail_response.continuous_failure.path"].str())
|
||||
{
|
||||
println!(" is in continuous failure");
|
||||
log!("Checker": "Is in continuous failure");
|
||||
return CheckResponse::Fail;
|
||||
}
|
||||
for (i, directive) in directives.iter().enumerate() {
|
||||
if directive.check_and_insert(connection, discrim_ip, transformed_ua) {
|
||||
if directive.costly {
|
||||
println!(" matched costly directive {i}");
|
||||
if (!known.contains(&ClientID::IpAddr(discrim_ip))
|
||||
|| !known.contains(&ClientID::TransformedUA(transformed_ua.to_owned())))
|
||||
log!("Checker": "Matched costly directive {i}");
|
||||
if (!known.contains_key(&ClientID::IpAddr(discrim_ip))
|
||||
|| !known.contains_key(&ClientID::TransformedUA(transformed_ua.to_owned())))
|
||||
&& let Some(d) = directive.fail_if_first
|
||||
{
|
||||
println!(" request is first, but directive forbids it");
|
||||
log!("Checker": "Request is first, but directive forbids it");
|
||||
return CheckResponse::FailAsFirst(d);
|
||||
}
|
||||
if directive.counter_is_bad(discrim_ip, transformed_ua) {
|
||||
println!(" counter is bad");
|
||||
log!("Checker": "Counter is bad");
|
||||
return CheckResponse::Fail;
|
||||
}
|
||||
} else {
|
||||
println!(" matched non-costly directive {i} :)");
|
||||
log!("Checker": "Matched non-costly directive {i} :)");
|
||||
return CheckResponse::Okay;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,28 @@ use std::{collections::BTreeMap, fs, sync::LazyLock};
|
|||
|
||||
use nanoserde::{Toml, TomlParser};
|
||||
|
||||
use crate::log;
|
||||
|
||||
const DEFAULT_CONFIG: &str = include_str!("./bombai.toml");
|
||||
pub static CONFIG: LazyLock<BTreeMap<String, Toml>> = LazyLock::new(|| {
|
||||
pub static CONFIG: LazyLock<BTreeMap<String, Toml>> = LazyLock::new(load_config);
|
||||
|
||||
fn load_config() -> BTreeMap<String, Toml> {
|
||||
let s = {
|
||||
match fs::read_to_string("./bombai.toml") {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
fs::write("./bombai.toml", DEFAULT_CONFIG).unwrap();
|
||||
println!("dropped default config file because no other config was found");
|
||||
log!("Config": "Dropped default config file because no other config was found");
|
||||
DEFAULT_CONFIG.to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
TomlParser::parse(&s).unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
/// not technically thread-safe, but who cares.
|
||||
pub fn reload_config() {
|
||||
unsafe {
|
||||
*deborrow::ref_to_mut(&*CONFIG) = load_config();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,8 @@
|
|||
font-size: 14pt;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #2c2525;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -33,16 +30,16 @@
|
|||
<h1> Bombai Dashboard </h1>
|
||||
<table>
|
||||
<tr><th class=title>Total</th></tr>
|
||||
<tr><th>Handled</th><td>%requests_handled%</td></tr>
|
||||
<tr><th>Successes</th><td>%requests_succeeded%</td></tr>
|
||||
<tr><th>Fails</th><td>%requests_failed%</td></tr>
|
||||
<tr><th>Fail%</th><td>%percent_fail_total%%</td></tr>
|
||||
<tr><th>Handled</th><td>%total_handled%</td></tr>
|
||||
<tr><th>Failed</th><td>%total_failed%</td></tr>
|
||||
<tr><th>Succeeded</th><td>%total_succeeded%</td></tr>
|
||||
<tr><th>Fail%</th><td>%total_percent_failed%%</td></tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr><th class=title>Per second</th><th>Current</th><th>Average</th></tr>
|
||||
<tr><th>Handled</th><td>%handled/s%</td><td>%handled/savg%</td></tr>
|
||||
<tr><th>Successes</th><td>%successes/s%</td><td>%successes/savg%</td></tr>
|
||||
<tr><th>Fails</th><td>%fails/s%</td><td>%fails/savg%</td></tr>
|
||||
<tr><th>Fail%</th><td>%percent_fail_second%%</td></tr>
|
||||
<tr><th>Handled</th><td>%second_handled%</td><td>%second_avg_handled%</td></tr>
|
||||
<tr><th>Failed</th><td>%second_failed%</td><td>%second_avg_failed%</td></tr>
|
||||
<tr><th>Succeeded</th><td>%second_succeeded%</td><td>%second_avg_succeeded%</td></tr>
|
||||
<tr><th>Fail%</th><td>%second_percent_failed%%</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ use microlock::timer::{Timed, Timer, TimerDuration};
|
|||
|
||||
use crate::START_TIME;
|
||||
|
||||
pub struct DashboardHandler(pub String);
|
||||
|
||||
const HTML: &str = include_str!("dashboard.html");
|
||||
pub static REQUESTS_HANDLED: AtomicU64 = AtomicU64::new(0);
|
||||
pub static REQUESTS_FAILED: AtomicU64 = AtomicU64::new(0);
|
||||
|
|
@ -55,54 +53,129 @@ pub fn update_successes(has_just_succeeded: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Stats {
|
||||
pub total_handled: u64,
|
||||
pub total_failed: u64,
|
||||
pub total_succeeded: u64,
|
||||
|
||||
pub second_handled: u64,
|
||||
pub second_failed: u64,
|
||||
pub second_succeeded: u64,
|
||||
|
||||
pub second_avg_handled: f64,
|
||||
pub second_avg_failed: f64,
|
||||
pub second_avg_succeeded: f64,
|
||||
|
||||
pub total_percent_failed: f64,
|
||||
pub second_percent_failed: f64,
|
||||
}
|
||||
|
||||
impl Stats {
|
||||
pub fn get() -> Self {
|
||||
update_fails(false);
|
||||
update_successes(false);
|
||||
let total_handled = REQUESTS_HANDLED.load(Ordering::Relaxed);
|
||||
let total_failed = REQUESTS_FAILED.load(Ordering::Relaxed);
|
||||
let second_failed = FAILS_LAST_SECOND_VALUE.load(Ordering::Relaxed);
|
||||
let second_succeeded = SUCCESSES_LAST_SECOND_VALUE.load(Ordering::Relaxed);
|
||||
let total_succeeded = total_handled - total_failed;
|
||||
let seconds = START_TIME.elapsed().as_secs_f64();
|
||||
let second_handled = second_failed + second_succeeded;
|
||||
Self {
|
||||
total_handled,
|
||||
total_failed,
|
||||
total_succeeded,
|
||||
second_handled,
|
||||
second_failed,
|
||||
second_succeeded,
|
||||
second_avg_handled: total_handled as f64 / seconds,
|
||||
second_avg_failed: total_failed as f64 / seconds,
|
||||
second_avg_succeeded: total_succeeded as f64 / seconds,
|
||||
total_percent_failed: total_failed as f64 / total_handled as f64 * 100.,
|
||||
second_percent_failed: second_failed as f64 / second_handled as f64 * 100.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_metrics_str(&self) -> String {
|
||||
format!(
|
||||
"\
|
||||
total_handled {}\n\
|
||||
total_failed {}\n\
|
||||
total_succeeded {}\n\
|
||||
second_handled {}\n\
|
||||
second_failed {}\n\
|
||||
second_succeeded {}\n\
|
||||
second_avg_handled {}\n\
|
||||
second_avg_failed {}\n\
|
||||
second_avg_succeeded {}\n\
|
||||
total_percent_failed {}\n\
|
||||
second_percent_failed {}",
|
||||
self.total_handled,
|
||||
self.total_failed,
|
||||
self.total_succeeded,
|
||||
self.second_handled,
|
||||
self.second_failed,
|
||||
self.second_succeeded,
|
||||
self.second_avg_handled,
|
||||
self.second_avg_failed,
|
||||
self.second_avg_succeeded,
|
||||
self.total_percent_failed,
|
||||
self.second_percent_failed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn substitute(&self, s: &str) -> String {
|
||||
s.replace("%total_handled%", &self.total_handled.to_string())
|
||||
.replace("%total_failed%", &self.total_failed.to_string())
|
||||
.replace("%total_succeeded%", &self.total_succeeded.to_string())
|
||||
.replace("%second_handled%", &self.second_handled.to_string())
|
||||
.replace("%second_failed%", &self.second_failed.to_string())
|
||||
.replace("%second_succeeded%", &self.second_succeeded.to_string())
|
||||
.replace(
|
||||
"%second_avg_handled%",
|
||||
&round(self.second_avg_handled).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%second_avg_failed%",
|
||||
&round(self.second_avg_failed).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%second_avg_succeeded%",
|
||||
&round(self.second_avg_succeeded).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%total_percent_failed%",
|
||||
&round(self.total_percent_failed).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%second_percent_failed%",
|
||||
&round(self.second_percent_failed).to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DashboardHandler(pub String);
|
||||
|
||||
impl ConnectionState for DashboardHandler {
|
||||
fn handle(
|
||||
self: Box<Self>,
|
||||
_connection: &mut horrorhttp::Connection,
|
||||
) -> Option<Box<dyn ConnectionState>> {
|
||||
update_fails(false);
|
||||
update_successes(false);
|
||||
let fails_last_second = FAILS_LAST_SECOND_VALUE.load(Ordering::Relaxed);
|
||||
let successes_last_second = SUCCESSES_LAST_SECOND_VALUE.load(Ordering::Relaxed);
|
||||
let handled = REQUESTS_HANDLED.load(Ordering::Relaxed);
|
||||
let failed = REQUESTS_FAILED.load(Ordering::Relaxed);
|
||||
let total_second = fails_last_second + successes_last_second;
|
||||
let successes = handled - failed;
|
||||
let html = HTML
|
||||
.replace("%requests_handled%", &handled.to_string())
|
||||
.replace("%requests_succeeded%", &successes.to_string())
|
||||
.replace("%requests_failed%", &failed.to_string())
|
||||
.replace(
|
||||
"%handled/s%",
|
||||
&(fails_last_second + successes_last_second).to_string(),
|
||||
)
|
||||
.replace("%fails/s%", &fails_last_second.to_string())
|
||||
.replace("%successes/s%", &successes_last_second.to_string())
|
||||
.replace(
|
||||
"%fails/savg%",
|
||||
&round(failed as f64 / START_TIME.elapsed().as_secs_f64()).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%handled/savg%",
|
||||
&round(handled as f64 / START_TIME.elapsed().as_secs_f64()).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%successes/savg%",
|
||||
&round(successes as f64 / START_TIME.elapsed().as_secs_f64()).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%percent_fail_total%",
|
||||
&round(failed as f64 / handled as f64 * 100.).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"%percent_fail_second%",
|
||||
&round(fails_last_second as f64 / total_second as f64 * 100.).to_string(),
|
||||
);
|
||||
Some(Box::new(
|
||||
ResponseWriter::new()
|
||||
.with_header("Content-Type", "text/html")
|
||||
.with_body(html.into_bytes()),
|
||||
))
|
||||
let stats = Stats::get();
|
||||
if self.0 == "/metrics" {
|
||||
Some(Box::new(
|
||||
ResponseWriter::new()
|
||||
.with_header("Content-Type", "text/plain")
|
||||
.with_body(stats.to_metrics_str().into_bytes()),
|
||||
))
|
||||
} else {
|
||||
let html = stats.substitute(HTML);
|
||||
Some(Box::new(
|
||||
ResponseWriter::new()
|
||||
.with_header("Content-Type", "text/html")
|
||||
.with_body(html.into_bytes()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
6
src/log.rs
Normal file
6
src/log.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($area:literal: $($a:tt)*) => {
|
||||
println!("[{}] [Bombai] [{}] {}", ::chrono::Local::now().time().format("%H:%M:%S"), $area, format!($($a)*));
|
||||
};
|
||||
}
|
||||
108
src/main.rs
108
src/main.rs
|
|
@ -1,8 +1,10 @@
|
|||
pub mod checker;
|
||||
pub mod config;
|
||||
pub mod dashboard;
|
||||
pub mod log;
|
||||
pub mod processing;
|
||||
pub mod responder;
|
||||
pub mod timed;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
|
|
@ -10,14 +12,22 @@ use std::{
|
|||
net::{IpAddr, TcpListener},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock, Mutex, MutexGuard, atomic::Ordering},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use deborrow::deborrow;
|
||||
use horrorhttp::{BodyReaderHeader, ConnectionState, ResponseWriter};
|
||||
use microlock::timer::{Timed, Timer, TimerDuration};
|
||||
use microlock::{
|
||||
Lock, TimedLock,
|
||||
timer::{Timed, Timer, TimerDuration},
|
||||
};
|
||||
|
||||
use crate::{dashboard::DashboardHandler, responder::BullshitResponder};
|
||||
use crate::{
|
||||
dashboard::DashboardHandler,
|
||||
responder::BullshitResponder,
|
||||
timed::{Expire, Timeout},
|
||||
};
|
||||
|
||||
pub use crate::config::*;
|
||||
use crate::processing::*;
|
||||
|
|
@ -68,14 +78,16 @@ impl ClientID {
|
|||
}
|
||||
}
|
||||
|
||||
type VisitedPathsSet = Mutex<HashSet<String>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Directive {
|
||||
pub costly: bool,
|
||||
pub cached: bool,
|
||||
pub paths: Vec<String>,
|
||||
pub default_counter: Counter,
|
||||
pub visited_paths: Arc<Mutex<HashMap<ClientID, Mutex<HashSet<String>>>>>,
|
||||
pub counter: Arc<Mutex<HashMap<ClientID, Mutex<Counter>>>>,
|
||||
pub visited_paths: Arc<Mutex<HashMap<ClientID, Timeout<VisitedPathsSet>>>>,
|
||||
pub counter: Arc<Mutex<HashMap<ClientID, Timeout<Mutex<Counter>>>>>,
|
||||
pub fail_if_first: Option<Duration>,
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +95,7 @@ impl Directive {
|
|||
pub fn get_checker_data<'a, T: CheckerData>(
|
||||
&self,
|
||||
id: &ClientID,
|
||||
hashmap: &'a mut HashMap<ClientID, Mutex<T>>,
|
||||
hashmap: &'a mut HashMap<ClientID, Timeout<Mutex<T>>>,
|
||||
) -> MutexGuard<'a, T> {
|
||||
// SAFETY: this is safe - the lifetime is judged to be longer than it actually is and
|
||||
// thus interferes with the modifcation later in this function, after we have
|
||||
|
|
@ -95,16 +107,16 @@ impl Directive {
|
|||
}
|
||||
}
|
||||
|
||||
hashmap.insert(id.clone(), Mutex::new(T::get_default(self)));
|
||||
hashmap.insert(id.clone(), timeout(Mutex::new(T::get_default(self))));
|
||||
hashmap.get(id).unwrap().lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Intercept {
|
||||
directives: &'static Vec<Directive>,
|
||||
addr_timeouts: Arc<Mutex<HashMap<IpAddr, Timer>>>,
|
||||
ua_timeouts: Arc<Mutex<HashMap<String, Timer>>>,
|
||||
known: Arc<Mutex<HashSet<ClientID>>>,
|
||||
addr_timeouts: Arc<Mutex<HashMap<IpAddr, Timeout<Timer>>>>,
|
||||
ua_timeouts: Arc<Mutex<HashMap<String, Timeout<Timer>>>>,
|
||||
known: Arc<Mutex<HashMap<ClientID, Timeout<()>>>>,
|
||||
}
|
||||
|
||||
impl ConnectionState for Intercept {
|
||||
|
|
@ -142,12 +154,12 @@ impl ConnectionState for Intercept {
|
|||
let mut known = self.known.lock().unwrap();
|
||||
|
||||
if CONFIG["by_ua.enable"].boolean() {
|
||||
println!(
|
||||
log!("Handler":
|
||||
"Handling request from {ip} ({discrim_ip} with {transformed_ua}) for {}.",
|
||||
connection.path
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
log!("Handler":
|
||||
"Handling request from {ip} ({discrim_ip}) for {}.",
|
||||
connection.path
|
||||
);
|
||||
|
|
@ -163,18 +175,18 @@ impl ConnectionState for Intercept {
|
|||
if result.is_okay()
|
||||
&& addr_timeouts
|
||||
.get(&discrim_ip)
|
||||
.unwrap_or(&Timer::new(TimerDuration::Elapsed))
|
||||
.unwrap_or(&timeout(Timer::new(TimerDuration::Elapsed)))
|
||||
.has_elapsed()
|
||||
&& ua_timeouts
|
||||
.get(&transformed_ua)
|
||||
.unwrap_or(&Timer::new(TimerDuration::Elapsed))
|
||||
.unwrap_or(&timeout(Timer::new(TimerDuration::Elapsed)))
|
||||
.has_elapsed()
|
||||
{
|
||||
known.insert(ClientID::IpAddr(discrim_ip));
|
||||
known.insert(ClientID::TransformedUA(transformed_ua));
|
||||
known.insert(ClientID::IpAddr(discrim_ip), timeout(()));
|
||||
known.insert(ClientID::TransformedUA(transformed_ua), timeout(()));
|
||||
dashboard::update_successes(true);
|
||||
addr_timeouts.remove(&discrim_ip);
|
||||
println!("Request is OK.");
|
||||
log!("Handler": "Request is OK.");
|
||||
Some(Box::new(ResponseWriter::new().with_status(
|
||||
CONFIG["handler.pass_response"].num() as u32,
|
||||
"Misdirected Request",
|
||||
|
|
@ -184,16 +196,19 @@ impl ConnectionState for Intercept {
|
|||
dashboard::update_fails(true);
|
||||
let timeout = result.get_timeout();
|
||||
if !timeout.is_zero() {
|
||||
addr_timeouts.insert(discrim_ip, Timer::new(TimerDuration::Real(timeout)));
|
||||
addr_timeouts.insert(
|
||||
discrim_ip,
|
||||
crate::timeout(Timer::new(TimerDuration::Real(timeout))),
|
||||
);
|
||||
if CONFIG["by_ua.timeout"].boolean() {
|
||||
ua_timeouts.insert(
|
||||
transformed_ua.clone(),
|
||||
Timer::new(TimerDuration::Real(timeout)),
|
||||
crate::timeout(Timer::new(TimerDuration::Real(timeout))),
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("Request is not OK. Sending you to the gallows.");
|
||||
println!(" User-Agent: {transformed_ua}");
|
||||
log!("Handler": "Request is not OK. Sending you to the gallows.");
|
||||
log!("Handler": "User-Agent: {transformed_ua}");
|
||||
Some(Box::new(BullshitResponder))
|
||||
}
|
||||
}
|
||||
|
|
@ -215,11 +230,18 @@ fn main() {
|
|||
|
||||
let addr_timeouts = Arc::new(Mutex::new(HashMap::new()));
|
||||
let ua_timeouts = Arc::new(Mutex::new(HashMap::new()));
|
||||
let known = Arc::new(Mutex::new(HashSet::new()));
|
||||
let known = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
thread::spawn(cleanup(
|
||||
addr_timeouts.clone(),
|
||||
ua_timeouts.clone(),
|
||||
known.clone(),
|
||||
unsafe { deborrow(&directives) },
|
||||
));
|
||||
|
||||
let port = CONFIG["handler.listen_port"].num() as u16;
|
||||
let server = TcpListener::bind(("::0", port)).unwrap();
|
||||
println!("listening on [::0]:{port}");
|
||||
log!("Handler": "listening on [::0]:{port}");
|
||||
while let Ok((stream, _addr)) = server.accept() {
|
||||
horrorhttp::handle(
|
||||
stream,
|
||||
|
|
@ -233,3 +255,45 @@ fn main() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(
|
||||
addr_timeouts: Arc<Mutex<HashMap<IpAddr, Timeout<Timer>>>>,
|
||||
ua_timeouts: Arc<Mutex<HashMap<String, Timeout<Timer>>>>,
|
||||
known: Arc<Mutex<HashMap<ClientID, Timeout<()>>>>,
|
||||
directives: &[Directive],
|
||||
) -> impl FnOnce() {
|
||||
move || {
|
||||
let lock = TimedLock::unlocked();
|
||||
loop {
|
||||
if !lock.is_locked() {
|
||||
lock.lock_for(Duration::from_secs_f64(CONFIG["cleanup_time"].num() * 15.0).into());
|
||||
|
||||
log!("GC": "Garbage collecting...");
|
||||
let mut total_removed = 0;
|
||||
fn remove_expired<'a, T: Expire>(mut x: MutexGuard<'a, T>) -> usize {
|
||||
let len = x.len();
|
||||
x.remove_expired();
|
||||
len - x.len()
|
||||
}
|
||||
total_removed += remove_expired(addr_timeouts.lock().unwrap());
|
||||
total_removed += remove_expired(ua_timeouts.lock().unwrap());
|
||||
total_removed += remove_expired(known.lock().unwrap());
|
||||
for directive in directives {
|
||||
total_removed += remove_expired(directive.visited_paths.lock().unwrap());
|
||||
}
|
||||
log!("GC": "Finished. Removed {total_removed} objects");
|
||||
}
|
||||
log!("Config": "Reloading config");
|
||||
config::reload_config();
|
||||
|
||||
lock.wait_here_for(Duration::from_mins(1).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timeout<T>(inner: T) -> Timeout<T> {
|
||||
Timeout::new(
|
||||
inner,
|
||||
Duration::from_mins(CONFIG["cleanup_time"].num() as u64),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ pub fn transform_ua(input: &str) -> String {
|
|||
}
|
||||
if input.contains("+http") {
|
||||
let a = &input[input.rfind("+http").unwrap()..];
|
||||
return a[..a.rfind(')').unwrap_or(a.len())]
|
||||
.to_owned();
|
||||
return a[..a.rfind(')').unwrap_or(a.len())].to_owned();
|
||||
}
|
||||
|
||||
input.to_owned()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::time::SystemTime;
|
|||
use flate2::{Compression, write::GzEncoder};
|
||||
use horrorhttp::{Connection, ConnectionState, ResponseWriter};
|
||||
|
||||
use crate::CONFIG;
|
||||
use crate::{CONFIG, log};
|
||||
|
||||
static BODY: LazyLock<Vec<u8>> = LazyLock::new(gen_body);
|
||||
static KILOBYTE: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
||||
|
|
@ -66,7 +66,7 @@ impl ConnectionState for GeneratedBullshitSpammer {
|
|||
u64::MAX
|
||||
};
|
||||
|
||||
println!(" i am spammer of bytes");
|
||||
log!("Responder": "Spamming bytes");
|
||||
|
||||
// write begin
|
||||
let _ = connection.socket.write_all(&begin);
|
||||
|
|
@ -88,14 +88,14 @@ impl ConnectionState for GeneratedBullshitSpammer {
|
|||
let _ = connection.socket.write_all(end);
|
||||
self.0 += end.len() as u64;
|
||||
|
||||
println!(" stopped spamming after {} bytes", self.0);
|
||||
log!("Responder": "Stopped spamming after {} bytes", self.0);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_body() -> Vec<u8> {
|
||||
if CONFIG["fail_response.generated.gzip"].boolean() {
|
||||
println!(" i am constructor of zip bomb");
|
||||
log!("Responder": "Constructing zip bomb");
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
|
||||
|
||||
encoder.write_all(&get_begin()).unwrap();
|
||||
|
|
@ -109,7 +109,7 @@ fn gen_body() -> Vec<u8> {
|
|||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
println!(" done");
|
||||
log!("Responder": "Done");
|
||||
encoder.finish().unwrap()
|
||||
} else {
|
||||
vec![]
|
||||
|
|
|
|||
75
src/timed.rs
Normal file
75
src/timed.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use microlock::timer::{Timed, Timer};
|
||||
|
||||
pub struct Timeout<T> {
|
||||
inner: T,
|
||||
timeout: Timer,
|
||||
}
|
||||
|
||||
impl<T> Timeout<T> {
|
||||
pub fn new(inner: T, timeout: Duration) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
timeout: Timer::new(timeout.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expired(&self) -> bool {
|
||||
self.timeout.has_elapsed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Timeout<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<T> for Timeout<T> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
&self.inner == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for Timeout<T> {}
|
||||
|
||||
impl<T> Deref for Timeout<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Timeout<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for Timeout<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Expire {
|
||||
fn remove_expired(&mut self);
|
||||
fn len(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<K, V> Expire for HashMap<K, Timeout<V>> {
|
||||
fn remove_expired(&mut self) {
|
||||
self.retain(|_, v| !v.expired());
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
HashMap::len(self)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue