Задача написать обработку http-запросов.
- «TCP/IP сокеты»
- Запусти программу, посмотри в ее выводе в консоль IP-адрес и порт, по которому она ожидает подключение.
- Сделай запрос из браузера по этому адресу и убедись, что страница недоступна. В Chrome возвращается ошибка «ERR_INVALID_HTTP_RESPONSE» о том, что ответ получен, но он некорретный.
- Разберись, как устроен
AsynchronousSocketListener
. В этом тебе помогут комментарии в коде. - Найди метод
ProcessRequest
. На вход он получает объектrequest
. Этот объект создается путем анализа байтов от клиента. Можешь посмотретьRequest.StupidParse
, чтобы посмотреть «наивную» реализацию анализа http-запросов.
Пример http-запроса, который успешно преобразуется в Request
:
GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: text/html
Connection: close
- «Ничего не найдено» Сделай так, чтобы при запросе любой страницы возвращался ответ «404 Not Found». Для этого надо вернуть байты, содержащие вот такую строчку:
HTTP/1.1 404 Not Found
Этот ответ содержит только status line без заголовков и тела.
Тем не менее, заголовки отделяются от тела ответа дополнительной пустой строкой!
Хоть в этом запросе тело пустое, дополнительная пустая строка должна быть.
Обрати внимание, что в качестве переноса строки в HTTP-заголовках по стандарту используется символы CRLF ("\r\n"
).
Да, даже на Linux и Mac OS, ведь это стандарт HTTP!
Подсказка!
В строке ABC\r\nDEF\r\n\r\nXYZ\r\n
есть одна пустая строчка и она соответствует
ABC
DEF
XYZ
А вот в строке ABC\r\nDEF\r\nXYZ\r\n
нет пустой строчки и она соответствует
ABC
DEF
XYZ
- «Страница Hello»
Сделай так, чтобы при запросе по путям
/
или/hello.html
возвращалось содержимое файлаhello.html
.
Запрошенный путь можно получить через request.RequestUri
.
hello.html
автоматически копируется в папку bin
, поэтому его содержимое можно зачитать так:
File.ReadAllBytes("hello.html")
Пример ответа браузеру:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 100500
<html>...</html>
Заголовок Content-Type
сообщает браузеру, что будет передано в качестве message body.
В примере это HTML в кодировке UTF-8.
Заголовок Content-Length
сообщает браузеру размер message body,
чтобы браузер его полностью корректно зачитал.
И не забудь про пустую строчку между заголовками и телом ответа с помощью дополнительных символов CRLF!
-
«GIF» Страница
hello.html
содержит закомментированный тэгimg
. Если его раскомментировать, то браузер после получения страницы попытается получить картинку. Раскомментируйimg
и сделай так, чтобы браузер по пути/groot.gif
получал содержимое файлаgroot.gif
. Зачитать содержимое файла можно так:File.ReadAllBytes(filename)
. У GIF-изображений Content-type такой:image/gif
. -
«Mustache» Часто на страницах надо выводить что-то динамическое, например, текущее серверное время. Чтобы создавать странички с динамическим содержимым на сервере используются шаблонизаторы. Шаблонизатор берет файл-шаблон и заменяет специальную разметку на динамически посчитанное содержимое. В результате получается html-страничка, которую можно отдать браузеру. Пример шаблонизатора: http://mustache.github.io
Сделай так, чтобы при запросе по пути /time.html
возвращалось содержимое файла time.template.html
,
в котором {{ServerTime}}
было бы заменено на текущее серверное время (DateTime.Now
).
Сделай это с помощью метода Replace у String.
Строку из байтов в кодировке UTF-8 можно получить так: Encoding.UTF8.GetString(bytes)
.
Строку в байты в кодировке UTF-8 можно перевести так: Encoding.UTF8.GetBytes(str)
.
Заметь, что кодировка должна совпадать с charset
в заголовке Content-Type
.
- «Query String»
После пути в запросе могут передаваться дополнительные параметры.
Пример:
/path/to/page?param1=value1¶m2=value2
Эти дополнительные параметры, которые идут после знака?
называютсяquery string
.
Прокачай страницу hello.html
!
Сделай так, чтобы при передаче параметра name
вместо "{{World}}" отображалось это имя,
а при передаче параметра greeting
вместо "{{Hello}}" отображалось значение из greeting
.
Например, при запросе /hello.html?greeting=Hey&name=Rocket
отображалось "Hey Rocket".
Можно реализовать разбор query string
самостоятельно, но в .NET уже встроен метод для этого:
HttpUtility.ParseQueryString
из System.Web
.
Конечно, перед использованием метода надо будет отделить query string
от пути.
Метод ParseQueryString
возвращает NameValueCollection
.
Значения из нее можно получать как из словаря:
var greeting = collection["greeting"];
Если значение по ключу не задано, то вернется null
.
После того, как заработает для латинских имен посмотри,
что получается при таком запросе /hello.html?name=%D0%90%D0%BB%D0%B5%D0%BD%D1%83%D1%88%D0%BA%D0%B0
.
Заметь, что в request.RequestUri
передается строка в том же виде, в котором отправляется.
Все дело в том, что URL работает с ограниченным множеством символов,
а остальные символы кодируются.
Браузер умеет кодировать все неподходящие символы, а сервер должен производить декодирование.
Благо декодирование значений уже встроено в метод HttpUtility.ParseQueryString
.
Также доступны методы HttpUtility.UrlDecode
и HttpUtility.UrlEncode
.
- «XSS»
Перейди на страницу с серверным временем. А затем перейди на страницу
/hello.html
с помощью ссылки "to hello". Если ты все правильно сделал на предыдущем шаге, то переход по этому адресу тебя скорее всего огорчит...
Все дело в том, что никогда нельзя доверять параметрам, которые были переданы с клиента. В нашем случае перед вставкой данных в HTML надо было использовать html-кодирование.
Воспользуйся методом HttpUtility.HtmlEncode
для кодирования значений greeting
и name
перед их вставкой в шаблон.
Убедись, что теперь при переходе со страницы серверного времени все работает как надо.
Загляни в HTML страницы. Символ <
преобразовался в <
, а символ >
в >
.
Все потенциально опасные, значимые для HTML символы преобразуются
в специальные последовательности, называемые html-сущностями.
- «Cookie»
Сделай так, чтобы имя, переданное через query string запоминалось и показывалось при следующих запросах,
в которых параметр
name
уже не передается.
Для этого используй Cookie. Cookie работают так: если в одном из ответов с сервера задать куки, то браузер будет передавать эти куки при последующих запросах на сервер.
Задать куки без дополнительных опций можно таким заголовком в ответе с сервера:
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
Куки от браузера придут в заголовке Cookie в таком формате:
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Подробнее про куки можно почитать тут
Итак, к задаче.
При передаче параметра name
через query string надо задать куки с именем.
При последующих запросах надо пытаться достать имя из куки, если name
не задан через query string.
Вот и все!
Дежавю :)
После того, как заработает для латинских имен посмотри,
что получается при таком запросе /hello.html?name=%D0%90%D0%BB%D0%B5%D0%BD%D1%83%D1%88%D0%BA%D0%B0
.
А затем таком запросе ``/hello.html`.
В значениях куки также как и в URL нельзя использовать произвольные символы, поэтому значения куки обычно кодируются с помощью URL-кодирования или Base64-кодирования.
Можешь использовать любой из подходов, чтобы добиться корректной работы для имен на кириллице.
Base64 преобразует массив байт в строку и в .NET есть встроенные методы:
- Кодирование
string System.Convert.ToBase64String(byte[] data)
- Декодирование
byte[] System.Convert.FromBase64String(string encodedData)
URL-кодирование преобразует строку в строку:
- Кодирование
string HttpUtility.UrlEncode(string data)
- Декодирование
string HttpUtility.UrlDecode(string encodedData)