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

No files matched your search

-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 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
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_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
View File
@@ -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,
+73 -43
View File
@@ -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 => `
<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>
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;">
<span class="dot ${dotClass}"></span>${escapeHtml(p.name)}
<span style="color:var(--muted); font-size:0.85rem;">(${escapeHtml(ipStr)}:${p.port})</span>
</div>
<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>
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.ip}', ${!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
View File
@@ -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(),
))
+81 -65
View File
@@ -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()))