]> git.kaiwu.me - nginx.git/commitdiff
Add RFC 9218 priority types and parser
authorAndrew Clayton <a.clayton@nginx.com>
Wed, 23 Sep 2026 20:09:36 +0000 (21:09 +0100)
committerAndrew Clayton <a.clayton@nginx.com>
Mon, 28 Sep 2026 21:01:20 +0000 (22:01 +0100)
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
auto/modules
auto/sources
src/http/ngx_http.h
src/http/ngx_http_priority.c [new file with mode: 0644]
src/http/ngx_http_priority.h [new file with mode: 0644]

index 3082c8fde838628ea6cb55a05563359c8afebfab..0f037b2788862c516ebeffb2efeb3ca750836351 100644 (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
index 00c8b05ad7ea98d3b2c51893144739105de77c11..537b582ff8357d7b05256476ef791fbc003a7268 100644 (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
index 1b0e369f3bcfdf207921949cfa176b2bc2ed2b38..fdbd1a208d330e596b66bc234bd57c642c9bba37 100644 (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)
diff --git a/src/http/ngx_http_priority.c b/src/http/ngx_http_priority.c
new file mode 100644 (file)
index 0000000..9121772
--- /dev/null
@@ -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;
+    }
+}
diff --git a/src/http/ngx_http_priority.h b/src/http/ngx_http_priority.h
new file mode 100644 (file)
index 0000000..f059b03
--- /dev/null
@@ -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_ */