Skip to content

kontur-web-courses/sockets

Repository files navigation

Задача написать обработку http-запросов.

  1. «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

  1. «Ничего не найдено» Сделай так, чтобы при запросе любой страницы возвращался ответ «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
  1. «Страница 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!

  1. «GIF» Страница hello.html содержит закомментированный тэг img. Если его раскомментировать, то браузер после получения страницы попытается получить картинку. Раскомментируй img и сделай так, чтобы браузер по пути /groot.gif получал содержимое файла groot.gif. Зачитать содержимое файла можно так: File.ReadAllBytes(filename). У GIF-изображений Content-type такой: image/gif.

  2. «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.

  1. «Query String» После пути в запросе могут передаваться дополнительные параметры. Пример: /path/to/page?param1=value1&param2=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.

  1. «XSS» Перейди на страницу с серверным временем. А затем перейди на страницу /hello.html с помощью ссылки "to hello". Если ты все правильно сделал на предыдущем шаге, то переход по этому адресу тебя скорее всего огорчит...

Все дело в том, что никогда нельзя доверять параметрам, которые были переданы с клиента. В нашем случае перед вставкой данных в HTML надо было использовать html-кодирование.

Воспользуйся методом HttpUtility.HtmlEncode для кодирования значений greeting и name перед их вставкой в шаблон. Убедись, что теперь при переходе со страницы серверного времени все работает как надо. Загляни в HTML страницы. Символ < преобразовался в &lt;, а символ > в &gt;. Все потенциально опасные, значимые для HTML символы преобразуются в специальные последовательности, называемые html-сущностями.

  1. «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)