Ориентация главы

Глава 90 остановилась на пустой строке в конце блока заголовков; тело начинается там — и «как тело кончается» — самый ошибкоопасный вопрос HTTP/1. Эта глава иллюстрирует три ограничения: фиксированная длина (Content-Length — прочитать ровно N и стоп), chunked (Transfer-Encoding: chunked — построчные длины кусков, завершение нулевым куском), закрытие соединения (без объявлений; закрытие — конец, семантика HTTP/1.0). Ядро дизайна — Plan прежде всего: xrtHttp1RequestBodyPlan/ResponseBodyPlan сперва выводят из заголовка «как читать» (с семантикой метода — HEAD/204 без тела), затем по Plan инициализируется потоковый читатель xrtHttp1BodyInit/Read/Done; сторона записи зеркально даёт ChunkWrite/ChunkEndWrite. Trailer (блок полей в хвосте chunked) и согласование TE (заявка возможностей клиента) — тоже здесь. Это полная специализация главы 72 для HTTP: машина состояний мельче, но скелет «инкрементально поглощать слипшиеся и недоехавшие пакеты» неизменен.

91.1引入

Прокси пересылает ответ на 2 ГБ: вверх по течению chunked, вниз клиент понимает только HTTP/1.0? Потоково читать и пересылать кусок за куском или сперва накопить 2 ГБ? Как серверу объявить ограничение для динамически генерируемого контента (общая длина неизвестна до отправки)? Как клиенту получить trailer в хвосте chunked (целостностные дайджесты через прокси)? — Ответы держатся на слое кадрирования: он решает, возможно ли «пересылать по мере прибытия» (память не зависит от размера тела), как выразить «динамическую длину» (chunked) и как получить «отложенные метаданные» (trailer).

Цена неверного кадрирования не абстрактна: двусмысленность ограничения — это контрабанда запросов (request smuggling) — два последовательных запроса в одном соединении по-разному понятны на границе; атакующий прячет второй запрос в «тело» первого и протаскивает через прокси. Что при одновременных CL и TE (обязан отказ), что при нескольких CL (обязан отказ) — контракты главы записывают эти правила в камень.

91.2概念

Три ограничения и вывод Plan

1Фиксированная длинаContent-Length: N - прочитать N байтов и всё (простейшее; общая длина обязана быть известна до отправки)
2chunkedTransfer-Encoding: chunked - «16-ричная длина\r\nданные\r\n» на кусок, «0\r\n» завершает, хвостовой блок trailer
3Ограничение закрытиемни того ни другого - закрытие соединения и есть конец (семантика HTTP/1.0; в ответах сервера 1.1 запрещено, клиент 1.1 читает до закрытия)
4Planвыводится из полей заголовка + метода - свои правила на роль (на стороне ответа HEAD/204/304 без тела)

xrtHttp1ResponseBodyPlan(&Head, 方法, &Plan) (&Head, метод, &Plan) и RequestBodyPlan(&Head, &Plan) производят полное решение «как читать»: способ ограничения, общую длину (при фиксированной), допустимость trailer.Метод участвует в семантике: ответ HEAD и ответы 204/304 объявили CL — тела всё равно нет; Plan обнуляет по роли.Отказ от двусмысленности: CL вместе с TE, несколько несогласованных CL — Plan возвращает отказ (первый шлюз защиты от контрабанды стоит в слое вывода).

Потоковое чтение: машина состояний Body

XHTTP1_BODY_DATA消费一段正文(Data 视图 + Consumed 字节) →XHTTP1_BODY_DATA
XHTTP1_BODY_DATA定界满足(定长读够 / 0 块到达) →XHTTP1_BODY_DONE
任意超上限 / 非法块行 / trailer 块超限 →XHTTP1_BODY_ERROR

xrtHttp1BodyRead(&Body, 输入视图, 是否终结, &消费, &数据视图, NULL) (&Body, входное представление, финальный ли, &потреблено, &представление данных, NULL) — ядро цикла: подаёте текущий буфер; возвращается одно из трёх — «данные» (заимствованное представление Data — прямое чтение без копий), «завершено», «ошибка», — а Consumed сообщает, сколько потреблено в этом раунде (оставшиеся байты принадлежат следующему сообщу/вне тела).Потолочная оборона (xhttp1bodylimits): MaxBody (общий объём тела), длина строки куска, число trailer — злобно гигантский кусок останавливается до входа. Trailer собирается через привязанный массив (передайте массив trailer в BodyInit), доступен после Done — та же форма, что у массива полей главы 90.

