Add RFC 9218 priority types and parser

Implement a strict RFC 8941 Structured Fields Dictionary parser for the
RFC 9218 Extensible Prioritization Scheme, shared by HTTP/2 (and, later,
HTTP/3).

New files:
- ngx_http_priority.h: the ngx_http_priority_t and
  ngx_http_priority_state_t types and the parse/state API.
- ngx_http_priority.c: the parser plus ngx_http_priority_state_init()
  and ngx_http_priority_state_update() (client/server merge).

The field is parsed strictly: a syntax error fails the whole field
(RFC 8941, Section 4.2, treats it as absent), and only on a successful
parse are the RFC 9218, Section 4, rules applied, ignoring unknown
members, out-of-range urgency and members of an unexpected type.  On
error the caller keeps any previously signalled priority.  The parser
bounds the number of members, inner-list items and parameters, and the
length of individual strings, tokens and byte sequences, that it will
accept.

The sources are built only with HTTP/2 (for now), alongside the Huffman
sources, rather than unconditionally into the core HTTP module.

Assisted-by: opencode:claude-opus-4.8
This commit is contained in:
Andrew Clayton 2026-09-23 21:09:36 +01:00
parent 939334efff
commit b5bd4b3487
5 changed files with 721 additions and 0 deletions

View file

@ -124,6 +124,12 @@ if [ $HTTP = YES ]; then
fi
if [ $HTTP_V2 = YES ]; then
HTTP_DEPS="$HTTP_DEPS $HTTP_PRIORITY_DEPS"
HTTP_SRCS="$HTTP_SRCS $HTTP_PRIORITY_SRCS"
fi
# the module order is important
# ngx_http_static_module
# ngx_http_gzip_static_module

View file

@ -272,3 +272,6 @@ HTTP_FILE_CACHE_SRCS=src/http/ngx_http_file_cache.c
HTTP_HUFF_SRCS="src/http/ngx_http_huff_decode.c
src/http/ngx_http_huff_encode.c"
HTTP_PRIORITY_DEPS=src/http/ngx_http_priority.h
HTTP_PRIORITY_SRCS=src/http/ngx_http_priority.c

View file

