feat(protocol): add initial parser for common message header
This commit is contained in:
Generated
+21
@@ -208,6 +208,7 @@ name = "nocloud-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"local-ip-address",
|
"local-ip-address",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -346,6 +347,26 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.52.3"
|
||||||
|
|||||||
+2
-1
@@ -12,4 +12,5 @@ panic = "abort"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
local-ip-address = "0.6"
|
local-ip-address = "0.6"
|
||||||
|
thiserror = "2.0"
|
||||||
+60
-35
@@ -4,6 +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};
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
id: u64,
|
id: u64,
|
||||||
client_addr: SocketAddr,
|
client_addr: SocketAddr,
|
||||||
@@ -25,48 +26,72 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self) -> io::Result<()> {
|
pub async fn run(mut self) -> io::Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Обработчик {} создан для {}",
|
"Обработчик {} создан для {}",
|
||||||
self.id,
|
self.id, self.client_addr
|
||||||
self.client_addr
|
);
|
||||||
);
|
|
||||||
|
|
||||||
let mut buffer = [0_u8; 1024];
|
// Буфер для данных тела сообщения (теперь он не хранит заголовок)
|
||||||
|
let mut body_buffer = vec![0_u8; 1024];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let bytes_read = self.stream.read(&mut buffer).await?;
|
// 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!(
|
println!(
|
||||||
"Обработчик {}: клиент {} отключился",
|
"Обработчик {}: клиент {} отключился (или прислал неполный заголовок)",
|
||||||
self.id,
|
self.id, self.client_addr
|
||||||
self.client_addr
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
return Err(e); // Другие системные ошибки ввода-вывода (например, обрыв связи)
|
||||||
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?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+59
-2
@@ -2,5 +2,62 @@
|
|||||||
|
|
||||||
## 📌 Текущее состояние
|
## 📌 Текущее состояние
|
||||||
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
|
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
|
||||||
* **Проблема:** Не удалось проверить эхо-ответ через PowerShell.
|
Проверочная строка: smikhai@DebHOME:~$ printf '\x4E\x43\x4C\x44\x01\x01\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x03\xFB' \
|
||||||
* **Следующий шаг:** Найти рабочий способ отправки тестового запроса в PowerShell или использовать альтернативный инструмент.
|
> | 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`.
|
||||||
|
* [ ] Читать данные файла порциями, а не целиком в память.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<Header, ParseError> {
|
||||||
|
// Получаем флаги
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user