haproxy/src/qpack-enc.c
Amaury Denoyelle d8b34459b5 BUG/MINOR: h3: ensure that invalid status code are not encoded (FE side)
On frontend side, H3 layer transcodes HTX status code into HTTP/3
HEADERS frame. This is done by calling qpack_encode_int_status().

Prior to this patch, the latter function was also responsible to reject
an invalid value, which guarantee that only valid codes are encoded
(between 100 and 999 values). However, this is not practical as it is
impossible to differentiate between an invalid code error and a buffer
room exhaustation.

Changes this so that now HTTP/3 layer first ensures that HTX code is
valid. The stream is closed with H3_INTERNAL_ERROR if invalid value is
present. Thus, qpack_encode_int_status() will only report an error due
to buffer room exhaustion. If a small buffer is used, a standard buffer
will be reallocated which should be sufficient to encode the response.

The impact of this bug is minimal. Its main benefit is code clarity,
while also removing an unnecessary realloc when confronting with an
invalid HTTP code.

This should be backported at least up to 3.1. Prior to it, smallbuf
mechanism isn't present, hence the impact of this patch is less
important. However, it may still be backported to older versions, which
should facilitate picking patches for HTTP 1xx interim response support.
2025-07-15 18:39:23 +02:00

303 lines
6.9 KiB
C