@ -38,6 +38,7 @@ typedef u_char *(*ngx_http_log_handler_pt)(ngx_http_request_t *r,
#include <ngx_http_core_module.h>
#if (NGX_HTTP_V2)
#include <ngx_http_priority.h>
#include <ngx_http_v2.h>
#endif
#if (NGX_HTTP_V3)

View file

@ -0,0 +1,676 @@
/*
* Copyright (C) Nginx, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#define NGX_HTTP_PRIORITY_DEFAULT_URGENCY 3
#define NGX_HTTP_PRIORITY_URGENCY_MIN 0
#define NGX_HTTP_PRIORITY_URGENCY_MAX 7
/* bare item types we distinguish; others are parsed and discarded */
#define NGX_HTTP_PRIORITY_INTEGER 0
#define NGX_HTTP_PRIORITY_BOOLEAN 1
#define NGX_HTTP_PRIORITY_OTHER 2
/*
* Parse limits. RFC 8941 requires parsers to support at least these many
* elements and characters; bounding the counts and individual item lengths
* keeps parse time proportional to a well-formed field rather than to what a
* peer chooses to send.
*/
#define NGX_HTTP_PRIORITY_MAX_MEMBERS 1024
#define NGX_HTTP_PRIORITY_MAX_INNER_ITEMS 256
#define NGX_HTTP_PRIORITY_MAX_PARAMS 256
#define NGX_HTTP_PRIORITY_MAX_STRING 1024
#define NGX_HTTP_PRIORITY_MAX_TOKEN 512
#define NGX_HTTP_PRIORITY_MAX_BINARY 21848 /* 16384 after decoding */
static ngx_int_t ngx_http_priority_parse_key(u_char **pos, u_char *end,
ngx_str_t *key);
static ngx_int_t ngx_http_priority_parse_member(u_char **pos, u_char *end,
ngx_str_t *key, ngx_int_t *type, ngx_int_t *value);
static ngx_int_t ngx_http_priority_parse_value(u_char **pos, u_char *end,
ngx_int_t *type, ngx_int_t *value);
static ngx_int_t ngx_http_priority_parse_inner_list(u_char **pos, u_char *end);
static ngx_int_t ngx_http_priority_parse_bare_item(u_char **pos, u_char *end,
ngx_int_t *type, ngx_int_t *value);
static ngx_int_t ngx_http_priority_parse_params(u_char **pos, u_char *end);
static ngx_int_t ngx_http_priority_parse_number(u_char **pos, u_char *end,
ngx_int_t *type, ngx_int_t *value);
static ngx_int_t ngx_http_priority_parse_string(u_char **pos, u_char *end);
static ngx_int_t ngx_http_priority_parse_token(u_char **pos, u_char *end);
static ngx_int_t ngx_http_priority_parse_binary(u_char **pos, u_char *end);
void
ngx_http_priority_state_init(ngx_http_priority_state_t *ps)
{
ngx_memzero(ps, sizeof(ngx_http_priority_state_t));
ps->client.urgency = NGX_HTTP_PRIORITY_DEFAULT_URGENCY;
ps->server.urgency = NGX_HTTP_PRIORITY_DEFAULT_URGENCY;
ps->effective.urgency = NGX_HTTP_PRIORITY_DEFAULT_URGENCY;
}
/*
* Parse an RFC 9218 Priority field, a Structured Fields Dictionary
* (RFC 8941). The dictionary is parsed strictly: any syntax error fails
* the whole field (RFC 8941, Section 4.2), and only then are the RFC 9218
* rules applied, ignoring unknown members, out-of-range urgency and members
* of an unexpected type. On error the field is treated as absent and p is
* left at defaults; callers keep any previously signalled priority.
*/
ngx_int_t
ngx_http_priority_parse(ngx_str_t *field, ngx_http_priority_t *p)
{
u_char *last, *end;
ngx_str_t key;
ngx_int_t type, value;
ngx_uint_t members;
p->urgency = NGX_HTTP_PRIORITY_DEFAULT_URGENCY;
p->incremental = 0;
p->urgency_set = 0;
p->incremental_set = 0;
if (field == NULL) {
return NGX_OK;
}
/* RFC 8941, Section 4.2.2 */
last = field->data;
end = last + field->len;
members = 0;
/* Section 4.2, step 2: discard leading SP */
while (last < end && *last == ' ') {
last++;
}
while (last < end) {
if (++members > NGX_HTTP_PRIORITY_MAX_MEMBERS) {
return NGX_ERROR;
}
if (ngx_http_priority_parse_member(&last, end, &key, &type, &value)
!= NGX_OK)
{
return NGX_ERROR;
}
/*
* RFC 8941, Section 4.2.2, step 2.4: a duplicate key replaces the
* earlier member. RFC 9218, Section 4: unknown members and members
* of an unexpected type are ignored. A repeated u or i therefore
* resets to the default first, so a later out-of-range or
* wrong-typed value leaves the parameter absent rather than keeping
* the earlier one.
*/
if (key.len == 1 && key.data[0] == 'u') {
p->urgency = NGX_HTTP_PRIORITY_DEFAULT_URGENCY;
p->urgency_set = 0;
if (type == NGX_HTTP_PRIORITY_INTEGER
&& value >= NGX_HTTP_PRIORITY_URGENCY_MIN
&& value <= NGX_HTTP_PRIORITY_URGENCY_MAX)
{
p->urgency = (unsigned) value;
p->urgency_set = 1;
}
} else if (key.len == 1 && key.data[0] == 'i') {
p->incremental = 0;
p->incremental_set = 0;
if (type == NGX_HTTP_PRIORITY_BOOLEAN) {
p->incremental = (unsigned) value;
p->incremental_set = 1;
}
}
while (last < end && (*last == ' ' || *last == '\t')) {
last++;
}
if (last == end) {
break;
}
if (*last++ != ',') {
return NGX_ERROR;
}
while (last < end && (*last == ' ' || *last == '\t')) {
last++;
}
/* trailing comma */
if (last == end) {
return NGX_ERROR;
}
}
return NGX_OK;
}
/*
* Parse one dictionary member and its parameters (RFC 8941, Section 4.2.2).
* A bare key (no "=") is a Boolean true; type/value then describe the
* member's value so the caller can pick out u and i and ignore the rest.
*/
static ngx_int_t
ngx_http_priority_parse_member(u_char **pos, u_char *end, ngx_str_t *key,
ngx_int_t *type, ngx_int_t *value)
{
u_char *last;
last = *pos;
if (ngx_http_priority_parse_key(&last, end, key) != NGX_OK) {
return NGX_ERROR;
}
if (last < end && *last == '=') {
last++;
if (ngx_http_priority_parse_value(&last, end, type, value) != NGX_OK) {
return NGX_ERROR;
}
} else {
*type = NGX_HTTP_PRIORITY_BOOLEAN;
*value = 1;
if (ngx_http_priority_parse_params(&last, end) != NGX_OK) {
return NGX_ERROR;
}
}
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.1.1: an Item or an Inner List, each with Parameters */
static ngx_int_t
ngx_http_priority_parse_value(u_char **pos, u_char *end, ngx_int_t *type,
ngx_int_t *value)
{
u_char *last;
last = *pos;
if (last < end && *last == '(') {
*type = NGX_HTTP_PRIORITY_OTHER;
*value = 0;
if (ngx_http_priority_parse_inner_list(&last, end) != NGX_OK) {
return NGX_ERROR;
}
} else {
if (ngx_http_priority_parse_bare_item(&last, end, type, value)
!= NGX_OK)
{
return NGX_ERROR;
}
}
if (ngx_http_priority_parse_params(&last, end) != NGX_OK) {
return NGX_ERROR;
}
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.3.3 */
static ngx_int_t
ngx_http_priority_parse_key(u_char **pos, u_char *end, ngx_str_t *key)
{
u_char *last, ch;
last = *pos;
if (last == end) {
return NGX_ERROR;
}
ch = *last;
if (!((ch >= 'a' && ch <= 'z') || ch == '*')) {
return NGX_ERROR;
}
key->data = last;
do {
last++;
if (last == end) {
break;
}
ch = *last;
} while ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|| ch == '_' || ch == '-' || ch == '.' || ch == '*');
key->len = last - *pos;
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.3.2 */
static ngx_int_t
ngx_http_priority_parse_params(u_char **pos, u_char *end)
{
u_char *last;
ngx_str_t key;
ngx_int_t type, value;
ngx_uint_t params;
last = *pos;
params = 0;
while (last < end && *last == ';') {
last++;
if (++params > NGX_HTTP_PRIORITY_MAX_PARAMS) {
return NGX_ERROR;
}
/* Section 4.2.3.2, step 2.3: discard leading SP */
while (last < end && *last == ' ') {
last++;
}
if (ngx_http_priority_parse_key(&last, end, &key) != NGX_OK) {
return NGX_ERROR;
}
if (last < end && *last == '=') {
last++;
if (ngx_http_priority_parse_bare_item(&last, end, &type, &value)
!= NGX_OK)
{
return NGX_ERROR;
}
}
}
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.1.2: Inner List */
static ngx_int_t
ngx_http_priority_parse_inner_list(u_char **pos, u_char *end)
{
u_char *last;
ngx_int_t type, value;
ngx_uint_t items;
last = *pos + 1;
items = 0;
for ( ;; ) {
while (last < end && *last == ' ') {
last++;
}
if (last < end && *last == ')') {
*pos = last + 1;
return NGX_OK;
}
if (++items > NGX_HTTP_PRIORITY_MAX_INNER_ITEMS) {
return NGX_ERROR;
}
if (ngx_http_priority_parse_bare_item(&last, end, &type, &value)
!= NGX_OK)
{
return NGX_ERROR;
}
if (ngx_http_priority_parse_params(&last, end) != NGX_OK) {
return NGX_ERROR;
}
if (last == end) {
return NGX_ERROR;
}
if (*last != ' ' && *last != ')') {
return NGX_ERROR;
}
}
}
/* RFC 8941, Section 4.2.3.1 */
static ngx_int_t
ngx_http_priority_parse_bare_item(u_char **pos, u_char *end, ngx_int_t *type,
ngx_int_t *value)
{
u_char ch;
if (*pos == end) {
return NGX_ERROR;
}
ch = **pos;
if (ch == '-' || (ch >= '0' && ch <= '9')) {
return ngx_http_priority_parse_number(pos, end, type, value);
}
if (ch == '?') {
(*pos)++;
if (*pos == end || (**pos != '0' && **pos != '1')) {
return NGX_ERROR;
}
*type = NGX_HTTP_PRIORITY_BOOLEAN;
*value = (**pos == '1');
(*pos)++;
return NGX_OK;
}
*type = NGX_HTTP_PRIORITY_OTHER;
if (ch == '"') {
return ngx_http_priority_parse_string(pos, end);
}
if (ch == ':') {
return ngx_http_priority_parse_binary(pos, end);
}
if (((ch | 0x20) >= 'a' && (ch | 0x20) <= 'z') || ch == '*') {
return ngx_http_priority_parse_token(pos, end);
}
return NGX_ERROR;
}
/* RFC 8941, Section 4.2.4: Integer or Decimal */
static ngx_int_t
ngx_http_priority_parse_number(u_char **pos, u_char *end, ngx_int_t *type,
ngx_int_t *value)
{
u_char *last, *start;
ngx_int_t n;
ngx_uint_t neg, decimal, digits, frac;
last = *pos;
neg = 0;
if (*last == '-') {
neg = 1;
last++;
}
start = last;
decimal = 0;
digits = 0;
frac = 0;
/* RFC 8941, Section 4.2.4 */
while (last < end) {
if (*last >= '0' && *last <= '9') {
digits++;
/* step 7.5: an Integer has at most 15 digits */
if (!decimal && digits > 15) {
return NGX_ERROR;
}
/* step 9.2: a Decimal has at most three fractional digits */
if (decimal && ++frac > 3) {
return NGX_ERROR;
}
last++;
continue;
}
/* step 8: a single "." starts the fractional part of a Decimal */
if (*last == '.' && !decimal) {
/* step 8.1: at most 12 digits before the decimal point */
if (digits == 0 || digits > 12) {
return NGX_ERROR;
}
decimal = 1;
last++;
continue;
}
break;
}
if (digits == 0) {
return NGX_ERROR;
}
if (decimal) {
/* step 9.1: a Decimal must not end with "." */
if (frac == 0) {
return NGX_ERROR;
}
/* a valid Decimal, but not an Integer: parsed and discarded */
*type = NGX_HTTP_PRIORITY_OTHER;
*value = 0;
*pos = last;
return NGX_OK;
}
n = ngx_atoi(start, last - start);
if (n == NGX_ERROR) {
return NGX_ERROR;
}
*type = NGX_HTTP_PRIORITY_INTEGER;
*value = neg ? -n : n;
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.5: String */
static ngx_int_t
ngx_http_priority_parse_string(u_char **pos, u_char *end)
{
u_char *last, ch;
ngx_uint_t len;
last = *pos + 1;
len = 0;
while (last < end) {
ch = *last++;
if (ch == '\\') {
if (last == end || (*last != '"' && *last != '\\')) {
return NGX_ERROR;
}
last++;
if (++len > NGX_HTTP_PRIORITY_MAX_STRING) {
return NGX_ERROR;
}
continue;
}
if (ch == '"') {
*pos = last;
return NGX_OK;
}
if (ch < 0x20 || ch >= 0x7f) {
return NGX_ERROR;
}
if (++len > NGX_HTTP_PRIORITY_MAX_STRING) {
return NGX_ERROR;
}
}
return NGX_ERROR;
}
/* RFC 8941, Section 4.2.6: Token */
static ngx_int_t
ngx_http_priority_parse_token(u_char **pos, u_char *end)
{
u_char *last, ch;
last = *pos + 1;
while (last < end) {
ch = *last;
if (ch == ':' || ch == '/'
|| ((ch | 0x20) >= 'a' && (ch | 0x20) <= 'z')
|| (ch >= '0' && ch <= '9')
|| ch == '!' || ch == '#' || ch == '$' || ch == '%'
|| ch == '&' || ch == '\'' || ch == '*' || ch == '+'
|| ch == '-' || ch == '.' || ch == '^' || ch == '_'
|| ch == '`' || ch == '|' || ch == '~')
{
if ((ngx_uint_t) (last - *pos) >= NGX_HTTP_PRIORITY_MAX_TOKEN) {
return NGX_ERROR;
}
last++;
continue;
}
break;
}
*pos = last;
return NGX_OK;
}
/* RFC 8941, Section 4.2.7: Byte Sequence */
static ngx_int_t
ngx_http_priority_parse_binary(u_char **pos, u_char *end)
{
u_char *last, ch;
ngx_uint_t len;
last = *pos + 1;
len = 0;
while (last < end) {
ch = *last++;
if (ch == ':') {
/* the content has to decode: 4n + 1 characters cannot */
if (len % 4 == 1) {
return NGX_ERROR;
}
*pos = last;
return NGX_OK;
}
if (!(((ch | 0x20) >= 'a' && (ch | 0x20) <= 'z')
|| (ch >= '0' && ch <= '9')
|| ch == '+' || ch == '/' || ch == '='))
{
return NGX_ERROR;
}
if (++len > NGX_HTTP_PRIORITY_MAX_BINARY) {
return NGX_ERROR;
}
}
return NGX_ERROR;
}
void
ngx_http_priority_state_update(ngx_http_priority_state_t *ps)
{
ngx_http_priority_t *e;
e = &ps->effective;
*e = ps->client;
if (ps->server.urgency_set) {
e->urgency = ps->server.urgency;
e->urgency_set = 1;
}
if (ps->server.incremental_set) {
e->incremental = ps->server.incremental;
e->incremental_set = 1;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_HTTP_PRIORITY_H_INCLUDED_
#define _NGX_HTTP_PRIORITY_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
typedef struct {
unsigned urgency:3; /* 0-7, default 3, lower is higher */
unsigned incremental:1;
unsigned urgency_set:1;
unsigned incremental_set:1;
} ngx_http_priority_t;
typedef struct {
ngx_http_priority_t client;
ngx_http_priority_t server;
ngx_http_priority_t effective;
} ngx_http_priority_state_t;
void ngx_http_priority_state_init(ngx_http_priority_state_t *ps);
ngx_int_t ngx_http_priority_parse(ngx_str_t *field, ngx_http_priority_t *p);
void ngx_http_priority_state_update(ngx_http_priority_state_t *ps);
#endif /* _NGX_HTTP_PRIORITY_H_INCLUDED_ */