a.0.0.3
This commit is contained in:
1 parent
69e2329695
commit
8b954f757b
11 files changed
+558
-523
No files matched your search
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cert_hex": "308201633082010aa00302010202146efbf8af961267a91917c46293c5bd3d935ae12e300a06082a8648ce3d0403023021311f301d06035504030c16726367656e2073656c66207369676e656420636572743020170d3735303130313030303030305a180f34303936303130313030303030305a3021311f301d06035504030c16726367656e2073656c66207369676e656420636572743059301306072a8648ce3d020106082a8648ce3d0301070342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256a31e301c301a0603551d110413301182096c6f63616c686f73748704c0a80aeb300a06082a8648ce3d040302034700304402204437caaf5909dd019cee3d49a2cc7f4e8b8e10b6365b6a4bbaa23ce2db902e820220493a698f69b58bf1968ae3eb00dec0254ed586d6366c2ae6dfec272187253833",
|
||||
"key_hex": "308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420758dc92a43dc0c8ed274a080e3d890f5ae8530a4828c499d5470f40994a2d6e0a1440342000479aa3ec3285dd2082ddcaf5cdb2839b6dda69b6d7b27cb9c7429f4697a840549949a92013865f0e61accf6bba49c11a7e1579b6604c33ec22cc5fe9bf1483256",
|
||||
"fingerprint": "ebc9c6df58d1637e2998d70976f463a910a59459f87157932690f93d26aaa930"
|
||||
}
|
||||
Generated
+2
-176
@@ -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"
|
||||
|
||||
+2
-2
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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<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]
|
||||
async fn main() {
|
||||
// 0. Регистрация глобального CryptoProvider (ring) для rustls 0.23+
|
||||
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 p2p_port: u16 = 8443;
|
||||
let mut ui_port: u16 = 8080;
|
||||
let mut custom_ip: Option<String> = None;
|
||||
|
||||
let args: Vec<String> = 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();
|
||||
}
|
||||
-60
@@ -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
@@ -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<String>,
|
||||
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<VecDeque<TextItem>>,
|
||||
pub disk_media: RwLock<VecDeque<MediaItem>>,
|
||||
pub peers: RwLock<HashMap<String, PeerNode>>,
|
||||
pub trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>,
|
||||
/// Ключ таблицы — постоянный fingerprint (ID) устройства
|
||||
pub peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
|
||||
pub http_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)]
|
||||
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;
|
||||
|
||||
+43
-5
@@ -10,21 +10,52 @@ use axum::{
|
||||
};
|
||||
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!({
|
||||
"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<Arc<AppState>>,
|
||||
Json(payload): Json<IncomingTextPayload>,
|
||||
) -> 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<Arc<AppState>>,
|
||||
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,
|
||||
|
||||
+68
-38
@@ -3,17 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LAN Share — Текст в ОЗУ / Медиа на Диск</title>
|
||||
<title>LAN Share — Постоянный PIN | Текст в ОЗУ | Медиа на Диск</title>
|
||||
<style>
|
||||
: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; }
|
||||
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; } }
|
||||
.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; }
|
||||
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-ram { background: rgba(16, 185, 129, 0.2); color: #34d399; }
|
||||
.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; }
|
||||
.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; }
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -41,30 +44,33 @@
|
||||
<div style="font-size: 0.85rem; color: var(--muted);" id="my-ip"></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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<!-- 1. Устройства в сети + ручной ввод -->
|
||||
<!-- 1. Устройства в сети -->
|
||||
<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;">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 2. Панель отправки (Только по нажатию) -->
|
||||
<!-- 2. Отправка данных -->
|
||||
<div class="card">
|
||||
<h2>2. Отправка данных</h2>
|
||||
<h2>2. Отправка данных (по нажатию)</h2>
|
||||
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
|
||||
<select id="target-peer"><option value="">-- Выберите устройство --</option></select>
|
||||
|
||||
@@ -76,7 +82,7 @@
|
||||
<button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button>
|
||||
</div>
|
||||
|
||||
<!-- 3. Принятый текст (Строго в ОЗУ) -->
|
||||
<!-- 3. Принятый текст в ОЗУ -->
|
||||
<div class="card">
|
||||
<h2>
|
||||
<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>
|
||||
|
||||
<!-- 4. Принятые медиафайлы (Сохранены на диске) -->
|
||||
<!-- 4. Принятые медиафайлы на диске -->
|
||||
<div class="card">
|
||||
<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>
|
||||
@@ -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 = '<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 {
|
||||
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>
|
||||
<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-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>
|
||||
<div style="font-weight:600;">
|
||||
<span class="dot ${dotClass}"></span>${escapeHtml(p.name)}
|
||||
<span style="color:var(--muted); font-size:0.85rem;">(${escapeHtml(ipStr)}:${p.port})</span>
|
||||
</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 ? 'Отозвать' : 'Заверить'}
|
||||
</button>
|
||||
</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) {
|
||||
knownPeersKey = currentSelectKey;
|
||||
const select = document.getElementById('target-peer');
|
||||
const prevVal = select.value;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+47
-23
@@ -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<RwLock<HashMap<String, String>>>,
|
||||
peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
|
||||
provider: Arc<CryptoProvider>,
|
||||
}
|
||||
|
||||
impl PinnedCertVerifier {
|
||||
pub fn new(trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>) -> Self {
|
||||
pub fn new(peers: Arc<RwLock<HashMap<String, PeerDevice>>>) -> 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<ServerCertVerified, TlsError> {
|
||||
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<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 CertifiedKey { cert, key_pair } =
|
||||
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);
|
||||
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<RwLock<HashMap<String, String>>>,
|
||||
peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
|
||||
) -> 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(),
|
||||
))
|
||||
|
||||
@@ -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<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
let trusted_map = state.trusted_fingerprints.read().unwrap();
|
||||
let mut peers_list: Vec<PeerNode> = state
|
||||
let mut peers_list: Vec<PeerDevice> = 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<TextItem> = state.ram_texts.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!({
|
||||
"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(),
|
||||
"peers": peers_list,
|
||||
"texts": texts,
|
||||
"media": media
|
||||
"media": media,
|
||||
"logs": logs
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -55,76 +49,73 @@ pub async fn ui_add_manual_peer(
|
||||
Json(req): Json<UiManualPeerRequest>,
|
||||
) -> Result<StatusCode, (StatusCode, 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 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<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(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UiTrustRequest>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
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<Arc<AppState>>) -> 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<Arc<AppState>>,
|
||||
Json(req): Json<UiSendTextRequest>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
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<StatusCode, (StatusCode, String)> {
|
||||
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()))
|
||||
|
||||
Reference in new issue
Block a user