From 82177e0eec0512e82f9b87cdcacc7a7b676f7091 Mon Sep 17 00:00:00 2001 From: Victor Julien Date: Tue, 20 Oct 2026 13:56:04 +0000 Subject: [PATCH] lua/http: read trailer fields from Lua Header functions in the Lua http library read the header tables and the raw header buffer. Once trailer fields moved out of those, a script could no longer see them at all, and nothing replaced the access: the only way to act on a trailer was a rule. Add the trailer twins over the tables and blocks the http.trailer buffers use: tx:request_trailer(), tx:request_trailers(), tx:request_trailers_raw() and the response versions. Name lookup is case insensitive and reports a miss as an error, like the header functions. Repeated fields need no merging here: the parser folds them while storing, so a name has one entry. HTTP/2 is out of reach of these functions: the Lua http library is driven by htp, so it has no HTTP/2 transaction to read. Ticket: #8256. --- doc/userguide/lua/libs/http.rst | 51 ++++++++++++-- doc/userguide/upgrade.rst | 13 ++-- src/util-lua-http.c | 116 ++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/doc/userguide/lua/libs/http.rst b/doc/userguide/lua/libs/http.rst index 169020f77a..d5a5a5945e 100644 --- a/doc/userguide/lua/libs/http.rst +++ b/doc/userguide/lua/libs/http.rst @@ -41,12 +41,8 @@ HTTP is transaction based, and the current transaction must be obtained before u print(err) end -All other functions are methods on the transaction table. - -.. note:: - - The header functions below cover the header section only. HTTP trailer fields are not - reachable from Lua; match them with the ``http.trailer`` buffers in a rule instead. +All other functions are methods on the transaction table. Header functions cover the +header section; trailer fields have the functions at the end of this page. Transaction Methods ~~~~~~~~~~~~~~~~~~~ @@ -177,6 +173,49 @@ Example:: http_response_headers = tx:response_headers() print(http_response_headers) +``request_trailer()`` +^^^^^^^^^^^^^^^^^^^^^ + +Get the value of an HTTP request trailer field by name, or nil and an error if the +transaction has no field of that name. Names are matched case insensitively. + +Example:: + + local tx = http.get_tx() + local value, err = tx:request_trailer("X-Trailed") + +``response_trailer()`` +^^^^^^^^^^^^^^^^^^^^^^ + +Get the value of an HTTP response trailer field by name. + +``request_trailers()`` +^^^^^^^^^^^^^^^^^^^^^^ + +Get all HTTP request trailer fields as a table. + +Example:: + + local tx = http.get_tx() + for name, value in pairs(tx:request_trailers()) do + print(name, value) + end + +``response_trailers()`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Get all HTTP response trailer fields as a table. + +``request_trailers_raw()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the raw HTTP request trailer block, as it appeared on the wire. + +``response_trailers_raw()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the raw HTTP response trailer block, as it appeared on the wire. + ``request_body()`` ^^^^^^^^^^^^^^^^^^ diff --git a/doc/userguide/upgrade.rst b/doc/userguide/upgrade.rst index 4d382ce05f..e8d6a6951b 100644 --- a/doc/userguide/upgrade.rst +++ b/doc/userguide/upgrade.rst @@ -65,11 +65,14 @@ Major Changes ``http.header_names`` or a field keyword such as ``http.user_agent`` stops matching and needs one of the trailer buffers. This also closes a hole where a trailer could extend a header value that detection and logging had already accepted. - For HTTP/1 they also drop out of the ``http.request_headers`` and - ``http.response_headers`` EVE arrays; no EVE key for trailer fields exists yet. - The Lua http library loses them the same way: ``tx:request_header()``, - ``tx:request_headers()``, ``tx:request_headers_raw()`` and the response twins read the - header tables and the raw header buffer, and that library has no trailer accessor. + EVE output still covers them: with ``dump-all-headers`` set they appear in the + ``http.request_headers`` and ``http.response_headers`` arrays as entries of their own, + instead of being appended to the value of a header field that shares their name. So a name + used in both blocks is listed twice, once per block. No EVE key for trailer fields exists + yet. + The Lua http library has accessors of its own for them now: ``tx:request_trailer()``, + ``tx:request_trailers()``, ``tx:request_trailers_raw()`` and the response twins, next to + the header functions that keep reading the header section only. - A HTTP/1 header block spanning several packets is no longer re-inspected while it is still arriving: a rule on ``http.header`` or ``http.header.raw`` now alerts once per transaction diff --git a/src/util-lua-http.c b/src/util-lua-http.c index f5599a4bb5..4b059f4b95 100644 --- a/src/util-lua-http.c +++ b/src/util-lua-http.c @@ -251,6 +251,116 @@ static int LuaHttpGetResponseHeaders(lua_State *luastate) return LuaHttpGetHeaders(luastate, 1); } +/* Trailers live in their own table now that they are kept out of the headers. Repeated + * fields are merged while parsing, so a name has a single entry. */ +static int LuaHttpGetTrailer(lua_State *luastate, int dir) +{ + struct LuaTx *tx = luaL_testudata(luastate, 1, htp_tx); + if (tx == NULL) { + lua_pushnil(luastate); + return 1; + } + + /* since arg was added at last, it must be on top of the stack */ + const char *name = LuaGetStringArgument(luastate, lua_gettop(luastate)); + if (name == NULL) { + return LuaCallbackError(luastate, "argument missing, empty or wrong type"); + } + + const htp_headers_t *table = + dir ? htp_tx_response_trailers(tx->tx) : htp_tx_request_trailers(tx->tx); + const size_t nlen = strlen(name); + const size_t count = table != NULL ? htp_headers_size(table) : 0; + for (size_t i = 0; i < count; i++) { + const htp_header_t *h = htp_headers_get_index(table, i); + if ((size_t)htp_header_name_len(h) == nlen && + strncasecmp((const char *)htp_header_name_ptr(h), name, nlen) == 0) { + return LuaPushStringBuffer(luastate, htp_header_value_ptr(h), htp_header_value_len(h)); + } + } + return LuaCallbackError(luastate, "trailer not found"); +} + +static int LuaHttpGetRequestTrailer(lua_State *luastate) +{ + return LuaHttpGetTrailer(luastate, 0 /* request */); +} + +static int LuaHttpGetResponseTrailer(lua_State *luastate) +{ + return LuaHttpGetTrailer(luastate, 1 /* response */); +} + +static int LuaHttpGetRawTrailers(lua_State *luastate, int dir) +{ + struct LuaTx *tx = luaL_testudata(luastate, 1, htp_tx); + if (tx == NULL) { + lua_pushnil(luastate); + return 1; + } + HtpTxUserData *htud = (HtpTxUserData *)htp_tx_get_user_data(tx->tx); + + uint8_t *raw = htud->request_trailers_raw; + uint32_t raw_len = htud->request_trailers_raw_len; + if (dir == 1) { + raw = htud->response_trailers_raw; + raw_len = htud->response_trailers_raw_len; + } + + if (raw == NULL || raw_len == 0) + return LuaCallbackError(luastate, "no raw trailers"); + + return LuaPushStringBuffer(luastate, raw, raw_len); +} + +static int LuaHttpGetRawRequestTrailers(lua_State *luastate) +{ + return LuaHttpGetRawTrailers(luastate, 0); +} + +static int LuaHttpGetRawResponseTrailers(lua_State *luastate) +{ + return LuaHttpGetRawTrailers(luastate, 1); +} + +static int LuaHttpGetTrailers(lua_State *luastate, int dir) +{ + struct LuaTx *tx = luaL_testudata(luastate, 1, htp_tx); + if (tx == NULL) { + lua_pushnil(luastate); + return 1; + } + + const htp_headers_t *table = + dir ? htp_tx_response_trailers(tx->tx) : htp_tx_request_trailers(tx->tx); + if (table == NULL) { + lua_pushnil(luastate); + return 1; + } + + lua_newtable(luastate); + const size_t count = htp_headers_size(table); + for (size_t i = 0; i < count; i++) { + const htp_header_t *h = htp_headers_get_index(table, i); + LuaPushStringBuffer(luastate, htp_header_name_ptr(h), htp_header_name_len(h)); + LuaPushStringBuffer(luastate, htp_header_value_ptr(h), htp_header_value_len(h)); + lua_settable(luastate, -3); + } + return 1; +} + +/** \brief return request trailers as lua table */ +static int LuaHttpGetRequestTrailers(lua_State *luastate) +{ + return LuaHttpGetTrailers(luastate, 0); +} + +/** \brief return response trailers as lua table */ +static int LuaHttpGetResponseTrailers(lua_State *luastate) +{ + return LuaHttpGetTrailers(luastate, 1); +} + static int LuaHttpGetBody(lua_State *luastate, int dir) { struct LuaTx *tx = luaL_testudata(luastate, 1, htp_tx); @@ -319,6 +429,12 @@ static const struct luaL_Reg txlib[] = { {"request_uri_normalized", LuaHttpGetRequestUriNormalized}, {"request_headers", LuaHttpGetRequestHeaders}, {"response_headers", LuaHttpGetResponseHeaders}, + {"request_trailer", LuaHttpGetRequestTrailer}, + {"response_trailer", LuaHttpGetResponseTrailer}, + {"request_trailers", LuaHttpGetRequestTrailers}, + {"response_trailers", LuaHttpGetResponseTrailers}, + {"request_trailers_raw", LuaHttpGetRawRequestTrailers}, + {"response_trailers_raw", LuaHttpGetRawResponseTrailers}, {"request_host", LuaHttpGetRequestHost}, {"request_body", LuaHttpGetRequestBody}, {"response_body", LuaHttpGetResponseBody},