From 8b954f757b333265d747a0ad588defe0a796e545 Mon Sep 17 00:00:00 2001 From: netadmin Date: Fri, 23 Oct 2026 12:36:37 +0300 Subject: [PATCH] a.0.0.3 --- .identity_8443.json | 5 + Cargo.lock | 178 +----------------------------- Cargo.toml | 4 +- src/discovery.rs | 113 ------------------- src/main.rs | 248 ++++++++++++++++++++++++++++++++++++------ src/mdns.rs | 60 ---------- src/models.rs | 103 ++++++++++++++++-- src/p2p.rs | 48 +++++++- src/static/index.html | 116 ++++++++++++-------- src/tls.rs | 70 ++++++++---- src/ui.rs | 146 ++++++++++++++----------- 11 files changed, 563 insertions(+), 528 deletions(-) create mode 100644 .identity_8443.json delete mode 100644 src/discovery.rs delete mode 100644 src/mdns.rs diff --git a/.identity_8443.json b/.identity_8443.json new file mode 100644 index 0000000..4387ddc --- /dev/null +++ b/.identity_8443.json @@ -0,0 +1,5 @@ +{ + "cert_hex": "308201633082010aa00302010202146efbf8af961267a91917c46293c5bd3d935ae12e300a06082a8648ce3d0403023021311f301d06035504030c16726367656e2073656c66207369676e656420636572743020170d3735303130313030303030305a180f34303936303130313030303030305a3021311f301d06035504030c16726367656e2073656c66207369676e656420636572743059301306072a8648ce3d020106082a8648ce3d0301070342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256a31e301c301a0603551d110413301182096c6f63616c686f73748704c0a80aeb300a06082a8648ce3d040302034700304402204437caaf5909dd019cee3d49a2cc7f4e8b8e10b6365b6a4bbaa23ce2db902e820220493a698f69b58bf1968ae3eb00dec0254ed586d6366c2ae6dfec272187253833", + "key_hex": "308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420758dc92a43dc0c8ed274a080e3d890f5ae8530a4828c499d5470f40994a2d6e0a1440342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256", + "fingerprint": "ebc9c6df58d1637e2998d70976f463a910a59459f87157932690f93d26aaa930" +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1352532..92f53d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,23 +197,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core", -] - [[package]] name = "cmake" version = "0.1.58" @@ -232,15 +215,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -465,10 +439,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -478,11 +450,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", - "rand_core", - "wasm-bindgen", ] [[package]] @@ -607,7 +576,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -825,12 +793,6 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -[[package]] -name = "lru-slab" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" - [[package]] name = "matchit" version = "0.7.3" @@ -976,62 +938,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quinn" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.5", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" -dependencies = [ - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.5", - "tracing", - "windows-sys 0.61.2", -] - [[package]] name = "quote" version = "1.0.47" @@ -1049,7 +955,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "ramshare_ui" -version = "0.2.0" +version = "0.3.0" dependencies = [ "axum", "axum-server", @@ -1066,32 +972,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "rand" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core", -] - [[package]] name = "rcgen" version = "0.13.2" @@ -1133,7 +1013,6 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "serde", @@ -1149,7 +1028,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", ] [[package]] @@ -1166,12 +1044,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - [[package]] name = "rustls" version = "0.23.46" @@ -1202,7 +1074,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "web-time", "zeroize", ] @@ -1309,7 +1180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1421,26 +1292,6 @@ dependencies = [ "syn 3.0.9", ] -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "time" version = "0.3.55" @@ -1470,12 +1321,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - [[package]] name = "tokio" version = "1.53.3" @@ -1723,25 +1568,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 15e3d35..e149dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ramshare_ui" -version = "0.2.0" +version = "0.3.0" edition = "2021" [dependencies] @@ -10,7 +10,7 @@ axum-server = { version = "0.7", features = ["tls-rustls"] } bytes = "1.7" socket2 = "0.5" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-no-provider", "json"] } rcgen = "0.13" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } diff --git a/src/discovery.rs b/src/discovery.rs deleted file mode 100644 index 3a62bd8..0000000 --- a/src/discovery.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::models::{AppState, PeerNode}; -use serde::{Deserialize, Serialize}; -use socket2::{Domain, Protocol, Socket, Type}; -use std::{net::SocketAddr, sync::Arc, time::Duration}; -use tokio::net::UdpSocket; - -const BROADCAST_PORT: u16 = 5354; -const BROADCAST_TARGET: &str = "255.255.255.255:5354"; - -#[derive(Serialize, Deserialize)] -struct BeaconPayload { - name: String, - port: u16, - fingerprint: String, -} - -pub fn start_discovery(state: Arc) { - // 1. Поток передачи маяка (Broadcast Transmitter) - let send_state = state.clone(); - tokio::spawn(async move { - let sender = match UdpSocket::bind("0.0.0.0:0").await { - Ok(s) => s, - Err(e) => { - eprintln!("[UDP Beacon] Не удалось открыть сокет отправки: {}", e); - return; - } - }; - let _ = sender.set_broadcast(true); - - loop { - let payload = BeaconPayload { - name: send_state.device_name.clone(), - port: send_state.p2p_port, - fingerprint: send_state.my_fingerprint.clone(), - }; - - if let Ok(data) = serde_json::to_vec(&payload) { - let _ = sender.send_to(&data, BROADCAST_TARGET).await; - } - tokio::time::sleep(Duration::from_secs(2)).await; - } - }); - - // 2. Поток прослушивания маяков (Broadcast Receiver) - tokio::spawn(async move { - let bind_addr: SocketAddr = format!("0.0.0.0:{}", BROADCAST_PORT).parse().unwrap(); - let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) - .expect("Ошибка создания UDP сокета"); - - // Кроссплатформенное разрешение повторного использования порта - let _ = socket.set_reuse_address(true); - let _ = socket.set_broadcast(true); - let _ = socket.set_nonblocking(true); - - if let Err(e) = socket.bind(&bind_addr.into()) { - eprintln!( - "[UDP Beacon] Порт {} занят. Фоновое автообнаружение ограничено: {}", - BROADCAST_PORT, e - ); - return; - } - - let std_socket: std::net::UdpSocket = socket.into(); - let receiver = match UdpSocket::from_std(std_socket) { - Ok(s) => s, - Err(e) => { - eprintln!("[UDP Beacon] Ошибка tokio::UdpSocket: {}", e); - return; - } - }; - - let mut buf = [0u8; 1500]; - loop { - match receiver.recv_from(&mut buf).await { - Ok((len, remote_addr)) => { - if let Ok(beacon) = serde_json::from_slice::(&buf[..len]) { - // Игнорируем собственный маяк - if beacon.fingerprint == state.my_fingerprint || beacon.fingerprint.len() < 8 { - continue; - } - - let ip_str = remote_addr.ip().to_string(); - let pin_code = beacon.fingerprint[..8].to_uppercase(); - - let mut peers = state.peers.write().unwrap(); - let is_verified = state - .trusted_fingerprints - .read() - .unwrap() - .get(&ip_str) - .map(|trusted_fp| trusted_fp.eq_ignore_ascii_case(&beacon.fingerprint)) - .unwrap_or(false); - - peers.insert( - ip_str.clone(), - PeerNode { - name: beacon.name, - ip: ip_str, - port: beacon.port, - fingerprint: beacon.fingerprint, - pin_code, - verified: is_verified, - }, - ); - } - } - Err(_) => { - tokio::time::sleep(Duration::from_millis(100)).await; - } - } - } - }); -} diff --git a/src/main.rs b/src/main.rs index cf2fc78..b720ba7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod discovery; mod models; mod p2p; mod tls; @@ -11,27 +10,191 @@ use axum::{ }; use axum_server::tls_rustls::RustlsConfig; use local_ip_address::local_ip; -use models::{AppState, MAX_PAYLOAD_SIZE, MAX_RAM_TEXTS, MEDIA_DIR}; +use models::{ + now_sec, AppState, BeaconPacket, DISCOVERY_PORT, MAX_PAYLOAD_SIZE, MAX_RAM_TEXTS, MEDIA_DIR, +}; +use socket2::{Domain, Protocol, Socket, Type}; use std::{ collections::{HashMap, VecDeque}, - net::SocketAddr, + net::{Ipv4Addr, SocketAddr, UdpSocket as StdUdpSocket}, sync::{Arc, RwLock}, + time::Duration, }; +use tokio::net::UdpSocket; + +/// Точное определение основного LAN IP через таблицу маршрутизации ядра ОС (игнорирует Docker/WSL) +fn detect_lan_ip() -> String { + if let Ok(sock) = StdUdpSocket::bind("0.0.0.0:0") { + if sock.connect("8.8.8.8:80").is_ok() { + if let Ok(local_addr) = sock.local_addr() { + let ip = local_addr.ip().to_string(); + if !ip.starts_with("127.") && !ip.starts_with("172.17.") { + return ip; + } + } + } + } + local_ip() + .map(|addr| addr.to_string()) + .unwrap_or_else(|_| "127.0.0.1".to_string()) +} + +/// Вычисляет широковещательный адрес подсети /24 (например, 192.168.1.255) +fn subnet_broadcast_addr(ip_str: &str, port: u16) -> Option { + let ipv4: Ipv4Addr = ip_str.parse().ok()?; + let octets = ipv4.octets(); + Some(format!("{}.{}.{}.255:{}", octets[0], octets[1], octets[2], port)) +} + +/// Опрос всех известных узлов и актуализация их статуса +pub async fn ping_all_peers(state: &Arc) { + let targets: Vec<(String, Vec, u16)> = { + let guard = state.peers.read().unwrap(); + guard + .values() + .map(|p| (p.id.clone(), p.ips.clone(), p.port)) + .collect() + }; + + for (peer_id, ips, port) in targets { + let mut reachable = false; + for ip in ips { + let url = format!("https://{}:{}/p2p/info", ip, port); + let res = state + .probe_client + .get(&url) + .header("X-Node-Fp", &state.my_fingerprint) + .header("X-Node-Name", &state.device_name) + .header("X-Node-Ip", &state.my_ip) + .header("X-Node-Port", state.p2p_port.to_string()) + .send() + .await; + + if let Ok(resp) = res { + if resp.status().is_success() { + if let Ok(info) = resp.json::().await { + let name = info["name"].as_str().unwrap_or("Node").to_string(); + let fp = info["fingerprint"].as_str().unwrap_or("").to_string(); + if !fp.is_empty() { + state.upsert_peer(name, ip, port, fp); + reachable = true; + break; + } + } + } + } + } + + if !reachable { + let mut guard = state.peers.write().unwrap(); + if let Some(peer) = guard.get_mut(&peer_id) { + if now_sec().saturating_sub(peer.last_seen) > 15 { + peer.online = false; + } + } + } + } +} + +/// Фоновый UDP-маяк (рассылает пакеты и в направленный broadcast подсети, и в 255.255.255.255) +fn start_udp_discovery(state: Arc) { + // 1. Поток рассылки маяков + let tx_state = state.clone(); + tokio::spawn(async move { + let bind_target = format!("{}:0", tx_state.my_ip); + let sender = match UdpSocket::bind(&bind_target).await { + Ok(s) => s, + Err(_) => match UdpSocket::bind("0.0.0.0:0").await { + Ok(s) => s, + Err(e) => { + tx_state.add_log(format!("Ошибка открытия UDP сокета отправки: {}", e)); + return; + } + }, + }; + let _ = sender.set_broadcast(true); + + let global_bcast = format!("255.255.255.255:{}", DISCOVERY_PORT); + let subnet_bcast = subnet_broadcast_addr(&tx_state.my_ip, DISCOVERY_PORT); + + loop { + let packet = BeaconPacket { + name: tx_state.device_name.clone(), + ip: tx_state.my_ip.clone(), + port: tx_state.p2p_port, + fingerprint: tx_state.my_fingerprint.clone(), + }; + + if let Ok(bytes) = serde_json::to_vec(&packet) { + if let Some(ref sub_target) = subnet_bcast { + let _ = sender.send_to(&bytes, sub_target).await; + } + let _ = sender.send_to(&bytes, &global_bcast).await; + } + tokio::time::sleep(Duration::from_secs(3)).await; + } + }); + + // 2. Поток приема маяков + tokio::spawn(async move { + let bind_addr: SocketAddr = format!("0.0.0.0:{}", DISCOVERY_PORT).parse().unwrap(); + let socket = match Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) { + Ok(s) => s, + Err(_) => return, + }; + + let _ = socket.set_reuse_address(true); + let _ = socket.set_broadcast(true); + let _ = socket.set_nonblocking(true); + + if let Err(e) = socket.bind(&bind_addr.into()) { + state.add_log(format!("UDP порт {} занят ({})", DISCOVERY_PORT, e)); + return; + } + + let std_sock: StdUdpSocket = socket.into(); + let receiver = match UdpSocket::from_std(std_sock) { + Ok(s) => s, + Err(_) => return, + }; + + let mut buf = [0u8; 2048]; + loop { + match receiver.recv_from(&mut buf).await { + Ok((len, remote_addr)) => { + if let Ok(beacon) = serde_json::from_slice::(&buf[..len]) { + if beacon.fingerprint == state.my_fingerprint { + continue; + } + let peer_ip = if !beacon.ip.is_empty() && beacon.ip != "0.0.0.0" { + beacon.ip + } else { + remote_addr.ip().to_string() + }; + state.upsert_peer(beacon.name, peer_ip, beacon.port, beacon.fingerprint); + } + } + Err(_) => { + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + } + }); +} #[tokio::main] async fn main() { - // 0. Регистрация глобального CryptoProvider (ring) для rustls 0.23+ let _ = rustls::crypto::ring::default_provider().install_default(); - let args: Vec = std::env::args().collect(); - let mut device_name = "My-Device".to_string(); let mut p2p_port: u16 = 8443; let mut ui_port: u16 = 8080; let mut custom_ip: Option = None; + let args: Vec = std::env::args().collect(); let mut i = 1; let mut positional_idx = 0; + while i < args.len() { match args[i].as_str() { "--ip" => { @@ -88,28 +251,22 @@ async fn main() { let (bind_ip, my_ip) = match custom_ip { Some(ip) => { if ip == "0.0.0.0" { - let detected = local_ip() - .map(|addr| addr.to_string()) - .unwrap_or_else(|_| "127.0.0.1".to_string()); - ("0.0.0.0".to_string(), detected) + ("0.0.0.0".to_string(), detect_lan_ip()) } else { (ip.clone(), ip) } } - None => { - let detected = local_ip().expect("Не удалось определить локальный IP").to_string(); - ("0.0.0.0".to_string(), detected) - } + None => ("0.0.0.0".to_string(), detect_lan_ip()), }; tokio::fs::create_dir_all(MEDIA_DIR) .await - .expect("Не удалось создать папку received_media на диске"); + .expect("Не удалось создать папку received_media"); - // 1. Генерация сертификата в ОЗУ под выбранный IP и настройка HTTPS-клиентов - let (cert_der, key_der, my_fingerprint) = tls::generate_in_memory_cert(&my_ip); - let trusted_fingerprints = Arc::new(RwLock::new(HashMap::new())); - let http_client = tls::build_pinned_http_client(trusted_fingerprints.clone()); + // Загружаем постоянный сертификат и PIN-код (или создаем его один раз) + let (cert_der, key_der, my_fingerprint) = tls::load_or_create_identity(&my_ip, p2p_port); + let peers = Arc::new(RwLock::new(HashMap::new())); + let http_client = tls::build_pinned_http_client(peers.clone()); let probe_client = tls::build_probe_http_client(); let state = Arc::new(AppState { @@ -117,18 +274,37 @@ async fn main() { my_ip: my_ip.clone(), p2p_port, my_fingerprint: my_fingerprint.clone(), + ping_interval_sec: 5, ram_texts: RwLock::new(VecDeque::with_capacity(MAX_RAM_TEXTS)), disk_media: RwLock::new(VecDeque::new()), - peers: RwLock::new(HashMap::new()), - trusted_fingerprints, + peers, http_client, probe_client, + transfers: RwLock::new(Vec::new()), + logs: RwLock::new(VecDeque::new()), }); - // 2. Запуск широковещательного UDP-маяка на порт 5354 - discovery::start_discovery(state.clone()); + state.add_log(format!( + "Узел '{}' запущен на {}:{} (PIN: {})", + device_name, + my_ip, + p2p_port, + &my_fingerprint[..8].to_uppercase() + )); - // 3. Запуск внешнего P2P HTTPS-сервера + // Запуск UDP-обнаружения + start_udp_discovery(state.clone()); + + // Фоновый опрос известных узлов + let ping_state = state.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(ping_state.ping_interval_sec)).await; + ping_all_peers(&ping_state).await; + } + }); + + // Запуск P2P HTTPS-сервера let tls_config = RustlsConfig::from_der(vec![cert_der], key_der) .await .expect("Ошибка конфигурации TLS"); @@ -142,7 +318,7 @@ async fn main() { let p2p_addr: SocketAddr = format!("{}:{}", bind_ip, p2p_port) .parse() - .unwrap_or_else(|_| panic!("Некорректный сокет: {}:{}", bind_ip, p2p_port)); + .unwrap_or_else(|_| panic!("Некорректный адрес P2P: {}:{}", bind_ip, p2p_port)); tokio::spawn(async move { axum_server::bind_rustls(p2p_addr, tls_config) @@ -151,11 +327,12 @@ async fn main() { .expect("Ошибка P2P HTTPS-сервера"); }); - // 4. Запуск локального UI-сервера (127.0.0.1) + // Запуск локального UI-сервера (127.0.0.1) let ui_router = Router::new() .route("/", get(ui::ui_index)) .route("/ui/state", get(ui::ui_get_state)) .route("/ui/manual-peer", post(ui::ui_add_manual_peer)) + .route("/ui/ping", post(ui::ui_ping_peers)) .route("/ui/trust", post(ui::ui_trust_peer)) .route("/ui/clear-ram", post(ui::ui_clear_ram)) .route("/ui/send-text", post(ui::ui_send_text)) @@ -164,17 +341,26 @@ async fn main() { .layer(DefaultBodyLimit::max(MAX_PAYLOAD_SIZE)) .with_state(state); - let ui_addr: SocketAddr = format!("127.0.0.1:{}", ui_port).parse().unwrap(); + let ui_addr: SocketAddr = format!("127.0.0.1:{}", ui_port) + .parse() + .expect("Некорректный порт UI"); + println!("=============================================================="); println!(" Устройство: {}", device_name); println!(" P2P сокет (HTTPS): https://{}:{}", bind_ip, p2p_port); - println!(" Маяк обнаружения (UDP): 255.255.255.255:5354"); - println!(" КОД СВЕРКИ СЕРТИФИКАТА (PIN): [ {} ]", &my_fingerprint[..8].to_uppercase()); - println!(" Хранилище медиа: ./{}/", MEDIA_DIR); + println!(" Сетевой IP в локальной сети: {}", my_ip); + println!( + " ПОСТОЯННЫЙ PIN УСТРОЙСТВА: [ {} ]", + &my_fingerprint[..8].to_uppercase() + ); + println!(" Файл постоянной идентичности: .identity_{}.json", p2p_port); + println!(" Папка для медиа на диске: ./{}/", MEDIA_DIR); println!("--------------------------------------------------------------"); println!(" ОТКРОЙТЕ ИНТЕРФЕЙС В БРАУЗЕРЕ: http://127.0.0.1:{}", ui_port); println!("=============================================================="); - let listener = tokio::net::TcpListener::bind(ui_addr).await.unwrap(); + let listener = tokio::net::TcpListener::bind(ui_addr) + .await + .expect("Не удалось открыть порт локального UI"); axum::serve(listener, ui_router).await.unwrap(); } diff --git a/src/mdns.rs b/src/mdns.rs deleted file mode 100644 index b4904db..0000000 --- a/src/mdns.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::models::{AppState, PeerNode, SERVICE_TYPE}; -use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo}; -use std::sync::Arc; - -pub fn start_mdns_service(state: Arc) { - let mdns = ServiceDaemon::new().expect("Не удалось запустить mDNS"); - - let instance_name = format!("{}_{}", state.device_name, &state.my_fingerprint[..6]); - let host_name = format!("{}.local.", instance_name); - let properties = [ - ("fp", state.my_fingerprint.as_str()), - ("name", state.device_name.as_str()), - ]; - - let service_info = ServiceInfo::new( - SERVICE_TYPE, - &instance_name, - &host_name, - &state.my_ip, - state.p2p_port, - &properties[..], - ) - .expect("Ошибка создания mDNS записи"); - - mdns.register(service_info).expect("Ошибка регистрации mDNS"); - - let receiver = mdns.browse(SERVICE_TYPE).expect("Ошибка mDNS browse"); - tokio::spawn(async move { - while let Ok(event) = receiver.recv_async().await { - if let ServiceEvent::ServiceResolved(info) = event { - let fp = info - .get_property_val_str("fp") - .unwrap_or_default() - .to_string(); - let name = info - .get_property_val_str("name") - .unwrap_or("Unknown") - .to_string(); - - if fp == state.my_fingerprint || fp.len() < 8 { - continue; - } - - if let Some(ip) = info.get_addresses().iter().next() { - let ip_str = ip.to_string(); - let pin_code = fp[..8].to_uppercase(); - let peer = PeerNode { - name, - ip: ip_str.clone(), - port: info.get_port(), - fingerprint: fp, - pin_code, - verified: false, - }; - state.peers.write().unwrap().insert(ip_str, peer); - } - } - } - }); -} diff --git a/src/models.rs b/src/models.rs index 6bfcb0a..6638f3a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -7,16 +7,25 @@ use std::{ pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024; pub const MAX_RAM_TEXTS: usize = 50; +pub const MAX_LOGS: usize = 100; pub const MEDIA_DIR: &str = "received_media"; +pub const DISCOVERY_PORT: u16 = 5354; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SavedIdentity { + pub cert_hex: String, + pub key_hex: String, + pub fingerprint: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct TextItem { pub sender: String, pub text: String, pub timestamp: u64, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MediaItem { pub sender: String, pub filename: String, @@ -26,14 +35,25 @@ pub struct MediaItem { pub timestamp: u64, } -#[derive(Clone, Serialize, Deserialize)] -pub struct PeerNode { +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PeerDevice { + pub id: String, pub name: String, - pub ip: String, + pub ips: Vec, pub port: u16, pub fingerprint: String, pub pin_code: String, pub verified: bool, + pub online: bool, + pub last_seen: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BeaconPacket { + pub name: String, + pub ip: String, + pub port: u16, + pub fingerprint: String, } pub struct AppState { @@ -41,29 +61,91 @@ pub struct AppState { pub my_ip: String, pub p2p_port: u16, pub my_fingerprint: String, + pub ping_interval_sec: u64, pub ram_texts: RwLock>, pub disk_media: RwLock>, - pub peers: RwLock>, - pub trusted_fingerprints: Arc>>, + /// Ключ таблицы — постоянный fingerprint (ID) устройства + pub peers: Arc>>, pub http_client: reqwest::Client, pub probe_client: reqwest::Client, + pub transfers: RwLock>, + pub logs: RwLock>, +} + +impl AppState { + pub fn add_log(&self, msg: impl Into) { + let text = msg.into(); + println!("[LOG] {}", text); + let mut logs = self.logs.write().unwrap(); + if logs.len() >= MAX_LOGS { + logs.pop_back(); + } + logs.push_front(format!("[{}] {}", now_sec(), text)); + } + + /// Добавляет или обновляет найденное устройство по его постоянному fingerprint + pub fn upsert_peer(&self, name: String, ip: String, port: u16, fingerprint: String) { + if fingerprint == self.my_fingerprint || fingerprint.len() < 8 { + return; + } + if ip.is_empty() || ip == "0.0.0.0" || ip == "127.0.0.1" { + return; + } + + let pin_code = fingerprint[..8].to_uppercase(); + let now = now_sec(); + + let mut peers = self.peers.write().unwrap(); + if let Some(existing) = peers.get_mut(&fingerprint) { + existing.name = name; + existing.port = port; + existing.online = true; + existing.last_seen = now; + if let Some(pos) = existing.ips.iter().position(|x| x == &ip) { + existing.ips.remove(pos); + } + existing.ips.insert(0, ip); + } else { + drop(peers); + self.add_log(format!( + "Обнаружено устройство '{}' ({}:{}) PIN: [{}]", + name, ip, port, pin_code + )); + let mut peers = self.peers.write().unwrap(); + peers.insert( + fingerprint.clone(), + PeerDevice { + id: fingerprint.clone(), + name, + ips: vec![ip], + port, + fingerprint, + pin_code, + verified: false, + online: true, + last_seen: now, + }, + ); + } + } } #[derive(Serialize, Deserialize)] pub struct IncomingTextPayload { pub sender: String, + pub sender_fp: String, pub text: String, } #[derive(Deserialize)] pub struct UiSendTextRequest { - pub target_ip: String, + pub peer_id: String, pub text: String, } #[derive(Deserialize)] pub struct UiTrustRequest { - pub target_ip: String, + pub peer_id: String, pub trust: bool, } @@ -86,7 +168,8 @@ pub fn url_decode(input: &str) -> String { while let Some(b) = chars.next() { if b == b'%' { if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) { - if let Ok(decoded) = u8::from_str_radix(&format!("{}{}", c1 as char, c2 as char), 16) + if let Ok(decoded) = + u8::from_str_radix(&format!("{}{}", c1 as char, c2 as char), 16) { bytes.push(decoded); continue; diff --git a/src/p2p.rs b/src/p2p.rs index 5b0f294..9418f25 100644 --- a/src/p2p.rs +++ b/src/p2p.rs @@ -10,21 +10,52 @@ use axum::{ }; use std::{path::PathBuf, sync::Arc}; -/// Информационный эндпоинт для прямого зондирования узла -pub async fn p2p_info(State(state): State>) -> Json { +/// Публичный эндпоинт рукопожатия и взаимного обнаружения +pub async fn p2p_info( + State(state): State>, + headers: HeaderMap, +) -> Json { + let caller_fp = headers + .get("X-Node-Fp") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default() + .to_string(); + let caller_name = headers + .get("X-Node-Name") + .and_then(|v| v.to_str().ok()) + .map(url_decode) + .unwrap_or_default(); + let caller_ip = headers + .get("X-Node-Ip") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default() + .to_string(); + let caller_port: u16 = headers + .get("X-Node-Port") + .and_then(|v| v.to_str().ok()) + .and_then(|p| p.parse().ok()) + .unwrap_or(8443); + + if !caller_fp.is_empty() && !caller_ip.is_empty() { + state.upsert_peer(caller_name, caller_ip, caller_port, caller_fp); + } + Json(serde_json::json!({ "name": state.device_name, + "ip": state.my_ip, "port": state.p2p_port, "fingerprint": state.my_fingerprint, - "pin": state.my_fingerprint[..8].to_uppercase() + "pin_code": state.my_fingerprint[..8].to_uppercase() })) } -/// Прием текста -> СТРОГО В ОЗУ +/// Прием текста -> сохраняется СТРОГО В ОЗУ pub async fn p2p_receive_text( State(state): State>, Json(payload): Json, ) -> StatusCode { + state.add_log(format!("Получен текст в ОЗУ от '{}'", payload.sender)); + let mut ram_texts = state.ram_texts.write().unwrap(); if ram_texts.len() >= MAX_RAM_TEXTS { ram_texts.pop_back(); @@ -37,7 +68,7 @@ pub async fn p2p_receive_text( StatusCode::OK } -/// Прием медиаконтента -> НА ДИСК +/// Прием медиаконтента -> сохраняется НА ДИСК в папку ./received_media pub async fn p2p_receive_media( State(state): State>, headers: HeaderMap, @@ -77,6 +108,13 @@ pub async fn p2p_receive_media( .display() .to_string(); + state.add_log(format!( + "Сохранен медиафайл '{}' ({} байт) от '{}'", + final_filename, + body.len(), + sender + )); + let mut media_list = state.disk_media.write().unwrap(); media_list.push_front(MediaItem { sender, diff --git a/src/static/index.html b/src/static/index.html index a06b377..b6c3e95 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -3,17 +3,17 @@ -LAN Share — Текст в ОЗУ / Медиа на Диск +LAN Share — Постоянный PIN | Текст в ОЗУ | Медиа на Диск @@ -41,30 +44,33 @@
- Код сверки сертификата (PIN): + Постоянный PIN устройства: --------
- +
-

1. Устройства в сети UDP Broadcast

+

+ 1. Устройства в сети + +

- +
-
Поиск устройств через маяк 255.255.255.255:5354...
+
Поиск устройств в локальной сети...
- +
-

2. Отправка данных

+

2. Отправка данных (по нажатию)

@@ -76,7 +82,7 @@
- +

Входящий текст ТОЛЬКО В ОЗУ @@ -85,7 +91,7 @@

- +

Входящие медиафайлы НА ДИСКЕ (./received_media)

@@ -110,33 +116,43 @@ async function fetchState() { const data = await res.json(); document.getElementById('my-name').textContent = `Устройство: ${data.device_name}`; - document.getElementById('my-ip').textContent = `HTTPS P2P: ${data.my_ip}:${data.p2p_port} | SHA-256: ${data.my_fingerprint.slice(0, 24)}...`; + document.getElementById('my-ip').textContent = `Наш адрес: ${data.my_ip}:${data.p2p_port} | ID: ${data.my_fingerprint.slice(0, 20)}...`; document.getElementById('my-pin').textContent = data.my_pin; const peersContainer = document.getElementById('peers-list'); if (data.peers.length === 0) { - peersContainer.innerHTML = '
Устройства пока не найдены. Введите IP вручную, если в сети отключен broadcast.
'; + peersContainer.innerHTML = '
Устройства пока не найдены. Если роутер блокирует broadcast, введите IP соседа выше — оба устройства сразу увидят друг друга.
'; } else { - peersContainer.innerHTML = data.peers.map(p => ` -
-
-
${escapeHtml(p.name)} (${p.ip}:${p.port})
-
PIN: [${p.pin_code}] | Статус: ${p.verified ? 'Заверен' : 'Не заверен'}
+ peersContainer.innerHTML = data.peers.map(p => { + const ipStr = p.ips.join(', '); + const dotClass = p.online ? 'dot-on' : 'dot-off'; + return ` +
+
+
+ ${escapeHtml(p.name)} + (${escapeHtml(ipStr)}:${p.port}) +
+
+ PIN: [${p.pin_code}] | + Статус: ${p.verified ? 'Заверен' : 'Не заверен'} +
+
+
- -
- `).join(''); + `; + }).join(''); } - const currentSelectKey = data.peers.map(p => `${p.ip}:${p.verified}`).join('|'); + const currentSelectKey = data.peers.map(p => `${p.id}:${p.verified}:${p.ips[0]}`).join('|'); if (currentSelectKey !== knownPeersKey) { knownPeersKey = currentSelectKey; const select = document.getElementById('target-peer'); const prevVal = select.value; select.innerHTML = '' + - data.peers.map(p => ``).join(''); + data.peers.map(p => ``).join(''); if (prevVal) select.value = prevVal; } @@ -182,19 +198,27 @@ async function fetchState() { async function addManualPeer() { const input = document.getElementById('manual-ip-input'); - const ip = input.value.trim(); - if (!ip) return showStatus('Введите IP-адрес для поиска', true); + const raw = input.value.trim(); + if (!raw) return showStatus('Введите IP-адрес устройства', true); - showStatus(`Опрос узла ${ip}...`); + let ip = raw; + let port = 8443; + if (raw.includes(':')) { + const parts = raw.split(':'); + ip = parts[0]; + port = parseInt(parts[1], 10) || 8443; + } + + showStatus(`Связь с узлом ${ip}:${port}...`); const res = await fetch('/ui/manual-peer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ip, port: 8443 }) + body: JSON.stringify({ ip, port }) }); if (res.ok) { input.value = ''; - showStatus(`Узел ${ip} успешно найден!`); + showStatus(`Узел ${ip} подключен (оба устройства обменялись PIN-кодами)!`); fetchState(); } else { const err = await res.text(); @@ -202,34 +226,40 @@ async function addManualPeer() { } } -async function toggleTrust(ip, trust) { +async function pingPeers() { + await fetch('/ui/ping', { method: 'POST' }); + showStatus('Запущен опрос узлов сети'); + setTimeout(fetchState, 600); +} + +async function toggleTrust(peer_id, trust) { const res = await fetch('/ui/trust', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ target_ip: ip, trust }) + body: JSON.stringify({ peer_id, trust }) }); if (res.ok) { - showStatus(trust ? `Сертификат узла ${ip} заверен в ОЗУ` : `Доверие узлу ${ip} отозвано`); + showStatus(trust ? 'Сертификат устройства заверен' : 'Доверие устройству отозвано'); fetchState(); } } async function sendText() { - const target_ip = document.getElementById('target-peer').value; + const peer_id = document.getElementById('target-peer').value; const input = document.getElementById('text-input'); const text = input.value.trim(); - if (!target_ip) return showStatus('Выберите устройство-получатель!', true); + if (!peer_id) return showStatus('Выберите устройство-получатель!', true); if (!text) return showStatus('Введите текст для отправки!', true); const res = await fetch('/ui/send-text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ target_ip, text }) + body: JSON.stringify({ peer_id, text }) }); if (res.ok) { input.value = ''; - showStatus(`Текст доставлен в ОЗУ узла ${target_ip}`); + showStatus('Текст успешно доставлен в ОЗУ получателя'); } else { const err = await res.text(); showStatus(err, true); @@ -237,9 +267,9 @@ async function sendText() { } async function sendMedia() { - const target_ip = document.getElementById('target-peer').value; + const peer_id = document.getElementById('target-peer').value; const fileInput = document.getElementById('media-input'); - if (!target_ip) return showStatus('Выберите устройство-получатель!', true); + if (!peer_id) return showStatus('Выберите устройство-получатель!', true); if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true); const file = fileInput.files[0]; @@ -248,7 +278,7 @@ async function sendMedia() { const res = await fetch('/ui/send-media', { method: 'POST', headers: { - 'X-Target-Ip': target_ip, + 'X-Target-Id': peer_id, 'X-Filename': encodeURIComponent(file.name), 'Content-Type': file.type || 'application/octet-stream' }, @@ -257,7 +287,7 @@ async function sendMedia() { if (res.ok) { fileInput.value = ''; - showStatus(`Файл "${file.name}" сохранен на диске узла ${target_ip}`); + showStatus(`Файл "${file.name}" сохранен на диске получателя`); } else { const err = await res.text(); showStatus(err, true); diff --git a/src/tls.rs b/src/tls.rs index 3c4e708..40c1c7f 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,3 +1,4 @@ +use crate::models::{PeerDevice, SavedIdentity}; use rcgen::{generate_simple_self_signed, CertifiedKey}; use rustls::{ client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, @@ -8,20 +9,21 @@ use rustls::{ use sha2::{Digest, Sha256}; use std::{ collections::HashMap, + path::Path, sync::{Arc, RwLock}, time::Duration, }; #[derive(Debug)] pub struct PinnedCertVerifier { - trusted_fingerprints: Arc>>, + peers: Arc>>, provider: Arc, } impl PinnedCertVerifier { - pub fn new(trusted_fingerprints: Arc>>) -> Self { + pub fn new(peers: Arc>>) -> Self { Self { - trusted_fingerprints, + peers, provider: Arc::new(rustls::crypto::ring::default_provider()), } } @@ -38,29 +40,23 @@ impl ServerCertVerifier for PinnedCertVerifier { &self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], - server_name: &ServerName<'_>, + _server_name: &ServerName<'_>, _ocsp_response: &[u8], _now: UnixTime, ) -> Result { let actual_fp = Self::cert_fingerprint(end_entity); - let host = match server_name { - ServerName::DnsName(name) => name.as_ref().to_string(), - ServerName::IpAddress(ip) => std::net::IpAddr::from(*ip).to_string(), - _ => return Err(TlsError::General("Неподдерживаемое имя узла".into())), - }; + let pin = actual_fp.get(..8).unwrap_or("UNKNOWN").to_uppercase(); - let trusted = self.trusted_fingerprints.read().unwrap(); - match trusted.get(&host) { - Some(expected_fp) if expected_fp.eq_ignore_ascii_case(&actual_fp) => { - Ok(ServerCertVerified::assertion()) - } - Some(_) => Err(TlsError::General(format!( - "ВНИМАНИЕ: Сертификат узла {} не совпадает с заверенным!", - host + let peers_guard = self.peers.read().unwrap(); + match peers_guard.get(&actual_fp) { + Some(peer) if peer.verified => Ok(ServerCertVerified::assertion()), + Some(peer) => Err(TlsError::General(format!( + "Устройство '{}' [PIN: {}] не заверено! Нажмите «Заверить» в списке устройств.", + peer.name, pin ))), None => Err(TlsError::General(format!( - "Узел {} не заверен! Нажмите «Заверить» в списке устройств.", - host + "Неизвестный сертификат устройства [PIN: {}]. Дождитесь обнаружения или добавьте по IP.", + pin ))), } } @@ -100,7 +96,6 @@ impl ServerCertVerifier for PinnedCertVerifier { } } -/// Верификатор, используемый исключительно для зондирования узла при ручном добавлении по IP #[derive(Debug)] pub struct AnyCertVerifier { provider: Arc, @@ -161,7 +156,26 @@ impl ServerCertVerifier for AnyCertVerifier { } } -pub fn generate_in_memory_cert(ip: &str) -> (Vec, Vec, String) { +/// Загружает постоянный сертификат устройства или создает его один раз при первом запуске +pub fn load_or_create_identity(ip: &str, port: u16) -> (Vec, Vec, String) { + let identity_file = format!(".identity_{}.json", port); + let path = Path::new(&identity_file); + + if let Ok(content) = std::fs::read_to_string(path) { + if let Ok(saved) = serde_json::from_str::(&content) { + if let (Ok(cert_der), Ok(key_der)) = + (hex::decode(&saved.cert_hex), hex::decode(&saved.key_hex)) + { + let mut hasher = Sha256::new(); + hasher.update(&cert_der); + let actual_fp = hex::encode(hasher.finalize()); + if actual_fp == saved.fingerprint { + return (cert_der, key_der, actual_fp); + } + } + } + } + let san = vec!["localhost".to_string(), ip.to_string()]; let CertifiedKey { cert, key_pair } = generate_simple_self_signed(san).expect("Ошибка генерации сертификата"); @@ -173,13 +187,23 @@ pub fn generate_in_memory_cert(ip: &str) -> (Vec, Vec, String) { hasher.update(&cert_der); let fingerprint = hex::encode(hasher.finalize()); + let to_save = SavedIdentity { + cert_hex: hex::encode(&cert_der), + key_hex: hex::encode(&key_der), + fingerprint: fingerprint.clone(), + }; + + if let Ok(json_str) = serde_json::to_string_pretty(&to_save) { + let _ = std::fs::write(path, json_str); + } + (cert_der, key_der, fingerprint) } pub fn build_pinned_http_client( - trusted_fingerprints: Arc>>, + peers: Arc>>, ) -> reqwest::Client { - let verifier = Arc::new(PinnedCertVerifier::new(trusted_fingerprints)); + let verifier = Arc::new(PinnedCertVerifier::new(peers)); let rustls_client_config = ClientConfig::builder_with_provider(Arc::new( rustls::crypto::ring::default_provider(), )) diff --git a/src/ui.rs b/src/ui.rs index 2462074..d50f804 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,5 @@ use crate::models::{ - sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerNode, TextItem, + sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerDevice, TextItem, UiManualPeerRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR, }; use axum::{ @@ -18,25 +18,18 @@ pub async fn ui_index() -> Html<&'static str> { } pub async fn ui_get_state(State(state): State>) -> Json { - let trusted_map = state.trusted_fingerprints.read().unwrap(); - let mut peers_list: Vec = state + let mut peers_list: Vec = state .peers .read() .unwrap() .values() .cloned() - .map(|mut p| { - p.verified = trusted_map - .get(&p.ip) - .map(|fp| fp.eq_ignore_ascii_case(&p.fingerprint)) - .unwrap_or(false); - p - }) .collect(); peers_list.sort_by(|a, b| a.name.cmp(&b.name)); let texts: Vec = state.ram_texts.read().unwrap().iter().cloned().collect(); let media: Vec = state.disk_media.read().unwrap().iter().cloned().collect(); + let logs: Vec = state.logs.read().unwrap().iter().cloned().collect(); Json(serde_json::json!({ "device_name": state.device_name, @@ -46,7 +39,8 @@ pub async fn ui_get_state(State(state): State>) -> Json, ) -> Result { let ip = req.ip.trim().to_string(); - let port = req.port.unwrap_or(8443); + let port = req.port.unwrap_or(state.p2p_port); let url = format!("https://{}:{}/p2p/info", ip, port); let resp = state .probe_client .get(&url) + .header("X-Node-Fp", &state.my_fingerprint) + .header("X-Node-Name", &state.device_name) + .header("X-Node-Ip", &state.my_ip) + .header("X-Node-Port", state.p2p_port.to_string()) .send() .await - .map_err(|e| (StatusCode::BAD_REQUEST, format!("Узел недоступен: {}", e)))?; + .map_err(|e| (StatusCode::BAD_REQUEST, format!("Узел {} недоступен: {}", ip, e)))?; if !resp.status().is_success() { - return Err((StatusCode::BAD_GATEWAY, "Узел вернул ошибку".into())); + return Err((StatusCode::BAD_GATEWAY, "Удаленный узел вернул ошибку".into())); } let info: serde_json::Value = resp .json() .await - .map_err(|_| (StatusCode::BAD_GATEWAY, "Неверный ответ узла".into()))?; + .map_err(|_| (StatusCode::BAD_GATEWAY, "Некорректный JSON от узла".into()))?; - let name = info["name"].as_str().unwrap_or("Manual-Node").to_string(); + let name = info["name"].as_str().unwrap_or("Remote-Node").to_string(); let fingerprint = info["fingerprint"].as_str().unwrap_or("").to_string(); + let remote_port = info["port"].as_u64().unwrap_or(port as u64) as u16; - if fingerprint.is_empty() { - return Err((StatusCode::BAD_GATEWAY, "Отсутствует отпечаток узла".into())); + if fingerprint.len() < 8 { + return Err((StatusCode::BAD_GATEWAY, "Недействительный отпечаток сертификата".into())); } - let pin_code = fingerprint[..8].to_uppercase(); - let is_verified = state - .trusted_fingerprints - .read() - .unwrap() - .get(&ip) - .map(|fp| fp.eq_ignore_ascii_case(&fingerprint)) - .unwrap_or(false); - - state.peers.write().unwrap().insert( - ip.clone(), - PeerNode { - name, - ip, - port, - fingerprint, - pin_code, - verified: is_verified, - }, - ); - + state.upsert_peer(name, ip, remote_port, fingerprint); Ok(StatusCode::OK) } +pub async fn ui_ping_peers(State(state): State>) -> StatusCode { + let state_clone = state.clone(); + tokio::spawn(async move { + crate::ping_all_peers(&state_clone).await; + }); + StatusCode::OK +} + pub async fn ui_trust_peer( State(state): State>, Json(req): Json, ) -> Result { - let peers = state.peers.read().unwrap(); + let mut peers = state.peers.write().unwrap(); let peer = peers - .get(&req.target_ip) - .ok_or((StatusCode::NOT_FOUND, "Узел не найден".to_string()))?; + .get_mut(&req.peer_id) + .ok_or((StatusCode::NOT_FOUND, "Устройство не найдено".to_string()))?; - let mut trusted = state.trusted_fingerprints.write().unwrap(); - if req.trust { - trusted.insert(peer.ip.clone(), peer.fingerprint.clone()); + peer.verified = req.trust; + let msg = if req.trust { + format!("Сертификат '{}' [{}] ЗАВЕРЕН", peer.name, peer.pin_code) } else { - trusted.remove(&peer.ip); - } + format!("Доверие устройству '{}' [{}] ОТОЗВАНО", peer.name, peer.pin_code) + }; + drop(peers); + state.add_log(msg); + Ok(StatusCode::OK) } pub async fn ui_clear_ram(State(state): State>) -> StatusCode { state.ram_texts.write().unwrap().clear(); + state.add_log("Текстовый буфер в ОЗУ очищен"); StatusCode::OK } @@ -132,17 +123,29 @@ pub async fn ui_send_text( State(state): State>, Json(req): Json, ) -> Result { - let port = state - .peers - .read() - .unwrap() - .get(&req.target_ip) - .map(|p| p.port) - .unwrap_or(state.p2p_port); + let (target_ip, port, peer_name) = { + let peers = state.peers.read().unwrap(); + let peer = peers + .get(&req.peer_id) + .ok_or((StatusCode::NOT_FOUND, "Получатель не найден".to_string()))?; + if !peer.verified { + return Err(( + StatusCode::FORBIDDEN, + format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code), + )); + } + let ip = peer + .ips + .first() + .cloned() + .ok_or((StatusCode::BAD_REQUEST, "У получателя нет известного IP".to_string()))?; + (ip, peer.port, peer.name.clone()) + }; - let url = format!("https://{}:{}/p2p/text", req.target_ip, port); + let url = format!("https://{}:{}/p2p/text", target_ip, port); let payload = IncomingTextPayload { sender: state.device_name.clone(), + sender_fp: state.my_fingerprint.clone(), text: req.text, }; @@ -155,6 +158,7 @@ pub async fn ui_send_text( .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?; if resp.status().is_success() { + state.add_log(format!("Текст доставлен в ОЗУ устройства '{}'", peer_name)); Ok(StatusCode::OK) } else { Err((resp.status(), "Удаленный узел отклонил запрос".to_string())) @@ -166,10 +170,10 @@ pub async fn ui_send_media( headers: HeaderMap, body: Bytes, ) -> Result { - let target_ip = headers - .get("X-Target-Ip") + let peer_id = headers + .get("X-Target-Id") .and_then(|v| v.to_str().ok()) - .ok_or((StatusCode::BAD_REQUEST, "Не указан IP получателя".to_string()))?; + .ok_or((StatusCode::BAD_REQUEST, "Не указан ID получателя".to_string()))?; let filename = headers .get("X-Filename") @@ -181,13 +185,24 @@ pub async fn ui_send_media( .and_then(|v| v.to_str().ok()) .unwrap_or("application/octet-stream"); - let port = state - .peers - .read() - .unwrap() - .get(target_ip) - .map(|p| p.port) - .unwrap_or(state.p2p_port); + let (target_ip, port, peer_name) = { + let peers = state.peers.read().unwrap(); + let peer = peers + .get(peer_id) + .ok_or((StatusCode::NOT_FOUND, "Получатель не найден".to_string()))?; + if !peer.verified { + return Err(( + StatusCode::FORBIDDEN, + format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code), + )); + } + let ip = peer + .ips + .first() + .cloned() + .ok_or((StatusCode::BAD_REQUEST, "У получателя нет IP".to_string()))?; + (ip, peer.port, peer.name.clone()) + }; let url = format!("https://{}:{}/p2p/media", target_ip, port); @@ -203,6 +218,7 @@ pub async fn ui_send_media( .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?; if resp.status().is_success() { + state.add_log(format!("Медиафайл отправлен на диск устройства '{}'", peer_name)); Ok(StatusCode::OK) } else { Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string()))