#include <haproxy/qpack-enc.h>
#include <haproxy/buf.h>
#include <haproxy/intops.h>
/* Returns the byte size required to encode <i> as a <prefix_size>-prefix
* integer.
*/
static size_t qpack_get_prefix_int_size(int i, int prefix_size)
{
int n = (1 << prefix_size) - 1;
if (i < n) {
return 1;
}
else {
size_t result = 0;
while (i) {
++result;
i >>= 7;
}
return 1 + result;
}
}
/* Encode the integer <i> in the buffer <out> in a <prefix_size>-bit prefix
* integer. The caller must ensure there is enough size in the buffer. The
* prefix is OR-ed with <before_prefix> byte.
*
* Returns 0 if success else non-zero.
*/
static int qpack_encode_prefix_integer(struct buffer *out, int i,
int prefix_size,
unsigned char before_prefix)
{
const int mod = (1 << prefix_size) - 1;
BUG_ON_HOT(!prefix_size);
if (i < mod) {
if (b_room(out) < 1)
return 1;
b_putchr(out, before_prefix | i);
}
else {
int to_encode = i - mod;
const size_t sz = to_encode / mod;
if (b_room(out) < sz)
return 1;
b_putchr(out, before_prefix | mod);
while (1) {
if (to_encode > 0x7f) {
b_putchr(out, 0x80 | (to_encode & 0x7f));
to_encode >>= 7;
}
else {
b_putchr(out, to_encode & 0x7f);
break;
}
}
}
return 0;
}
/* Returns 0 on success else non-zero. */
int qpack_encode_int_status(struct buffer *out, unsigned int status)
{
int status_size, idx = 0;
/* HTTP layer must not encode invalid status codes. */
BUG_ON(status < 100 || status > 999);
switch (status) {
case 103: idx = 24; break;
case 200: idx = 25; break;
case 304: idx = 26; break;
case 404: idx = 27; break;
case 503: idx = 28; break;
case 100: idx = 63; break;
case 204: idx = 64; break;
case 206: idx = 65; break;
case 302: idx = 66; break;
case 400: idx = 67; break;
case 403: idx = 68; break;
case 421: idx = 69; break;
case 425: idx = 70; break;
case 500: idx = 71; break;
/* status code not in QPACK static table, idx is null. */
default: break;
}
if (idx) {
/* status code present in QPACK static table
* -> indexed field line
*/
status_size = qpack_get_prefix_int_size(idx, 6);
if (b_room(out) < status_size)
return 1;
qpack_encode_prefix_integer(out, idx, 6, 0xc0);
}
else {
/* status code not present in QPACK static table
* -> literal field line with name reference
*/
char a, b, c;
a = '0' + status / 100;
status -= (status / 100 * 100);
b = '0' + status / 10;
status -= (status / 10 * 10);
c = '0' + status;
/* field name */
if (qpack_encode_prefix_integer(out, 24, 4, 0x50))
return 1;
/* field value length */
if (qpack_encode_prefix_integer(out, 3, 7, 0x00))
return 1;
if (b_room(out) < 3)
return 1;
b_putchr(out, a);
b_putchr(out, b);
b_putchr(out, c);
}
return 0;
}
/* Returns 0 on success else non-zero. */
int qpack_encode_method(struct buffer *out, enum http_meth_t meth, struct ist other)
{
int sz, idx = 0;
size_t i;
switch (meth) {
case HTTP_METH_CONNECT: idx = 15; break;
case HTTP_METH_DELETE: idx = 16; break;
case HTTP_METH_GET: idx = 17; break;
case HTTP_METH_HEAD: idx = 18; break;
case HTTP_METH_OPTIONS: idx = 19; break;
case HTTP_METH_POST: idx = 20; break;
case HTTP_METH_PUT: idx = 21; break;
default: break;
}
if (idx) {
/* indexed field line */
if (b_room(out) < 2)
return 1;
qpack_encode_prefix_integer(out, idx, 6, 0xc0);
}
else {
BUG_ON(!istlen(other)); /* non standard method, <other> must be set. */
sz = 2 + qpack_get_prefix_int_size(istlen(other), 7) + istlen(other);
if (b_room(out) < sz)
return 1;
/* literal field line with name ref */
qpack_encode_prefix_integer(out, 15, 4, 0x50);
qpack_encode_prefix_integer(out, istlen(other), 7, 0);
for (i = 0; i < istlen(other); ++i)
b_putchr(out, istptr(other)[i]);
}
return 0;
}
/* Returns 0 on success else non-zero. */
int qpack_encode_scheme(struct buffer *out, const struct ist scheme)
{
size_t i;
int sz;
if (unlikely(!isteq(scheme, ist("https"))) && !isteq(scheme, ist("http"))) {
sz = 2 + qpack_get_prefix_int_size(istlen(scheme), 7) + istlen(scheme);
if (b_room(out) < sz)
return 1;
/* literal field line with name ref */
qpack_encode_prefix_integer(out, 23, 4, 0x50);
qpack_encode_prefix_integer(out, istlen(scheme), 7, 0);
for (i = 0; i < istlen(scheme); ++i)
b_putchr(out, istptr(scheme)[i]);
}
else {
const int idx = isteq(scheme, ist("https")) ?
23 : /* :scheme: https */
22; /* :scheme: http */
if (b_room(out) < 2)
return 1;
/* :scheme: http[s] */
qpack_encode_prefix_integer(out, idx, 6, 0xc0);
}
return 0;
}
/* Returns 0 on success else non-zero. */
int qpack_encode_path(struct buffer *out, const struct ist path)
{
size_t sz, i;
if (unlikely(isteq(path, ist("/")))) {
if (!b_room(out))
return 1;
qpack_encode_prefix_integer(out, 1, 6, 0xc0);
return 0;
}
else {
sz = 1 + qpack_get_prefix_int_size(istlen(path), 7) + istlen(path);
if (b_room(out) < sz)
return 1;
qpack_encode_prefix_integer(out, 1, 4, 0x50);
qpack_encode_prefix_integer(out, istlen(path), 7, 0);
for (i = 0; i < istlen(path); ++i)
b_putchr(out, istptr(path)[i]);
return 0;
}
}
/* Encode pseudo-header authority defined to <auth> into <out> buffer.
*
* Returns 0 on success else non-zero.
*/
int qpack_encode_auth(struct buffer *out, const struct ist auth)
{
size_t sz, i;
sz = 1 + qpack_get_prefix_int_size(istlen(auth), 7) + istlen(auth);
if (b_room(out) < sz)
return 1;
qpack_encode_prefix_integer(out, 0, 4, 0x50);
qpack_encode_prefix_integer(out, istlen(auth), 7, 0);
for (i = 0; i < istlen(auth); ++i)
b_putchr(out, istptr(auth)[i]);
return 0;
}
/* Returns 0 on success else non-zero. */
int qpack_encode_field_section_line(struct buffer *out)
{
char qpack_field_section[] = {
'\x00', /* required insert count */
'\x00', /* S + delta base */
};
if (b_room(out) < 2)
return 1;
b_putblk(out, qpack_field_section, 2);
return 0;
}
#define QPACK_LFL_WLN_BIT 0x20 // Literal field line with literal name
/* Encode a header in literal field line with literal name.
* Returns 0 on success else non-zero.
*/
int qpack_encode_header(struct buffer *out, const struct ist n, const struct ist v)
{
int i;
size_t sz = qpack_get_prefix_int_size(n.len, 3) + n.len +
qpack_get_prefix_int_size(v.len, 7) + v.len;
if (sz > b_room(out))
return 1;
/* literal field line with literal name
* | 0 | 0 | 1 | N | H | . | . | . |
* N :(allow an intermediary to add the header in a dynamic table)
* H: huffman encoded
* name len
*/
qpack_encode_prefix_integer(out, n.len, 3, QPACK_LFL_WLN_BIT);
/* name */
for (i = 0; i < n.len; ++i)
b_putchr(out, n.ptr[i]);
/* | 0 | . | . | . | . | . | . | . |
* value len
*/
qpack_encode_prefix_integer(out, v.len, 7, 0x00);
/* value */
for (i = 0; i < v.len; ++i)
b_putchr(out, v.ptr[i]);
return 0;
}