Files
netrunner-landing/proxy-ws/src/main.rs
T
2026-04-24 13:04:00 +07:00

232 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::{http::StatusCode, response::IntoResponse, routing::post, Json, Router};
use regex::Regex;
use reqwest::Client;
use rustrict::CensorStr;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
#[derive(Deserialize, Serialize, Debug)]
struct CommentPayload {
nickname: String,
message: String,
}
static BAD_WORDS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(хуй|хуя|хуе|хул|пизд|ебат|ебан|ебл|ебуч|бляд|блят|шлюх|сука|сучк|жоп|пидор|пидар|педик|голубо|гондон|гандон|залуп|мудак|мудил|мраз|уеб|чмо|долбое|даун|чурк|нигер|нигга|хач|хохол|кацап|жид|порно|наркот|убить|насилие|соли|закладк|мефедрон|гашиш|спайс|heroin|meth|murder|gore|cpam|scam|crypto|binance|bitco)").unwrap()
});
const SAFE_WORDS: &[&str] = &[
// Защита корня "ебл/ебат" (самые частые ложные срабатывания)
"колебат",
"оскорблят",
"употреблят",
"расслаблят",
"потреблят",
"истреблят",
"хлебат",
"щебет",
"мебел",
"стебл",
"ослабл",
"гребл",
"хлебл",
"дебел",
"колебл",
// Защита корня "хуй"
"страхуй",
"подстрахуй",
// Защита корня "бля"
"рубл",
"сабл",
"челяб",
"влюбл",
"ограбл",
"дубл",
"гибл",
"корабл",
// Защита корня "жоп"
"джоп",
"джобс",
"прожора",
// Защита корня "пидар/пидор"
"скипидар",
"купидор",
"коридор",
// Защита корня "чмо"
"чмок",
"ячмен",
"черемух",
// Защита корня "мраз"
"мразер",
"замороз",
"мороз",
"мрамор",
// Защита корня "сука"
"рисука",
"присука",
"бамбука",
// Защита корня "жид"
"ожид",
"жидкос",
"жидк",
// Защита корня "даун"
"даунгрейд",
"адаун",
];
// Заменяет легальные слова-исключения на плейсхолдер перед проверкой
fn apply_allowlist(text: &str) -> String {
let mut safe_text = text.to_string();
for &safe_word in SAFE_WORDS {
safe_text = safe_text.replace(safe_word, "[SAFE]");
}
safe_text
}
// Функция жесткой нормализации текста перед проверкой
fn normalize_for_filter(input: &str) -> String {
let mut normalized = String::new();
let mut prev_char: Option<char> = None;
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' => 'в',
'c' => 'с',
'e' | '3' => 'е',
'o' | '0' => 'о',
'p' => 'р',
'x' => 'х',
'y' => 'у',
'm' => 'м',
't' => 'т',
'k' => 'к',
'h' => 'н',
'4' => 'ч',
_ => ch,
};
// Сжатие повторяющихся символов (хххуууууууй -> хуй)
// Не сжимаем пробелы, только буквы
if let Some(p) = prev_char {
if p == mapped && mapped != ' ' {
continue;
}
}
normalized.push(mapped);
prev_char = Some(mapped);
}
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;
if input.len() > 10 && uppercase_ratio > 0.7 {
return true;
}
// Проверка на зажатую клавиатуру (aaaaaaaaaaaa)
for word in input.split_whitespace() {
// Игнорируем длинные ссылки (http/https)
if word.len() > 35 && !word.starts_with("http") {
return true;
}
}
false
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/api/comments", post(create_comment));
println!("🚀 Netrunner Gateway started on 0.0.0.0:3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
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 (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Spam pattern detected.",
);
}
// --- 2. Английский фильтр (Crate rustrict) ---
if payload.message.is_inappropriate() || payload.nickname.is_inappropriate() {
println!("🛡️ Blocked by rustrict: {}", payload.nickname);
return (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Policy violation.",
);
}
// --- 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);
if BAD_WORDS_REGEX.is_match(&safe_msg) || BAD_WORDS_REGEX.is_match(&safe_nick) {
println!(
"🛡️ Blocked by Deep Regex: [Original: {}] -> [Normalized: {}]",
payload.message, safe_msg
);
return (
StatusCode::NOT_ACCEPTABLE,
"Transmission rejected: Policy violation.",
);
}
// --- 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);
let client = Client::new();
let pb_response = client.post(pb_url).json(&payload).send().await;
match pb_response {
Ok(res) if res.status().is_success() => {
(StatusCode::OK, "Transmission secured and delivered.")
}
Ok(res) => {
println!("⚠️ PocketBase rejected: {:?}", res.status());
(StatusCode::BAD_REQUEST, "Database rejected transmission.")
}
Err(e) => {
println!("💥 PB Connection error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Uplink offline.")
}
}
}