a.0.0.5
This commit is contained in:
1 parent
e88dc07696
commit
5d01dfb63a
12 files changed
+2438
-1891
No files matched your search
+511
-202
@@ -1,24 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
net::Ipv4Addr,
|
||||
sync::{Arc, RwLock},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
use crate::storage::{self, TrustedEntry};
|
||||
use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use parking_lot::{Mutex as PlMutex, RwLock};
|
||||
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, BTreeSet, HashMap, VecDeque},
|
||||
fmt::Display,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024;
|
||||
// ───────────────────────── Лимиты ─────────────────────────
|
||||
pub const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // один файл
|
||||
pub const MAX_MEDIA_QUOTA: u64 = 10 * 1024 * 1024 * 1024; // всего в received_media
|
||||
pub const MAX_TEXT_BYTES: usize = 64 * 1024;
|
||||
pub const MAX_RAM_TEXTS: usize = 50;
|
||||
pub const MAX_LOGS: usize = 100;
|
||||
pub const MEDIA_DIR: &str = "received_media";
|
||||
pub const MAX_PEERS: usize = 256;
|
||||
pub const MAX_PEER_IPS: usize = 4;
|
||||
pub const MAX_TRANSFERS: usize = 20;
|
||||
pub const MAX_INCOMING: usize = 4;
|
||||
pub const MAX_MEDIA_LIST: usize = 500;
|
||||
pub const DISCOVERY_PORT: u16 = 5354;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SavedIdentity {
|
||||
pub cert_hex: String,
|
||||
pub key_hex: String,
|
||||
pub fingerprint: String,
|
||||
// ───────────────────────── Ошибки ─────────────────────────
|
||||
pub struct AppError(pub StatusCode, pub String);
|
||||
|
||||
impl AppError {
|
||||
pub fn new(status: StatusCode, msg: impl Into<String>) -> Self {
|
||||
Self(status, msg.into())
|
||||
}
|
||||
/// Детали пишем в stderr, наружу отдаём общий текст (без путей и внутренностей).
|
||||
pub fn internal(ctx: &str, e: impl Display) -> Self {
|
||||
eprintln!("[ERR] {}: {}", ctx, e);
|
||||
Self(StatusCode::INTERNAL_SERVER_ERROR, "Внутренняя ошибка".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(self.0, self.1).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────── DTO ─────────────────────────
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TextItem {
|
||||
pub sender: String,
|
||||
@@ -32,8 +67,10 @@ pub struct MediaItem {
|
||||
pub filename: String,
|
||||
pub saved_path: String,
|
||||
pub mime: String,
|
||||
pub size: usize,
|
||||
pub size: u64,
|
||||
pub timestamp: u64,
|
||||
pub previewable: bool,
|
||||
pub risky: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -43,18 +80,34 @@ pub struct PeerDevice {
|
||||
pub ips: Vec<String>,
|
||||
pub port: u16,
|
||||
pub fingerprint: String,
|
||||
/// Код сверки (SAS): зависит от ОБОИХ сертификатов, на двух устройствах одинаков.
|
||||
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,
|
||||
impl PeerDevice {
|
||||
pub fn new(fp: &str, name: String, ips: Vec<String>, port: u16, my_fp: &str, verified: bool) -> Self {
|
||||
Self {
|
||||
id: fp.to_string(),
|
||||
name,
|
||||
ips,
|
||||
port,
|
||||
fingerprint: fp.to_string(),
|
||||
pin_code: sas_code(my_fp, fp),
|
||||
verified,
|
||||
online: false,
|
||||
last_seen: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Direction {
|
||||
In,
|
||||
Out,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -62,10 +115,9 @@ pub struct TransferProgress {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub peer_name: String,
|
||||
/// "out" (отправка) или "in" (прием)
|
||||
pub direction: String,
|
||||
pub transferred_bytes: usize,
|
||||
pub total_bytes: usize,
|
||||
pub direction: Direction,
|
||||
pub transferred_bytes: u64,
|
||||
pub total_bytes: u64,
|
||||
pub percent: u8,
|
||||
pub done: bool,
|
||||
pub error: Option<String>,
|
||||
@@ -81,99 +133,8 @@ pub struct ScanProgress {
|
||||
pub found: usize,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub device_name: String,
|
||||
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: Arc<RwLock<HashMap<String, PeerDevice>>>,
|
||||
pub http_client: reqwest::Client,
|
||||
pub probe_client: reqwest::Client,
|
||||
pub transfers: RwLock<Vec<TransferProgress>>,
|
||||
pub scan_status: RwLock<ScanProgress>,
|
||||
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));
|
||||
}
|
||||
|
||||
pub fn upsert_transfer(&self, item: TransferProgress) {
|
||||
let mut list = self.transfers.write().unwrap();
|
||||
if let Some(existing) = list.iter_mut().find(|t| t.id == item.id) {
|
||||
*existing = item;
|
||||
} else {
|
||||
list.insert(0, item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_transfer(&self, id: &str) {
|
||||
let mut list = self.transfers.write().unwrap();
|
||||
list.retain(|t| t.id != id);
|
||||
}
|
||||
|
||||
pub fn upsert_peer(&self, name: String, ip: String, port: u16, fingerprint: String) -> bool {
|
||||
if fingerprint == self.my_fingerprint || fingerprint.len() < 8 {
|
||||
return false;
|
||||
}
|
||||
if ip.is_empty() || ip == "0.0.0.0" || ip == "127.0.0.1" {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
false
|
||||
} 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,
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IncomingTextPayload {
|
||||
pub sender: String,
|
||||
pub sender_fp: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
@@ -201,6 +162,240 @@ pub struct UiScanRangeRequest {
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
// ───────────────────────── Состояние ─────────────────────────
|
||||
pub struct AppState {
|
||||
pub device_name: String,
|
||||
pub my_ip: String,
|
||||
pub p2p_port: u16,
|
||||
pub my_fingerprint: String,
|
||||
pub cert_der: Vec<u8>,
|
||||
pub key_der: Vec<u8>,
|
||||
pub data_dir: PathBuf,
|
||||
pub media_dir: PathBuf,
|
||||
pub ram_texts: RwLock<VecDeque<TextItem>>,
|
||||
pub disk_media: RwLock<VecDeque<MediaItem>>,
|
||||
pub peers: Arc<RwLock<HashMap<String, PeerDevice>>>,
|
||||
pub probe_client: reqwest::Client,
|
||||
pub transfers: RwLock<Vec<TransferProgress>>,
|
||||
pub scan_status: RwLock<ScanProgress>,
|
||||
pub logs: RwLock<VecDeque<String>>,
|
||||
pub incoming_slots: Arc<Semaphore>,
|
||||
pub ping_lock: Mutex<()>,
|
||||
pub trust_dirty: AtomicBool,
|
||||
pub probe_cooldown: PlMutex<HashMap<IpAddr, Instant>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn add_log(&self, msg: impl Into<String>) {
|
||||
// защита от log-injection: управляющие символы в именах и т.п.
|
||||
let text: String = msg
|
||||
.into()
|
||||
.chars()
|
||||
.map(|c| if c.is_control() { ' ' } else { c })
|
||||
.collect();
|
||||
println!("[LOG] {}", text);
|
||||
let mut logs = self.logs.write();
|
||||
if logs.len() >= MAX_LOGS {
|
||||
logs.pop_back();
|
||||
}
|
||||
logs.push_front(format!("[{}] {}", now_sec(), text));
|
||||
}
|
||||
|
||||
// ── передачи ──
|
||||
pub fn add_transfer(&self, t: TransferProgress) {
|
||||
let mut l = self.transfers.write();
|
||||
l.insert(0, t);
|
||||
l.truncate(MAX_TRANSFERS);
|
||||
}
|
||||
|
||||
pub fn update_transfer_bytes(&self, id: &str, bytes: u64) {
|
||||
let mut l = self.transfers.write();
|
||||
if let Some(t) = l.iter_mut().find(|t| t.id == id) {
|
||||
t.transferred_bytes = bytes;
|
||||
t.percent = percent(bytes, t.total_bytes);
|
||||
t.updated_at = now_sec();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_transfer(&self, id: &str) {
|
||||
let mut l = self.transfers.write();
|
||||
if let Some(t) = l.iter_mut().find(|t| t.id == id) {
|
||||
t.transferred_bytes = t.total_bytes;
|
||||
t.percent = 100;
|
||||
t.done = true;
|
||||
t.updated_at = now_sec();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fail_transfer(&self, id: &str, msg: String) {
|
||||
let mut l = self.transfers.write();
|
||||
if let Some(t) = l.iter_mut().find(|t| t.id == id) {
|
||||
t.done = true;
|
||||
t.error = Some(msg);
|
||||
t.updated_at = now_sec();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_transfer(&self, id: &str) {
|
||||
self.transfers.write().retain(|t| t.id != id);
|
||||
}
|
||||
|
||||
// ── доверие ──
|
||||
pub fn trusted_name(&self, fp: &str) -> Option<String> {
|
||||
self.peers
|
||||
.read()
|
||||
.get(fp)
|
||||
.filter(|p| p.verified)
|
||||
.map(|p| p.name.clone())
|
||||
}
|
||||
|
||||
pub fn persist_trust(&self) {
|
||||
let entries: Vec<TrustedEntry> = self
|
||||
.peers
|
||||
.read()
|
||||
.values()
|
||||
.filter(|p| p.verified)
|
||||
.map(|p| TrustedEntry {
|
||||
fp: p.id.clone(),
|
||||
name: p.name.clone(),
|
||||
ips: p.ips.clone(),
|
||||
port: p.port,
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = storage::save_trusted(&self.data_dir, &entries) {
|
||||
self.add_log(format!("Не удалось сохранить список доверенных: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_for_peer(&self, fp: &str, streaming: bool) -> Result<reqwest::Client, AppError> {
|
||||
crate::tls::pinned_client(
|
||||
fp.to_string(),
|
||||
self.peers.clone(),
|
||||
&self.cert_der,
|
||||
&self.key_der,
|
||||
streaming,
|
||||
)
|
||||
.map_err(|e| AppError::internal("tls client", e))
|
||||
}
|
||||
|
||||
// ── пиры ──
|
||||
/// Регистрирует/обновляет пира. Вызывать ТОЛЬКО с отпечатком, полученным из TLS-рукопожатия.
|
||||
pub fn upsert_peer(&self, name: &str, ip: IpAddr, port: u16, fp: &str) -> bool {
|
||||
if fp == self.my_fingerprint || !is_valid_fp(fp) || port == 0 {
|
||||
return false;
|
||||
}
|
||||
if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() {
|
||||
return false;
|
||||
}
|
||||
let mut name = clean_label(name, 48);
|
||||
if name.is_empty() {
|
||||
name = "Unknown".into();
|
||||
}
|
||||
let ip_s = ip.to_string();
|
||||
let now = now_sec();
|
||||
let mut created = false;
|
||||
let mut dirty = false;
|
||||
|
||||
{
|
||||
let mut peers = self.peers.write();
|
||||
if !peers.contains_key(fp) && peers.len() >= MAX_PEERS {
|
||||
let victim = peers
|
||||
.values()
|
||||
.filter(|p| !p.verified && !p.online)
|
||||
.min_by_key(|p| p.last_seen)
|
||||
.map(|p| p.id.clone());
|
||||
match victim {
|
||||
Some(v) => {
|
||||
peers.remove(&v);
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
match peers.entry(fp.to_string()) {
|
||||
Entry::Occupied(mut e) => {
|
||||
let p = e.get_mut();
|
||||
let changed = p.ips.first() != Some(&ip_s) || p.port != port;
|
||||
p.name = name.clone();
|
||||
p.port = port;
|
||||
p.online = true;
|
||||
p.last_seen = now;
|
||||
p.ips.retain(|x| x != &ip_s);
|
||||
p.ips.insert(0, ip_s.clone());
|
||||
p.ips.truncate(MAX_PEER_IPS);
|
||||
if p.verified && changed {
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
let mut p = PeerDevice::new(fp, name.clone(), vec![ip_s.clone()], port, &self.my_fingerprint, false);
|
||||
p.online = true;
|
||||
p.last_seen = now;
|
||||
v.insert(p);
|
||||
created = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dirty {
|
||||
self.trust_dirty.store(true, Ordering::Relaxed);
|
||||
}
|
||||
if created {
|
||||
self.add_log(format!(
|
||||
"Обнаружено устройство '{}' ({}:{}), код сверки: {}",
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
sas_code(&self.my_fingerprint, fp)
|
||||
));
|
||||
}
|
||||
created
|
||||
}
|
||||
|
||||
/// Пир уже недавно виден по этому адресу — повторный probe по beacon не нужен.
|
||||
pub fn is_fresh(&self, fp: &str, ip: IpAddr) -> bool {
|
||||
let ip_s = ip.to_string();
|
||||
self.peers.read().get(fp).map_or(false, |p| {
|
||||
p.online
|
||||
&& p.ips.first().map(String::as_str) == Some(ip_s.as_str())
|
||||
&& now_sec().saturating_sub(p.last_seen) < 10
|
||||
})
|
||||
}
|
||||
|
||||
pub fn probe_allowed(&self, ip: IpAddr) -> bool {
|
||||
let mut m = self.probe_cooldown.lock();
|
||||
let now = Instant::now();
|
||||
if m.len() > 512 {
|
||||
m.retain(|_, t| now.duration_since(*t) < Duration::from_secs(30));
|
||||
}
|
||||
match m.get(&ip) {
|
||||
Some(t) if now.duration_since(*t) < Duration::from_secs(10) => false,
|
||||
_ => {
|
||||
m.insert(ip, now);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintenance(&self) {
|
||||
let now = now_sec();
|
||||
self.peers
|
||||
.write()
|
||||
.retain(|_, p| p.verified || p.online || now.saturating_sub(p.last_seen) < 600);
|
||||
if self.trust_dirty.swap(false, Ordering::Relaxed) {
|
||||
self.persist_trust();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_transfer_later(state: &Arc<AppState>, id: String, secs: u64) {
|
||||
let st = state.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(secs)).await;
|
||||
st.remove_transfer(&id);
|
||||
});
|
||||
}
|
||||
|
||||
// ───────────────────────── Утилиты ─────────────────────────
|
||||
pub fn now_sec() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -208,132 +403,246 @@ pub fn now_sec() -> u64 {
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
pub fn url_decode(input: &str) -> String {
|
||||
let mut bytes = Vec::with_capacity(input.len());
|
||||
let mut chars = input.as_bytes().iter().copied();
|
||||
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)
|
||||
{
|
||||
bytes.push(decoded);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if b == b'+' {
|
||||
bytes.push(b' ');
|
||||
} else {
|
||||
bytes.push(b);
|
||||
}
|
||||
pub fn percent(done: u64, total: u64) -> u8 {
|
||||
if total == 0 {
|
||||
100
|
||||
} else {
|
||||
((done as u128 * 100) / total as u128).min(100) as u8
|
||||
}
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
pub fn fingerprint(der: &[u8]) -> String {
|
||||
hex::encode(Sha256::digest(der))
|
||||
}
|
||||
|
||||
pub fn is_valid_fp(fp: &str) -> bool {
|
||||
fp.len() == 64 && fp.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
|
||||
}
|
||||
|
||||
pub fn short_id(fp: &str) -> String {
|
||||
fp.get(..8).unwrap_or(fp).to_uppercase()
|
||||
}
|
||||
|
||||
/// SAS-код: SHA-256(sorted(fpA, fpB)), 80 бит, группы по 4 символа.
|
||||
/// Подбор ключа «под префикс» не работает: код зависит от обоих сертификатов.
|
||||
pub fn sas_code(a: &str, b: &str) -> String {
|
||||
let (x, y) = if a <= b { (a, b) } else { (b, a) };
|
||||
let mut h = Sha256::new();
|
||||
h.update(b"ramshare-sas-v1");
|
||||
h.update(x.as_bytes());
|
||||
h.update(b"|");
|
||||
h.update(y.as_bytes());
|
||||
let digest = h.finalize();
|
||||
let s = hex::encode_upper(&digest[..10]);
|
||||
s.as_bytes()
|
||||
.chunks(4)
|
||||
.map(|c| std::str::from_utf8(c).unwrap_or(""))
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
}
|
||||
|
||||
pub fn random_hex(n: usize) -> String {
|
||||
let mut b = vec![0u8; n];
|
||||
getrandom::getrandom(&mut b).expect("ОС не предоставила случайные байты");
|
||||
hex::encode(b)
|
||||
}
|
||||
|
||||
/// Имя устройства и т.п.: без управляющих и bidi-символов, ограниченной длины.
|
||||
pub fn clean_label(s: &str, max_chars: usize) -> String {
|
||||
s.chars()
|
||||
.filter(|c| {
|
||||
!c.is_control() && !matches!(*c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}' | '\u{200e}' | '\u{200f}')
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.chars()
|
||||
.take(max_chars)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn pct_decode(s: &str) -> String {
|
||||
percent_decode_str(s).decode_utf8_lossy().into_owned()
|
||||
}
|
||||
|
||||
pub fn pct_encode(s: &str) -> String {
|
||||
utf8_percent_encode(s, NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
||||
pub fn hdr<'a>(h: &'a HeaderMap, name: &str) -> Option<&'a str> {
|
||||
h.get(name).and_then(|v| v.to_str().ok())
|
||||
}
|
||||
|
||||
pub fn is_lan_v4(ip: Ipv4Addr) -> bool {
|
||||
ip.is_private() || ip.is_link_local()
|
||||
}
|
||||
|
||||
/// Принимает уже раскодированное имя. Ничего не декодирует.
|
||||
pub fn sanitize_filename(name: &str) -> String {
|
||||
let decoded = url_decode(name);
|
||||
let clean: String = decoded
|
||||
let clean: String = name
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_',
|
||||
_ => c,
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
c if c.is_control() => '_',
|
||||
c => c,
|
||||
})
|
||||
.collect();
|
||||
let trimmed = clean.trim_matches('.').trim();
|
||||
let trimmed = clean.trim_matches(|c: char| c == '.' || c.is_whitespace());
|
||||
if trimmed.is_empty() {
|
||||
"media_file.bin".to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
return "file.bin".into();
|
||||
}
|
||||
const MAX: usize = 120;
|
||||
if trimmed.chars().count() <= MAX {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
match trimmed.rfind('.') {
|
||||
Some(i) if trimmed[i..].chars().count() <= 16 => {
|
||||
let ext = &trimmed[i..];
|
||||
let keep = MAX - ext.chars().count();
|
||||
format!("{}{}", trimmed[..i].chars().take(keep).collect::<String>(), ext)
|
||||
}
|
||||
_ => trimmed.chars().take(MAX).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Парсер диапазонов IP-адресов:
|
||||
/// Поддерживает: "all", "192.168.1.1-254", "192.168.1.10-50", "192.168.1.10-192.168.1.80", "192.168.1.0/24"
|
||||
pub fn parse_ip_range(input: &str, my_ip: &str) -> Result<(String, Vec<String>), String> {
|
||||
/// Диапазоны: "all", "192.168.1.1-254", "192.168.1.10-192.168.1.80", "192.168.1.0/24" (маски /22–/32),
|
||||
/// одиночные адреса и списки через запятую. Только локальные (private/link-local) IPv4.
|
||||
pub fn parse_ip_range(input: &str, my_ip: &str) -> Result<(String, Vec<Ipv4Addr>), String> {
|
||||
const MAX_TOTAL: usize = 1024;
|
||||
let raw = input.trim();
|
||||
let mine: Option<Ipv4Addr> = my_ip.parse().ok();
|
||||
|
||||
let effective = if raw.is_empty() || raw.eq_ignore_ascii_case("all") || raw.eq_ignore_ascii_case("full") {
|
||||
let base: Ipv4Addr = my_ip
|
||||
.parse()
|
||||
.map_err(|_| "Не удалось определить подсеть автоматически. Введите диапазон вручную.".to_string())?;
|
||||
let base = mine.ok_or("Не удалось определить подсеть автоматически. Введите диапазон вручную.")?;
|
||||
let o = base.octets();
|
||||
format!("{}.{}.{}.1-254", o[0], o[1], o[2])
|
||||
} else {
|
||||
raw.to_string()
|
||||
};
|
||||
if effective.len() > 256 {
|
||||
return Err("Слишком длинная строка диапазона".into());
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut set: BTreeSet<u32> = BTreeSet::new();
|
||||
let mut tokens = 0;
|
||||
|
||||
for part in effective.split(',') {
|
||||
let token = part.trim();
|
||||
if token.is_empty() {
|
||||
continue;
|
||||
}
|
||||
tokens += 1;
|
||||
if tokens > 16 {
|
||||
return Err("Слишком много элементов в списке (максимум 16)".into());
|
||||
}
|
||||
|
||||
if let Some(cidr_base) = token.strip_suffix("/24") {
|
||||
let ip: Ipv4Addr = cidr_base
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| format!("Некорректный CIDR адрес: {}", token))?;
|
||||
let o = ip.octets();
|
||||
for last in 1..=254u8 {
|
||||
result.push(format!("{}.{}.{}.{}", o[0], o[1], o[2], last));
|
||||
let (start, end): (u32, u32) = if let Some((base, pfx)) = token.split_once('/') {
|
||||
let ip: Ipv4Addr = base.trim().parse().map_err(|_| format!("Некорректный CIDR адрес: {}", token))?;
|
||||
let p: u32 = pfx.trim().parse().map_err(|_| format!("Некорректная маска: {}", token))?;
|
||||
if !(22..=32).contains(&p) {
|
||||
return Err("Поддерживаются маски от /22 до /32".into());
|
||||
}
|
||||
let mask: u32 = !0u32 << (32 - p);
|
||||
let s = u32::from(ip) & mask;
|
||||
(s, s | !mask)
|
||||
} else if let Some((left, right)) = token.split_once('-') {
|
||||
let left = left.trim();
|
||||
let right = right.trim();
|
||||
let start_ip: Ipv4Addr = left
|
||||
.parse()
|
||||
.map_err(|_| format!("Некорректный начальный IP: {}", left))?;
|
||||
|
||||
if right.contains('.') {
|
||||
let end_ip: Ipv4Addr = right
|
||||
.parse()
|
||||
.map_err(|_| format!("Некорректный конечный IP: {}", right))?;
|
||||
let start_u = u32::from(start_ip);
|
||||
let end_u = u32::from(end_ip);
|
||||
if end_u < start_u {
|
||||
return Err("Конечный IP меньше начального".into());
|
||||
}
|
||||
if end_u - start_u > 1024 {
|
||||
return Err("Диапазон слишком велик (максимум 1024 адреса за раз)".into());
|
||||
}
|
||||
for val in start_u..=end_u {
|
||||
let ip = Ipv4Addr::from(val);
|
||||
let last = ip.octets()[3];
|
||||
if last != 0 && last != 255 {
|
||||
result.push(ip.to_string());
|
||||
}
|
||||
}
|
||||
let (left, right) = (left.trim(), right.trim());
|
||||
let s_ip: Ipv4Addr = left.parse().map_err(|_| format!("Некорректный начальный IP: {}", left))?;
|
||||
let s = u32::from(s_ip);
|
||||
let e = if right.contains('.') {
|
||||
let e_ip: Ipv4Addr = right.parse().map_err(|_| format!("Некорректный конечный IP: {}", right))?;
|
||||
u32::from(e_ip)
|
||||
} else {
|
||||
let end_last: u8 = right
|
||||
.parse()
|
||||
.map_err(|_| format!("Некорректный конец диапазона: {}", right))?;
|
||||
let o = start_ip.octets();
|
||||
let start_last = o[3];
|
||||
if end_last < start_last {
|
||||
return Err(format!("Конец диапазона ({}) меньше начала ({})", end_last, start_last));
|
||||
}
|
||||
for last in start_last..=end_last {
|
||||
if last != 0 && last != 255 {
|
||||
result.push(format!("{}.{}.{}.{}", o[0], o[1], o[2], last));
|
||||
}
|
||||
}
|
||||
let last: u8 = right.parse().map_err(|_| format!("Некорректный конец диапазона: {}", right))?;
|
||||
(s & !0xFF) | last as u32
|
||||
};
|
||||
if e < s {
|
||||
return Err("Конец диапазона меньше начала".into());
|
||||
}
|
||||
(s, e)
|
||||
} else {
|
||||
let ip: Ipv4Addr = token
|
||||
.parse()
|
||||
.map_err(|_| format!("Некорректный IP-адрес: {}", token))?;
|
||||
result.push(ip.to_string());
|
||||
let ip: Ipv4Addr = token.parse().map_err(|_| format!("Некорректный IP-адрес: {}", token))?;
|
||||
(u32::from(ip), u32::from(ip))
|
||||
};
|
||||
|
||||
if end - start > MAX_TOTAL as u32 {
|
||||
return Err(format!("Диапазон слишком велик (максимум {} адресов)", MAX_TOTAL));
|
||||
}
|
||||
for v in start..=end {
|
||||
let ip = Ipv4Addr::from(v);
|
||||
let last = ip.octets()[3];
|
||||
if start != end && (last == 0 || last == 255) {
|
||||
continue;
|
||||
}
|
||||
if !is_lan_v4(ip) {
|
||||
return Err(format!("Сканирование разрешено только в локальных сетях: {}", ip));
|
||||
}
|
||||
set.insert(v);
|
||||
if set.len() > MAX_TOTAL {
|
||||
return Err(format!("Слишком много адресов (максимум {})", MAX_TOTAL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.retain(|ip| ip != my_ip && ip != "127.0.0.1" && ip != "0.0.0.0");
|
||||
result.dedup();
|
||||
let result: Vec<Ipv4Addr> = set
|
||||
.into_iter()
|
||||
.map(Ipv4Addr::from)
|
||||
.filter(|ip| Some(*ip) != mine && !ip.is_loopback() && !ip.is_unspecified())
|
||||
.collect();
|
||||
|
||||
if result.is_empty() {
|
||||
return Err("Диапазон не содержит доступных для сканирования IP-адресов".into());
|
||||
}
|
||||
|
||||
Ok((effective, result))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn range_short_form() {
|
||||
let (_, v) = parse_ip_range("192.168.1.10-12", "192.168.1.11").unwrap();
|
||||
assert_eq!(v.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_rejects_public() {
|
||||
assert!(parse_ip_range("8.8.8.8", "192.168.1.2").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_all_and_dedup() {
|
||||
let (_, v) = parse_ip_range("all", "10.0.0.5").unwrap();
|
||||
assert_eq!(v.len(), 253);
|
||||
let (_, v) = parse_ip_range("10.0.0.1,10.0.0.1,10.0.0.2", "10.0.0.5").unwrap();
|
||||
assert_eq!(v.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_too_big() {
|
||||
assert!(parse_ip_range("10.0.0.0/16", "10.0.0.5").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filenames() {
|
||||
assert_eq!(sanitize_filename("../../etc/passwd"), "_.._etc_passwd");
|
||||
assert_eq!(sanitize_filename("a+b%25.txt"), "a+b%25.txt");
|
||||
assert_eq!(sanitize_filename("..."), "file.bin");
|
||||
assert!(sanitize_filename(&"x".repeat(500)).chars().count() <= 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fp_validation() {
|
||||
assert!(is_valid_fp(&"a".repeat(64)));
|
||||
assert!(!is_valid_fp("x');alert(1)//"));
|
||||
assert!(!is_valid_fp(&"A".repeat(64)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sas_symmetric() {
|
||||
let a = "a".repeat(64);
|
||||
let b = "b".repeat(64);
|
||||
assert_eq!(sas_code(&a, &b), sas_code(&b, &a));
|
||||
assert_eq!(sas_code(&a, &b).len(), 24);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user