From 0f2cb0384c400760c32ff49443063dfc9bdf1142 Mon Sep 17 00:00:00 2001 From: Stanislav N Mikhailov Date: Tue, 21 Jul 2026 22:57:50 +0300 Subject: [PATCH] feat(protocol): parse and validate binary packet headers --- Cargo.lock | 16 ++++++++ Cargo.toml | 3 +- src/connection.rs | 55 ++++++++++++++++++++------- src/main.MD | 6 +-- src/protocol.rs | 94 ++++++++++++++++++++++++++++------------------- 5 files changed, 120 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8132d9..6992929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,12 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "mio" version = "1.2.1" @@ -208,10 +214,20 @@ name = "nocloud-core" version = "0.1.0" dependencies = [ "local-ip-address", + "nom", "thiserror", "tokio", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "parking_lot" version = "0.12.5" diff --git a/Cargo.toml b/Cargo.toml index 3680b07..b175da7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ panic = "abort" [dependencies] tokio = { version = "1", features = ["full"] } local-ip-address = "0.6" -thiserror = "2.0" \ No newline at end of file +thiserror = "2.0" +nom = "8.0.0" diff --git a/src/connection.rs b/src/connection.rs index ee91716..72ed7a2 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -4,7 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpStream}; use std::io; -use crate::protocol::{parce_header, ParseError}; +use crate::protocol::parse_header; pub struct Connection { id: u64, client_addr: SocketAddr, @@ -54,25 +54,54 @@ impl Connection { } // 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")); + let header = match parse_header(&header_buffer) { + Ok((_remaining, header)) => { + println!( + "Обработчик {}: nom оставил {} байт", + self.id, + _remaining.len() + ); + header } - // Если вы добавите новые ошибки, например FilenameTooLong: - Err(e) => { - println!("Обработчик {}: Ошибка парсинга: {}", self.id, e); - return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())); - } - }; + + Err(error) => { + println!( + "Обработчик {}: ошибка парсинга заголовка: {:?}", + self.id, error + ); + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid header: {error:?}"), + )); + } +}; + +// +if let Err(error) = header.validate() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + error, + )); +} self.message_count += 1; println!("Обработчик {}: Успешно распарсен заголовок №{}: {:?}", self.id, self.message_count, header); // 4. Используем данные из распарсенного заголовка! // Теперь мы знаем точный размер тела сообщения благодаря header.body_size + const MAX_BODY_SIZE: u64 = 1024 * 1024; // 1 МиБ + + if header.body_size() > MAX_BODY_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Размер тела {} превышает лимит {}", + header.body_size(), + MAX_BODY_SIZE, + ), + )); + } + let body_len = header.body_size() as usize; // Подгоняем размер буфера под размер тела (чтобы не читать лишнего) diff --git a/src/main.MD b/src/main.MD index a193e44..3c7e25f 100644 --- a/src/main.MD +++ b/src/main.MD @@ -14,9 +14,9 @@ * **Следующий шаг:** ## Чек-лист: исправление парсера общего заголовка - * [ ] Исправить имя функции `parce_header` → `parse_header`. - * [ ] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод. - * [ ] Убрать `.await` при вызове `parse_header`. + * [✔] Исправить имя функции `parce_header` → `parse_header`. + * [✔] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод. + * [✔] Убрать `.await` при вызове `parse_header`. * [ ] Исправить порядок полей согласно протоколу: * `bytes[0..4]` — `magic`; diff --git a/src/protocol.rs b/src/protocol.rs index 93cc8dd..03ff6ec 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,5 +1,28 @@ +// Парсим заголовок на базе Nom +use nom::{ + bytes::complete::tag, + combinator::verify, + number::complete::{ + be_u8, + be_u16, + be_u32, + be_u64, + }, + IResult, + Parser, +}; + use thiserror::Error; +#[derive(Debug, Error)] +pub enum HeaderValidationError { + #[error("Неподдерживаемая версия протокола: {0}")] + UnsupportedVersion(u8), + + #[error("Неизвестная команда протокола: {0}")] + UnsupportedCommand(u8), +} + #[derive(Debug)] pub struct Header { version: u8, @@ -9,46 +32,27 @@ pub struct Header { 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); - } - +pub fn parse_header( + input: &[u8], + ) -> IResult<&[u8],Header> { + //1. Парсим магические байты "NCLD" + let (input, _) = tag(&b"NCLD"[..]).parse(input)?; + //2. Парсим и валидируем версию + let (input, version) = be_u8(input)?; + //3. Парсим и валидируем Header + let (input, command) = be_u8(input)?; + let (input, flags) = be_u16(input)?; + let (input, request_id) = be_u32(input)?; + let (input, body_size) = be_u64(input)?; let local_header = Header{ - version: bytes[0], - command: *_cmd, - flags: flags_raw, - request_id: request_id_raw, - body_size: body_size_raw, + version, + command, + flags, + request_id, + body_size, }; - Ok(local_header) + Ok((input,local_header)) } // Реализация публичных методов-геттеров для безопасного доступа к приватным полям @@ -78,4 +82,20 @@ impl Header { pub fn body_size(&self) -> u64 { self.body_size } + // Валидация Header + pub fn validate(&self) -> Result<(), HeaderValidationError> { + if self.version != 1 { + return Err( + HeaderValidationError::UnsupportedVersion(self.version) + ); + } + + if self.command != 1 { + return Err( + HeaderValidationError::UnsupportedCommand(self.command) + ); + } + + Ok(()) +} } \ No newline at end of file