flusql/src/http.rs

355 lines
13 KiB
Rust
Raw Normal View History

2026-01-08 18:30:33 +03:00
// 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(())
}