Сторона записи: генерация кусков и терминатора

xrtHttp1ChunkWrite(数据, 输出, 容量, &长度) (данные, вывод, ёмкость, &длина) генерирует полный кусок «16-ричная длина\r\n + данные\r\n»; xrtHttp1ChunkEndWrite(trailer数组, 数量, ...) (массив trailer, количество, ...) генерирует терминатор «0\r\n» с опциональным блоком trailer. Двумя инструментами собирается любой поток: динамический контент генерируется и отправляется по ходу (в паре с Vec-отправкой главы 68, амортизирующей системные вызовы); большие файлы циклом с фиксированным куском. Фиксированное тело проще — CL в заголовке, тело отправляется напрямую.Правило выбора: общая длина известна → CL (экономия строк по 6 байтов на кусок и стоимости разбора); неизвестна или потоковая генерация → chunked.

Trailer: метаданные после тела

chunked разрешает блок полей после терминатора — Digest (целостность содержимого), трассировочные ID через прокси — стандартное место отложенных метаданных. Две дисциплины: сторона чтения — превышение ёмкости массива trailer ошибка (та же логика, что у потолка полей); согласование на отправителя — только клиентский TE: trailers заявляет знание trailer (следующий раздел); сервер пользуется ими после подтверждения; не поддерживающему прокси легально их снять.

Согласование TE: заявка возможностей транспортного кодирования

xrtHttpTeParse(字段数组, 数量, &Info) (массив полей, количество, &Info) суммирует (повторяемые) поля TE: список принимаемых транспортных кодирований, флаг XHTTP_TE_ACCEPTS_TRAILERS (клиент заявляет знание trailer) и q-значения по кодированиям. xrtHttpTeQuality(字段, 数量, 编码名) (поля, количество, имя кодирования) возвращает целое в тысячных (gzip;q=0.5 → 500) — устраняя ошибки точности и платформенные расхождения сравнения плавающих. Сервер выбирает из этого приемлемое для обеих сторон кодирование; q=0 — явное «не надо». Модель согласования «целые q-значения + флаги» едина с другими семействами полей.

91.3示例

Первая полная программа: Plan, потоковое чтение + поблочная запись

Программа ниже из examples/http/http1_body/main.c — полный контур стороны чтения и стороны записи:

examples/http/http1_body/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>



/* 解析响应分帧并以零拷贝视图逐段读取 chunked 正文。 */
static bool readResponseBody(void)
{
	static const uint8 Message[] =
		"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
		"7\r\nchunked\r\n5\r\n body\r\n0\r\nDigest: ok\r\n\r\n";
	xhttpfield Fields[8];
	xhttpfield Trailers[4];
	xhttp1bodylimits Limits;
	xhttp1bodyplan Plan;
	xhttp1head Head;
	xhttp1body Body;
	size_t iOffset;

	xrtHttp1HeadInit(&Head, Fields, 8);
	if ( xrtHttp1ResponseParse(
		(xbytesview){ Message, sizeof(Message) - 1u },
		&Head, NULL, NULL
	) != XHTTP1_READY ) {
		return false;
	}
	if ( !xrtHttp1ResponseBodyPlan(
		&Head, XRT_STR_LITERAL("GET"), &Plan
	) ) {
		return false;
	}
	xrtHttp1BodyLimitsInit(&Limits);
	Limits.MaxBody = 1024;
	if ( !xrtHttp1BodyInit(
		&Body, &Plan, Trailers, 4, &Limits
	) ) {
		return false;
	}
	iOffset = Head.Bytes;
	while ( !xrtHttp1BodyDone(&Body) ) {
		xbytesview Data;
		size_t iConsumed;
		xhttp1bodystatus Status = xrtHttp1BodyRead(
			&Body,
			(xbytesview){ Message + iOffset, sizeof(Message) - 1u - iOffset },
			false, &iConsumed, &Data, NULL
		);

		if ( Status == XHTTP1_BODY_DATA ) {
			fwrite(Data.Data, 1, Data.Size, stdout);
		} else if ( Status != XHTTP1_BODY_DONE ) {
			return false;
		}
		iOffset += iConsumed;
	}
	putchar('\n');
	return true;
}



