This commit is contained in:
netadmin committed 2026-10-23 12:36:37 +03:00
1 parent 69e2329695
commit 8b954f757b
11 files changed
+558 -523

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
{
"cert_hex": "308201633082010aa00302010202146efbf8af961267a91917c46293c5bd3d935ae12e300a06082a8648ce3d0403023021311f301d06035504030c16726367656e2073656c66207369676e656420636572743020170d3735303130313030303030305a180f34303936303130313030303030305a3021311f301d06035504030c16726367656e2073656c66207369676e656420636572743059301306072a8648ce3d020106082a8648ce3d0301070342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256a31e301c301a0603551d110413301182096c6f63616c686f73748704c0a80aeb300a06082a8648ce3d040302034700304402204437caaf5909dd019cee3d49a2cc7f4e8b8e10b6365b6a4bbaa23ce2db902e820220493a698f69b58bf1968ae3eb00dec0254ed586d6366c2ae6dfec272187253833",
"key_hex": "308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420758dc92a43dc0c8ed274a080e3d890f5ae8530a4828c499d5470f40994a2d6e0a1440342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256",
"fingerprint": "ebc9c6df58d1637e2998d70976f463a910a59459f87157932690f93d26aaa930"
}
Generated
+2 -176
View File
@@ -197,23 +197,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" 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]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.58" version = "0.1.58"
@@ -232,15 +215,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -465,10 +439,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -478,11 +450,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"rand_core",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -607,7 +576,6 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots",
] ]
[[package]] [[package]]
@@ -825,12 +793,6 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@@ -976,62 +938,6 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -1049,7 +955,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "ramshare_ui" name = "ramshare_ui"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-server", "axum-server",
@@ -1066,32 +972,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "rcgen" name = "rcgen"
version = "0.13.2" version = "0.13.2"
@@ -1133,7 +1013,6 @@ dependencies = [
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
@@ -1149,7 +1028,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots",
] ]
[[package]] [[package]]
@@ -1166,12 +1044,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.46" version = "0.23.46"
@@ -1202,7 +1074,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@@ -1309,7 +1180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures",
"digest", "digest",
] ]
@@ -1421,26 +1292,6 @@ dependencies = [
"syn 3.0.9", "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]] [[package]]
name = "time" name = "time"
version = "0.3.55" version = "0.3.55"
@@ -1470,12 +1321,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.53.3" version = "1.53.3"
@@ -1723,25 +1568,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ramshare_ui" name = "ramshare_ui"
version = "0.2.0" version = "0.3.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@@ -10,7 +10,7 @@ axum-server = { version = "0.7", features = ["tls-rustls"] }
bytes = "1.7" bytes = "1.7"
socket2 = "0.5" 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" rcgen = "0.13"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
-113
View File
@@ -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<AppState>) {
// 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::<BeaconPayload>(&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;
}
}
}
});
}
+217 -31
View File
@@ -1,4 +1,3 @@
mod discovery;
mod models; mod models;
mod p2p; mod p2p;
mod tls; mod tls;
@@ -11,27 +10,191 @@ use axum::{
}; };
use axum_server::tls_rustls::RustlsConfig; use axum_server::tls_rustls::RustlsConfig;
use local_ip_address::local_ip; 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::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
net::SocketAddr, net::{Ipv4Addr, SocketAddr, UdpSocket as StdUdpSocket},
sync::{Arc, RwLock}, 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<String> {
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<AppState>) {
let targets: Vec<(String, Vec<String>, 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::<serde_json::Value>().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<AppState>) {
// 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::<BeaconPacket>(&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] #[tokio::main]
async fn main() { async fn main() {
// 0. Регистрация глобального CryptoProvider (ring) для rustls 0.23+
let _ = rustls::crypto::ring::default_provider().install_default(); let _ = rustls::crypto::ring::default_provider().install_default();
let args: Vec<String> = std::env::args().collect();
let mut device_name = "My-Device".to_string(); let mut device_name = "My-Device".to_string();
let mut p2p_port: u16 = 8443; let mut p2p_port: u16 = 8443;
let mut ui_port: u16 = 8080; let mut ui_port: u16 = 8080;
let mut custom_ip: Option<String> = None; let mut custom_ip: Option<String> = None;
let args: Vec<String> = std::env::args().collect();
let mut i = 1; let mut i = 1;
let mut positional_idx = 0; let mut positional_idx = 0;
while i < args.len() { while i < args.len() {
match args[i].as_str() { match args[i].as_str() {
"--ip" => { "--ip" => {
@@ -88,28 +251,22 @@ async fn main() {
let (bind_ip, my_ip) = match custom_ip { let (bind_ip, my_ip) = match custom_ip {
Some(ip) => { Some(ip) => {
if ip == "0.0.0.0" { if ip == "0.0.0.0" {
let detected = local_ip() ("0.0.0.0".to_string(), detect_lan_ip())
.map(|addr| addr.to_string())
.unwrap_or_else(|_| "127.0.0.1".to_string());
("0.0.0.0".to_string(), detected)
} else { } else {
(ip.clone(), ip) (ip.clone(), ip)
} }
} }
None => { None => ("0.0.0.0".to_string(), detect_lan_ip()),
let detected = local_ip().expect("Не удалось определить локальный IP").to_string();
("0.0.0.0".to_string(), detected)
}
}; };
tokio::fs::create_dir_all(MEDIA_DIR) tokio::fs::create_dir_all(MEDIA_DIR)
.await .await
.expect("Не удалось создать папку received_media на диске"); .expect("Не удалось создать папку received_media");
// 1. Генерация сертификата в ОЗУ под выбранный IP и настройка HTTPS-клиентов // Загружаем постоянный сертификат и PIN-код (или создаем его один раз)
let (cert_der, key_der, my_fingerprint) = tls::generate_in_memory_cert(&my_ip); let (cert_der, key_der, my_fingerprint) = tls::load_or_create_identity(&my_ip, p2p_port);
let trusted_fingerprints = Arc::new(RwLock::new(HashMap::new())); let peers = Arc::new(RwLock::new(HashMap::new()));
let http_client = tls::build_pinned_http_client(trusted_fingerprints.clone()); let http_client = tls::build_pinned_http_client(peers.clone());
let probe_client = tls::build_probe_http_client(); let probe_client = tls::build_probe_http_client();
let state = Arc::new(AppState { let state = Arc::new(AppState {
@@ -117,18 +274,37 @@ async fn main() {
my_ip: my_ip.clone(), my_ip: my_ip.clone(),
p2p_port, p2p_port,
my_fingerprint: my_fingerprint.clone(), my_fingerprint: my_fingerprint.clone(),
ping_interval_sec: 5,
ram_texts: RwLock::new(VecDeque::with_capacity(MAX_RAM_TEXTS)), ram_texts: RwLock::new(VecDeque::with_capacity(MAX_RAM_TEXTS)),
disk_media: RwLock::new(VecDeque::new()), disk_media: RwLock::new(VecDeque::new()),
peers: RwLock::new(HashMap::new()), peers,
trusted_fingerprints,
http_client, http_client,
probe_client, probe_client,
transfers: RwLock::new(Vec::new()),
logs: RwLock::new(VecDeque::new()),
}); });
// 2. Запуск широковещательного UDP-маяка на порт 5354 state.add_log(format!(
discovery::start_discovery(state.clone()); "Узел '{}' запущен на {}:{} (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) let tls_config = RustlsConfig::from_der(vec![cert_der], key_der)
.await .await
.expect("Ошибка конфигурации TLS"); .expect("Ошибка конфигурации TLS");
@@ -142,7 +318,7 @@ async fn main() {
let p2p_addr: SocketAddr = format!("{}:{}", bind_ip, p2p_port) let p2p_addr: SocketAddr = format!("{}:{}", bind_ip, p2p_port)
.parse() .parse()
.unwrap_or_else(|_| panic!("Некорректный сокет: {}:{}", bind_ip, p2p_port)); .unwrap_or_else(|_| panic!("Некорректный адрес P2P: {}:{}", bind_ip, p2p_port));
tokio::spawn(async move { tokio::spawn(async move {
axum_server::bind_rustls(p2p_addr, tls_config) axum_server::bind_rustls(p2p_addr, tls_config)
@@ -151,11 +327,12 @@ async fn main() {
.expect("Ошибка P2P HTTPS-сервера"); .expect("Ошибка P2P HTTPS-сервера");
}); });
// 4. Запуск локального UI-сервера (127.0.0.1) // Запуск локального UI-сервера (127.0.0.1)
let ui_router = Router::new() let ui_router = Router::new()
.route("/", get(ui::ui_index)) .route("/", get(ui::ui_index))
.route("/ui/state", get(ui::ui_get_state)) .route("/ui/state", get(ui::ui_get_state))
.route("/ui/manual-peer", post(ui::ui_add_manual_peer)) .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/trust", post(ui::ui_trust_peer))
.route("/ui/clear-ram", post(ui::ui_clear_ram)) .route("/ui/clear-ram", post(ui::ui_clear_ram))
.route("/ui/send-text", post(ui::ui_send_text)) .route("/ui/send-text", post(ui::ui_send_text))
@@ -164,17 +341,26 @@ async fn main() {
.layer(DefaultBodyLimit::max(MAX_PAYLOAD_SIZE)) .layer(DefaultBodyLimit::max(MAX_PAYLOAD_SIZE))
.with_state(state); .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!("==============================================================");
println!(" Устройство: {}", device_name); println!(" Устройство: {}", device_name);
println!(" P2P сокет (HTTPS): https://{}:{}", bind_ip, p2p_port); println!(" P2P сокет (HTTPS): https://{}:{}", bind_ip, p2p_port);
println!(" Маяк обнаружения (UDP): 255.255.255.255:5354"); println!(" Сетевой IP в локальной сети: {}", my_ip);
println!(" КОД СВЕРКИ СЕРТИФИКАТА (PIN): [ {} ]", &my_fingerprint[..8].to_uppercase()); println!(
println!(" Хранилище медиа: ./{}/", MEDIA_DIR); " ПОСТОЯННЫЙ PIN УСТРОЙСТВА: [ {} ]",
&my_fingerprint[..8].to_uppercase()
);
println!(" Файл постоянной идентичности: .identity_{}.json", p2p_port);
println!(" Папка для медиа на диске: ./{}/", MEDIA_DIR);
println!("--------------------------------------------------------------"); println!("--------------------------------------------------------------");
println!(" ОТКРОЙТЕ ИНТЕРФЕЙС В БРАУЗЕРЕ: http://127.0.0.1:{}", ui_port); println!(" ОТКРОЙТЕ ИНТЕРФЕЙС В БРАУЗЕРЕ: http://127.0.0.1:{}", ui_port);
println!("=============================================================="); 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(); axum::serve(listener, ui_router).await.unwrap();
} }
-60
View File
@@ -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<AppState>) {
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);
}
}
}
});
}
+93 -10
View File
@@ -7,16 +7,25 @@ use std::{
pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024; pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024;
pub const MAX_RAM_TEXTS: usize = 50; pub const MAX_RAM_TEXTS: usize = 50;
pub const MAX_LOGS: usize = 100;
pub const MEDIA_DIR: &str = "received_media"; 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 struct TextItem {
pub sender: String, pub sender: String,
pub text: String, pub text: String,
pub timestamp: u64, pub timestamp: u64,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaItem { pub struct MediaItem {
pub sender: String, pub sender: String,
pub filename: String, pub filename: String,
@@ -26,14 +35,25 @@ pub struct MediaItem {
pub timestamp: u64, pub timestamp: u64,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeerNode { pub struct PeerDevice {
pub id: String,
pub name: String, pub name: String,
pub ip: String, pub ips: Vec<String>,
pub port: u16, pub port: u16,
pub fingerprint: String, pub fingerprint: String,
pub pin_code: String, pub pin_code: String,
pub verified: bool, 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 { pub struct AppState {
@@ -41,29 +61,91 @@ pub struct AppState {
pub my_ip: String, pub my_ip: String,
pub p2p_port: u16, pub p2p_port: u16,
pub my_fingerprint: String, pub my_fingerprint: String,
pub ping_interval_sec: u64,
pub ram_texts: RwLock<VecDeque<TextItem>>, pub ram_texts: RwLock<VecDeque<TextItem>>,
pub disk_media: RwLock<VecDeque<MediaItem>>, pub disk_media: RwLock<VecDeque<MediaItem>>,
pub peers: RwLock<HashMap<String, PeerNode>>, /// Ключ таблицы — постоянный fingerprint (ID) устройства
pub trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>, pub peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
pub http_client: reqwest::Client, pub http_client: reqwest::Client,
pub probe_client: reqwest::Client, pub probe_client: reqwest::Client,
pub transfers: RwLock<Vec<String>>,
pub logs: RwLock<VecDeque<String>>,
}
impl AppState {
pub fn add_log(&self, msg: impl Into<String>) {
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)] #[derive(Serialize, Deserialize)]
pub struct IncomingTextPayload { pub struct IncomingTextPayload {
pub sender: String, pub sender: String,
pub sender_fp: String,
pub text: String, pub text: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UiSendTextRequest { pub struct UiSendTextRequest {
pub target_ip: String, pub peer_id: String,
pub text: String, pub text: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UiTrustRequest { pub struct UiTrustRequest {
pub target_ip: String, pub peer_id: String,
pub trust: bool, pub trust: bool,
} }
@@ -86,7 +168,8 @@ pub fn url_decode(input: &str) -> String {
while let Some(b) = chars.next() { while let Some(b) = chars.next() {
if b == b'%' { if b == b'%' {
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) { 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); bytes.push(decoded);
continue; continue;
+43 -5
View File
@@ -10,21 +10,52 @@ use axum::{
}; };
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
/// Информационный эндпоинт для прямого зондирования узла /// Публичный эндпоинт рукопожатия и взаимного обнаружения
pub async fn p2p_info(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> { pub async fn p2p_info(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Json<serde_json::Value> {
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!({ Json(serde_json::json!({
"name": state.device_name, "name": state.device_name,
"ip": state.my_ip,
"port": state.p2p_port, "port": state.p2p_port,
"fingerprint": state.my_fingerprint, "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( pub async fn p2p_receive_text(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<IncomingTextPayload>, Json(payload): Json<IncomingTextPayload>,
) -> StatusCode { ) -> StatusCode {
state.add_log(format!("Получен текст в ОЗУ от '{}'", payload.sender));
let mut ram_texts = state.ram_texts.write().unwrap(); let mut ram_texts = state.ram_texts.write().unwrap();
if ram_texts.len() >= MAX_RAM_TEXTS { if ram_texts.len() >= MAX_RAM_TEXTS {
ram_texts.pop_back(); ram_texts.pop_back();
@@ -37,7 +68,7 @@ pub async fn p2p_receive_text(
StatusCode::OK StatusCode::OK
} }
/// Прием медиаконтента -> НА ДИСК /// Прием медиаконтента -> сохраняется НА ДИСК в папку ./received_media
pub async fn p2p_receive_media( pub async fn p2p_receive_media(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
@@ -77,6 +108,13 @@ pub async fn p2p_receive_media(
.display() .display()
.to_string(); .to_string();
state.add_log(format!(
"Сохранен медиафайл '{}' ({} байт) от '{}'",
final_filename,
body.len(),
sender
));
let mut media_list = state.disk_media.write().unwrap(); let mut media_list = state.disk_media.write().unwrap();
media_list.push_front(MediaItem { media_list.push_front(MediaItem {
sender, sender,
+68 -38
View File
@@ -3,17 +3,17 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LAN Share — Текст в ОЗУ / Медиа на Диск</title> <title>LAN Share — Постоянный PIN | Текст в ОЗУ | Медиа на Диск</title>
<style> <style>
:root { --bg: #0f172a; --card: #1e293b; --border: #334155; --accent: #3b82f6; --green: #10b981; --red: #ef4444; --text: #f8fafc; --muted: #94a3b8; } :root { --bg: #0f172a; --card: #1e293b; --border: #334155; --accent: #3b82f6; --green: #10b981; --red: #ef4444; --text: #f8fafc; --muted: #94a3b8; }
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; } * { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
body { margin: 0; padding: 20px; background: var(--bg); color: var(--text); } body { margin: 0; padding: 20px; background: var(--bg); color: var(--text); }
.container { max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .container { max-width: 1120px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 800px) { .container { grid-template-columns: 1fr; } } @media (max-width: 800px) { .container { grid-template-columns: 1fr; } }
.header { grid-column: 1 / -1; background: var(--card); border: 1px solid var(--border); padding: 16px 20px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } .header { grid-column: 1 / -1; background: var(--card); border: 1px solid var(--border); padding: 16px 20px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.pin-badge { background: #1d4ed8; padding: 6px 14px; border-radius: 8px; font-weight: 700; letter-spacing: 1px; font-family: monospace; font-size: 1.1rem; } .pin-badge { background: #1d4ed8; padding: 6px 14px; border-radius: 8px; font-weight: 700; letter-spacing: 1px; font-family: monospace; font-size: 1.15rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 14px; } .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 14px; }
h2 { margin: 0; font-size: 1.1rem; display: flex; justify-content: space-between; align-items: center; } h2 { margin: 0; font-size: 1.08rem; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.tag { font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-weight: 600; } .tag { font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
.tag-ram { background: rgba(16, 185, 129, 0.2); color: #34d399; } .tag-ram { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.tag-disk { background: rgba(59, 130, 246, 0.2); color: #60a5fa; } .tag-disk { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
@@ -30,6 +30,9 @@
.msg-text { font-size: 0.95rem; white-space: pre-wrap; word-break: break-word; width: 100%; margin: 4px 0; } .msg-text { font-size: 0.95rem; white-space: pre-wrap; word-break: break-word; width: 100%; margin: 4px 0; }
.path-box { font-family: monospace; font-size: 0.78rem; color: #93c5fd; background: rgba(59,130,246,0.1); padding: 4px 8px; border-radius: 4px; word-break: break-all; } .path-box { font-family: monospace; font-size: 0.78rem; color: #93c5fd; background: rgba(59,130,246,0.1); padding: 4px 8px; border-radius: 4px; word-break: break-all; }
.preview-img { max-width: 100%; max-height: 180px; border-radius: 6px; margin-top: 6px; } .preview-img { max-width: 100%; max-height: 180px; border-radius: 6px; margin-top: 6px; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.dot-on { background: #10b981; }
.dot-off { background: #64748b; }
#status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; } #status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; }
</style> </style>
</head> </head>
@@ -41,30 +44,33 @@
<div style="font-size: 0.85rem; color: var(--muted);" id="my-ip"></div> <div style="font-size: 0.85rem; color: var(--muted);" id="my-ip"></div>
</div> </div>
<div> <div>
<span style="font-size: 0.85rem; color: var(--muted); margin-right: 8px;">Код сверки сертификата (PIN):</span> <span style="font-size: 0.85rem; color: var(--muted); margin-right: 8px;">Постоянный PIN устройства:</span>
<span class="pin-badge" id="my-pin">--------</span> <span class="pin-badge" id="my-pin">--------</span>
</div> </div>
</div> </div>
<div id="status"></div> <div id="status"></div>
<!-- 1. Устройства в сети + ручной ввод --> <!-- 1. Устройства в сети -->
<div class="card"> <div class="card">
<h2>1. Устройства в сети <span class="tag tag-disk">UDP Broadcast</span></h2> <h2>
<span>1. Устройства в сети</span>
<button class="btn-sm" onclick="pingPeers()">Обновить сеть</button>
</h2>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
<input type="text" id="manual-ip-input" placeholder="IP устройства (напр. 192.168.1.55)"> <input type="text" id="manual-ip-input" placeholder="IP:порт (напр. 192.168.1.55 или 192.168.1.55:8443)">
<button class="btn-sm" style="white-space: nowrap;" onclick="addManualPeer()">Найти по IP</button> <button class="btn-sm" style="white-space: nowrap;" onclick="addManualPeer()">Найти по IP</button>
</div> </div>
<div id="peers-list" style="display:flex; flex-direction:column; gap:8px;"> <div id="peers-list" style="display:flex; flex-direction:column; gap:8px;">
<div style="color:var(--muted); font-size:0.9rem;">Поиск устройств через маяк 255.255.255.255:5354...</div> <div style="color:var(--muted); font-size:0.9rem;">Поиск устройств в локальной сети...</div>
</div> </div>
</div> </div>
<!-- 2. Панель отправки (Только по нажатию) --> <!-- 2. Отправка данных -->
<div class="card"> <div class="card">
<h2>2. Отправка данных</h2> <h2>2. Отправка данных (по нажатию)</h2>
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label> <label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
<select id="target-peer"><option value="">-- Выберите устройство --</option></select> <select id="target-peer"><option value="">-- Выберите устройство --</option></select>
@@ -76,7 +82,7 @@
<button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button> <button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button>
</div> </div>
<!-- 3. Принятый текст (Строго в ОЗУ) --> <!-- 3. Принятый текст в ОЗУ -->
<div class="card"> <div class="card">
<h2> <h2>
<span>Входящий текст <span class="tag tag-ram">ТОЛЬКО В ОЗУ</span></span> <span>Входящий текст <span class="tag tag-ram">ТОЛЬКО В ОЗУ</span></span>
@@ -85,7 +91,7 @@
<div id="ram-texts" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div> <div id="ram-texts" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
</div> </div>
<!-- 4. Принятые медиафайлы (Сохранены на диске) --> <!-- 4. Принятые медиафайлы на диске -->
<div class="card"> <div class="card">
<h2>Входящие медиафайлы <span class="tag tag-disk">НА ДИСКЕ (./received_media)</span></h2> <h2>Входящие медиафайлы <span class="tag tag-disk">НА ДИСКЕ (./received_media)</span></h2>
<div id="disk-media" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div> <div id="disk-media" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
@@ -110,33 +116,43 @@ async function fetchState() {
const data = await res.json(); const data = await res.json();
document.getElementById('my-name').textContent = `Устройство: ${data.device_name}`; 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; document.getElementById('my-pin').textContent = data.my_pin;
const peersContainer = document.getElementById('peers-list'); const peersContainer = document.getElementById('peers-list');
if (data.peers.length === 0) { if (data.peers.length === 0) {
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства пока не найдены. Введите IP вручную, если в сети отключен broadcast.</div>'; peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства пока не найдены. Если роутер блокирует broadcast, введите IP соседа выше — оба устройства сразу увидят друг друга.</div>';
} else { } else {
peersContainer.innerHTML = data.peers.map(p => ` peersContainer.innerHTML = data.peers.map(p => {
const ipStr = p.ips.join(', ');
const dotClass = p.online ? 'dot-on' : 'dot-off';
return `
<div class="peer-item"> <div class="peer-item">
<div> <div>
<div style="font-weight:600;">${escapeHtml(p.name)} <span style="color:var(--muted); font-size:0.85rem;">(${p.ip}:${p.port})</span></div> <div style="font-weight:600;">
<div style="font-size:0.8rem; color:var(--muted); font-family:monospace;">PIN: <b style="color:#fff;">[${p.pin_code}]</b> | Статус: ${p.verified ? '<span style="color:#34d399;">Заверен</span>' : '<span style="color:#f87171;">Не заверен</span>'}</div> <span class="dot ${dotClass}"></span>${escapeHtml(p.name)}
<span style="color:var(--muted); font-size:0.85rem;">(${escapeHtml(ipStr)}:${p.port})</span>
</div> </div>
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.ip}', ${!p.verified})"> <div style="font-size:0.8rem; color:var(--muted); font-family:monospace; margin-top:3px;">
PIN: <b style="color:#fff;">[${p.pin_code}]</b> |
Статус: ${p.verified ? '<span style="color:#34d399;">Заверен</span>' : '<span style="color:#f87171;">Не заверен</span>'}
</div>
</div>
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.id}', ${!p.verified})">
${p.verified ? 'Отозвать' : 'Заверить'} ${p.verified ? 'Отозвать' : 'Заверить'}
</button> </button>
</div> </div>
`).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) { if (currentSelectKey !== knownPeersKey) {
knownPeersKey = currentSelectKey; knownPeersKey = currentSelectKey;
const select = document.getElementById('target-peer'); const select = document.getElementById('target-peer');
const prevVal = select.value; const prevVal = select.value;
select.innerHTML = '<option value="">-- Выберите устройство --</option>' + select.innerHTML = '<option value="">-- Выберите устройство --</option>' +
data.peers.map(p => `<option value="${p.ip}">${escapeHtml(p.name)} (${p.ip}) ${p.verified ? '✓' : '🔒 [Не заверен]'}</option>`).join(''); data.peers.map(p => `<option value="${p.id}">${escapeHtml(p.name)} [${p.pin_code}] (${p.ips[0] || ''}) ${p.verified ? '✓' : '🔒 [Не заверен]'}</option>`).join('');
if (prevVal) select.value = prevVal; if (prevVal) select.value = prevVal;
} }
@@ -182,19 +198,27 @@ async function fetchState() {
async function addManualPeer() { async function addManualPeer() {
const input = document.getElementById('manual-ip-input'); const input = document.getElementById('manual-ip-input');
const ip = input.value.trim(); const raw = input.value.trim();
if (!ip) return showStatus('Введите IP-адрес для поиска', true); 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', { const res = await fetch('/ui/manual-peer', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ip, port: 8443 }) body: JSON.stringify({ ip, port })
}); });
if (res.ok) { if (res.ok) {
input.value = ''; input.value = '';
showStatus(`Узел ${ip} успешно найден!`); showStatus(`Узел ${ip} подключен (оба устройства обменялись PIN-кодами)!`);
fetchState(); fetchState();
} else { } else {
const err = await res.text(); 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', { const res = await fetch('/ui/trust', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip: ip, trust }) body: JSON.stringify({ peer_id, trust })
}); });
if (res.ok) { if (res.ok) {
showStatus(trust ? `Сертификат узла ${ip} заверен в ОЗУ` : `Доверие узлу ${ip} отозвано`); showStatus(trust ? 'Сертификат устройства заверен' : 'Доверие устройству отозвано');
fetchState(); fetchState();
} }
} }
async function sendText() { 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 input = document.getElementById('text-input');
const text = input.value.trim(); const text = input.value.trim();
if (!target_ip) return showStatus('Выберите устройство-получатель!', true); if (!peer_id) return showStatus('Выберите устройство-получатель!', true);
if (!text) return showStatus('Введите текст для отправки!', true); if (!text) return showStatus('Введите текст для отправки!', true);
const res = await fetch('/ui/send-text', { const res = await fetch('/ui/send-text', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip, text }) body: JSON.stringify({ peer_id, text })
}); });
if (res.ok) { if (res.ok) {
input.value = ''; input.value = '';
showStatus(`Текст доставлен в ОЗУ узла ${target_ip}`); showStatus('Текст успешно доставлен в ОЗУ получателя');
} else { } else {
const err = await res.text(); const err = await res.text();
showStatus(err, true); showStatus(err, true);
@@ -237,9 +267,9 @@ async function sendText() {
} }
async function sendMedia() { 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'); 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); if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true);
const file = fileInput.files[0]; const file = fileInput.files[0];
@@ -248,7 +278,7 @@ async function sendMedia() {
const res = await fetch('/ui/send-media', { const res = await fetch('/ui/send-media', {
method: 'POST', method: 'POST',
headers: { headers: {
'X-Target-Ip': target_ip, 'X-Target-Id': peer_id,
'X-Filename': encodeURIComponent(file.name), 'X-Filename': encodeURIComponent(file.name),
'Content-Type': file.type || 'application/octet-stream' 'Content-Type': file.type || 'application/octet-stream'
}, },
@@ -257,7 +287,7 @@ async function sendMedia() {
if (res.ok) { if (res.ok) {
fileInput.value = ''; fileInput.value = '';
showStatus(`Файл "${file.name}" сохранен на диске узла ${target_ip}`); showStatus(`Файл "${file.name}" сохранен на диске получателя`);
} else { } else {
const err = await res.text(); const err = await res.text();
showStatus(err, true); showStatus(err, true);
+47 -23
View File
@@ -1,3 +1,4 @@
use crate::models::{PeerDevice, SavedIdentity};
use rcgen::{generate_simple_self_signed, CertifiedKey}; use rcgen::{generate_simple_self_signed, CertifiedKey};
use rustls::{ use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
@@ -8,20 +9,21 @@ use rustls::{
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::Path,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::Duration, time::Duration,
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct PinnedCertVerifier { pub struct PinnedCertVerifier {
trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>, peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
provider: Arc<CryptoProvider>, provider: Arc<CryptoProvider>,
} }
impl PinnedCertVerifier { impl PinnedCertVerifier {
pub fn new(trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>) -> Self { pub fn new(peers: Arc<RwLock<HashMap<String, PeerDevice>>>) -> Self {
Self { Self {
trusted_fingerprints, peers,
provider: Arc::new(rustls::crypto::ring::default_provider()), provider: Arc::new(rustls::crypto::ring::default_provider()),
} }
} }
@@ -38,29 +40,23 @@ impl ServerCertVerifier for PinnedCertVerifier {
&self, &self,
end_entity: &CertificateDer<'_>, end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>], _intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>, _server_name: &ServerName<'_>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
_now: UnixTime, _now: UnixTime,
) -> Result<ServerCertVerified, TlsError> { ) -> Result<ServerCertVerified, TlsError> {
let actual_fp = Self::cert_fingerprint(end_entity); let actual_fp = Self::cert_fingerprint(end_entity);
let host = match server_name { let pin = actual_fp.get(..8).unwrap_or("UNKNOWN").to_uppercase();
ServerName::DnsName(name) => name.as_ref().to_string(),
ServerName::IpAddress(ip) => std::net::IpAddr::from(*ip).to_string(),
_ => return Err(TlsError::General("Неподдерживаемое имя узла".into())),
};
let trusted = self.trusted_fingerprints.read().unwrap(); let peers_guard = self.peers.read().unwrap();
match trusted.get(&host) { match peers_guard.get(&actual_fp) {
Some(expected_fp) if expected_fp.eq_ignore_ascii_case(&actual_fp) => { Some(peer) if peer.verified => Ok(ServerCertVerified::assertion()),
Ok(ServerCertVerified::assertion()) Some(peer) => Err(TlsError::General(format!(
} "Устройство '{}' [PIN: {}] не заверено! Нажмите «Заверить» в списке устройств.",
Some(_) => Err(TlsError::General(format!( peer.name, pin
"ВНИМАНИЕ: Сертификат узла {} не совпадает с заверенным!",
host
))), ))),
None => Err(TlsError::General(format!( None => Err(TlsError::General(format!(
"Узел {} не заверен! Нажмите «Заверить» в списке устройств.", "Неизвестный сертификат устройства [PIN: {}]. Дождитесь обнаружения или добавьте по IP.",
host pin
))), ))),
} }
} }
@@ -100,7 +96,6 @@ impl ServerCertVerifier for PinnedCertVerifier {
} }
} }
/// Верификатор, используемый исключительно для зондирования узла при ручном добавлении по IP
#[derive(Debug)] #[derive(Debug)]
pub struct AnyCertVerifier { pub struct AnyCertVerifier {
provider: Arc<CryptoProvider>, provider: Arc<CryptoProvider>,
@@ -161,7 +156,26 @@ impl ServerCertVerifier for AnyCertVerifier {
} }
} }
pub fn generate_in_memory_cert(ip: &str) -> (Vec<u8>, Vec<u8>, String) { /// Загружает постоянный сертификат устройства или создает его один раз при первом запуске
pub fn load_or_create_identity(ip: &str, port: u16) -> (Vec<u8>, Vec<u8>, 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::<SavedIdentity>(&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 san = vec!["localhost".to_string(), ip.to_string()];
let CertifiedKey { cert, key_pair } = let CertifiedKey { cert, key_pair } =
generate_simple_self_signed(san).expect("Ошибка генерации сертификата"); generate_simple_self_signed(san).expect("Ошибка генерации сертификата");
@@ -173,13 +187,23 @@ pub fn generate_in_memory_cert(ip: &str) -> (Vec<u8>, Vec<u8>, String) {
hasher.update(&cert_der); hasher.update(&cert_der);
let fingerprint = hex::encode(hasher.finalize()); 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) (cert_der, key_der, fingerprint)
} }
pub fn build_pinned_http_client( pub fn build_pinned_http_client(
trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>, peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
) -> reqwest::Client { ) -> 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( let rustls_client_config = ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(), rustls::crypto::ring::default_provider(),
)) ))
+81 -65
View File
@@ -1,5 +1,5 @@
use crate::models::{ use crate::models::{
sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerNode, TextItem, sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerDevice, TextItem,
UiManualPeerRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR, UiManualPeerRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR,
}; };
use axum::{ use axum::{
@@ -18,25 +18,18 @@ pub async fn ui_index() -> Html<&'static str> {
} }
pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> { pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let trusted_map = state.trusted_fingerprints.read().unwrap(); let mut peers_list: Vec<PeerDevice> = state
let mut peers_list: Vec<PeerNode> = state
.peers .peers
.read() .read()
.unwrap() .unwrap()
.values() .values()
.cloned() .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(); .collect();
peers_list.sort_by(|a, b| a.name.cmp(&b.name)); peers_list.sort_by(|a, b| a.name.cmp(&b.name));
let texts: Vec<TextItem> = state.ram_texts.read().unwrap().iter().cloned().collect(); let texts: Vec<TextItem> = state.ram_texts.read().unwrap().iter().cloned().collect();
let media: Vec<MediaItem> = state.disk_media.read().unwrap().iter().cloned().collect(); let media: Vec<MediaItem> = state.disk_media.read().unwrap().iter().cloned().collect();
let logs: Vec<String> = state.logs.read().unwrap().iter().cloned().collect();
Json(serde_json::json!({ Json(serde_json::json!({
"device_name": state.device_name, "device_name": state.device_name,
@@ -46,7 +39,8 @@ pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json
"my_pin": state.my_fingerprint[..8].to_uppercase(), "my_pin": state.my_fingerprint[..8].to_uppercase(),
"peers": peers_list, "peers": peers_list,
"texts": texts, "texts": texts,
"media": media "media": media,
"logs": logs
})) }))
} }
@@ -55,76 +49,73 @@ pub async fn ui_add_manual_peer(
Json(req): Json<UiManualPeerRequest>, Json(req): Json<UiManualPeerRequest>,
) -> Result<StatusCode, (StatusCode, String)> { ) -> Result<StatusCode, (StatusCode, String)> {
let ip = req.ip.trim().to_string(); 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 url = format!("https://{}:{}/p2p/info", ip, port);
let resp = state let resp = state
.probe_client .probe_client
.get(&url) .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() .send()
.await .await
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Узел недоступен: {}", e)))?; .map_err(|e| (StatusCode::BAD_REQUEST, format!("Узел {} недоступен: {}", ip, e)))?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err((StatusCode::BAD_GATEWAY, "Узел вернул ошибку".into())); return Err((StatusCode::BAD_GATEWAY, "Удаленный узел вернул ошибку".into()));
} }
let info: serde_json::Value = resp let info: serde_json::Value = resp
.json() .json()
.await .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 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() { if fingerprint.len() < 8 {
return Err((StatusCode::BAD_GATEWAY, "Отсутствует отпечаток узла".into())); return Err((StatusCode::BAD_GATEWAY, "Недействительный отпечаток сертификата".into()));
} }
let pin_code = fingerprint[..8].to_uppercase(); state.upsert_peer(name, ip, remote_port, fingerprint);
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,
},
);
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }
pub async fn ui_ping_peers(State(state): State<Arc<AppState>>) -> 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( pub async fn ui_trust_peer(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(req): Json<UiTrustRequest>, Json(req): Json<UiTrustRequest>,
) -> Result<StatusCode, (StatusCode, String)> { ) -> Result<StatusCode, (StatusCode, String)> {
let peers = state.peers.read().unwrap(); let mut peers = state.peers.write().unwrap();
let peer = peers let peer = peers
.get(&req.target_ip) .get_mut(&req.peer_id)
.ok_or((StatusCode::NOT_FOUND, "Узел не найден".to_string()))?; .ok_or((StatusCode::NOT_FOUND, "Устройство не найдено".to_string()))?;
let mut trusted = state.trusted_fingerprints.write().unwrap(); peer.verified = req.trust;
if req.trust { let msg = if req.trust {
trusted.insert(peer.ip.clone(), peer.fingerprint.clone()); format!("Сертификат '{}' [{}] ЗАВЕРЕН", peer.name, peer.pin_code)
} else { } else {
trusted.remove(&peer.ip); format!("Доверие устройству '{}' [{}] ОТОЗВАНО", peer.name, peer.pin_code)
} };
drop(peers);
state.add_log(msg);
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }
pub async fn ui_clear_ram(State(state): State<Arc<AppState>>) -> StatusCode { pub async fn ui_clear_ram(State(state): State<Arc<AppState>>) -> StatusCode {
state.ram_texts.write().unwrap().clear(); state.ram_texts.write().unwrap().clear();
state.add_log("Текстовый буфер в ОЗУ очищен");
StatusCode::OK StatusCode::OK
} }
@@ -132,17 +123,29 @@ pub async fn ui_send_text(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(req): Json<UiSendTextRequest>, Json(req): Json<UiSendTextRequest>,
) -> Result<StatusCode, (StatusCode, String)> { ) -> Result<StatusCode, (StatusCode, String)> {
let port = state let (target_ip, port, peer_name) = {
.peers let peers = state.peers.read().unwrap();
.read() let peer = peers
.unwrap() .get(&req.peer_id)
.get(&req.target_ip) .ok_or((StatusCode::NOT_FOUND, "Получатель не найден".to_string()))?;
.map(|p| p.port) if !peer.verified {
.unwrap_or(state.p2p_port); 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 { let payload = IncomingTextPayload {
sender: state.device_name.clone(), sender: state.device_name.clone(),
sender_fp: state.my_fingerprint.clone(),
text: req.text, text: req.text,
}; };
@@ -155,6 +158,7 @@ pub async fn ui_send_text(
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?; .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?;
if resp.status().is_success() { if resp.status().is_success() {
state.add_log(format!("Текст доставлен в ОЗУ устройства '{}'", peer_name));
Ok(StatusCode::OK) Ok(StatusCode::OK)
} else { } else {
Err((resp.status(), "Удаленный узел отклонил запрос".to_string())) Err((resp.status(), "Удаленный узел отклонил запрос".to_string()))
@@ -166,10 +170,10 @@ pub async fn ui_send_media(
headers: HeaderMap, headers: HeaderMap,
body: Bytes, body: Bytes,
) -> Result<StatusCode, (StatusCode, String)> { ) -> Result<StatusCode, (StatusCode, String)> {
let target_ip = headers let peer_id = headers
.get("X-Target-Ip") .get("X-Target-Id")
.and_then(|v| v.to_str().ok()) .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 let filename = headers
.get("X-Filename") .get("X-Filename")
@@ -181,13 +185,24 @@ pub async fn ui_send_media(
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream"); .unwrap_or("application/octet-stream");
let port = state let (target_ip, port, peer_name) = {
.peers let peers = state.peers.read().unwrap();
.read() let peer = peers
.unwrap() .get(peer_id)
.get(target_ip) .ok_or((StatusCode::NOT_FOUND, "Получатель не найден".to_string()))?;
.map(|p| p.port) if !peer.verified {
.unwrap_or(state.p2p_port); 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); 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)))?; .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?;
if resp.status().is_success() { if resp.status().is_success() {
state.add_log(format!("Медиафайл отправлен на диск устройства '{}'", peer_name));
Ok(StatusCode::OK) Ok(StatusCode::OK)
} else { } else {
Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string())) Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string()))