flusql/src/http.rs
2026-01-08 18:30:33 +03:00

355 lines
13 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.

// src/server/http.rs
//! Lock-free HTTP/HTTPS сервер для Futriix Database
//!
//! Этот модуль реализует веб-интерфейс для базы данных с поддержкой:
//! - HTTP/1.1 и HTTP/2 протоколов
//! - TLS/HTTPS для безопасного соединения
//! - Обслуживание статических файлов (HTML, CSS, JS)
//! - REST API для доступа к данным
//! - Статистику запросов без блокировок
use std::sync::Arc;
use hyper::{Body, Request, Response, Server, StatusCode};
use hyper::service::{make_service_fn, service_fn};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::server::database::Database;
/// Конфигурация статических файлов
/// Определяет параметры обслуживания статических ресурсов
#[derive(Clone)]
pub struct StaticFilesConfig {
pub enabled: bool, // Включено ли обслуживание статических файлов
pub directory: String, // Директория со статическими файлами
}
impl Default for StaticFilesConfig {
fn default() -> Self {
Self {
enabled: true,
directory: "static".to_string(),
}
}
}
/// Конфигурация TLS для HTTPS сервера
/// Содержит пути к сертификатам и ключам
#[derive(Clone)]
pub struct TlsConfig {
pub enabled: bool, // Включен ли TLS
pub cert_path: String, // Путь к файлу сертификата
pub key_path: String, // Путь к файлу приватного ключа
}
impl Default for TlsConfig {
fn default() -> Self {
Self {
enabled: true,
cert_path: "certs/cert.pem".to_string(),
key_path: "certs/key.pem".to_string(),
}
}
}
/// Конфигурация HTTP сервера
/// Определяет параметры протокола и порты
#[derive(Clone)]
pub struct HttpConfig {
pub enabled: bool, // Включен ли HTTP сервер
pub port: u16, // Порт для привязки
pub http2_enabled: bool, // Включена ли поддержка HTTP/2
}
/// Конфигурация Access Control List (ACL)
/// Позволяет управлять доступом по IP-адресам
#[derive(Clone)]
pub struct AclConfig {
pub enabled: bool, // Включена ли проверка ACL
pub allowed_ips: Vec<String>, // Разрешенные IP-адреса
pub denied_ips: Vec<String>, // Запрещенные IP-адреса
}
impl Default for AclConfig {
fn default() -> Self {
Self {
enabled: false,
allowed_ips: vec!["127.0.0.1".to_string(), "::1".to_string()],
denied_ips: vec![],
}
}
}
/// Lock-free обработчик HTTP запросов
/// Диспетчеризирует запросы по типам и выполняет соответствующие операции
async fn handle_request(
req: Request<Body>,
db: Arc<Database>,
static_config: StaticFilesConfig,
acl_config: AclConfig,
) -> Result<Response<Body>, crate::common::FutriixError> {
// Проверка ACL с lock-free доступом
let acl_check = if acl_config.enabled {
let mut allowed = false;
if let Some(remote_addr) = req.extensions().get::<std::net::SocketAddr>() {
let ip = remote_addr.ip().to_string();
// Проверка запрещенных IP
if acl_config.denied_ips.contains(&ip) {
return Ok(Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Access denied"))
.unwrap());
}
// Проверка разрешенных IP (если список не пустой)
if !acl_config.allowed_ips.is_empty() {
allowed = acl_config.allowed_ips.contains(&ip);
} else {
allowed = true;
}
}
allowed
} else {
true
};
if !acl_check {
return Ok(Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Access denied"))
.unwrap());
}
let path = req.uri().path();
// Логируем запрос без блокировок в файл
crate::server::log_to_file(&format!("HTTP Request: {}", path));
// Обработка API запросов
let result = if path.starts_with("/api/") {
handle_api_request(req, db).await
}
// Обслуживание статических файлов
else if static_config.enabled {
handle_static_file(path, static_config).await
}
// Корневой путь
else if path == "/" {
// Корневой путь перенаправляет на главную страницу
Ok(Response::builder()
.status(StatusCode::TEMPORARY_REDIRECT)
.header("Location", "/index.html")
.body(Body::from("Redirecting to main page"))
.unwrap())
}
// 404 для остальных запросов
else {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not Found"))
.unwrap())
};
result
}
/// Lock-free обработка API запросов
/// Обрабатывает REST API endpoints для доступа к данным
async fn handle_api_request(
req: Request<Body>,
db: Arc<Database>,
) -> Result<Response<Body>, crate::common::FutriixError> {
// Разбираем путь API
let path = req.uri().path();
let parts: Vec<&str> = path.trim_start_matches("/api/").split('/').collect();
if parts.is_empty() {
return Ok(Response::builder()
.header("Content-Type", "application/json")
.body(Body::from(r#"{"status": "ok", "message": "Server API is running"}"#))
.unwrap());
}
match parts[0] {
"status" => {
// Статус сервера
Ok(Response::builder()
.header("Content-Type", "application/json")
.body(Body::from(r#"{"status": "running", "timestamp": ""#.to_owned() +
&chrono::Utc::now().to_rfc3339() + r#"", "version": "1.0.0"}"#))
.unwrap())
}
_ => {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "application/json")
.body(Body::from(r#"{"status": "error", "message": "API endpoint not found"}"#))
.unwrap())
}
}
}
/// Lock-free обслуживание статических файлов
/// Читает файлы из директории и отдает их с правильным Content-Type
async fn handle_static_file(
path: &str,
config: StaticFilesConfig,
) -> Result<Response<Body>, crate::common::FutriixError> {
// Убираем начальный слеш из пути
let clean_path = path.trim_start_matches('/');
// Определяем путь к файлу
let file_path = if clean_path.is_empty() {
format!("{}/index.html", config.directory)
} else {
format!("{}/{}", config.directory, clean_path)
};
match File::open(&file_path).await {
Ok(mut file) => {
let mut contents = Vec::new();
if let Err(e) = file.read_to_end(&mut contents).await {
crate::server::log_to_file(&format!("Failed to read file {}: {}", file_path, e));
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Internal server error"))
.unwrap());
}
let content_type = get_content_type(&file_path);
Ok(Response::builder()
.header("Content-Type", content_type)
.body(Body::from(contents))
.unwrap())
}
Err(e) => {
crate::server::log_to_file(&format!("File not found: {} (error: {})", file_path, e));
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("File not found"))
.unwrap())
}
}
}
/// Определение Content-Type по расширению файла
/// Сопоставляет расширения файлов с соответствующими MIME-типами
fn get_content_type(file_path: &str) -> &'static str {
if file_path.ends_with(".html") || file_path.ends_with(".htm") {
"text/html; charset=utf-8"
} else if file_path.ends_with(".css") {
"text/css; charset=utf-8"
} else if file_path.ends_with(".js") {
"application/javascript; charset=utf-8"
} else if file_path.ends_with(".png") {
"image/png"
} else if file_path.ends_with(".jpg") || file_path.ends_with(".jpeg") {
"image/jpeg"
} else if file_path.ends_with(".json") {
"application/json; charset=utf-8"
} else if file_path.ends_with(".ico") {
"image/x-icon"
} else if file_path.ends_with(".svg") {
"image/svg+xml"
} else if file_path.ends_with(".txt") {
"text/plain; charset=utf-8"
} else {
"application/octet-stream"
}
}
/// Запуск HTTP сервера с lock-free архитектурой
/// Создает сервер Hyper, настраивает обработчики и запускает его
pub async fn start_http_server(
addr: &str,
db: Arc<Database>,
static_config: StaticFilesConfig,
http_config: HttpConfig,
acl_config: AclConfig,
) -> Result<(), crate::common::FutriixError> {
let addr_parsed: std::net::SocketAddr = addr.parse()
.map_err(|e: std::net::AddrParseError| crate::common::FutriixError::HttpError(e.to_string()))?;
let db_clone = db.clone();
let static_clone = static_config.clone();
let acl_clone = acl_config.clone();
// Создание lock-free сервиса
let make_svc = make_service_fn(move |_conn| {
let db = db_clone.clone();
let static_config = static_clone.clone();
let acl_config = acl_clone.clone();
async move {
Ok::<_, hyper::Error>(service_fn(move |req| {
let db = db.clone();
let static_config = static_config.clone();
let acl_config = acl_config.clone();
async move {
handle_request(req, db, static_config, acl_config).await
}
}))
}
});
crate::server::log_to_file(&format!("HTTP server starting on {}", addr));
// Запускаем сервер
let server = Server::bind(&addr_parsed).serve(make_svc);
if let Err(e) = server.await {
crate::server::log_to_file(&format!("HTTP server error: {}", e));
return Err(crate::common::FutriixError::HttpError(e.to_string()));
}
Ok(())
}
/// Запуск HTTPS сервера с lock-free архитектурой
/// Настраивает TLS и запускает защищенный сервер
pub async fn start_https_server(
addr: &str,
db: Arc<Database>,
static_config: StaticFilesConfig,
tls_config: TlsConfig,
acl_config: AclConfig,
) -> Result<(), crate::common::FutriixError> {
use tokio::net::TcpListener;
if !tls_config.enabled {
crate::server::log_to_file("HTTPS disabled: TLS not enabled");
return Ok(());
}
crate::server::log_to_file(&format!("HTTPS server would start on {} (TLS configuration needed)", addr));
// Запускаем обычный HTTP сервер на HTTPS порту для тестирования
let http_config = HttpConfig {
enabled: true,
port: 8443,
http2_enabled: false,
};
let owned_addr = addr.to_string();
let owned_db = db.clone();
let owned_static_config = static_config.clone();
let owned_acl_config = acl_config.clone();
let server_future = async move {
start_http_server(&owned_addr, owned_db, owned_static_config, http_config, owned_acl_config).await
};
tokio::spawn(async move {
if let Err(e) = server_future.await {
crate::server::log_to_file(&format!("HTTPS (HTTP fallback) server error: {}", e));
}
});
Ok(())
}