237 lines
31 KiB
Markdown
237 lines
31 KiB
Markdown
# HTTP-сервер
|
||
|
||
В этом задании вам предстоит реализовать простой HTTP-сервер для хранения файлов.
|
||
|
||
На практике вы, скорее всего, будете использовать готовые библиотеки, в которых работа с HTTP уже реализована. Но полезно хотя бы раз сделать это «руками», чтобы лучше понять протокол: как клиент передаёт запрос, как сервер его читает и формирует ответ.
|
||
|
||
Формат сообщений и работу соединений описывает [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112.html), семантику методов и заголовков — [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html). В задании используется ограниченное подмножество HTTP/1.1, описанное ниже. Прочитайте условие целиком перед началом работы.
|
||
|
||
## Запуск сервера
|
||
|
||
При запуске сервер получает адрес и порт для приёма соединений, рабочую директорию и ожидаемое доменное имя. Эти настройки можно задать параметрами командной строки или переменными окружения:
|
||
|
||
| Параметр | Переменная окружения | По умолчанию | Назначение |
|
||
|---|---|---|---|
|
||
| `--host` | `SERVER_HOST` | `0.0.0.0` | Адрес прослушивания TCP-соединений |
|
||
| `--port` | `SERVER_PORT` | `8080` | Порт сервера |
|
||
| `--working-directory` | `SERVER_WORKING_DIRECTORY` | Нет | Абсолютный путь к существующей рабочей директории |
|
||
| `--server-domain` | `SERVER_DOMAIN` | `localhost` | Ожидаемое доменное имя в заголовке `Host` |
|
||
|
||
Параметр командной строки имеет приоритет над переменной окружения, а переменная окружения — над значением по умолчанию. Если рабочая директория не указана ни одним способом или задана пустой строкой, сервер завершается с кодом 1. В Python-заготовке эта настройка уже реализована.
|
||
|
||
Параметры `host` и `port` определяют, где сервер принимает TCP-соединения. Параметр `server-domain` используется для проверки заголовка `Host` в запросах; он не меняет адрес прослушивания и не требует настройки DNS.
|
||
|
||
### Проверка готовности
|
||
|
||
Тестер ждёт готовности сервера не более **10 секунд**, отправляя запросы `GET / HTTP/1.1` с заголовком `Host`, равным `server-domain`. Готовность подтверждает любой корректный HTTP-ответ, включая ответ об ошибке.
|
||
|
||
**Сервер должен отвечать на GET `/` во всех группах тестов.** В G1 и G2 можно вернуть код 200 с коротким текстом в теле ответа. Начиная с G3, этот запрос должен возвращать список файлов и директорий в рабочей директории.
|
||
|
||
Если сервер не начнёт отвечать вовремя, тестер завершит запуск с ошибкой `server did not start in 10 seconds`. В этом случае проверьте вывод сервера в журнале тестирования: проблема может быть как в запуске, так и в обработке GET `/`.
|
||
|
||
## Работа с файлами
|
||
|
||
Сервер поддерживает четыре метода: GET читает файл или содержимое директории, POST создаёт файл или директорию, PUT заменяет содержимое существующего файла, а DELETE удаляет файл или директорию. Реализовывать HEAD и OPTIONS не требуется.
|
||
|
||
Метод задаёт действие, а путь в запросе — файл или директорию, к которым это действие применяется. Все четыре метода используют одну схему путей: путь отсчитывается от рабочей директории сервера, заданной при запуске. Например, при `--working-directory /srv/files` запросы к `/docs/file.txt` работают с `/srv/files/docs/file.txt`. Начальный `/` в запросе обозначает рабочую директорию сервера, а не корень файловой системы компьютера. Отдельные адреса вроде `/get` или `/upload` создавать не нужно.
|
||
|
||
Ниже записи вида `GET /docs/file.txt` обозначают метод и путь запроса. В передаваемом по TCP HTTP-сообщении к ним добавляются версия протокола, заголовки и, при необходимости, тело.
|
||
|
||
### GET: чтение файла или директории
|
||
|
||
Если путь указывает на файл, сервер возвращает его содержимое в теле ответа. Например, `GET /docs/file.txt` возвращает байты файла `/srv/files/docs/file.txt`.
|
||
|
||
Если путь указывает на директорию, сервер возвращает список её непосредственных дочерних файлов и директорий — листинг. Так, `GET /docs` возвращает имена объектов внутри `/srv/files/docs`, а `GET /` — внутри самой рабочей директории. Вложенные директории обходить не нужно.
|
||
|
||
В списке должны присутствовать все имена, включая скрытые файлы, но без `.` и `..`. Порядок и оформление произвольные: можно вернуть текст или HTML. Размеры, даты и права доступа не требуются; дополнительные сведения допустимы.
|
||
|
||
### POST: создание файла или директории
|
||
|
||
Путь в POST-запросе задаёт имя создаваемого объекта целиком. Например, `POST /docs/new.txt` создаёт файл `/srv/files/docs/new.txt` и записывает в него тело запроса. Директория `/srv/files/docs` к этому моменту должна существовать. Если тело пустое, создаётся пустой файл.
|
||
|
||
Чтобы создать директорию, клиент передаёт заголовок `Create-Directory: True`. Например, `POST /docs/archive` с этим заголовком создаёт пустую директорию `/srv/files/docs/archive`. Сервер создаёт только указанный объект: промежуточные директории автоматически не создаются. Если по этому пути уже есть файл или директория, POST завершается ошибкой и не заменяет их.
|
||
|
||
### PUT: замена содержимого файла
|
||
|
||
PUT записывает тело запроса в существующий файл по указанному пути. Например, `PUT /docs/file.txt` полностью заменяет содержимое `/srv/files/docs/file.txt`. Если новые данные короче прежних, остаток прежнего содержимого должен исчезнуть; пустое тело делает файл пустым.
|
||
|
||
В этом задании PUT применяется только к существующим файлам. Создавать новый файл или заменять директорию этим методом нельзя.
|
||
|
||
### DELETE: удаление файла или директории
|
||
|
||
Запрос `DELETE /docs/file.txt` удаляет файл `/srv/files/docs/file.txt`. Для удаления директории клиент должен явно передать `Remove-Directory: True`. Например, `DELETE /docs/archive` с этим заголовком удаляет `/srv/files/docs/archive` вместе со всеми вложенными файлами и директориями. Без разрешающего заголовка сервер должен отказать в удалении директории, даже если она пустая.
|
||
|
||
### Коды ответов и общие правила
|
||
|
||
В таблице собраны коды ответов для всех перечисленных операций. Там, где указаны два кода, можно выбрать любой из них.
|
||
|
||
| Запрос и состояние пути | Действие и ответ |
|
||
|---|---|
|
||
| GET существующего файла | Вернуть содержимое файла с кодом 200 |
|
||
| GET существующей директории | Вернуть список её непосредственных дочерних файлов и директорий с кодом 200 |
|
||
| GET отсутствующего пути | Вернуть 404 |
|
||
| POST нового объекта, родительская директория существует | Создать объект и вернуть 200 или 201 |
|
||
| POST существующего файла или директории | Вернуть 409 |
|
||
| POST без родительской директории | Вернуть 404, не создавать промежуточные директории |
|
||
| PUT существующего файла | Полностью заменить содержимое телом запроса и вернуть 200 или 204 |
|
||
| PUT директории | Вернуть 409 |
|
||
| PUT отсутствующего пути | Вернуть 404, не создавать файл |
|
||
| DELETE существующего файла | Удалить файл и вернуть 200 |
|
||
| DELETE `/` | Вернуть 403: удаление рабочей директории запрещено независимо от заголовка `Remove-Directory` |
|
||
| DELETE директории с `Remove-Directory: True` | Удалить директорию со всем содержимым и вернуть 200 |
|
||
| DELETE директории без `Remove-Directory: True` | Вернуть 406 |
|
||
| DELETE отсутствующего пути | Вернуть 404 |
|
||
|
||
Заголовки `Create-Directory` и `Remove-Directory` включают соответствующее действие только со значением `True`. Значение `False` равнозначно отсутствию заголовка; других значений в запросах не будет.
|
||
|
||
Для упрощения работы с путями в тестах используются только ASCII-имена из букв, цифр, дефиса, подчёркивания и точки. Компоненты `.` и `..`, параметры после `?`, percent-encoding и символические ссылки исключены. Если один из промежуточных компонентов пути является файлом, такой путь обрабатывается как отсутствующий.
|
||
|
||
При ошибке файловая система должна остаться без изменений. Ответ об ошибке содержит непустой понятный текст; точная формулировка не задана. Успешные ответы на POST, PUT и DELETE могут иметь пустое тело. Ответ 204 всегда передаётся без тела.
|
||
|
||
## HTTP и заголовки
|
||
|
||
Каждое соединение содержит один запрос. Сервер отправляет ответ с `Connection: close` и закрывает соединение. Одновременная обработка нескольких соединений не требуется.
|
||
|
||
Запросы синтаксически корректны и используют HTTP/1.1. В них нет повторяющихся заголовков, `Transfer-Encoding` и `Expect`. Длина тела задаётся корректным `Content-Length`; если заголовка нет, тела нет. Имена заголовков сравниваются без учёта регистра. Неизвестные заголовки можно игнорировать.
|
||
|
||
TCP передаёт поток байтов: результат одного чтения из сокета может содержать часть заголовка либо конец заголовков вместе с началом тела. Сервер должен работать при любом таком разбиении и не ждать закрытия соединения клиентом, чтобы определить конец запроса. Файлы могут содержать произвольные байты.
|
||
|
||
| Заголовок | Требование |
|
||
|---|---|
|
||
| `Host` в запросе | В функциональных тестах содержит доменное имя без порта. Сравнивается с `server-domain` без учёта регистра; при несовпадении вернуть 400, не выполняя файловую операцию |
|
||
| `Content-Length` в ответе | Обязателен, кроме ответа 204. Равен размеру передаваемого тела в байтах; для пустого тела — 0, для gzip — размеру сжатых данных. В ответе 204 этот заголовок запрещён |
|
||
| `Connection` в ответе | `close` |
|
||
| `Content-Type` в ответе | Описывает тип содержимого тела. Для непустого тела нужен корректный MIME-тип. Для листинга допустимы `text/plain` и `text/html`. Для любых файлов, включая текстовые, разрешено использовать `application/octet-stream`; определять MIME-тип по расширению не требуется |
|
||
| `Server` в ответе | Непустое название сервера |
|
||
| `Accept-Encoding` в запросе | Сообщает, что клиент готов принять сжатые данные. В задании поддерживается только значение `gzip` |
|
||
| `Content-Encoding` в ответе | Сообщает, каким алгоритмом сжато тело ответа. При сжатии имеет значение `gzip`; в несжатом ответе отсутствует |
|
||
|
||
Проверка `Host`, `Content-Type` и `Server` входит в G5 и G7. Коды ответа, длина и границы тела, закрытие соединения и заданное условием содержимое проверяются во всех группах.
|
||
|
||
В G7 успешные GET-ответы на запрос с `Accept-Encoding: gzip` должны содержать сжатые данные. Это относится и к файлам, и к листингам. Без этого заголовка ответ не сжимается и не содержит `Content-Encoding`. Сжатие ошибок не требуется. Готовые библиотеки gzip использовать можно. `Transfer-Encoding` в ответах не используется.
|
||
|
||
## Компоненты и оценивание
|
||
|
||
Автоматические тесты определяют предварительную оценку. Можно реализовать часть задания: каждая группа проверяет только перечисленные для неё возможности. Например, в G1 достаточно чтения существующих текстовых файлов и ответа на служебный GET `/`, а создание и удаление файлов появляются в G4.
|
||
|
||
Для получения баллов за группу нужно пройти все её проверки. Группы оцениваются независимо: ошибка в одной группе не лишает баллов за успешно пройденные другие. За все семь групп можно получить 10 баллов.
|
||
|
||
| Группа | Баллы | Что проверяется |
|
||
|---|---|---|
|
||
| G1 | 3 | GET существующих текстовых файлов в ASCII, размером ≤ 8 МБ; настройка и запуск сервера |
|
||
| G2 | 1 | GET существующих файлов с произвольными байтами, размером ≤ 8 МБ |
|
||
| G3 | 1 | GET файлов и директорий, ошибки отсутствующих путей; файлы в ASCII, размером ≤ 8 МБ |
|
||
| G4 | 2 | GET, POST, PUT и DELETE, листинг и ошибки; произвольные файлы размером ≤ 8 МБ |
|
||
| G5 | 1 | Возможности G4 и проверка `Host`, `Content-Type`, `Server` |
|
||
| G6 | 1 | Возможности G4 и работа с большими файлами |
|
||
| G7 | 1 | Возможности G6, дополнительные заголовки из G5 и gzip |
|
||
|
||
В G1 и G3 используются текстовые файлы в кодировке ASCII. В остальных группах файлы могут содержать произвольные байты, поэтому их содержимое нельзя считать текстом.
|
||
|
||
В G1–G5 размер каждого файла не превышает 8 МБ. Это ограничение действует и на содержимое файлов, передаваемое в запросах POST и PUT. В G6 и G7 файлы могут быть больше 8 МБ. В этом задании 1 МБ = 1024 × 1024 байта.
|
||
|
||
В тестирующей системе решение работает в Docker с лимитом памяти **128 МБ**. В G6 и G7 файлы могут превышать объём доступной памяти: нужно уметь и отдавать их клиенту, и принимать при POST и PUT, не загружая целиком в память. Проверить соблюдение лимита можно локально с Docker по инструкции ниже. Запуск без Docker этот лимит не проверяет.
|
||
|
||
Для передачи большого файла с gzip и корректным `Content-Length` можно предварительно сжать его во временный файл, определить размер результата и затем передать его клиенту по частям. Само сжатие также должно укладываться в ограничение памяти.
|
||
|
||
### Отчёт и защита
|
||
|
||
Вместе с кодом сдайте краткий отчёт `solution/readme.md`. Опишите устройство решения и используемые библиотеки, а также объясните следующие механизмы, указав соответствующие файлы и функции в своём коде:
|
||
|
||
- как определяются границы заголовков и тела;
|
||
- как обрабатываются большие файлы и от чего зависит расход памяти, если реализованы G6/G7;
|
||
- как определяется длина сжатого ответа, если реализована G7.
|
||
|
||
Без отчёта автоматические тесты запускаются, но решение не засчитывается по [общим правилам сдачи](../readme.md). Использование LLM укажите согласно [политике курса](../../llm-policy.md).
|
||
|
||
На защите нужно объяснить сданную реализацию, разобрать предложенный сценарий и при необходимости внести небольшую правку либо проследить выполнение кода. Ответы подтверждают баллы соответствующих компонентов; прохождение тестов само по себе не гарантирует итоговую оценку.
|
||
|
||
Если вы используете заготовку, на защите нужно понимать, как ваш код взаимодействует с ней: как запускается сервер, откуда берутся параметры и как соединение передаётся обработчику запроса.
|
||
|
||
## Заготовка и ограничения реализации
|
||
|
||
Нельзя использовать готовые библиотеки HTTP или парсеры HTTP-сообщений, в том числе из стандартной библиотеки языка. Можно использовать TCP-сокеты, `socketserver`, библиотеки для файлов, командной строки и gzip.
|
||
|
||
В `solution` находится Python-заготовка с настройкой параметров, TCP-сервером, логированием и структурами сообщений. Вам предстоит реализовать чтение и разбор запроса, обработку методов и формирование ответа. Методы в `http_messages.py` предназначены для стартовой строки и заголовков; тело обрабатывается отдельно. Наличие констант других методов не означает, что их нужно поддерживать.
|
||
|
||
Можно изменить структуру заготовки или выбрать другой язык; использовать именно её классы необязательно. Решение должно поддерживать описанные параметры командной строки и переменные окружения. В `solution/Dockerfile` должно быть описание сборки образа и запуска вашего сервера. Если вы меняете язык или способ запуска, обновите этот файл.
|
||
|
||
Весь код решения, необходимые для сборки файлы и отчёт разместите в папке `solution`. При сдаче отправляется только эта папка; изменения в тестах, `tests/launch.tmpl` и других файлах вне неё в тестирующую систему не попадут.
|
||
|
||
## Тестирование
|
||
|
||
Тесты написаны на Go и находятся в папке `tests`; точка входа — [TestHW](./tests/hw_test.go). Во время разработки удобно запускать их без Docker для быстрой проверки после изменений. Перед сдачей проверьте решение и с Docker: этот режим воспроизводит окружение тестирующей системы и ограничение памяти.
|
||
|
||
### Как устроены тесты
|
||
|
||
Внутри каждой группы есть несколько запусков. Для каждого запуска тестер создаёт набор файлов и директорий, запускает ваш сервер с соответствующей рабочей директорией и отправляет ему последовательность HTTP-запросов. Сервер работает до конца этого запуска, поэтому изменения после POST, PUT и DELETE влияют на последующие запросы. Запуск считается успешным, если сервер правильно обработал все запросы; для получения баллов нужно пройти все запуски группы.
|
||
|
||
Наборы файлов и запросов генерируются случайным образом, но воспроизводятся при повторном запуске той же версии тестов. Числовые идентификаторы в журнале позволяют повторить нужную последовательность. Помимо сгенерированных запросов, есть отдельные проверки конкретных случаев, например пустого файла или разбиения запроса между несколькими чтениями TCP-потока. Тесты помогают находить ошибки, но не заменяют проверку соответствия всему условию.
|
||
|
||
### Без Docker
|
||
|
||
Установите Go. В `tests/launch.tmpl` задана команда, которой тестер запускает сервер. При необходимости измените её для своей системы или языка; в Windows вместо `python3` может потребоваться `python`. Сохраните в шаблоне `{{.CommandLineArgs}}`: на это место тестер подставляет параметры запуска.
|
||
|
||
Если используете Python-заготовку, установите её зависимости. Затем запустите тесты следующими командами из папки задания:
|
||
|
||
```sh
|
||
python3 -m pip install -r solution/requirements.txt
|
||
cd tests
|
||
go test
|
||
```
|
||
|
||
Для отдельной группы или запуска используйте фильтр:
|
||
|
||
```sh
|
||
go test -run 'TestHW/G1'
|
||
go test -run 'TestHW/G1/1337'
|
||
```
|
||
|
||
Имена проверок видны в журнале. Например, `TestHW/G2/93/42300` обозначает группу G2, запуск 93 и запрос 42300. В G1 и G2 запросы только читают файлы, поэтому можно повторить отдельный запрос, указав его полный идентификатор: `go test -run 'TestHW/G2/93/42300'`.
|
||
|
||
В группах с POST, PUT и DELETE запросы используют общее изменяемое состояние файлов. Поэтому ошибку в такой группе воспроизводите всем запуском: отдельный запрос может получить другое начальное состояние.
|
||
|
||
### С Docker
|
||
|
||
Перед первым запуском и после объявления об обновлении тестов загрузите образ:
|
||
|
||
```sh
|
||
docker pull distsys.ru/course/http-server:latest
|
||
```
|
||
|
||
Запускайте из папки задания:
|
||
|
||
```sh
|
||
docker run --privileged --rm -v ./solution:/hw/solution distsys.ru/course/http-server:latest
|
||
```
|
||
|
||
Для отдельной группы добавьте `-run 'TestHW/G1'` после имени образа. Тестер собирает контейнер вашего сервера и запускает его с лимитом памяти 128 МБ. Общий лимит запуска образа тестов — 10 минут. Скорость зависит от компьютера и Docker-окружения.
|
||
|
||
Можно собрать образ сервера самостоятельно и проверить его локальным тестером:
|
||
|
||
```sh
|
||
cd solution
|
||
docker build -t hw3img .
|
||
cd ../tests
|
||
go test --docker -timeout 10m
|
||
```
|
||
|
||
Для сборки самого образа тестов выполните из папки задания:
|
||
|
||
```sh
|
||
docker build -t hw3tests ./tests
|
||
docker run --privileged --rm -v ./solution:/hw/solution hw3tests
|
||
```
|
||
|
||
### Как читать результаты
|
||
|
||
В журнале видны команда запуска с параметрами и переменными окружения, весь вывод сервера в stdout/stderr и причины ошибок. При отладке начните с первого неудачного запроса: его идентификатор позволяет повторить соответствующий запуск с помощью фильтра `-run`.
|
||
|
||
После ошибки оставшиеся запросы этого запуска и оставшиеся запуски группы пропускаются, а за группу начисляется 0 баллов. Тестирование других групп продолжается. В конце выводятся баллы по группам и строка `SCORE: N`. Если использовался фильтр, эта оценка учитывает только выбранные проверки; оценку за всё задание показывает полный прогон.
|
||
|
||
После локальной проверки отправьте решение по [общей инструкции](../readme.md).
|
||
|
||
### Бонус за пробелы в тестах
|
||
|
||
Если найдёте ошибку в тестах или требование, нарушение которого они не обнаруживают, опишите ситуацию в отчёте; при необходимости приложите пример ошибочного решения. За подтверждённую проблему можно получить 1 бонусный балл, а за тест, обнаруживающий её, или описание его логики — ещё 1 балл.
|