Files
fileshare-app/src/models.rs
T
2026-10-25 23:16:47 +03:00

848 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tokio::sync::{Mutex, Notify, Semaphore};
// ───────────────────────── Лимиты ─────────────────────────
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 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;
pub const MIN_PING_SEC: u64 = 10;
pub const MAX_PING_SEC: u64 = 30;
pub const DEFAULT_PING_SEC: u64 = 15;
pub const MAX_PING_FAILS: u8 = 5;
// ───────────────────────── Ошибки ─────────────────────────
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,
pub text: String,
pub timestamp: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaItem {
pub sender: String,
pub filename: String,
pub saved_path: String,
pub mime: String,
pub size: u64,
pub timestamp: u64,
pub previewable: bool,
pub risky: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeerDevice {
pub id: String,
pub name: String,
pub ips: Vec<String>,
pub port: u16,
pub fingerprint: String,
/// Код сверки (SAS): зависит от ОБОИХ сертификатов, на двух устройствах одинаков.
pub pin_code: String,
/// Мы сверили код и подтвердили это устройство (наше решение; по нему принимаем/отправляем).
pub verified: bool,
/// Собеседник подтвердил сверку с НАШИМ устройством (узнаём по mTLS-каналу из /p2p/info).
pub trusts_me: bool,
pub online: bool,
/// Когда устройство последний раз отвечало по сети (unix timestamp, сек).
pub last_seen: u64,
/// Когда мы последний раз опрашивали (пинговали) это устройство (unix timestamp, сек).
pub last_ping: u64,
/// Количество подряд неудачных попыток пинга (при достижении 5 автоопрос прекращается).
pub fail_count: u8,
}
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,
trusts_me: false,
online: false,
last_seen: 0,
last_ping: 0,
fail_count: 0,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Direction {
In,
Out,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransferProgress {
pub id: String,
pub filename: String,
pub peer_name: String,
pub direction: Direction,
pub transferred_bytes: u64,
pub total_bytes: u64,
pub percent: u8,
pub done: bool,
pub error: Option<String>,
pub updated_at: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ScanProgress {
pub active: bool,
pub range: String,
pub scanned: usize,
pub total: usize,
pub found: usize,
}
#[derive(Serialize, Deserialize)]
pub struct IncomingTextPayload {
pub text: String,
}
#[derive(Deserialize)]
pub struct UiSendTextRequest {
pub peer_id: String,
pub text: String,
}
#[derive(Deserialize)]
pub struct UiTrustRequest {
pub peer_id: String,
pub trust: bool,
}
#[derive(Deserialize)]
pub struct UiPingPeerRequest {
pub peer_id: String,
}
#[derive(Deserialize)]
pub struct UiManualPeerRequest {
pub ip: String,
pub port: Option<u16>,
}
#[derive(Deserialize)]
pub struct UiScanRangeRequest {
pub range: String,
pub port: Option<u16>,
}
#[derive(Deserialize)]
pub struct UiSettingsRequest {
pub ping_interval_sec: u64,
}
// ───────────────────────── Состояние ─────────────────────────
pub struct AppState {
pub device_name: String,
pub my_ip: RwLock<String>,
pub fixed_ip: bool,
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, u16), Instant>>,
/// Интервал опроса известных пиров (сек), меняется на лету
pub ping_interval_sec: AtomicU64,
/// Время последнего глобального опроса устройств (unix timestamp, сек)
pub last_ping: AtomicU64,
pub ping_notify: Notify,
/// true в режиме `serve`: журнал дублируется в stdout (в CLI-командах stdout занят результатом)
pub log_stdout: bool,
/// true в `serve --no-ui`: входящий текст печатается в терминал
pub headless: bool,
/// mtime trusted.json, чтобы подхватывать правки из CLI (trust/untrust) работающим узлом
pub trust_mtime: PlMutex<Option<SystemTime>>,
}
impl AppState {
pub fn current_ip(&self) -> String {
self.my_ip.read().clone()
}
pub fn update_my_ip(&self, ip: Ipv4Addr) -> bool {
if self.fixed_ip {
return false;
}
let s = ip.to_string();
let mut cur = self.my_ip.write();
if *cur != s {
*cur = s;
true
} else {
false
}
}
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();
if self.log_stdout {
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,
trusts_me: p.trusts_me,
last_seen: p.last_seen,
})
.collect();
match storage::save_trusted(&self.data_dir, &entries) {
Ok(()) => {
if let Ok(mt) = std::fs::metadata(self.data_dir.join("trusted.json")).and_then(|m| m.modified()) {
*self.trust_mtime.lock() = Some(mt);
}
}
Err(e) => 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;
p.name = name.clone();
p.port = port;
p.online = true;
p.fail_count = 0;
p.last_seen = now;
p.last_ping = 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.fail_count = 0;
p.last_seen = now;
p.last_ping = 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
}
/// Обновляет активный IP и время активности уже известного пира (при входящем/исходящем обмене).
pub fn touch_peer_ip(&self, fp: &str, ip: IpAddr) {
if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() {
return;
}
let ip_s = ip.to_string();
let now = now_sec();
let mut dirty = false;
if let Some(p) = self.peers.write().get_mut(fp) {
let changed = p.ips.first() != Some(&ip_s);
p.online = true;
p.fail_count = 0;
p.last_seen = now;
if changed {
p.ips.retain(|x| x != &ip_s);
p.ips.insert(0, ip_s);
p.ips.truncate(MAX_PEER_IPS);
if p.verified {
dirty = true;
}
}
}
if dirty {
self.trust_dirty.store(true, Ordering::Relaxed);
}
}
/// Сбрасывает счётчик неудачных пингов у всех известных устройств (вызывается при ручном опросе или сканировании).
pub fn reset_all_ping_failures(&self) {
let mut peers = self.peers.write();
for p in peers.values_mut() {
p.fail_count = 0;
}
}
/// Пир уже недавно виден по этому адресу и порту — повторный probe по beacon не нужен.
pub fn is_fresh(&self, fp: &str, ip: IpAddr, port: u16) -> bool {
let ip_s = ip.to_string();
self.peers.read().get(fp).map_or(false, |p| {
p.online
&& p.port == port
&& 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, port: u16) -> 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(15));
}
match m.get(&(ip, port)) {
Some(t) if now.duration_since(*t) < Duration::from_secs(4) => false,
_ => {
m.insert((ip, port), now);
true
}
}
}
/// Меняет интервал опроса: применяется сразу и сохраняется в config.json.
pub fn set_ping_interval(&self, sec: u64) -> Result<(), String> {
if !(MIN_PING_SEC..=MAX_PING_SEC).contains(&sec) {
return Err(format!("Интервал должен быть от {} до {} секунд", MIN_PING_SEC, MAX_PING_SEC));
}
self.ping_interval_sec.store(sec, Ordering::Relaxed);
self.ping_notify.notify_one(); // будим цикл опроса, чтобы он взял новое значение
if let Err(e) = storage::update_config(&self.data_dir, |c| c.ping_interval_sec = Some(sec)) {
self.add_log(format!("Не удалось сохранить config.json: {}", e));
}
self.add_log(format!("Интервал опроса устройств: {} с", sec));
Ok(())
}
/// Бит «собеседник подтвердил сверку с нами». Источник — только аутентифицированный (mTLS)
/// канал: заголовок/ответ /p2p/info от владельца отпечатка.
pub fn set_remote_trust(&self, fp: &str, v: bool) {
let (msg, hint) = {
let mut peers = self.peers.write();
let Some(p) = peers.get_mut(fp) else { return };
if p.trusts_me == v {
return;
}
p.trusts_me = v;
if v && !p.verified {
(
format!(
"«{}» запрашивает взаимную сверку. Код сверки: {} — сравните его на экране устройства и подтвердите",
p.name, p.pin_code
),
Some(short_id(fp)),
)
} else if v {
(format!("«{}» подтвердил сверку — доверие взаимное", p.name), None)
} else {
(format!("«{}» отозвал доверие к вашему устройству", p.name), None)
}
};
self.trust_dirty.store(true, Ordering::Relaxed);
self.add_log(msg);
if let (true, Some(id)) = (self.headless, hint) {
println!(" → подтвердить: ramshare_ui trust {}", id);
}
}
/// Подхватывает изменения trusted.json, сделанные другим процессом (CLI: trust/untrust).
pub fn sync_trust_from_disk(&self) {
let path = self.data_dir.join("trusted.json");
let Ok(mt) = std::fs::metadata(&path).and_then(|m| m.modified()) else { return };
{
let mut last = self.trust_mtime.lock();
if *last == Some(mt) {
return;
}
*last = Some(mt);
}
let Some(entries) = storage::read_trusted_strict(&self.data_dir) else { return };
let mut peers = self.peers.write();
for p in peers.values_mut() {
p.verified = entries.iter().any(|e| e.fp == p.id);
}
for e in entries {
if !is_valid_fp(&e.fp) || e.fp == self.my_fingerprint || peers.contains_key(&e.fp) {
continue;
}
if peers.len() >= MAX_PEERS {
break;
}
let ips: Vec<String> = e
.ips
.into_iter()
.filter(|s| s.parse::<IpAddr>().is_ok())
.take(MAX_PEER_IPS)
.collect();
let port = if e.port == 0 { 8443 } else { e.port };
let mut np = PeerDevice::new(&e.fp, clean_label(&e.name, 48), ips, port, &self.my_fingerprint, true);
np.trusts_me = e.trusts_me;
np.last_seen = e.last_seen;
peers.insert(e.fp.clone(), np);
}
}
pub fn maintenance(&self) {
self.sync_trust_from_disk();
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)
.unwrap_or_default()
.as_secs()
}
pub fn percent(done: u64, total: u64) -> u8 {
if total == 0 {
100
} else {
((done as u128 * 100) / total as u128).min(100) as u8
}
}
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()
}
/// Для печати чужого текста в терминал: убирает управляющие/escape-символы (кроме \n и \t).
pub fn strip_ctrl(s: &str) -> String {
s.chars()
.map(|c| if c.is_control() && c != '\n' && c != '\t' { '\u{fffd}' } else { c })
.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())
}
/// Локальные IPv4: RFC 1918 (10/8, 172.16/12, 192.168/16), Link-Local (169.254/16) и CGNAT/VPN (100.64/10).
pub fn is_lan_v4(ip: Ipv4Addr) -> bool {
let o = ip.octets();
ip.is_private() || ip.is_link_local() || (o[0] == 100 && (o[1] & 0b1100_0000) == 0b0100_0000)
}
/// Принимает уже раскодированное имя. Ничего не декодирует.
pub fn sanitize_filename(name: &str) -> String {
let clean: String = name
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
c if c.is_control() => '_',
c => c,
})
.collect();
let trimmed = clean.trim_matches(|c: char| c == '.' || c.is_whitespace());
if trimmed.is_empty() {
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(),
}
}
/// Диапазоны: "all", "192.168.1.1-254", "192.168.1.10-192.168.1.80", "192.168.1.0/24" (маски /22–/32),
/// одиночные адреса и списки через запятую. Только локальные 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::<Ipv4Addr>()
.ok()
.filter(|ip| is_lan_v4(*ip))
.or_else(crate::net::detect_lan_ip);
let effective = if raw.is_empty() || raw.eq_ignore_ascii_case("all") || raw.eq_ignore_ascii_case("full") {
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 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());
}
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, 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 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))?;
(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));
}
}
}
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);
}
}