feat(protocol): parse and validate binary packet headers
This commit is contained in:
Generated
+16
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user