From 9f2d7f635e7843e96247229aec14687747851f96 Mon Sep 17 00:00:00 2001 From: Stanislav N Mikhailov Date: Sat, 18 Jul 2026 21:58:32 +0300 Subject: [PATCH] feat(protocol): add initial parser for common message header --- Cargo.lock | 21 +++++++++++ Cargo.toml | 3 +- src/connection.rs | 95 ++++++++++++++++++++++++++++++----------------- src/main.MD | 61 +++++++++++++++++++++++++++++- src/protocol.rs | 81 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c39c5a8..b8132d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ name = "nocloud-core" version = "0.1.0" dependencies = [ "local-ip-address", + "thiserror", "tokio", ] @@ -346,6 +347,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.52.3" diff --git a/Cargo.toml b/Cargo.toml index c32aaf1..3680b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ panic = "abort" [dependencies] tokio = { version = "1", features = ["full"] } -local-ip-address = "0.6" \ No newline at end of file +local-ip-address = "0.6" +thiserror = "2.0" \ No newline at end of file diff --git a/src/connection.rs b/src/connection.rs index fb302e5..ee91716 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -4,6 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpStream}; use std::io; +use crate::protocol::{parce_header, ParseError}; pub struct Connection { id: u64, client_addr: SocketAddr, @@ -25,48 +26,72 @@ impl Connection { } } - pub async fn run(mut self) -> io::Result<()> { - println!( - "Обработчик {} создан для {}", - self.id, - self.client_addr - ); + pub async fn run(mut self) -> io::Result<()> { + println!( + "Обработчик {} создан для {}", + self.id, self.client_addr + ); - let mut buffer = [0_u8; 1024]; + // Буфер для данных тела сообщения (теперь он не хранит заголовок) + let mut body_buffer = vec![0_u8; 1024]; - loop { - let bytes_read = self.stream.read(&mut buffer).await?; + loop { + // 1. Создаем буфер СТРОГО под размер заголовка + let mut header_buffer = [0_u8; 20]; - if bytes_read == 0 { + // 2. Читаем из сети РОВНО 20 байт + // Если клиент закроет соединение до того, как пришлет 20 байт, + // read_exact вернет ошибку UnexpectedEof. + if let Err(e) = self.stream.read_exact(&mut header_buffer).await { + if e.kind() == io::ErrorKind::UnexpectedEof { println!( - "Обработчик {}: клиент {} отключился", - self.id, - self.client_addr + "Обработчик {}: клиент {} отключился (или прислал неполный заголовок)", + self.id, self.client_addr ); - return Ok(()); } - - self.message_count += 1; - - let received = - String::from_utf8_lossy(&buffer[..bytes_read]); - - println!( - "Обработчик {}: сообщение №{}: {:?}", - self.id, - self.message_count, - received - ); - - let response = format!( - "Обработчик {}: сообщение №{}: {}\n", - self.id, - self.message_count, - received - ); - - self.stream.write_all(response.as_bytes()).await?; + return Err(e); // Другие системные ошибки ввода-вывода (например, обрыв связи) } + + // 3. Вызываем ваш парсер заголовка + let header = match parce_header(&header_buffer).await { + Ok(h) => h, + Err(ParseError::InvalidFormat) => { + println!("Обработчик {}: Критическая ошибка! Неверный формат заголовка.", self.id); + // В сетевых протоколах при неверном заголовке соединение обычно сразу рвут + return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid header format")); + } + // Если вы добавите новые ошибки, например FilenameTooLong: + Err(e) => { + println!("Обработчик {}: Ошибка парсинга: {}", self.id, e); + return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())); + } + }; + + self.message_count += 1; + println!("Обработчик {}: Успешно распарсен заголовок №{}: {:?}", self.id, self.message_count, header); + + // 4. Используем данные из распарсенного заголовка! + // Теперь мы знаем точный размер тела сообщения благодаря header.body_size + let body_len = header.body_size() as usize; + + // Подгоняем размер буфера под размер тела (чтобы не читать лишнего) + if body_len > body_buffer.len() { + body_buffer.resize(body_len, 0); + } + + // Читаем из сети тело сообщения (ровно столько байт, сколько указано в заголовке) + self.stream.read_exact(&mut body_buffer[..body_len]).await?; + + let received_body = String::from_utf8_lossy(&body_buffer[..body_len]); + println!("Обработчик {}: Получено тело сообщения: {:?}", self.id, received_body); + + // 5. Отправляем ответ клиенту + let response = format!( + "Обработчик {}: Заголовок валиден (cmd={}). Тело принято.\n", + self.id, header.command() + ); + self.stream.write_all(response.as_bytes()).await?; } +} } \ No newline at end of file diff --git a/src/main.MD b/src/main.MD index 7e9bce4..a193e44 100644 --- a/src/main.MD +++ b/src/main.MD @@ -2,5 +2,62 @@ ## 📌 Текущее состояние * **Где остановился:** Сервер успешно запускается и слушает сетевой сокет. -* **Проблема:** Не удалось проверить эхо-ответ через PowerShell. -* **Следующий шаг:** Найти рабочий способ отправки тестового запроса в PowerShell или использовать альтернативный инструмент. + Проверочная строка: smikhai@DebHOME:~$ printf '\x4E\x43\x4C\x44\x01\x01\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x03\xFB' \ +> | nc 192.168.1.213 32768 + выдаёт ошибку парсера: + Сервер слушает 192.168.1.213:32768 + Обработчик 1 создан для 192.168.1.170:50978 + Обработчик 1: Критическая ошибка! Неверный формат заголовка. + Обработчик 1: ошибка: Invalid header format + что радует. +* **Проблема:** +* **Следующий шаг:** + ## Чек-лист: исправление парсера общего заголовка + + * [ ] Исправить имя функции `parce_header` → `parse_header`. + * [ ] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод. + * [ ] Убрать `.await` при вызове `parse_header`. + * [ ] Исправить порядок полей согласно протоколу: + + * `bytes[0..4]` — `magic`; + * `bytes[4]` — `version`; + * `bytes[5]` — `command`. + * [ ] Проверять `magic` как четыре байта `NCLD`. + * [ ] Проверять версию как число `1`, а не ASCII-символ `b'1'`. + * [ ] Пока разрешать только версию протокола `1`. + * [ ] Исправить команды `80 | 81` на `0x80 | 0x81`. + * [ ] Записывать в `Header.version` значение из `bytes[4]`. + * [ ] Для первого теста отправлять заголовок с `body_size = 0`. + + ## Проверка исправлений + + * [ ] Отправить корректный заголовок. + * [ ] Убедиться, что сервер вывел: + + ```text + Header { + version: 1, + command: 1, + flags: 0, + request_id: 17, + body_size: 0, + } + ``` + + * [ ] Отправить заголовок с неправильным `magic`. + * [ ] Убедиться, что сервер вернул `InvalidFormat`. + * [ ] Отправить заголовок с неподдерживаемой версией. + * [ ] Убедиться, что сервер вернул `InvalidFormat`. + * [ ] Отправить заголовок с неизвестной командой. + * [ ] Убедиться, что сервер вернул `InvalidFormat`. + * [ ] Отправить заголовок с запрещённым флагом, например `0x0008`. + * [ ] Убедиться, что сервер вернул `InvalidFormat`. + + ## Следующая итерация — пока не выполнять + + * [ ] Ограничить допустимый `body_size`. + * [ ] Не выделять буфер размером со всё тело сообщения. + * [ ] Начать отдельный разбор тела команды `UPLOAD`. + * [ ] Читать данные файла порциями, а не целиком в память. + + diff --git a/src/protocol.rs b/src/protocol.rs index e69de29..93cc8dd 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -0,0 +1,81 @@ +use thiserror::Error; + +#[derive(Debug)] +pub struct Header { + version: u8, + command: u8, + flags: u16, + request_id: u32, + body_size: u64, +} + +#[derive(Error, Debug)] +pub enum ParseError { + #[error("Заголовок пустой или имеет неверный формат")] + InvalidFormat, + #[error("Длина имени файла ({actual}) превышает допустимый лимит в {max} байт")] + FilenameTooLong { + actual: u16, + max: u16, +} +} + +pub async fn parce_header (bytes: &[u8;20]) -> Result { + // Получаем флаги + let flags_raw = u16::from_be_bytes([bytes[6],bytes[7]]); + let request_id_raw = u32::from_be_bytes([bytes[8],bytes[9],bytes[10],bytes[11]]); + let body_size_raw = u64::from_be_bytes([bytes[12],bytes[13],bytes[14],bytes[15],bytes[16],bytes[17],bytes[18],bytes[19]]); + // Валидация номера версии, magic и команды + let [ + b'1'..=b'3', + b'N', b'C', b'L', b'D', + _cmd @ (1..=4 | 80 | 81), + _, _, + .. + ] = bytes else { + return Err(ParseError::InvalidFormat); + }; + // Валидация флагов + if (flags_raw & 0xFFF8) !=0 { + return Err(ParseError::InvalidFormat); + } + + + let local_header = Header{ + version: bytes[0], + command: *_cmd, + flags: flags_raw, + request_id: request_id_raw, + body_size: body_size_raw, + }; + Ok(local_header) +} + +// Реализация публичных методов-геттеров для безопасного доступа к приватным полям +impl Header { + /// Возвращает версию протокола пакета + pub fn version(&self) -> u8 { + // Так как тип u8 реализует трейт Copy, значение просто копируется наружу + self.version + } + + /// Возвращает код команды (например, 1, 2, 80 или 81) + pub fn command(&self) -> u8 { + self.command + } + + /// Возвращает битовые флаги пакета (уже прошедшие валидацию) + pub fn flags(&self) -> u16 { + self.flags + } + + /// Возвращает уникальный идентификатор запроса клиента + pub fn request_id(&self) -> u32 { + self.request_id + } + + /// Возвращает размер тела сообщения в байтах + pub fn body_size(&self) -> u64 { + self.body_size + } +} \ No newline at end of file