В статье я хочу рассказать не столько об ошибке в RFC 2616, сколько о своем подходе к созданию парсера HTTP сообщений, показать его преимущества и недостатки. В основу моего подхода положено два принципа «лучше час потерять, потом за пять минут долететь» и «пусть компьютер работает, а я отдохну».
И так задача в целом: реализовать HTTP сервер, и HTTP парсер, в частности. Протокол версии 1.1 описан в RFC 2616. В этой спецификации можно выделить две сущности: описательная часть и BNF правила, которые определяют синтаксис соообщений. BNF правила хорошо формализованая вещь, для которой даже существует RFC 5234, где грамматика BNF описана с помощью опяь же BNF правил. Правда выпущена RFC 5234 была позже чем RFC 2616 (HTTP), и имеет несколько непринципиальных отличий.
BNF, краткий экскурс
BNF грамматика, довольно проста, поэтому я приведу пример с пояснениями, думаю этого будет достаточно, для того чтобы составить представление (для тех кто не знаком).
start-line = Request-Line | Status-Line
generic-message = start-line
*(message-header CRLF)
CRLF
[ message-body ]
Если перевести на русский язык, получится:
1) start-line — это Request-Line или Status-Line (это тоже правила, которые где-то описаны)
2) generic-message — это последовательность из start-line, *(message-header CRLF), CRLF и возможно message-body ([...] скобки определяют не обязательность). Где *(message-header CRLF) допускает, 0 или больше повторений конкатенации двух правил message-header и CRLF.
Чем-то похоже на регулярные выражения, что не удивительно.
Про ошибку
Для того чтобы проследить ошибку, ниже я привел ряд правил. Бегло пробегаясь по ним, можно увидить следующее: Запрос состоит из Request-Line и повторения заголовков (header) разделенных CRLF. В числе групп заголовков присутствует entity-header, который в отличии от general-header и request-header, содержит extension-header. Правило extension-header разрешает нестандартные заголовки, иначе говоря, именно оно разрашает добавить в запрос заголовок
My-Header: I am server
при этом запрос останется валидным. Кроме того, это правило открывает возмножность писать расширения протокола. Так как extension-header допускает любые заголовки, в том числе и стандартные (From, Accept, Host, Referer и т.д.), возникает такая ситуация: если сообщение содержит невалидный стандартный заголовок, он не будет допущен правилом его описывающим, но extension-header заголовок допустит, что не правильно.
Request = Request-Line ; Section 5.1
*(( general-header ; Section 4.5
| request-header ; Section 5.3
| entity-header ) CRLF) ; Section 7.1
CRLF
[ message-body ] ; Section 4.3
entity-header = Allow ; Section 14.7
| Content-Encoding ; Section 14.11
| Content-Language ; Section 14.12
| Content-Length ; Section 14.13
| Content-Location ; Section 14.14
| Content-MD5 ; Section 14.15
| Content-Range ; Section 14.16
| Content-Type ; Section 14.17
| Expires ; Section 14.21
| Last-Modified ; Section 14.29
| extension-header
extension-header = message-header
message-header = field-name ":" [ field-value ]
field-name = token
field-value = *( field-content | LWS )
field-content = <the OCTETs making up the field-value
and consisting of either *TEXT or combinations
of token, separators, and quoted-string>
К сожалению, в BNF нет возможности описать правило вида «что-то не включая что-то другое». В спецификации такие правила описываются неформально:
ctext = <any TEXT excluding "(" and ")">
qdtext = <any TEXT except <">>
Корректное правило field-name должно выглядеть примерно следующим образом:
field-name = <any token excluding "Accept", "Allow", ... all header names from rfc 2616, 2617 ...>
О главном
Я хотел рассказать об утилитах, которые строят ДКА на основе BNF. То есть, в идеале, генерируют парсер автоматически. Но как-то так получилось, что заголовок для привлечения внимания занял слишком много времени, поэтому про инструменты в следующий раз.
Автор: vf1