Ориентация главы
Том 9 начинается с фундаментного слоя HTTP — без сети и без версий, только определения «как выглядит поле, как распознаётся метод, как режется цель» — словарь, общий для всех верхних слоёв. xrtHttpFieldParse строго разбирает одиночное поле заголовка в заимствованные представления; xrtHttpTokenNext режет token-list по правилам HTTP; разбор Host и кодирование параметров обрабатывают два вида структурированного текста URL-слоя; FieldBlockWrite записывает представления полей обратно дословно. Парсер HTTP/1 главы 90, рантайм xhttp главы 98, ваши собственные шлюзы и посредники — всё стоит на этом наборе примитивов без распределений. Прочитав главу, вы увидите: безопасность и производительность обработки HTTP-заголовков — это, в сущности, три вещи: строгий разбор + заимствованные представления + атомарность ёмкости.
89.1引入
В чём внутренняя операция обратного прокси? «Поменять один заголовок и переслать» — разобрать заголовок запроса, найти поле по имени, изменить значение или добавить поле, затем записать весь блок заголовков обратно в поток байтов. Звучит просто, но на каждом шаге исторические ямы: как обрабатывать регистр имён полей? Как резать элементы token-list с пробелами и кавычками? Не взорвут ли парсер комментарии в значениях (устаревшие по RFC 7230, но встречающиеся в сети)? Что если буфера при перезаписи не хватает — писать половину?
Ответ фундаментного слоя — три дисциплины. Строгий разбор: нелегальный вход отвергается на месте (никаких тихих исправлений) — добрая половина HTTP-разборных CVE вырастает из «снисходительного разбора», обернувшегося двусмысленностью в руках атакующего. Заимствованные представления: результаты разбора — представления исходного входа (ноль копий, ноль распределений) — стоимость на запрос в высококонкурентном шлюзе стремится к нулю. Атомарность ёмкости: обратная запись либо полностью успешна, либо не трогает ни байта — самое коварное промежуточное состояние «полсообщения записано» исключается на уровне API.
89.2概念
Модель поля: имя, значение и заимствованные представления
xhttpfield — это два представления: Name и Value. xrtHttpFieldParse(文本, &字段) (текст, &поле) строго разбирает «имя: значение» — до двоеточия имя поля (проверка token-набора символов), после — значение, очищенное от краевого OWS (необязательных пробелов). Результат разбора заимствован: Name.Data/Value.Data указывают внутрь исходного текста — без распределений и копий. Три спутника: xrtHttpFieldCount/FieldFind/FieldNext (поиск и обход по массивам полей) и xrtHttpFieldBlockWrite(字段数组, 数量, 输出, 容量, &长度) (массив полей, количество, вывод, ёмкость, &длина) — запись массива полей обратно в полный блок «имя: значение\r\n...\r\n»; при нехватке ёмкости отказ без единого записанного байта (атомарность).
token-list: итератор культуры запятых
HTTP — мир, разделённый запятыми: Connection: keep-alive, Upgrade, Accept-Encoding: gzip, deflate, Cache-Control: no-cache, no-store. xrtHttpTokenNext(值视图, &偏移, &token) (представление значения, &смещение, &токен) — стандартный итератор: резать по , и ;, пропускать пробелы, возвращать XHTTP_NEXT_ITEM (получен элемент) / XHTTP_NEXT_END (чистое завершение).Как едут параметры: в gzip;q=0.5 часть q=0.5 — параметр после точки с запятой; итератор останавливается на ней, а разбор параметров остаётся специализированным слоям полей (например q-значениям TE из главы 91). Модель «токены впереди, параметры позади» пронизывает все списковые поля.
Методы, цели и разбор Host
- Методы:
xrtHttpMethodParseраспознаёт стандартные методы (GET/POST/...) и расширенные (пользовательские токены) — строгое token-множество символов;GET;с разделителем отвергается сразу. - Цели: четыре формы request-target (origin-form
/path?query, absolute-form полный URL, authority-form, asterisk-form) различает слой разбора;слой этой главы даёт распознавание формыxrtHttpTargetParseи разрезание путь/запрос. - Разбор Host:
xrtHttpHostParse(值, &authority)(значение, &authority) даёт представлениеxhttpauthority(хост/порт) — обрабатывая домены, IPv4 и IPv6 в квадратных скобках (скобки[2001:db8::1]:8443обязаны быть парными, литерал IPv6 проверяется группа за группой — двусмысленный вход вида домен-двоеточие-порт не распознаётся ошибочно).
Кодирование параметров: два пути строки запроса
xrtHttpParamBuild/xrtHttpParamWrite (кодирующая сторона) и xrtHttpParamCount/xrtHttpParamNext/xrtHttpParamFind (разбирающая) обрабатывают пары ключ-значение вида a=1&b=2: кодирование экранирует произвольные байты в percent-форму, декодирование восстанавливает. Две дисциплины: семантика + и %20 — декодирование следует правилам application/x-www-form-urlencoded (+ → пробел); пробелы и зарезервированные символы — кодирующая сторона экранирует только необходимое (сохраняя &/= как структурные разделители). Формы, строки запроса и значения Cookie делят этот набор.
Изоморфизм с главами 25 и 77
Вы обнаружите, что API этой главы изоморфны главе 25 (конвейер представлений Trim/Cut) и главе 77 (курсор DER): заимствованный вход → строгая проверка → представление на выходе → преобразование по надобности. Это сквозная философия обработки текста XRT во всей библиотеке — ноль копий доходит до протокольного слоя, «разобрал один раз — ссылайся везде».
Панорама конвейера разбора
89.3示例
Первая полная программа: разбор поля, итерация токенов и обратная запись
Программа ниже из examples/http/base/main.c — минимальный контур тройки фундамента:
#include <stdio.h> #include <xrt.h> */ /* 展示传输无关的字段解析与 token-list 迭代。 */ int main(void) { xhttpfield Field; xhttpnext Next; xstrview Token; char Output[128]; size_t iOffset = 0; size_t iSize; if ( !xrtHttpFieldParse( XRT_STR_LITERAL("Connection: keep-alive, Upgrade"), &Field ) ) { return 1; } while ( (Next = xrtHttpTokenNext( Field.Value, &iOffset, &Token )) == XHTTP_NEXT_ITEM ) { printf("%.*s\n", (int)Token.Size, Token.Data); } if ( (Next != XHTTP_NEXT_END) || !xrtHttpFieldBlockWrite( &Field, 1, Output, sizeof(Output), &iSize ) ) { return 2; } printf("%.*s", (int)iSize, Output); return 0; }
Что только что произошло. (1) FieldParse разбирает одно поле в заимствованные представления (Name=Connection, Value=keep-alive, Upgrade — пробел после двоеточия подрезан). (2) Цикл TokenNext вырезает два токена: keep-alive и Upgrade — смещение держит вызывающий, итератор без состояния; NEXT_END — чистое завершение, а не ошибка. (3) FieldBlockWrite записывает поле дословно в полный блок — обратите внимание на канонический формат после двоеточия в выводе (один пробел): разбор нормализовал OWS, обратная запись даёт каноническую форму — после «прочитал-изменил-записал» прокси байты в канале детерминированы. Когда ёмкости мало, не пишется ни байта — атомарность позволяет спокойно «сперва попробовать малый буфер, при неудаче взять большой».
Вторая полная программа: строгость разбора Host
Вторая программа из examples/http/host/main.c — каноническое разложение IPv6 в скобках и порта:
#include <stdio.h> #include <xrt.h> */ /* 解析 Host 字段并直接读取借用的主机与端口视图。 */ int main(void) { xhttpauthority Host; uint16 iPort; if ( !xrtHttpHostParse( XRT_STR_LITERAL("[2001:db8::1]:8443"), &Host ) || !xrtHttpAuthorityPort(&Host, 80u, &iPort) ) { return 1; } printf( "host=%.*s port=%u\n", (int)Host.Host.Size, Host.Host.Data, (unsigned)iPort ); return 0; }
Что только что произошло. (1) Вход [2001:db8::1]:8443 раскладывается на хост 2001:db8::1 и порт 8443 — скобки проверены на парность, литерал IPv6 сверена группа за группой. (2) Границы строгости: example.com:8443 (домен с портом) легален; 2001:db8::1 (голый IPv6 без скобок с намеренным портом) — двусмысленный вход, и парсер отвергает его, а не угадывает.Зачем столько щепетильности: Host/Authority — вход решений о маршрутизации и подлинности (IP-форма SAN-сопоставления главы 80, выбор цели прокси); двусмысленный разбор означает, что решения о маршрутизации строятся на понимании, отличном от понимания противоположной стороны. Сопроводительный examples/http/field_tour (3 строки вывода) обходит счёт блока полей, поиск, итерацию токенов и обратную запись — «исполняемая шпаргалка» главы.
89.4契约
- Заимствованные представления: результаты разбора полей/методов/целей/Host заимствуют исходный вход; действуют, пока жив вход (правила представлений главы 15).
- Строгий разбор: нелегальные token-символы, двусмысленный Host, непарные скобки, недопустимые символы в имени поля — всё отвергается на месте; без исправлений и догадок.
- Атомарность ёмкости: интерфейсы обратной записи вроде
FieldBlockWriteпри нехватке ёмкости пишут ноль байтов; длина публикуется только при успехе — полусообщений не существует. - Итерация токенов: резать по
,/;, пропускать пробелы; состоянияNEXT_ITEM/NEXT_END; итератор без внутреннего состояния (смещение принадлежит вызывающему). - Нормализация OWS: разбор подрезает краевые пробелы значения; обратная запись даёт каноническую форму («имя: значение»).
- Семантика Host: домен/IPv4/IPv6 в скобках; порт опционален; двусмысленный вход отвергается.
- Кодирование параметров:
+декодируется по семантике форм; кодирование экранирует только необходимое (&/=структурно сохранены). - Ноль распределений: все API главы не распределяют кучу — стоимость на запрос предсказуема.
- Обрезка:
XRT_MODULE_HTTPнезависимо; поддоменыHTTP_HOST/HTTP_PARAM/HTTP_TARGETобрезаются независимо.
Типичные ошибки
Симптом: для Accept-Encoding: gzip, deflate, br пишется цикл strchr(',') — на токенах с кавычками (filename="a,b.txt") или пустых элементах (a,,b) режется неверно.
Причина: запятая HTTP-списков несёт правила экранирования кавычек и комментариев; голый strchr о них не знает. Правила встроены в TokenNext — обработка списков всегда через него.
char* p = strtok(Value, ","); /* запятая внутри кавычек разрезана */ while ( p ) { handle(p); p = strtok(NULL, ","); }
size_t iOffset = 0; xstrview Token; while ( (Next = xrtHttpTokenNext(Value, &iOffset, &Token)) == XHTTP_NEXT_ITEM ) { handle(Token); /* кавычки/пробелы/граница параметров - дело итератора */ }
Симптом: самодельный прокси пишет перезапись «дописывающим» способом — буфер переполнился на середине: первая половина уже отправлена, вторая отброшена; противоположная сторона получает обрубленный блок, соединение гибнет.
Причина: стиль записи без сознания атомарности ёмкости. Блок HTTP-заголовков — неделимая единица; половина единицы — протокольная ошибка.
size_t iUsed = 0; for ( each field ) { iUsed += snprintf(Out + iUsed, Cap - iUsed, "%.*s: %.*s\r\n", ...); /* ёмкость кончилась на середине: полблока */ }
if ( !xrtHttpFieldBlockWrite(Fields, iCount, Out, sizeof(Out), &iSize) ) { /* взять буфер больше или стратегию chunks - Out нетронут (ноль записанных байтов) */ } send(Out, iSize);
Симптом: передача исходного значения Host: [2001:db8::1]:8443 в функцию соединения проваливается или попадает не на тот порт; домены, различающиеся регистром, считаются разными хостами (расщепление ключей кеша).
Причина: значение Host — структурированный текст, а не голое имя хоста: скобки, порт и нормализацию регистра следует сперва разобрать. Лишь после того как HostParse выдал хост и порт, вход годится для маршрутизации/соединения.
connect(g_Target /* исходное значение Host напрямую */, ...); /* исходное значение со скобками и портом - не имя хоста */
xhttpauthority Authority; if ( !xrtHttpHostParse(Field.Value, &Authority) ) { return reject("malformed host"); } нормализуйте регистр (домены регистронезависимы) прежде маршрутизации/ключей кеша */
Практические задания
Прогоните образцы base и field_tour, построчно помечая, какой API проверяет каждая строка вывода. Затем замените вход base на Connection: keep-alive,, Upgrade (пустой элемент) и пронаблюдайте поведение итератора. Критерии приёмки: можете объяснить, пустой элемент «пропускается» или «порождает пустой токен»; три строки field_tour зелёные.
Реализуйте rewrite_host(字段数组, 数量, 新主机, 输出, 容量, &长度) (массив полей, количество, новый хост, вывод, ёмкость, &длина): найти поле Host по имени (без учёта регистра), заменить значение, остальные поля без изменений, затем FieldBlockWrite обратно. Критерии приёмки: без Host — явный отказ; перезаписанный блок побайтово каноничен; при нехватке ёмкости ноль записей (проверить 8-байтовым буфером).
Прочитайте полный заголовок запроса (много строк) в массив полей (свой цикл + FieldParse), затем посчитайте: общее число полей, повторяющиеся имена, множество токенов Connection, результат разбора Host, число параметров строки запроса (ParamDecode). Для того же входа перепишите «X-Forwarded-For» и запишите блок обратно. Критерии приёмки: ноль распределений кучи (статистика главы 6); уродливый вход (пробел в имени поля, двусмысленный Host) отвергается с отчётом первой позиции ошибки.
Итоги
| Тема | Кратко |
|---|---|
| Три дисциплины фундамента | строгий разбор (без исправлений) + заимствованные представления (ноль копий) + атомарность ёмкости (без полусообщений) |
| Модель поля | xhttpfield = два представления Name/Value; Parse подрезает OWS, проверяет token-набор |
| Обратная запись | FieldBlockWrite превращает массив полей в канонический блок; нехватка ёмкости пишет ноль |
| Итерация токенов | TokenNext режет по ,/;; NEXT_ITEM/NEXT_END; смещение принадлежит вызывающему |
| Параметры списков | параметры после точки с запятой (q-значения и т.п.) - дело специализированных слоёв; токен-итератор останавливается на ней |
| Разбор Host | HostParse даёт xhttpauthority; домен/IPv4/IPv6 в скобках; двусмысленное отвергается |
| Метод/цель | стандартные + расширенные методы с проверкой token; распознавание четырёх форм target с разрезанием путь/запрос |
| Кодирование параметров | +→пробел (семантика форм); кодирование экранирует только необходимое; &/= структурно сохранены |
| Ноль распределений | во всей главе без кучи - стоимость на запрос предсказуема |
| Обрезка | XRT_MODULE_HTTP независимо; поддомен fields обрезается самостоятельно |
📖 Справочники API в этой главе:http (72 записей)http_connection (7 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки