web speedtest fixes

This commit is contained in:
2026-05-03 14:07:28 +07:00
parent ce77d9e4e3
commit 602631e4bd
2 changed files with 21 additions and 24 deletions
+14 -21
View File
@@ -1,6 +1,7 @@
use axum::{
body::Bytes,
http::{header, Method, StatusCode},
extract::DefaultBodyLimit,
http::{header, HeaderValue, Method, StatusCode},
response::IntoResponse,
routing::{get, post},
Json, Router,
@@ -10,7 +11,7 @@ use reqwest::Client;
use rustrict::CensorStr;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tower_http::cors::{Any, CorsLayer};
use tower_http::cors::CorsLayer;
#[derive(Deserialize, Serialize, Debug)]
struct CommentPayload {
@@ -80,6 +81,7 @@ const SAFE_WORDS: &[&str] = &[
"даунгрейд",
"адаун",
];
// Заменяет легальные слова-исключения на плейсхолдер перед проверкой
fn apply_allowlist(text: &str) -> String {
let mut safe_text = text.to_string();
@@ -88,6 +90,7 @@ fn apply_allowlist(text: &str) -> String {
}
safe_text
}
// Функция жесткой нормализации текста перед проверкой
fn normalize_for_filter(input: &str) -> String {
let mut normalized = String::new();
@@ -96,19 +99,16 @@ fn normalize_for_filter(input: &str) -> String {
let lower_input = input.to_lowercase();
for ch in lower_input.chars() {
// СОХРАНЯЕМ ПРОБЕЛЫ, чтобы не склеивать соседние слова ("нас укачало" != "насукачало")
if ch.is_whitespace() {
normalized.push(' ');
prev_char = Some(' ');
continue;
}
// Вырезаем пунктуацию (чтобы ловить х.у.й), но пропускаем дальше только буквы/цифры
if !ch.is_alphanumeric() {
continue;
}
// Замена "leetspeak" (англо-русская подмена и цифры)
let mapped = match ch {
'a' | '@' => 'а',
'b' => 'в',
@@ -126,8 +126,6 @@ fn normalize_for_filter(input: &str) -> String {
_ => ch,
};
// Сжатие повторяющихся символов (хххуууууууй -> хуй)
// Не сжимаем пробелы, только буквы
if let Some(p) = prev_char {
if p == mapped && mapped != ' ' {
continue;
@@ -141,14 +139,12 @@ fn normalize_for_filter(input: &str) -> String {
normalized
}
// Функция проверки на спам (КАПС или слишком длинные бессмысленные слова)
fn is_spam(input: &str) -> bool {
let alpha_chars: String = input.chars().filter(|c| c.is_alphabetic()).collect();
if alpha_chars.is_empty() {
return false;
}
// Проверка на КАПС (> 70% заглавных при длине > 10)
let uppercase_count = alpha_chars.chars().filter(|c| c.is_uppercase()).count();
let uppercase_ratio = uppercase_count as f64 / alpha_chars.len() as f64;
@@ -156,9 +152,7 @@ fn is_spam(input: &str) -> bool {
return true;
}
// Проверка на зажатую клавиатуру (aaaaaaaaaaaa)
for word in input.split_whitespace() {
// Игнорируем длинные ссылки (http/https)
if word.len() > 35 && !word.starts_with("http") {
return true;
}
@@ -169,10 +163,12 @@ fn is_spam(input: &str) -> bool {
#[tokio::main]
async fn main() {
// Настраиваем правила CORS
// 🔥 Исправленный CORS: Явно указываем протоколы и разрешаем локалхост для тестов
let cors = CorsLayer::new()
// Разрешаем запросы только с твоего фронтенда
.allow_origin("netrunner-vpn.com") // Для разработки можно Any, для прода лучше конкретный URL
.allow_origin([
"http://localhost:3000".parse::<HeaderValue>().unwrap(),
"https://netrunner-vpn.com".parse::<HeaderValue>().unwrap(),
])
.allow_methods([Method::GET, Method::POST])
.allow_headers([header::CONTENT_TYPE]);
@@ -181,7 +177,8 @@ async fn main() {
.route("/api/speedtest/ping", get(|| async { "pong" }))
.route("/api/speedtest/download", get(speedtest_download))
.route("/api/speedtest/upload", post(speedtest_upload))
// Применяем CORS ко всем роутам
// 🔥 Увеличиваем лимит тела запроса до 50 Мегабайт для спидтеста (дефолт 2MB)
.layer(DefaultBodyLimit::max(50 * 1024 * 1024))
.layer(cors);
println!("🚀 Netrunner Gateway started on 0.0.0.0:3001");
@@ -190,7 +187,6 @@ async fn main() {
}
async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoResponse {
// --- 1. Проверка на спам ---
if is_spam(&payload.message) || is_spam(&payload.nickname) {
println!("🗑️ Blocked by SPAM filter: {}", payload.nickname);
return (
@@ -199,7 +195,6 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
);
}
// --- 2. Английский фильтр (Crate rustrict) ---
if payload.message.is_inappropriate() || payload.nickname.is_inappropriate() {
println!("🛡️ Blocked by rustrict: {}", payload.nickname);
return (
@@ -208,11 +203,9 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
);
}
// --- 3. Русский хардкорный фильтр с защитой от ложных срабатываний ---
let normalized_msg = normalize_for_filter(&payload.message);
let normalized_nick = normalize_for_filter(&payload.nickname);
// Сначала защищаем нормальные слова, затем проверяем регуляркой
let safe_msg = apply_allowlist(&normalized_msg);
let safe_nick = apply_allowlist(&normalized_nick);
@@ -227,7 +220,6 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
);
}
// --- 4. ОТПРАВКА В POCKETBASE ---
let pb_base_url =
std::env::var("PB_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8090".to_string());
let pb_url = format!("{}/api/collections/comments/records", pb_base_url);
@@ -253,13 +245,14 @@ async fn create_comment(Json(payload): Json<CommentPayload>) -> impl IntoRespons
async fn speedtest_download() -> impl IntoResponse {
let payload_size = 25 * 1024 * 1024; // 25 MB
let data = vec![0u8; payload_size];
(
[(axum::http::header::CONTENT_TYPE, "application/octet-stream")],
data,
)
}
// 🔥 Возвращаем `body: Bytes`. Благодаря `DefaultBodyLimit` в main(),
// Axum 0.7 успешно примет в память файл до 50МБ без ручной работы со стримами.
async fn speedtest_upload(body: Bytes) -> impl IntoResponse {
(StatusCode::OK, format!("Received {} bytes", body.len()))
}