[Add] basic split
This commit is contained in:
parent
a0ff6b1082
commit
3cdf7c62d5
77 changed files with 506 additions and 648 deletions
56
web-ui/Cargo.toml
Normal file
56
web-ui/Cargo.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[package]
|
||||
name = "web-ui"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" }
|
||||
ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" }
|
||||
|
||||
actix-web = { version = "4", features = ["rustls-0_23"] }
|
||||
actix-web-actors = "4"
|
||||
aes-gcm = "0.10.3"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.43"
|
||||
crossterm = "*"
|
||||
dashmap = "6.1.0"
|
||||
futures = "*"
|
||||
futures-util = "*"
|
||||
hex = "*"
|
||||
hkdf = "0.12.4"
|
||||
hyper = { version = "1.8.1", features = [
|
||||
"capi",
|
||||
"client",
|
||||
"full",
|
||||
"http1",
|
||||
"http2",
|
||||
"nightly",
|
||||
"server",
|
||||
] }
|
||||
hyper-util = { version = "*" }
|
||||
json = "*"
|
||||
lazy_static = "1.5.0"
|
||||
once_cell = "1.21.3"
|
||||
open = "5.3.3"
|
||||
pnet = "0.35.0"
|
||||
rand = "0.8"
|
||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
||||
ratatui = "0.30.0"
|
||||
reqwest = "0.13.2"
|
||||
rusqlite = "0.39.0"
|
||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||
rustls-pemfile = "2.2.0"
|
||||
serde_json = "1.0.149"
|
||||
sha2 = "0.10.9"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.27.2"
|
||||
sysinfo = "0.38.3"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
||||
tungstenite = "*"
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
walkdir = "2.5.0"
|
||||
warp = "*"
|
||||
x448 = { version = "*" }
|
||||
zip = "6.0.0"
|
||||
190
web-ui/src/api.rs
Executable file
190
web-ui/src/api.rs
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
use crate::server::server::is_local_network;
|
||||
use crate::util::config_util::CONFIG;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
use serde_json::{Value, json};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn api_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/api")
|
||||
.route("/shutdown/", web::post().to(shutdown))
|
||||
.route("/reload/", web::post().to(reload))
|
||||
.route("/users/add/", web::post().to(users_add))
|
||||
.route("/users/remove/", web::post().to(users_remove))
|
||||
.route("/users/get/", web::get().to(users_get))
|
||||
.route("/communities/add/", web::post().to(communities_add))
|
||||
.route("/communities/get/", web::get().to(communities_get))
|
||||
.route("/settings/set/", web::post().to(settings_set))
|
||||
.route("/settings/get/", web::get().to(settings_get)),
|
||||
);
|
||||
}
|
||||
|
||||
async fn settings_set(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let key = req.headers().get("key").and_then(|v| v.to_str().ok());
|
||||
let value = req.headers().get("value").and_then(|v| v.to_str().ok());
|
||||
|
||||
match (key, value) {
|
||||
(Some(k), Some(v)) => {
|
||||
let _ = CONFIG
|
||||
.write()
|
||||
.await
|
||||
.config
|
||||
.insert(&k.to_string(), v.to_string());
|
||||
|
||||
success()
|
||||
}
|
||||
_ => error(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn settings_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
let config = CONFIG.read().await.config.clone();
|
||||
let serde_config: Value = serde_json::to_value(config.to_string()).unwrap();
|
||||
HttpResponse::Ok().json(serde_config)
|
||||
}
|
||||
|
||||
async fn communities_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let communities = crate::communities::community_manager::get_communities().await;
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
for c in communities {
|
||||
let val = c.frontend().await;
|
||||
let s_val: Value = serde_json::to_value(val.to_string()).unwrap();
|
||||
list.push(s_val);
|
||||
}
|
||||
HttpResponse::Ok().json(list)
|
||||
}
|
||||
|
||||
async fn communities_add(
|
||||
req: HttpRequest,
|
||||
ssl: web::Data<bool>,
|
||||
payload: web::Json<Value>,
|
||||
) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let name = payload["name"].as_str().unwrap_or("").to_string();
|
||||
let owner = payload["owner"].as_i64().unwrap_or(0);
|
||||
|
||||
let community = Arc::new(crate::communities::community::Community::create(name, owner).await);
|
||||
|
||||
crate::communities::community_manager::add_community(community).await;
|
||||
|
||||
success()
|
||||
}
|
||||
|
||||
async fn users_get(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let users = crate::users::user_manager::get_users();
|
||||
|
||||
let list: Vec<_> = users
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
let val = u.frontend();
|
||||
serde_json::to_value(val.to_string()).unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
HttpResponse::Ok().json(list)
|
||||
}
|
||||
|
||||
async fn users_remove(
|
||||
req: HttpRequest,
|
||||
ssl: web::Data<bool>,
|
||||
payload: web::Json<Value>,
|
||||
) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
|
||||
crate::users::user_manager::remove_user(uuid);
|
||||
crate::users::user_manager::save_users();
|
||||
|
||||
success()
|
||||
}
|
||||
|
||||
async fn users_add(
|
||||
req: HttpRequest,
|
||||
ssl: web::Data<bool>,
|
||||
payload: web::Json<Value>,
|
||||
) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
let username = match payload.get("username").and_then(|v| v.as_str()) {
|
||||
Some(u) => u,
|
||||
_ => return error(),
|
||||
};
|
||||
|
||||
if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await {
|
||||
let val = user.frontend();
|
||||
let s_val: Value = serde_json::to_value(val.to_string()).unwrap();
|
||||
HttpResponse::Ok().json(s_val)
|
||||
} else {
|
||||
error()
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
*crate::SHUTDOWN.write().await = true;
|
||||
success()
|
||||
}
|
||||
|
||||
async fn reload(req: HttpRequest, ssl: web::Data<bool>) -> impl Responder {
|
||||
if !is_allowed_req(&req, *ssl.get_ref()) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
*crate::SHUTDOWN.write().await = true;
|
||||
*crate::RELOAD.write().await = true;
|
||||
|
||||
success()
|
||||
}
|
||||
|
||||
fn forbidden() -> HttpResponse {
|
||||
HttpResponse::Forbidden().body("403 Forbidden")
|
||||
}
|
||||
|
||||
fn success() -> HttpResponse {
|
||||
HttpResponse::Ok().json(json!({ "type": "success" }))
|
||||
}
|
||||
|
||||
fn error() -> HttpResponse {
|
||||
HttpResponse::Ok().json(json!({ "type": "error" }))
|
||||
}
|
||||
|
||||
fn is_allowed(addr: SocketAddr, ssl: bool) -> bool {
|
||||
is_local_network(addr.ip()) || ssl
|
||||
}
|
||||
|
||||
fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool {
|
||||
if let Some(addr) = req.peer_addr() {
|
||||
is_allowed(addr, ssl)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
3
web-ui/src/lib.rs
Normal file
3
web-ui/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod api;
|
||||
pub mod server;
|
||||
pub mod web_path_parser;
|
||||
169
web-ui/src/server.rs
Normal file
169
web-ui/src/server.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use crate::log;
|
||||
use crate::server::api::api_config;
|
||||
use crate::server::web_path_parser;
|
||||
use crate::util::file_util::load_file_buf;
|
||||
use crate::{ACTIVE_TASKS, SHUTDOWN};
|
||||
use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web};
|
||||
use actix_web_actors::ws;
|
||||
use rustls::ServerConfig;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
io::{self, BufReader, ErrorKind},
|
||||
net::IpAddr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result<impl Responder, Error> {
|
||||
let path = req.path().to_string();
|
||||
log!("WS connection from {:?}", req.peer_addr());
|
||||
let session = WsSession::new(path);
|
||||
ws::start(session, &req, stream)
|
||||
}
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub async fn start(port: u16) -> bool {
|
||||
let (tx, rx) = oneshot::channel::<ServerHandle>();
|
||||
|
||||
let _ = tokio::spawn(async move {
|
||||
let server = match load_tls_config() {
|
||||
Ok(Some(tls_config)) => {
|
||||
log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port);
|
||||
let _config = (*tls_config).clone();
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(true))
|
||||
.configure(api_config)
|
||||
.service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler)))
|
||||
.default_service(web::to(web_path_parser::handle))
|
||||
})
|
||||
.bind(("0.0.0.0", port))
|
||||
.unwrap()
|
||||
.run()
|
||||
}
|
||||
Ok(_) => {
|
||||
log!("HTTP Server running on 0.0.0.0:{}", port);
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(false))
|
||||
.configure(api_config)
|
||||
.service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler)))
|
||||
.default_service(web::to(web_path_parser::handle))
|
||||
})
|
||||
.bind(("0.0.0.0", port))
|
||||
.unwrap()
|
||||
.run()
|
||||
}
|
||||
Err(e) => {
|
||||
log!("TLS config error: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let server_handle = server.handle();
|
||||
tx.send(server_handle).unwrap();
|
||||
|
||||
ACTIVE_TASKS.insert("WebServer".into());
|
||||
server.await.unwrap();
|
||||
ACTIVE_TASKS.remove("WebServer");
|
||||
log!("Web Server shutdown complete.");
|
||||
});
|
||||
|
||||
if let Ok(server_handle) = rx.await {
|
||||
tokio::spawn(async move {
|
||||
wait_for_shutdown(server_handle).await;
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_shutdown(server_handle: ServerHandle) {
|
||||
loop {
|
||||
if *SHUTDOWN.read().await {
|
||||
log!("Shutdown signal received.");
|
||||
server_handle.stop(true).await;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn load_tls_config() -> Result<Option<Arc<ServerConfig>>, Box<dyn StdError>> {
|
||||
let cert_file_res = load_file_buf("certs", "cert.pem");
|
||||
let key_file_res = load_file_buf("certs", "cert.key");
|
||||
|
||||
let cert_file_buf = match cert_file_res {
|
||||
Ok(b) => b,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
log!("TLS certificate 'certs/cert.pem' not found.");
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => return Err(e.into()), // Other IO error
|
||||
};
|
||||
|
||||
let key_file_buf = match key_file_res {
|
||||
Ok(b) => b,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
log!("TLS key 'certs/cert.key' not found.");
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => return Err(e.into()), // Other IO error
|
||||
};
|
||||
|
||||
let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf))
|
||||
.collect::<Result<Vec<CertificateDer>, _>>()?;
|
||||
|
||||
// PKCS8
|
||||
let mut key_reader = BufReader::new(key_file_buf);
|
||||
let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader)
|
||||
.map(|r| r.map(Into::into))
|
||||
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
|
||||
|
||||
if key_ders.is_empty() {
|
||||
// RSA
|
||||
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
|
||||
key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader)
|
||||
.map(|r| r.map(Into::into))
|
||||
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
|
||||
}
|
||||
|
||||
if key_ders.is_empty() {
|
||||
// EC
|
||||
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
|
||||
key_ders = rustls_pemfile::ec_private_keys(&mut key_reader)
|
||||
.map(|r| r.map(Into::into))
|
||||
.collect::<Result<Vec<PrivateKeyDer>, _>>()?;
|
||||
}
|
||||
|
||||
if key_ders.is_empty() {
|
||||
return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into());
|
||||
}
|
||||
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, key_ders.remove(0))
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Some(Arc::new(config)))
|
||||
}
|
||||
|
||||
pub fn is_local_network(addr: IpAddr) -> bool {
|
||||
match addr {
|
||||
IpAddr::V4(v4) => {
|
||||
let o = v4.octets();
|
||||
o[0] == 10
|
||||
|| (o[0] == 172 && (16..=31).contains(&o[1]))
|
||||
|| (o[0] == 192 && o[1] == 168)
|
||||
|| o[0] == 127
|
||||
|| (o[0] == 169 && o[1] == 254)
|
||||
}
|
||||
IpAddr::V6(v6) => {
|
||||
let s = v6.segments();
|
||||
(s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback()
|
||||
}
|
||||
}
|
||||
}
|
||||
77
web-ui/src/web_path_parser.rs
Executable file
77
web-ui/src/web_path_parser.rs
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::util::file_util::load_file_vec;
|
||||
|
||||
fn codec_for_ext(ext: &str) -> &'static str {
|
||||
match ext {
|
||||
"html" => "text/html; charset=utf-8",
|
||||
"css" => "text/css",
|
||||
"js" => "application/javascript",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"ico" => "image/x-icon",
|
||||
"woff2" => "font/woff2",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle(req: HttpRequest) -> HttpResponse {
|
||||
let req_path = req.path().trim_start_matches('/');
|
||||
|
||||
let mut fs_path = PathBuf::from("web");
|
||||
|
||||
if req_path.is_empty() {
|
||||
fs_path.push("index.html");
|
||||
} else {
|
||||
fs_path.extend(req_path.split('/'));
|
||||
}
|
||||
|
||||
if fs_path.is_dir() {
|
||||
fs_path.push("index.html");
|
||||
}
|
||||
|
||||
let ext_opt = fs_path.extension().and_then(|e| e.to_str());
|
||||
let mut final_name = fs_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if ext_opt.is_none() {
|
||||
if final_name.is_empty() {
|
||||
final_name = "index.html".to_string();
|
||||
} else {
|
||||
final_name.push_str(".html");
|
||||
}
|
||||
}
|
||||
|
||||
let content_type = codec_for_ext(
|
||||
fs_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("html"),
|
||||
);
|
||||
|
||||
let dir = fs_path.parent().unwrap_or(Path::new("web"));
|
||||
|
||||
match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) {
|
||||
Ok(content) => HttpResponse::Ok().content_type(content_type).body(content),
|
||||
|
||||
Err(_) => {
|
||||
let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
if matches!(ext, "js" | "css" | "woff2") {
|
||||
return HttpResponse::NotFound()
|
||||
.content_type("text/plain")
|
||||
.body("Not found");
|
||||
}
|
||||
|
||||
let fallback = load_file_vec("web", "404.html")
|
||||
.unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec());
|
||||
|
||||
HttpResponse::NotFound()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue