feat(protocol): parse and validate binary packet headers

This commit is contained in:
Stanislav N Mikhailov
2026-07-21 22:57:50 +03:00
parent 9f2d7f635e
commit 0f2cb0384c
5 changed files with 120 additions and 54 deletions
Generated
+16
View File
@@ -163,6 +163,12 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.1" version = "1.2.1"
@@ -208,10 +214,20 @@ name = "nocloud-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"local-ip-address", "local-ip-address",
"nom",
"thiserror", "thiserror",
"tokio", "tokio",
] ]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
+1
View File
@@ -14,3 +14,4 @@ panic = "abort"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
local-ip-address = "0.6" local-ip-address = "0.6"
thiserror = "2.0" thiserror = "2.0"
nom = "8.0.0"
+42 -13
View File
@@ -4,7 +4,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream}; use tokio::net::{TcpStream};
use std::io; use std::io;
use crate::protocol::{parce_header, ParseError}; use crate::protocol::parse_header;
pub struct Connection { pub struct Connection {
id: u64, id: u64,
client_addr: SocketAddr, client_addr: SocketAddr,
@@ -54,25 +54,54 @@ impl Connection {
} }
// 3. Вызываем ваш парсер заголовка // 3. Вызываем ваш парсер заголовка
let header = match parce_header(&header_buffer).await { let header = match parse_header(&header_buffer) {
Ok(h) => h, Ok((_remaining, header)) => {
Err(ParseError::InvalidFormat) => { println!(
println!("Обработчик {}: Критическая ошибка! Неверный формат заголовка.", self.id); "Обработчик {}: nom оставил {} байт",
// В сетевых протоколах при неверном заголовке соединение обычно сразу рвут self.id,
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid header format")); _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; self.message_count += 1;
println!("Обработчик {}: Успешно распарсен заголовок №{}: {:?}", self.id, self.message_count, header); println!("Обработчик {}: Успешно распарсен заголовок №{}: {:?}", self.id, self.message_count, header);
// 4. Используем данные из распарсенного заголовка! // 4. Используем данные из распарсенного заголовка!
// Теперь мы знаем точный размер тела сообщения благодаря header.body_size // Теперь мы знаем точный размер тела сообщения благодаря 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; let body_len = header.body_size() as usize;
// Подгоняем размер буфера под размер тела (чтобы не читать лишнего) // Подгоняем размер буфера под размер тела (чтобы не читать лишнего)
+3 -3
View File
@@ -14,9 +14,9 @@
* **Следующий шаг:** * **Следующий шаг:**
## Чек-лист: исправление парсера общего заголовка ## Чек-лист: исправление парсера общего заголовка
* [ ] Исправить имя функции `parce_header``parse_header`. * [✔] Исправить имя функции `parce_header``parse_header`.
* [ ] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод. * [✔] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод.
* [ ] Убрать `.await` при вызове `parse_header`. * [✔] Убрать `.await` при вызове `parse_header`.
* [ ] Исправить порядок полей согласно протоколу: * [ ] Исправить порядок полей согласно протоколу:
* `bytes[0..4]``magic`; * `bytes[0..4]``magic`;
+57 -37
View File
@@ -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; use thiserror::Error;
#[derive(Debug, Error)]
pub enum HeaderValidationError {
#[error("Неподдерживаемая версия протокола: {0}")]
UnsupportedVersion(u8),
#[error("Неизвестная команда протокола: {0}")]
UnsupportedCommand(u8),
}
#[derive(Debug)] #[derive(Debug)]
pub struct Header { pub struct Header {
version: u8, version: u8,
@@ -9,46 +32,27 @@ pub struct Header {
body_size: u64, body_size: u64,
} }
#[derive(Error, Debug)] pub fn parse_header(
pub enum ParseError { input: &[u8],
#[error("Заголовок пустой или имеет неверный формат")] ) -> IResult<&[u8],Header> {
InvalidFormat, //1. Парсим магические байты "NCLD"
#[error("Длина имени файла ({actual}) превышает допустимый лимит в {max} байт")] let (input, _) = tag(&b"NCLD"[..]).parse(input)?;
FilenameTooLong { //2. Парсим и валидируем версию
actual: u16, let (input, version) = be_u8(input)?;
max: u16, //3. Парсим и валидируем Header
} let (input, command) = be_u8(input)?;
} let (input, flags) = be_u16(input)?;
let (input, request_id) = be_u32(input)?;
pub async fn parce_header (bytes: &[u8;20]) -> Result<Header, ParseError> { let (input, body_size) = be_u64(input)?;
// Получаем флаги
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{ let local_header = Header{
version: bytes[0], version,
command: *_cmd, command,
flags: flags_raw, flags,
request_id: request_id_raw, request_id,
body_size: body_size_raw, body_size,
}; };
Ok(local_header) Ok((input,local_header))
} }
// Реализация публичных методов-геттеров для безопасного доступа к приватным полям // Реализация публичных методов-геттеров для безопасного доступа к приватным полям
@@ -78,4 +82,20 @@ impl Header {
pub fn body_size(&self) -> u64 { pub fn body_size(&self) -> u64 {
self.body_size 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(())
}
} }