/* 直接生成完整 chunk,也可只生成 size 行后配合向量发送正文。 */
static bool writeResponseBody(void)
{
	static const xhttpfield Trailers[] = {
		{ XRT_STR_INIT("Digest"), XRT_STR_INIT("sha-256=:demo:") }
	};
	uint8 Output[128];
	size_t iChunk;
	size_t iEnd;

	if ( !xrtHttp1ChunkWrite(
		(xbytesview){ (cbytes)"hello", 5 },
		Output, sizeof(Output), &iChunk
	) || !xrtHttp1ChunkEndWrite(
		Trailers, 1, Output + iChunk,
		sizeof(Output) - iChunk, &iEnd
	) ) {
		return false;
	}
	fwrite(Output, 1, iChunk + iEnd, stdout);
	return true;
}


























 */


/* 展示 HTTP/1 Body Plan、流式读取和分块写出。 */
int main(void)
{
	if ( !readResponseBody() || !writeResponseBody() ) {
		return 1;
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/http1_body/main.c -lws2_32 -liphlpapichunked body5hello0

Что только что произошло. Сторона чтения (первая из четырёх строк вывода): (1) заголовок ответа объявляет Transfer-Encoding: chunked; после ResponseParse ResponseBodyPlan (с методом GET) выводит план chunked; BodyLimitsInit ставит MaxBody=1024, затем BodyInit привязывает массив trailer. (2) Вход — «7\r\nchunked\r\n5\r\n body\r\n0\r\nDigest: ok\r\n\r\n» — цикл BodyRead выдаёт отрезки chunked и body (печать склейкой chunked body); по прибытии куска 0 trailer Digest: ok падает в привязанный массив, состояние DONE. Ноль копий: представление Data указывает прямо в исходный буфер.Сторона записи (последние три строки): (3) ChunkWrite("hello") даёт 5\r\nhello\r\n (печать 5 и hello двумя строками), ChunkEndWrite(带 Digest trailer) (с trailer Digest) даёт 0\r\nDigest: sha-256=:demo:\r\n\r\n (первая строка 0) — последовательность байтов, порождаемая стороной записи, в точности совпадает с форматом, потребляемым стороной чтения: это HTTP-версия тезиса главы 72 «читающая и пишущая стороны делят одну машину состояний кадрирования».

Вторая полная программа: чтение согласования TE

Вторая программа из examples/http/te/main.c — сводный запрос возможностей клиента:

examples/http/te/main.c源码 ↗
#include <xrt.h>

#include <stdio.h>





















 */


/* 汇总重复 TE 字段并读取客户端的传输能力。 */
int main(void)
{
	static const xhttpfield Fields[] = {
		{ XRT_STR_INIT("TE"), XRT_STR_INIT("trailers") },
		{ XRT_STR_INIT("TE"), XRT_STR_INIT("gzip;q=0.5") }
	};
	xhttpteinfo Info;

	if ( !xrtHttpTeParse(Fields, 2u, &Info) ) {
		return 1;
	}
	printf(
		"codings=%zu trailers=%s gzip=%u\n",
		Info.TransferCodingCount,
		(Info.Flags & XHTTP_TE_ACCEPTS_TRAILERS) != 0 ?
			"yes" : "no",
		(unsigned int)xrtHttpTeQuality(
			Fields, 2u, XRT_STR_LITERAL("gzip")
		)
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/http/te/main.c -lws2_32 -liphlpapicodings=1 trailers=yes gzip=500

Что только что произошло. (1) Два поля TE (HTTP разрешает повторение) суммируются TeParse в одну capability: транспортное кодирование одно (gzip — trailers это флаг возможности, не кодирование), приём trailer подтверждён. (2) TeQuality("gzip") возвращает 500 — целое-в-тысячных форма q=0.5; ловушки точности плавающих переговоров (семейство 0.1+0.2≠0.3) уничтожены на уровне представления. (3) Цепочка решения сервера: trailers=yes → можно слать Digest в хвосте chunked; gzip=500 → gzip пригоден, но с низким приоритетом. Сопроводительные examples/http1/variants и examples/http/trailer покрывают варианты ограничения и детали trailer — они пригодятся в упражнениях главы.

91.4契约

  • Plan прежде всего: перед чтением тела обязателен Plan (вывод из заголовка); метод участвует в семантике (на стороне ответа HEAD/204/304 без тела); двусмысленное ограничение (CL+TE вместе, несогласованные CL) отвергается в слое Plan — первый шлюз против контрабанды.
  • Три состояния Body: DATA (заимствованное представление Data + Consumed) / DONE (ограничение выполнено) / ERROR; цикл подачи до DONE; на ошибке — стоп.
  • Потолочная оборона: три линии — MaxBody/длина строки куска/число trailer; отказ сверх предела до потребления памяти — потолки суть решение развёртывания.
  • Сбор trailer: в привязанный массив; превышение ёмкости — ошибка; доступ после Done; сторона чтения не проверяет «заявил ли клиент trailers» (это переговоры отправителя).
  • Симметрия записи: ChunkWrite (кусок) + ChunkEndWrite (терминатор + опциональный trailer); общий формат с машиной чтения; атомарность ёмкости как в главе 88.
  • Выбор ограничения: длина известна → CL; потоковая генерация → chunked; серверу 1.1 голое закрытие запрещено.
  • Согласование TE: повторяемые поля суммируются; q целым-в-тысячных (без плавающих); флаг ACCEPTS_TRAILERS; неупомянутые кодирования q=0.
  • Ноль копий: представление Data заимствует вход; тело минует транзитный буфер — «пересылка по мере прибытия» держит память независимой от размера тела.
  • Слой Message: MessageParse комбинирует заголовок+тело за один проход (удобный путь малых сообщений); потоковые сценарии — семейство Body.

Типичные ошибки

Ловушка 1: «поверить одному» при сосуществовании CL и TE:Ловушка 1: «поверить одному» при сосуществовании CL и TE

Симптом: самодельный прокси обрабатывает запрос с Content-Length и Transfer-Encoding одновременно по CL, бэкенд — по TE; следующий запрос соединения «спрятан» в тело: контрабанда запросов.

Причина: два ограничения сразу — двусмысленный вход; расхождение пониманий фронта и тыла — сама конструкция атаки. Единственное безопасное действие — отказ.

错误写法
if ( has_content_length(Head) ) {
	plan_as_fixed(...);      /* я читаю по CL */
} else {
	plan_as_chunked(...);
/* если нижний поток понимает наоборот: граница сместилась -> окно контрабанды */
}
正确写法
/* отказ встроен в слой Plan: пусть говорит результат вывода */
if ( !xrtHttp1RequestBodyPlan(&Head, &Plan) ) {
	return reject("ambiguous framing");  /* двусмысленное ограничение: единственно безопасный ответ */
}
Ловушка 2: Consumed из BodyRead не подключён к смещению потребления:Ловушка 2: Consumed из BodyRead не подключён к смещению потребления

Симптом: потоковый прокси теряет байты или пересылает дважды — старт каждой подачи неверен.

Причина: вход BodyRead — «с какого места начать, решаете вы»: сколько потреблено (iConsumed) обязано накапливаться в смещении следующего раунда; непотреблённый хвост буфера может принадлежать следующему сообщению (pipelining).

错误写法
size_t iOff = Head.Bytes;
while ( !Done ) {
	BodyRead(&Body, view(In), false, &iUsed, &Data, NULL);
	forward(Data);
	/* iUsed потерян: следующий раунд снова с прежнего смещения */
}
正确写法
size_t iOff = Head.Bytes;
while ( !xrtHttp1BodyDone(&Body) ) {
	xhttp1bodystatus St = xrtHttp1BodyRead(&Body,
		(xbytesview){ In.Data + iOff, In.Size - iOff },
		bFin, &iUsed, &Data, NULL);
	iOff += iUsed;                 /* потребление подключено к смещению */
	if ( St == XHTTP1_BODY_DATA ) { forward(Data); }
	else if ( St != XHTTP1_BODY_DONE ) { return false; }
}
/* данные в In за iOff принадлежат следующему сообщению */
Ловушка 3: серверный динамический контент «сперва накопить, потом фиксированно»:Ловушка 3: серверный динамический контент «сперва накопить, потом фиксированно»

Симптом: чтобы написать Content-Length, весь динамический ответ копится в памяти — экспортный интерфейс на 2 ГБ валивает сервер в OOM.

Причина: CL требует знания общей длины до отправки. При потоковой генерации контента единственно верное решение — chunked: каждый кусок отправляется по мере генерации, память не зависит от объёма.

错误写法
buffer_whole_response(&Body);   /* накопили все 2 ГБ */
set_content_length(Body.Size);
send_all(Body);
正确写法
send_header_chunked();
while ( next_chunk(&Data) ) {
	xrtHttp1ChunkWrite(Data, Out, sizeof(Out), &iSize);
	send(Out, iSize);            /* генерируем и отправляем по ходу */
}
xrtHttp1ChunkEndWrite(Trailers, 1, Out, sizeof(Out), &iSize);
send(Out, iSize);               /* кусок 0 + trailer в финале */

Практические задания

Базовое: пройти каждое из трёх ограничений

Постройте три байтовых потока ответов (CL=11, chunked с двумя кусками, без объявлений); Plan + Body каждое до DONE, печатая последовательности действий (раунды потребления/отрезки данных). Критерии приёмки: три вывода сходятся с ручной выкладкой; у группы без объявлений «ограничение закрытием» даёт DONE при bFin=true (закрытие противоположной стороны).

Продвинутое: эксперимент с образцами контрабанды

Постройте четыре злых образца: CL+TE вместе, несогласованные двойные CL, сверх MaxBody, сверхдлинная строка куска — по одному проверьте отказ на уровне Plan/Body. Занесите уровень отказа каждого образца в таблицу. Критерии приёмки: все четыре отвергнуты; вы можете указать, в каком слое конвейера разбора стоит каждый шлюз.

Вызов: ядро потокового перекодирующего прокси

Ответ вверх по течению chunked, кусками: Body читает потоково → каждый отрезок данных «переводится в верхний регистр» → ChunkWrite перекусывает (фиксированный кусок 16 байтов) → пересылка вниз; trailer пересылается дословно (пересчёт Digest опционален). Критерии приёмки: память не зависит от общего объёма вверх по течению (сверка со статистикой главы 6); границы кусков вверх/вниз различаются (7+5 на входе, 16×N на выходе), а семантика сохранна; при обрыве верхнего потока нижний получает чистую ошибку, а не полкадра.

Итоги

ТемаКратко
Три ограниченияфиксированная длина CL / chunked (завершение куском 0) / закрытием (семантика 1.0)
Plan прежде всеговывод из заголовка + метода; HEAD/204 без тела; двусмысленное ограничение (CL+TE, несогласованный CL) отвергается
Три состояния BodyDATA (представление + Consumed) / DONE / ERROR; смещение потребления обязано накапливаться
Три линии потолковMaxBody / длина строки куска / число trailer; отказ до потребления; решение развёртывания
Trailerблок полей после терминатора; сбор в привязанный массив; отправителю нужен клиентский TE trailers
Пишущая параChunkWrite (строка длины + данные) / ChunkEndWrite (кусок 0 + trailer); общий формат с машиной чтения
Выбор ограничениядлина известна → CL; поток → chunked; серверу 1.1 голое закрытие запрещено
Согласование TEповторяемые поля суммируются; q целым-в-тысячных; флаг ACCEPTS_TRAILERS; неупомянутое q=0
Ноль копийData заимствует вход; пересылка по мере прибытия держит память вне зависимости от размера тела
Слой Messageзаголовок + тело за один проход (малые сообщения); семейство Body — потоковый примитив

📖 Справочники API в этой главе:http1 (39 записей)http_te (16 записей)http (72 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки