Compare commits
2 Commits
e4e0bf33f0
...
359337bc10
| Author | SHA1 | Date | |
|---|---|---|---|
| 359337bc10 | |||
| 1611d1c70b |
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
mod connection;
|
mod connection; //модуль отвечающий за соединения
|
||||||
mod protocol;
|
mod protocol; //модуль отвечающий за протокол
|
||||||
|
|
||||||
use tokio::net::{TcpListener};
|
use tokio::net::{TcpListener};
|
||||||
use tokio::io;
|
use tokio::io;
|
||||||
|
|||||||
+509
@@ -0,0 +1,509 @@
|
|||||||
|
# Protocol.MD NoCloud Protocol 0.1
|
||||||
|
## Задачи
|
||||||
|
1. Читать поля сообщения
|
||||||
|
2. Проверить их формат
|
||||||
|
2. Вернуть структуру Header c понятными полями заголовка:
|
||||||
|
a. version: u8 //Версия протокола
|
||||||
|
b. command: u8 //Тип сообщения
|
||||||
|
0x01 UPLOAD клиент → сервер
|
||||||
|
0x02 DOWNLOAD клиент → сервер
|
||||||
|
0x03 DELETE клиент → сервер
|
||||||
|
0x04 STAT клиент → сервер
|
||||||
|
0x80 RESPONSE в обе стороны
|
||||||
|
0x81 FILE_DATA сервер → клиент
|
||||||
|
c. flags: u16 //Дополнительные признаки
|
||||||
|
0x0001 — разрешено перезаписать существующий файл
|
||||||
|
0x0002 — передаётся контрольная сумма
|
||||||
|
0x0004 — передача является возобновлением (докачка)
|
||||||
|
0x0008 - 0x00FF - резерв
|
||||||
|
d. request_id:u32 //Номер запроса
|
||||||
|
c. body_size: u64 Размер тела после заголовка
|
||||||
|
3. И, если операция касается передячи файла - дополнительными полями следующими сразу за телом заголовка
|
||||||
|
a. file_size: u64
|
||||||
|
b. name_size u16
|
||||||
|
c. file_name UTF-8
|
||||||
|
d. file_data u8
|
||||||
|
|
||||||
|
## Обрабатываемые запросы
|
||||||
|
|
||||||
|
Протокол будет в два этажа:
|
||||||
|
|
||||||
|
1. Общий заголовок сообщения — позволяет понять, что за команда пришла.
|
||||||
|
2. Тело команды — устроено по-разному для загрузки, скачивания, ответа и т. д.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Правила транспортного уровня
|
||||||
|
|
||||||
|
* Протокол работает поверх одного TCP-соединения.
|
||||||
|
* Позже TCP будет обёрнут в TLS/mTLS, но формат сообщений не изменится.
|
||||||
|
* Все целые числа передаются в big-endian.
|
||||||
|
* Один запрос обрабатывается целиком, затем начинается следующий.
|
||||||
|
* Файлы читаются и записываются порциями, но прикладных «блоков файла» пока нет.
|
||||||
|
* Клиент не отправляет следующий запрос, пока не получил ответ на предыдущий.
|
||||||
|
|
||||||
|
Последнее ограничение сильно упрощает первую реализацию. `request_id` мы всё равно предусмотрим, чтобы позже разрешить несколько запросов в одном соединении.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Общий заголовок сообщения
|
||||||
|
|
||||||
|
Каждое сообщение начинается с одинаковых 20 байт:
|
||||||
|
|
||||||
|
| Поле | Тип | Размер | Назначение |
|
||||||
|
| ------------ | --------: | -----: | --------------------------- |
|
||||||
|
| `magic` | `[u8; 4]` | 4 | Сигнатура `NCLD` |
|
||||||
|
| `version` | `u8` | 1 | Версия протокола |
|
||||||
|
| `command` | `u8` | 1 | Тип сообщения |
|
||||||
|
| `flags` | `u16` | 2 | Дополнительные признаки |
|
||||||
|
| `request_id` | `u32` | 4 | Номер запроса |
|
||||||
|
| `body_size` | `u64` | 8 | Размер тела после заголовка |
|
||||||
|
|
||||||
|
Итого:
|
||||||
|
|
||||||
|
```text
|
||||||
|
4 + 1 + 1 + 2 + 4 + 8 = 20 байт
|
||||||
|
```
|
||||||
|
|
||||||
|
В сетевом потоке:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────┬─────────┬─────────┬───────┬────────────┬───────────┐
|
||||||
|
│ magic │ version │ command │ flags │ request_id │ body_size │
|
||||||
|
│ 4 байта│ 1 байт │ 1 байт │ 2 байта│ 4 байта │ 8 байт │
|
||||||
|
└────────┴─────────┴─────────┴───────┴────────────┴───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Зачем нужны эти поля
|
||||||
|
|
||||||
|
### `magic`
|
||||||
|
|
||||||
|
```text
|
||||||
|
NCLD
|
||||||
|
```
|
||||||
|
|
||||||
|
В байтах:
|
||||||
|
|
||||||
|
```text
|
||||||
|
4E 43 4C 44
|
||||||
|
```
|
||||||
|
|
||||||
|
Позволяет серверу понять:
|
||||||
|
|
||||||
|
> Передо мной действительно сообщение NoCloud, а не HTTP-запрос, мусор или поток со смещённой границей.
|
||||||
|
|
||||||
|
Если первые четыре байта не `NCLD`, соединение закрывается.
|
||||||
|
|
||||||
|
### `version`
|
||||||
|
|
||||||
|
Первоначально:
|
||||||
|
|
||||||
|
```text
|
||||||
|
version = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Если через год изменим формат протокола, сервер сможет отличать старых клиентов от новых.
|
||||||
|
|
||||||
|
### `command`
|
||||||
|
|
||||||
|
Определяет, как интерпретировать тело сообщения.
|
||||||
|
|
||||||
|
Первоначальный набор:
|
||||||
|
|
||||||
|
| Код | Название | Направление |
|
||||||
|
| -----: | ----------- | --------------- |
|
||||||
|
| `0x01` | `UPLOAD` | клиент → сервер |
|
||||||
|
| `0x02` | `DOWNLOAD` | клиент → сервер |
|
||||||
|
| `0x03` | `DELETE` | клиент → сервер |
|
||||||
|
| `0x04` | `STAT` | клиент → сервер |
|
||||||
|
| `0x80` | `RESPONSE` | в обе стороны |
|
||||||
|
| `0x81` | `FILE_DATA` | сервер → клиент |
|
||||||
|
|
||||||
|
На первом практическом этапе реализуем только:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UPLOAD
|
||||||
|
RESPONSE
|
||||||
|
```
|
||||||
|
|
||||||
|
Остальные пока просто резервируем.
|
||||||
|
|
||||||
|
### `flags`
|
||||||
|
|
||||||
|
В первой версии:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flags = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Позже сюда можно поместить признаки:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0x0001 — разрешено перезаписать существующий файл
|
||||||
|
0x0002 — передаётся контрольная сумма
|
||||||
|
0x0004 — передача является возобновлением
|
||||||
|
```
|
||||||
|
|
||||||
|
Если все флаги равны нулю, они нам сейчас не мешают, но формат заголовка не придётся ломать позднее.
|
||||||
|
|
||||||
|
### `request_id`
|
||||||
|
|
||||||
|
Клиент назначает каждому запросу номер:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UPLOAD request_id = 17
|
||||||
|
RESPONSE request_id = 17
|
||||||
|
```
|
||||||
|
|
||||||
|
Благодаря этому клиент понимает, на какой запрос ответил сервер.
|
||||||
|
|
||||||
|
Поначалу запросы будут строго последовательными, поэтому поле кажется избыточным. Но стоит оно всего четыре байта, а пригодится почти наверняка.
|
||||||
|
|
||||||
|
### `body_size`
|
||||||
|
|
||||||
|
Показывает, сколько байт идёт после общего заголовка.
|
||||||
|
|
||||||
|
Это позволяет:
|
||||||
|
|
||||||
|
* проверить допустимость размера;
|
||||||
|
* знать границу следующего сообщения;
|
||||||
|
* отбрасывать неизвестную команду;
|
||||||
|
* обнаруживать оборванную передачу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Команда `UPLOAD`
|
||||||
|
|
||||||
|
После общего заголовка идёт тело загрузки:
|
||||||
|
|
||||||
|
| Поле | Тип | Размер |
|
||||||
|
| ----------- | ----: | ---------------: |
|
||||||
|
| `file_size` | `u64` | 8 байт |
|
||||||
|
| `name_size` | `u16` | 2 байта |
|
||||||
|
| `file_name` | UTF-8 | `name_size` байт |
|
||||||
|
| `file_data` | байты | `file_size` байт |
|
||||||
|
|
||||||
|
В потоке:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Общий заголовок, 20 байт
|
||||||
|
┌───────────┬───────────┬────────────┬─────────────┐
|
||||||
|
│ file_size │ name_size │ file_name │ file_data │
|
||||||
|
│ 8 байт │ 2 байта │ N байт │ M байт │
|
||||||
|
└───────────┴───────────┴────────────┴─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Размер тела должен быть равен:
|
||||||
|
|
||||||
|
```text
|
||||||
|
body_size = 8 + 2 + name_size + file_size
|
||||||
|
```
|
||||||
|
|
||||||
|
Например, клиент отправляет файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hello.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
размером 1000 байт. Имя занимает 9 байт в UTF-8:
|
||||||
|
|
||||||
|
```text
|
||||||
|
file_size = 1000
|
||||||
|
name_size = 9
|
||||||
|
body_size = 8 + 2 + 9 + 1000 = 1019
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как сервер это обрабатывает
|
||||||
|
|
||||||
|
Сервер не создаёт структуру, содержащую весь `file_data`. Он действует последовательно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
прочитать общий заголовок
|
||||||
|
↓
|
||||||
|
убедиться, что command = UPLOAD
|
||||||
|
↓
|
||||||
|
прочитать file_size
|
||||||
|
↓
|
||||||
|
прочитать name_size
|
||||||
|
↓
|
||||||
|
проверить name_size
|
||||||
|
↓
|
||||||
|
прочитать имя
|
||||||
|
↓
|
||||||
|
проверить имя файла
|
||||||
|
↓
|
||||||
|
создать временный файл
|
||||||
|
↓
|
||||||
|
прочитать file_size байт порциями
|
||||||
|
↓
|
||||||
|
проверить успешное завершение
|
||||||
|
↓
|
||||||
|
переименовать временный файл в итоговый
|
||||||
|
↓
|
||||||
|
отправить RESPONSE
|
||||||
|
```
|
||||||
|
|
||||||
|
Буфер сервера может быть, например, 64 КиБ:
|
||||||
|
|
||||||
|
```text
|
||||||
|
файл размером 10 ГБ
|
||||||
|
↓
|
||||||
|
прочитали до 64 КиБ
|
||||||
|
↓
|
||||||
|
записали на диск
|
||||||
|
↓
|
||||||
|
прочитали следующую порцию
|
||||||
|
```
|
||||||
|
|
||||||
|
Оперативная память не зависит от размера файла.
|
||||||
|
|
||||||
|
## Почему сначала временный файл
|
||||||
|
|
||||||
|
Допустим, загружается:
|
||||||
|
|
||||||
|
```text
|
||||||
|
report.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Сервер сначала создаёт что-то вроде:
|
||||||
|
|
||||||
|
```text
|
||||||
|
report.pdf.nocloud-part-17
|
||||||
|
```
|
||||||
|
|
||||||
|
Если клиент оборвал передачу на 70%, мы не получим повреждённый `report.pdf`, выглядящий как готовый файл.
|
||||||
|
|
||||||
|
После успешной передачи:
|
||||||
|
|
||||||
|
```text
|
||||||
|
report.pdf.nocloud-part-17
|
||||||
|
↓ rename
|
||||||
|
report.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Ответ `RESPONSE`
|
||||||
|
|
||||||
|
Сервер отвечает на запрос сообщением с тем же `request_id`.
|
||||||
|
|
||||||
|
Тело ответа:
|
||||||
|
|
||||||
|
| Поле | Тип | Размер |
|
||||||
|
| -------------- | ----: | ------------------: |
|
||||||
|
| `status` | `u16` | 2 байта |
|
||||||
|
| `message_size` | `u16` | 2 байта |
|
||||||
|
| `message` | UTF-8 | `message_size` байт |
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
command = RESPONSE
|
||||||
|
request_id = 17
|
||||||
|
body_size = 6
|
||||||
|
|
||||||
|
status = 0
|
||||||
|
message_size = 2
|
||||||
|
message = "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
Первоначальные статусы:
|
||||||
|
|
||||||
|
| Код | Значение |
|
||||||
|
| --: | ----------------------- |
|
||||||
|
| `0` | Успех |
|
||||||
|
| `1` | Неизвестная команда |
|
||||||
|
| `2` | Некорректный заголовок |
|
||||||
|
| `3` | Недопустимое имя файла |
|
||||||
|
| `4` | Файл уже существует |
|
||||||
|
| `5` | Ошибка файловой системы |
|
||||||
|
| `6` | Не хватает места |
|
||||||
|
| `7` | Передача оборвана |
|
||||||
|
| `8` | Нет доступа |
|
||||||
|
| `9` | Неподдерживаемая версия |
|
||||||
|
|
||||||
|
Текст `message` предназначен для человека и журналов:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"file already exists"
|
||||||
|
```
|
||||||
|
|
||||||
|
Программа принимает решение по числовому `status`, а не сравнивает строки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Команда `DOWNLOAD`
|
||||||
|
|
||||||
|
Клиент запрашивает файл.
|
||||||
|
|
||||||
|
Тело запроса:
|
||||||
|
|
||||||
|
| Поле | Тип | Размер |
|
||||||
|
| ----------- | ----: | ---------------: |
|
||||||
|
| `name_size` | `u16` | 2 байта |
|
||||||
|
| `file_name` | UTF-8 | `name_size` байт |
|
||||||
|
|
||||||
|
```text
|
||||||
|
body_size = 2 + name_size
|
||||||
|
```
|
||||||
|
|
||||||
|
Если файл не существует, сервер возвращает обычный `RESPONSE` с ошибкой.
|
||||||
|
|
||||||
|
Если существует, сервер отвечает `FILE_DATA`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Ответ `FILE_DATA`
|
||||||
|
|
||||||
|
Тело ответа:
|
||||||
|
|
||||||
|
| Поле | Тип | Размер |
|
||||||
|
| ----------- | ----: | ---------------: |
|
||||||
|
| `file_size` | `u64` | 8 байт |
|
||||||
|
| `name_size` | `u16` | 2 байта |
|
||||||
|
| `file_name` | UTF-8 | `name_size` байт |
|
||||||
|
| `file_data` | байты | `file_size` байт |
|
||||||
|
|
||||||
|
То есть загрузка и скачивание используют почти одинаковое представление файла. Отличается направление и код команды:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UPLOAD:
|
||||||
|
клиент → сервер
|
||||||
|
|
||||||
|
FILE_DATA:
|
||||||
|
сервер → клиент
|
||||||
|
```
|
||||||
|
|
||||||
|
После успешного получения файла клиент может отправить серверу `RESPONSE` с тем же `request_id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Ограничения, которые сервер обязан проверять
|
||||||
|
|
||||||
|
Данным клиента доверять нельзя, даже когда позже появится mTLS. Известный клиент тоже может содержать ошибку.
|
||||||
|
|
||||||
|
Установлю стартовые ограничения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
максимальная длина имени: 1024 байта
|
||||||
|
максимальный текст ошибки: 4096 байт
|
||||||
|
максимальный размер файла: задаётся настройкой сервера
|
||||||
|
version должна быть равна 1
|
||||||
|
неизвестные flags запрещены
|
||||||
|
```
|
||||||
|
|
||||||
|
Имя файла не должно:
|
||||||
|
|
||||||
|
* быть пустым;
|
||||||
|
* начинаться с `/` или `\`;
|
||||||
|
* содержать `..`;
|
||||||
|
* содержать нулевой байт;
|
||||||
|
* превращаться в абсолютный путь;
|
||||||
|
* позволять выйти из каталога хранилища.
|
||||||
|
|
||||||
|
Иначе клиент сможет прислать:
|
||||||
|
|
||||||
|
```text
|
||||||
|
../../etc/passwd
|
||||||
|
```
|
||||||
|
|
||||||
|
и попытаться записать файл за пределами хранилища.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Контрольная сумма
|
||||||
|
|
||||||
|
Будет в следующих реализациях
|
||||||
|
|
||||||
|
TCP уже гарантирует, что доставленные байты не были незаметно переставлены или повреждены в пути. Хеш нужен на другом уровне:
|
||||||
|
|
||||||
|
* проверить файл целиком;
|
||||||
|
* идентифицировать одинаковые файлы;
|
||||||
|
* обнаружить ошибку хранения;
|
||||||
|
* в будущем возобновлять и дедуплицировать передачи.
|
||||||
|
|
||||||
|
Во второй итерации можно добавить флаг:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flags & 0x0002 != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
и после имени передавать:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hash_algorithm: u8
|
||||||
|
hash_size: u8
|
||||||
|
hash: [u8; hash_size]
|
||||||
|
```
|
||||||
|
|
||||||
|
Например SHA-256:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hash_algorithm = 1
|
||||||
|
hash_size = 32
|
||||||
|
hash = 32 байта
|
||||||
|
```
|
||||||
|
|
||||||
|
Но сначала надо добиться безошибочной передачи обычного файла.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Как выглядит полная загрузка
|
||||||
|
|
||||||
|
```text
|
||||||
|
Клиент
|
||||||
|
│
|
||||||
|
│ Общий заголовок:
|
||||||
|
│ command = UPLOAD
|
||||||
|
│ request_id = 17
|
||||||
|
│ body_size = ...
|
||||||
|
│
|
||||||
|
│ file_size
|
||||||
|
│ name_size
|
||||||
|
│ file_name
|
||||||
|
│ file_data
|
||||||
|
▼
|
||||||
|
Сервер
|
||||||
|
│
|
||||||
|
│ проверяет заголовок
|
||||||
|
│ создаёт временный файл
|
||||||
|
│ принимает данные порциями
|
||||||
|
│ переименовывает готовый файл
|
||||||
|
│
|
||||||
|
│ RESPONSE:
|
||||||
|
│ request_id = 17
|
||||||
|
│ status = 0
|
||||||
|
▼
|
||||||
|
Клиент
|
||||||
|
```
|
||||||
|
|
||||||
|
Если передача оборвалась посередине, ответ уже отправить некому. Сервер удаляет или сохраняет временный файл для будущего возобновления — это мы решим отдельно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Что сознательно не входит в версию 0.1
|
||||||
|
|
||||||
|
Пока отсутствуют:
|
||||||
|
|
||||||
|
* авторизация внутри протокола;
|
||||||
|
* каталоги и списки файлов;
|
||||||
|
* синхронизация изменений;
|
||||||
|
* время модификации;
|
||||||
|
* идентификатор файла;
|
||||||
|
* контрольная сумма;
|
||||||
|
* передача отдельных блоков;
|
||||||
|
* параллельные запросы;
|
||||||
|
* возобновление загрузки;
|
||||||
|
* разрешение конфликтов;
|
||||||
|
* сжатие.
|
||||||
|
|
||||||
|
Первая реализация задумана только для:
|
||||||
|
|
||||||
|
> Клиент отправляет один файл, сервер безопасно сохраняет его и возвращает подтверждение.
|
||||||
|
|
||||||
|
После этого поверх уже работающей передачи буду строить настоящую синхронизацию.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user