Compare commits

...

9 commits
v0.0.0 ... main

Author SHA1 Message Date
68424b5a30 lockfile update 2026-01-18 02:36:26 +01:00
76cda0d88b fix a hang 2026-01-04 18:18:52 +01:00
b49c8404b8 better logging & config reloading 2026-01-03 09:44:57 +01:00
e5f81110d0
periodically reload config 2025-12-31 19:59:47 +01:00
6831fdd97a
add data time to live (stops ram filling up so much) 2025-12-31 18:19:30 +01:00
9b8d2b213e
readme metrics 2025-12-29 04:36:14 +01:00
d27628929f
metrics 2025-12-29 03:41:10 +01:00
36b2c008e0
format 2025-12-28 23:24:07 +01:00
d0e72fb145
use color-scheme for dark mode 2025-12-28 22:03:59 +01:00
13 changed files with 617 additions and 108 deletions

285
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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>&nbsp;</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>

View file

@ -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
View 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)*));
};
}

View file

@ -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),
)
}

View file

@ -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()

View file

@ -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
View 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)
}
}