355 lines
13 KiB
Rust
355 lines
13 KiB
Rust
|
|
// 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(())
|
|||
|
|
}
|