You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2026/04/02 16:14:39 UTC
(trafficserver) branch master updated: hrw4u/header_rewrite: Add session-scope state variables (#12989)
This is an automated email from the ASF dual-hosted git repository.
zwoop pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new efeeffaffa hrw4u/header_rewrite: Add session-scope state variables (#12989)
efeeffaffa is described below
commit efeeffaffa661d0befcfaa26b564da6b0d9308b1
Author: Leif Hedstrom <zw...@apache.org>
AuthorDate: Thu Apr 2 10:14:27 2026 -0600
hrw4u/header_rewrite: Add session-scope state variables (#12989)
* hrw4u/header_rewrite: Add session-scope state variables
Add SESSION-FLAG, SESSION-INT8, and SESSION-INT16 conditions and
their corresponding set-session-flag, set-session-int8, and
set-session-int16 operators to the header_rewrite plugin. These
mirror the existing transaction-scoped state variables but persist
across keep-alive requests on the same connection, using a
TS_USER_ARGS_SSN slot. The condition and operator classes are
parameterized with a TSUserArgType scope argument to avoid code
duplication. The hrw4u transpiler adds a SESSION_VARS section for
declaring session-scoped variables, and the reverse transpiler
handles both scopes. Documentation and tests are included.
Co-Authored-By: Craig Taylor
* Address Copilot review comments
Use SESSION_VARS instead of VARS for session sandbox check.
Reserve user-arg slots lazily per scope in acquire_state_slot().
Fix _state_vars type annotation to include VarScope in key tuple.
* Address bneradt's review: wire SESSION_VARS into kg and LSP
kg_visitor.py was missing sessionVarSection dispatch in visitSection,
causing hrw4u-kg to silently drop SESSION_VARS blocks. lsp/strings.py
only detected VARS { ... } for declaration mode, leaving session-scoped
variables without hover/type metadata in hrw4u-lsp.
---
doc/admin-guide/configuration/hrw4u.en.rst | 18 +++++-
doc/admin-guide/plugins/header_rewrite.en.rst | 71 ++++++++++++++++++++
plugins/header_rewrite/conditions.cc | 26 ++++----
plugins/header_rewrite/conditions.h | 58 +++++++++++------
plugins/header_rewrite/factory.cc | 12 ++++
plugins/header_rewrite/operators.cc | 44 ++++++-------
plugins/header_rewrite/operators.h | 45 +++++++++----
plugins/header_rewrite/statement.cc | 43 +++++++++----
plugins/header_rewrite/statement.h | 47 ++++++++++++++
.../header_rewrite_bundle.replay.yaml | 63 ++++++++++++++++++
.../header_rewrite/rules/rule_session_vars.conf | 26 ++++++++
tools/hrw4u/grammar/hrw4u.g4 | 6 ++
tools/hrw4u/src/hrw_symbols.py | 75 +++++++++++++---------
tools/hrw4u/src/hrw_visitor.py | 14 ++--
tools/hrw4u/src/kg_visitor.py | 21 ++++++
tools/hrw4u/src/lsp/strings.py | 5 +-
tools/hrw4u/src/symbols.py | 11 ++--
tools/hrw4u/src/types.py | 37 ++++++++---
tools/hrw4u/src/visitor.py | 23 ++++++-
tools/hrw4u/tests/data/vars/session_assign.ast.txt | 1 +
.../hrw4u/tests/data/vars/session_assign.input.txt | 15 +++++
.../tests/data/vars/session_assign.output.txt | 4 ++
tools/hrw4u/tests/data/vars/session_bool.ast.txt | 1 +
tools/hrw4u/tests/data/vars/session_bool.input.txt | 9 +++
.../hrw4u/tests/data/vars/session_bool.output.txt | 3 +
tools/hrw4u/tests/data/vars/session_int16.ast.txt | 1 +
.../hrw4u/tests/data/vars/session_int16.input.txt | 9 +++
.../hrw4u/tests/data/vars/session_int16.output.txt | 3 +
tools/hrw4u/tests/data/vars/session_int8.ast.txt | 1 +
tools/hrw4u/tests/data/vars/session_int8.input.txt | 9 +++
.../hrw4u/tests/data/vars/session_int8.output.txt | 3 +
tools/hrw4u/tests/test_coverage.py | 8 +--
32 files changed, 580 insertions(+), 132 deletions(-)
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst
index 9e5c8fb39f..4a0fb3f696 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -405,7 +405,9 @@ TXN_CLOSE_HOOK TXN_CLOSE End of transaction
=============================== ======================== ================================
A special section `VARS` is used to declare variables. There is no equivalent in
-`header_rewrite`, where you managed the variables manually.
+`header_rewrite`, where you managed the variables manually. Similarly,
+`SESSION_VARS` declares session-scoped variables that persist across all
+transactions on the same client connection (session).
Variables and State Slots
^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -416,6 +418,10 @@ Each variable type has a limited number of slots available:
- ``int8`` - 4 slots (0-3)
- ``int16`` - 1 slot (0)
+These limits apply independently to both ``VARS`` (transaction-scoped) and
+``SESSION_VARS`` (session-scoped) sections. For example, you can have 16 bool
+transaction variables *and* 16 bool session variables.
+
By default, slots are assigned automatically in declaration order. You can explicitly assign
a slot number using the ``@`` syntax::
@@ -426,6 +432,16 @@ a slot number using the ``@`` syntax::
counter: int8 @2; # Explicitly use int8 slot 2
}
+ SESSION_VARS {
+ is_suspicious: bool; # Session-scoped, persists across requests
+ penalty_level: int8; # Session-scoped 8-bit integer
+ }
+
+Transaction variables (``VARS``) are reset for each new HTTP transaction, while
+session variables (``SESSION_VARS``) persist for the lifetime of the client
+connection. Session variables are useful for tracking state across keep-alive
+requests, such as marking a connection as suspicious after the first bad request.
+
Explicit slot assignment is useful when you need predictable slot numbers across configurations
or when integrating with existing header_rewrite rules that reference specific slot numbers. In
addition, a remap configuration can use ``@PPARAM`` to set one of these slot variables explicitly
diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst
index 58ac015f79..2d57f64f97 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -862,6 +862,41 @@ There's only one such integer, and its value is returned from this condition.
As such, the index, ``0``, is optional here. The initialized value of this
state variable is ``0``.
+SESSION-FLAG
+~~~~~~~~~~~~
+::
+
+ cond %{SESSION-FLAG:<n>}
+
+This condition allows you to check the state of a session-scoped flag. The
+``<n>`` is the number of the flag, from 0 to 15. Unlike ``STATE-FLAG`` which
+is scoped to the current transaction, session flags persist across all
+transactions on the same client connection (session). The default value of all
+flags are ``false``.
+
+SESSION-INT8
+~~~~~~~~~~~~
+::
+
+ cond %{SESSION-INT8:<n>}
+
+This condition allows you to check the state of a session-scoped 8-bit unsigned
+integer. The ``<n>`` is the number of the integer, from 0 to 3. The current
+value is returned, and all 4 integers are initialized to 0. Session integers
+persist across all transactions on the same client connection.
+
+SESSION-INT16
+~~~~~~~~~~~~~
+::
+
+ cond %{SESSION-INT16:<0>}
+
+This condition allows you to check the state of a session-scoped 16-bit unsigned
+integer. There's only one such integer, and its value is returned from this
+condition. As such, the index, ``0``, is optional here. The initialized value
+is ``0``. Session integers persist across all transactions on the same client
+connection.
+
STATUS
~~~~~~
::
@@ -1304,6 +1339,42 @@ The ``<value>`` is an unsigned 16-bit integer as well, 0-65535. It can also
be a condition, in which case thevalue of the condition is used. The index,
0, is always required eventhough there is only one 16-bit integer state variable.
+set-session-flag
+~~~~~~~~~~~~~~~~
+::
+
+ set-session-flag <n> <value>
+
+This operator allows you to set the state of a session-scoped flag. The ``<n>``
+is the number of the flag, from 0 to 15. The ``<value>`` is either ``true`` or
+``false``, turning the flag on or off. Unlike ``set-state-flag``, session flags
+persist across all transactions on the same client connection.
+
+set-session-int8
+~~~~~~~~~~~~~~~~
+::
+
+ set-session-int8 <n> <value>
+
+This operator allows you to set the state of a session-scoped 8-bit unsigned
+integer. The ``<n>`` is the number of the integer, from 0 to 3. The ``<value>``
+is an unsigned 8-bit integer, 0-255. It can also be a condition, in which case
+the value of the condition is used. Session integers persist across all
+transactions on the same client connection.
+
+set-session-int16
+~~~~~~~~~~~~~~~~~
+::
+
+ set-session-int16 0 <value>
+
+This operator allows you to set the state of a session-scoped 16-bit unsigned
+integer. The ``<value>`` is an unsigned 16-bit integer, 0-65535. It can also
+be a condition, in which case the value of the condition is used. The index,
+0, is always required even though there is only one 16-bit session integer
+state variable. Session integers persist across all transactions on the same
+client connection.
+
set-status
~~~~~~~~~~
::
diff --git a/plugins/header_rewrite/conditions.cc b/plugins/header_rewrite/conditions.cc
index faf4a19a52..fdf19120bd 100644
--- a/plugins/header_rewrite/conditions.cc
+++ b/plugins/header_rewrite/conditions.cc
@@ -1654,9 +1654,9 @@ ConditionStateFlag::set_qualifier(const std::string &q)
_flag_ix = strtol(q.c_str(), nullptr, 10);
if (_flag_ix < 0 || _flag_ix >= NUM_STATE_FLAGS) {
- TSError("[%s] STATE-FLAG index out of range: %s", PLUGIN_NAME, q.c_str());
+ TSError("[%s] %s-FLAG index out of range: %s", PLUGIN_NAME, _scope_label(_scope), q.c_str());
} else {
- Dbg(pi_dbg_ctl, "\tParsing %%{STATE-FLAG:%s}", q.c_str());
+ Dbg(pi_dbg_ctl, "\tParsing %%{%s-FLAG:%s}", _scope_label(_scope), q.c_str());
_mask = 1ULL << _flag_ix;
}
}
@@ -1665,15 +1665,15 @@ void
ConditionStateFlag::append_value(std::string &s, const Resources &res)
{
s += eval(res) ? "TRUE" : "FALSE";
- Dbg(pi_dbg_ctl, "Evaluating STATE-FLAG(%d)", _flag_ix);
+ Dbg(pi_dbg_ctl, "Evaluating %s-FLAG(%d)", _scope_label(_scope), _flag_ix);
}
bool
ConditionStateFlag::eval(const Resources &res)
{
- auto data = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto data = _get_state_data(_scope, res);
- Dbg(pi_dbg_ctl, "Evaluating STATE-FLAG()");
+ Dbg(pi_dbg_ctl, "Evaluating %s-FLAG()", _scope_label(_scope));
return (data & _mask) == _mask;
}
@@ -1696,9 +1696,9 @@ ConditionStateInt8::set_qualifier(const std::string &q)
_byte_ix = strtol(q.c_str(), nullptr, 10);
if (_byte_ix < 0 || _byte_ix >= NUM_STATE_INT8S) {
- TSError("[%s] STATE-INT8 index out of range: %s", PLUGIN_NAME, q.c_str());
+ TSError("[%s] %s-INT8 index out of range: %s", PLUGIN_NAME, _scope_label(_scope), q.c_str());
} else {
- Dbg(pi_dbg_ctl, "\tParsing %%{STATE-INT8:%s}", q.c_str());
+ Dbg(pi_dbg_ctl, "\tParsing %%{%s-INT8:%s}", _scope_label(_scope), q.c_str());
}
}
@@ -1709,7 +1709,7 @@ ConditionStateInt8::append_value(std::string &s, const Resources &res)
s += std::to_string(data);
- Dbg(pi_dbg_ctl, "Appending STATE-INT8(%d) to evaluation value -> %s", data, s.c_str());
+ Dbg(pi_dbg_ctl, "Appending %s-INT8(%d) to evaluation value -> %s", _scope_label(_scope), data, s.c_str());
}
bool
@@ -1717,7 +1717,7 @@ ConditionStateInt8::eval(const Resources &res)
{
uint8_t data = _get_data(res);
- Dbg(pi_dbg_ctl, "Evaluating STATE-INT8()");
+ Dbg(pi_dbg_ctl, "Evaluating %s-INT8()", _scope_label(_scope));
return static_cast<const MatcherType *>(_matcher.get())->test(data, res);
}
@@ -1742,9 +1742,9 @@ ConditionStateInt16::set_qualifier(const std::string &q)
long ix = strtol(q.c_str(), nullptr, 10);
if (ix != 0) {
- TSError("[%s] STATE-INT16 index out of range: %s", PLUGIN_NAME, q.c_str());
+ TSError("[%s] %s-INT16 index out of range: %s", PLUGIN_NAME, _scope_label(_scope), q.c_str());
} else {
- Dbg(pi_dbg_ctl, "\tParsing %%{STATE-INT16:%s}", q.c_str());
+ Dbg(pi_dbg_ctl, "\tParsing %%{%s-INT16:%s}", _scope_label(_scope), q.c_str());
}
}
}
@@ -1755,7 +1755,7 @@ ConditionStateInt16::append_value(std::string &s, const Resources &res)
uint16_t data = _get_data(res);
s += std::to_string(data);
- Dbg(pi_dbg_ctl, "Appending STATE-INT16(%d) to evaluation value -> %s", data, s.c_str());
+ Dbg(pi_dbg_ctl, "Appending %s-INT16(%d) to evaluation value -> %s", _scope_label(_scope), data, s.c_str());
}
bool
@@ -1763,7 +1763,7 @@ ConditionStateInt16::eval(const Resources &res)
{
uint16_t data = _get_data(res);
- Dbg(pi_dbg_ctl, "Evaluating STATE-INT8()");
+ Dbg(pi_dbg_ctl, "Evaluating %s-INT16()", _scope_label(_scope));
return static_cast<const MatcherType *>(_matcher.get())->test(data, res);
}
diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h
index 4393ecbab0..517fa4df71 100644
--- a/plugins/header_rewrite/conditions.h
+++ b/plugins/header_rewrite/conditions.h
@@ -800,17 +800,17 @@ private:
bool _end = false;
};
-// State Flags
+// State/Session Flags (parameterized by scope)
class ConditionStateFlag : public Condition
{
using SelfType = ConditionStateFlag;
// No matcher for this, it's all easy peasy
public:
- explicit ConditionStateFlag()
+ explicit ConditionStateFlag(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
- Dbg(dbg_ctl, "Calling CTOR for ConditionStateFlag");
+ Dbg(dbg_ctl, "Calling CTOR for ConditionStateFlag (scope=%s)", _scope_label(_scope));
}
// noncopyable
@@ -826,15 +826,22 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- int _flag_ix = -1;
- uint64_t _mask = 0;
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
+ int _flag_ix = -1;
+ uint64_t _mask = 0;
};
-// INT8 state variables
+// State/Session INT8 variables (parameterized by scope)
class ConditionStateInt8 : public Condition
{
using DataType = uint8_t;
@@ -842,10 +849,10 @@ class ConditionStateInt8 : public Condition
using SelfType = ConditionStateInt8;
public:
- explicit ConditionStateInt8()
+ explicit ConditionStateInt8(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
- Dbg(dbg_ctl, "Calling CTOR for ConditionStateInt8");
+ Dbg(dbg_ctl, "Calling CTOR for ConditionStateInt8 (scope=%s)", _scope_label(_scope));
}
// noncopyable
@@ -862,25 +869,31 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- // Little helper function to extract out the data from the TXN user pointer
uint8_t
_get_data(const Resources &res) const
{
TSAssert(_byte_ix >= 0 && _byte_ix < NUM_STATE_INT8S);
- auto ptr = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto ptr = _get_state_data(_scope, res);
uint8_t data = (ptr & STATE_INT8_MASKS[_byte_ix]) >> (NUM_STATE_FLAGS + _byte_ix * 8);
return data;
}
- int _byte_ix = -1;
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
+ int _byte_ix = -1;
};
-// INT16 state variables
+// State/Session INT16 variables (parameterized by scope)
class ConditionStateInt16 : public Condition
{
using DataType = uint16_t;
@@ -888,10 +901,10 @@ class ConditionStateInt16 : public Condition
using SelfType = ConditionStateInt16;
public:
- explicit ConditionStateInt16()
+ explicit ConditionStateInt16(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
- Dbg(dbg_ctl, "Calling CTOR for ConditionStateInt16");
+ Dbg(dbg_ctl, "Calling CTOR for ConditionStateInt16 (scope=%s)", _scope_label(_scope));
}
// noncopyable
@@ -908,18 +921,25 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- // Little helper function to extract out the data from the TXN user pointer
uint16_t
_get_data(const Resources &res) const
{
- auto ptr = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto ptr = _get_state_data(_scope, res);
return ((ptr & STATE_INT16_MASK) >> 48);
}
+
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
};
// Last regex capture
diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc
index ffc999f3e0..530cf4cfe5 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -87,6 +87,12 @@ operator_factory(const std::string &op)
o = new OperatorSetStateInt8();
} else if (op == "set-state-int16") {
o = new OperatorSetStateInt16();
+ } else if (op == "set-session-flag") {
+ o = new OperatorSetStateFlag(TS_USER_ARGS_SSN);
+ } else if (op == "set-session-int8") {
+ o = new OperatorSetStateInt8(TS_USER_ARGS_SSN);
+ } else if (op == "set-session-int16") {
+ o = new OperatorSetStateInt16(TS_USER_ARGS_SSN);
} else if (op == "set-effective-address") {
o = new OperatorSetEffectiveAddress();
} else if (op == "set-next-hop-strategy") {
@@ -191,6 +197,12 @@ condition_factory(const std::string &cond)
c = new ConditionStateInt8();
} else if (c_name == "STATE-INT16") {
c = new ConditionStateInt16();
+ } else if (c_name == "SESSION-FLAG") {
+ c = new ConditionStateFlag(TS_USER_ARGS_SSN);
+ } else if (c_name == "SESSION-INT8") {
+ c = new ConditionStateInt8(TS_USER_ARGS_SSN);
+ } else if (c_name == "SESSION-INT16") {
+ c = new ConditionStateInt16(TS_USER_ARGS_SSN);
} else if (c_name == "LAST-CAPTURE") {
c = new ConditionLastCapture();
} else {
diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc
index 76c4f6197b..20207bd24b 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -1409,7 +1409,7 @@ OperatorSetStateFlag::initialize(Parser &p)
_flag_ix = strtol(p.get_arg().c_str(), nullptr, 10);
if (_flag_ix < 0 || _flag_ix >= NUM_STATE_FLAGS) {
- TSError("[%s] state flag with index %d is out of range", PLUGIN_NAME, _flag_ix);
+ TSError("[%s] %s flag with index %d is out of range", PLUGIN_NAME, _scope_label(_scope), _flag_ix);
return;
}
@@ -1443,16 +1443,16 @@ OperatorSetStateFlag::initialize_hooks()
bool
OperatorSetStateFlag::exec(const Resources &res) const
{
- if (!res.state.txnp) {
- TSError("[%s] OperatorSetStateFlag() failed. Transaction is null", PLUGIN_NAME);
+ if (!_check_state_handle(_scope, res)) {
+ TSError("[%s] OperatorSetStateFlag() failed. %s handle is null", PLUGIN_NAME, _scope_label(_scope));
return false;
}
- Dbg(pi_dbg_ctl, " Setting state flag %d to %d", _flag_ix, _flag);
+ Dbg(pi_dbg_ctl, " Setting %s flag %d to %d", _scope_label(_scope), _flag_ix, _flag);
- auto data = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto data = _get_state_data(_scope, res);
- TSUserArgSet(res.state.txnp, _txn_slot, reinterpret_cast<void *>(_flag ? data | _mask : data & _mask));
+ _set_state_data(_scope, res, _flag ? data | _mask : data & _mask);
return true;
}
@@ -1465,7 +1465,7 @@ OperatorSetStateInt8::initialize(Parser &p)
_byte_ix = strtol(p.get_arg().c_str(), nullptr, 10);
if (_byte_ix < 0 || _byte_ix >= NUM_STATE_INT8S) {
- TSError("[%s] state int8 with index %d is out of range", PLUGIN_NAME, _byte_ix);
+ TSError("[%s] %s int8 with index %d is out of range", PLUGIN_NAME, _scope_label(_scope), _byte_ix);
return;
}
@@ -1474,7 +1474,7 @@ OperatorSetStateInt8::initialize(Parser &p)
int v = _value.get_int_value();
if (v < 0 || v > 255) {
- TSError("[%s] state int8 value %d is out of range", PLUGIN_NAME, v);
+ TSError("[%s] %s int8 value %d is out of range", PLUGIN_NAME, _scope_label(_scope), v);
return;
}
}
@@ -1497,12 +1497,12 @@ OperatorSetStateInt8::initialize_hooks()
bool
OperatorSetStateInt8::exec(const Resources &res) const
{
- if (!res.state.txnp) {
- TSError("[%s] OperatorSetStateInt8() failed. Transaction is null", PLUGIN_NAME);
+ if (!_check_state_handle(_scope, res)) {
+ TSError("[%s] OperatorSetStateInt8() failed. %s handle is null", PLUGIN_NAME, _scope_label(_scope));
return false;
}
- auto ptr = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto ptr = _get_state_data(_scope, res);
int val = 0;
if (_value.has_conds()) { // If there are conditions, we need to evaluate them, which gives us a string
@@ -1511,7 +1511,7 @@ OperatorSetStateInt8::exec(const Resources &res) const
_value.append_value(v, res);
val = strtol(v.c_str(), nullptr, 10);
if (val < 0 || val > 255) {
- TSWarning("[%s] state int8 value %d is out of range", PLUGIN_NAME, val);
+ TSWarning("[%s] %s int8 value %d is out of range", PLUGIN_NAME, _scope_label(_scope), val);
return false;
}
} else {
@@ -1519,10 +1519,10 @@ OperatorSetStateInt8::exec(const Resources &res) const
val = _value.get_int_value();
}
- Dbg(pi_dbg_ctl, " Setting state int8 %d to %d", _byte_ix, val);
+ Dbg(pi_dbg_ctl, " Setting %s int8 %d to %d", _scope_label(_scope), _byte_ix, val);
ptr &= ~STATE_INT8_MASKS[_byte_ix]; // Clear any old value
ptr |= (static_cast<uint64_t>(val) << (NUM_STATE_FLAGS + _byte_ix * 8));
- TSUserArgSet(res.state.txnp, _txn_slot, reinterpret_cast<void *>(ptr));
+ _set_state_data(_scope, res, ptr);
return true;
}
@@ -1535,7 +1535,7 @@ OperatorSetStateInt16::initialize(Parser &p)
int ix = strtol(p.get_arg().c_str(), nullptr, 10);
if (ix != 0) {
- TSError("[%s] state int16 with index %d is out of range", PLUGIN_NAME, ix);
+ TSError("[%s] %s int16 with index %d is out of range", PLUGIN_NAME, _scope_label(_scope), ix);
return;
}
@@ -1544,7 +1544,7 @@ OperatorSetStateInt16::initialize(Parser &p)
int v = _value.get_int_value();
if (v < 0 || v > 65535) {
- TSError("[%s] state int16 value %d is out of range", PLUGIN_NAME, v);
+ TSError("[%s] %s int16 value %d is out of range", PLUGIN_NAME, _scope_label(_scope), v);
return;
}
}
@@ -1567,12 +1567,12 @@ OperatorSetStateInt16::initialize_hooks()
bool
OperatorSetStateInt16::exec(const Resources &res) const
{
- if (!res.state.txnp) {
- TSError("[%s] OperatorSetStateInt16() failed. Transaction is null", PLUGIN_NAME);
+ if (!_check_state_handle(_scope, res)) {
+ TSError("[%s] OperatorSetStateInt16() failed. %s handle is null", PLUGIN_NAME, _scope_label(_scope));
return false;
}
- auto ptr = reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ auto ptr = _get_state_data(_scope, res);
int val = 0;
if (_value.has_conds()) { // If there are conditions, we need to evaluate them, which gives us a string
@@ -1581,7 +1581,7 @@ OperatorSetStateInt16::exec(const Resources &res) const
_value.append_value(v, res);
val = strtol(v.c_str(), nullptr, 10);
if (val < 0 || val > 65535) {
- TSWarning("[%s] state int8 value %d is out of range", PLUGIN_NAME, val);
+ TSWarning("[%s] %s int16 value %d is out of range", PLUGIN_NAME, _scope_label(_scope), val);
return false;
}
} else {
@@ -1589,10 +1589,10 @@ OperatorSetStateInt16::exec(const Resources &res) const
val = _value.get_int_value();
}
- Dbg(pi_dbg_ctl, " Setting state int16 to %d", val);
+ Dbg(pi_dbg_ctl, " Setting %s int16 to %d", _scope_label(_scope), val);
ptr &= ~STATE_INT16_MASK; // Clear any old value
ptr |= (static_cast<uint64_t>(val) << 48);
- TSUserArgSet(res.state.txnp, _txn_slot, reinterpret_cast<void *>(ptr));
+ _set_state_data(_scope, res, ptr);
return true;
}
diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h
index 2b48f813db..d20c08f405 100644
--- a/plugins/header_rewrite/operators.h
+++ b/plugins/header_rewrite/operators.h
@@ -547,7 +547,7 @@ private:
class OperatorSetStateFlag : public Operator
{
public:
- OperatorSetStateFlag()
+ explicit OperatorSetStateFlag(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
Dbg(dbg_ctl, "Calling CTOR for OperatorSetStateFlag");
@@ -566,19 +566,26 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- int _flag_ix = -1;
- int _flag = false;
- uint64_t _mask = 0;
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
+ int _flag_ix = -1;
+ int _flag = false;
+ uint64_t _mask = 0;
};
class OperatorSetStateInt8 : public Operator
{
public:
- OperatorSetStateInt8()
+ explicit OperatorSetStateInt8(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
Dbg(dbg_ctl, "Calling CTOR for OperatorSetStateInt8");
@@ -597,18 +604,25 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- int _byte_ix = -1;
- Value _value;
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
+ int _byte_ix = -1;
+ Value _value;
};
class OperatorSetStateInt16 : public Operator
{
public:
- OperatorSetStateInt16()
+ explicit OperatorSetStateInt16(TSUserArgType scope = TS_USER_ARGS_TXN) : _scope(scope)
{
static_assert(sizeof(void *) == 8, "State Variables requires a 64-bit system.");
Dbg(dbg_ctl, "Calling CTOR for OperatorSetStateInt16");
@@ -627,11 +641,18 @@ protected:
bool
need_txn_slot() const override
{
- return true;
+ return _scope == TS_USER_ARGS_TXN;
+ }
+
+ bool
+ need_ssn_slot() const override
+ {
+ return _scope == TS_USER_ARGS_SSN;
}
private:
- Value _value;
+ TSUserArgType _scope = TS_USER_ARGS_TXN;
+ Value _value;
};
class OperatorSetEffectiveAddress : public Operator
diff --git a/plugins/header_rewrite/statement.cc b/plugins/header_rewrite/statement.cc
index 57eab9d62f..6e2607da63 100644
--- a/plugins/header_rewrite/statement.cc
+++ b/plugins/header_rewrite/statement.cc
@@ -74,20 +74,41 @@ Statement::initialize_hooks()
}
int
-Statement::acquire_txn_slot()
+Statement::acquire_state_slot(TSUserArgType type)
{
- // Only call the index reservation once per plugin load
- static int txn_slot_index = []() -> int {
- int index = -1;
+ if (type == TS_USER_ARGS_SSN) {
+ static int ssn_slot_index = []() -> int {
+ int index = -1;
+ if (TS_ERROR == TSUserArgIndexReserve(TS_USER_ARGS_SSN, PLUGIN_NAME, "HRW ssn variables", &index)) {
+ TSError("[%s] failed to reserve ssn user arg index", PLUGIN_NAME);
+ }
+ return index;
+ }();
+
+ return ssn_slot_index;
+ } else {
+ static int txn_slot_index = []() -> int {
+ int index = -1;
+ if (TS_ERROR == TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_NAME, "HRW txn variables", &index)) {
+ TSError("[%s] failed to reserve txn user arg index", PLUGIN_NAME);
+ }
+ return index;
+ }();
+
+ return txn_slot_index;
+ }
+}
- if (TS_ERROR == TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_NAME, "HRW txn variables", &index)) {
- TSError("[%s] failed to reserve user arg index", PLUGIN_NAME);
- return -1; // Fallback value
- }
- return index;
- }();
+int
+Statement::acquire_txn_slot()
+{
+ return acquire_state_slot(TS_USER_ARGS_TXN);
+}
- return txn_slot_index;
+int
+Statement::acquire_ssn_slot()
+{
+ return acquire_state_slot(TS_USER_ARGS_SSN);
}
int
diff --git a/plugins/header_rewrite/statement.h b/plugins/header_rewrite/statement.h
index aac03d878a..2557d4482c 100644
--- a/plugins/header_rewrite/statement.h
+++ b/plugins/header_rewrite/statement.h
@@ -173,6 +173,9 @@ public:
if (need_txn_slot()) {
_txn_slot = acquire_txn_slot();
}
+ if (need_ssn_slot()) {
+ _ssn_slot = acquire_ssn_slot();
+ }
if (need_txn_private_slot()) {
_txn_private_slot = acquire_txn_private_slot();
}
@@ -194,6 +197,8 @@ public:
static int acquire_txn_slot();
static int acquire_txn_private_slot();
+ static int acquire_ssn_slot();
+ static int acquire_state_slot(TSUserArgType type);
protected:
virtual void initialize_hooks();
@@ -214,9 +219,51 @@ protected:
return false;
}
+ virtual bool
+ need_ssn_slot() const
+ {
+ return false;
+ }
+
+ // Scope-aware helpers for state variable access
+ uint64_t
+ _get_state_data(TSUserArgType scope, const Resources &res) const
+ {
+ if (scope == TS_USER_ARGS_SSN) {
+ return reinterpret_cast<uint64_t>(TSUserArgGet(res.state.ssnp, _ssn_slot));
+ }
+ return reinterpret_cast<uint64_t>(TSUserArgGet(res.state.txnp, _txn_slot));
+ }
+
+ void
+ _set_state_data(TSUserArgType scope, const Resources &res, uint64_t data) const
+ {
+ if (scope == TS_USER_ARGS_SSN) {
+ TSUserArgSet(res.state.ssnp, _ssn_slot, reinterpret_cast<void *>(data));
+ } else {
+ TSUserArgSet(res.state.txnp, _txn_slot, reinterpret_cast<void *>(data));
+ }
+ }
+
+ bool
+ _check_state_handle(TSUserArgType scope, const Resources &res) const
+ {
+ if (scope == TS_USER_ARGS_SSN) {
+ return res.state.ssnp != nullptr;
+ }
+ return res.state.txnp != nullptr;
+ }
+
+ static const char *
+ _scope_label(TSUserArgType scope)
+ {
+ return (scope == TS_USER_ARGS_SSN) ? "SESSION" : "STATE";
+ }
+
Statement *_next = nullptr; // Linked list
int _txn_slot = -1;
int _txn_private_slot = -1;
+ int _ssn_slot = -1;
private:
ResourceIDs _rsrc = RSRC_NONE;
diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
index 81245d5b0d..b3c057d07a 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
+++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
@@ -169,6 +169,13 @@ autest:
args:
- "rules/rule_server_conditions.conf"
+ - from: "http://www.example.com/from_17/"
+ to: "http://backend.ex:{SERVER_HTTP_PORT}/to_17/"
+ plugins:
+ - name: "header_rewrite.so"
+ args:
+ - "rules/rule_session_vars.conf"
+
log_validation:
traffic_out:
excludes:
@@ -1817,3 +1824,59 @@ sessions:
- [ X-Marker-Found, { value: "Yes", as: equal } ]
- [ X-Server-Host-Header, { value: "backend.ex", as: contains } ]
- [ X-Path-Match, { value: "Yes", as: equal } ]
+
+# Test 64: SESSION-FLAG persists across keep-alive transactions.
+# The first request sees the flag unset; set-session-flag marks it true.
+# The second request on the same keep-alive session sees the flag set.
+- transactions:
+ # First transaction - flag is unset, header absent; flag is then set.
+ - client-request:
+ method: "GET"
+ version: "1.1"
+ url: /from_17/
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Connection, keep-alive ]
+ - [ Content-Length, "0" ]
+ - [ uuid, 65 ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Server, microserver ]
+ - [ Content-Length, "0" ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Session-Seen, { as: absent } ]
+
+ # Second transaction - flag set by previous request; header must appear.
+ - client-request:
+ method: "GET"
+ version: "1.1"
+ url: /from_17/
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Connection, keep-alive ]
+ - [ Content-Length, "0" ]
+ - [ uuid, 66 ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Server, microserver ]
+ - [ Content-Length, "0" ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Session-Seen, { value: "yes", as: equal } ]
diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf
new file mode 100644
index 0000000000..d4b1376195
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Test SESSION-FLAG: on the first request the flag is unset so the header is
+# absent; set-session-flag then marks it true. On the second keep-alive
+# request the flag is still true, so X-Session-Seen is added.
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{SESSION-FLAG:0} =TRUE
+ set-header X-Session-Seen "yes"
+
+cond %{SEND_RESPONSE_HDR_HOOK}
+ set-session-flag 0 true
diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4
index 48fe316c5c..1527778716 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -21,6 +21,7 @@ grammar hrw4u;
// Lexer Rules
// -----------------------------
VARS : 'VARS';
+SESSION_VARS : 'SESSION_VARS';
IF : 'if';
ELIF : 'elif';
ELSE : 'else';
@@ -131,6 +132,7 @@ paramRef
section
: varSection
+ | sessionVarSection
| name=IDENT LBRACE sectionBody+ RBRACE
;
@@ -138,6 +140,10 @@ varSection
: VARS LBRACE variables RBRACE
;
+sessionVarSection
+ : SESSION_VARS LBRACE variables RBRACE
+ ;
+
sectionBody
: statement
| conditional
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index 2eaa9608cc..aa7ef58d4d 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -34,7 +34,7 @@ class InverseSymbolResolver(SymbolResolverBase):
def __init__(self, dbg: Dbg | None = None) -> None:
super().__init__(debug=False, dbg=dbg)
- self._state_vars: dict[tuple[types.VarType, int], str] = {}
+ self._state_vars: dict[tuple[types.VarType, int, types.VarScope], str] = {}
@cached_property
def _rev_conditions_exact(self) -> dict[str, str]:
@@ -99,11 +99,12 @@ class InverseSymbolResolver(SymbolResolverBase):
return f"{context_info}{payload}", False
return None
- def _get_or_create_var_name(self, var_type: types.VarType, index: int) -> str:
- key = (var_type, index)
+ def _get_or_create_var_name(self, var_type: types.VarType, index: int, scope: types.VarScope = types.VarScope.TXN) -> str:
+ key = (var_type, index, scope)
if key not in self._state_vars:
type_name = var_type.name.lower()
- self._state_vars[key] = f"{type_name}_{index}"
+ prefix = "ssn_" if scope == types.VarScope.SESSION else ""
+ self._state_vars[key] = f"{prefix}{type_name}_{index}"
return self._state_vars[key]
def _resolve_from_context_map(self, context_map: dict, section: SectionType | None, default: str) -> str:
@@ -152,15 +153,16 @@ class InverseSymbolResolver(SymbolResolverBase):
return None
- def _handle_state_tag(self, tag: str, payload: str | None) -> tuple[str, bool]:
- state_type = tag[6:]
+ def _handle_state_tag(self, tag: str, payload: str | None, scope: types.VarScope = types.VarScope.TXN) -> tuple[str, bool]:
+ prefix_len = len(scope.cond_prefix) + 1 # "STATE-" or "SESSION-"
+ state_type = tag[prefix_len:]
if payload is None:
raise SymbolResolutionError(f"%{{{tag}}}", f"Missing index for {tag}")
try:
index = int(payload)
for var_type in types.VarType:
if var_type.cond_tag == state_type:
- return self._get_or_create_var_name(var_type, index), False
+ return self._get_or_create_var_name(var_type, index, scope), False
raise SymbolResolutionError(f"%{{{tag}}}", f"Unknown state type: {state_type}")
except ValueError:
raise SymbolResolutionError(f"%{{{tag}}}", f"Invalid index for {tag}: {payload}")
@@ -363,12 +365,22 @@ class InverseSymbolResolver(SymbolResolverBase):
return '{' + content + '}'
return iprange_text
- def get_var_declarations(self) -> list[str]:
- """Get variable declarations in hrw4u format."""
- declarations = []
- for (var_type, _), var_name in sorted(self._state_vars.items(), key=lambda x: (x[0][0].name, x[0][1])):
- declarations.append(f"{var_name}: {var_type.name.lower()};")
- return declarations
+ def get_var_declarations(self) -> tuple[list[str], list[str]]:
+ """Get variable declarations in hrw4u format, separated by scope.
+
+ Returns a tuple of (txn_declarations, session_declarations).
+ """
+ txn_decls = []
+ ssn_decls = []
+
+ for (var_type, _, scope), var_name in sorted(self._state_vars.items(), key=lambda x: (x[0][2].name, x[0][0].name, x[0][1])):
+ decl = f"{var_name}: {var_type.name.lower()};"
+ if scope == types.VarScope.SESSION:
+ ssn_decls.append(decl)
+ else:
+ txn_decls.append(decl)
+
+ return txn_decls, ssn_decls
def negate_expression(self, term: str) -> str:
"""Negate a logical expression appropriately."""
@@ -419,7 +431,10 @@ class InverseSymbolResolver(SymbolResolverBase):
return percent, False
if tag.startswith("STATE-"):
- return self._handle_state_tag(tag, payload)
+ return self._handle_state_tag(tag, payload, types.VarScope.TXN)
+
+ if tag.startswith("SESSION-"):
+ return self._handle_state_tag(tag, payload, types.VarScope.SESSION)
if tag == "IP" and payload:
result = self._handle_ip_tag(payload)
@@ -470,21 +485,23 @@ class InverseSymbolResolver(SymbolResolverBase):
line = " ".join(toks)
for var_type in types.VarType:
- if cmd == var_type.op_tag:
- if len(toks) < 3:
- raise SymbolResolutionError(line, f"Missing arguments for {cmd}")
- try:
- index = int(toks[1])
- value = " ".join(toks[2:])
- except ValueError:
- raise SymbolResolutionError(line, f"Invalid index for {cmd}: {toks[1]}")
-
- var_name = self._get_or_create_var_name(var_type, index)
- if value.startswith('%{') and value.endswith('}'):
- rewritten_value, _ = self.percent_to_ident_or_func(value, section)
- else:
- rewritten_value = self._rewrite_inline_percents(value, section)
- return f"{var_name} = {rewritten_value}"
+ for scope in types.VarScope:
+ op_tag = f"{scope.op_prefix}-{var_type.op_suffix}"
+ if cmd == op_tag:
+ if len(toks) < 3:
+ raise SymbolResolutionError(line, f"Missing arguments for {cmd}")
+ try:
+ index = int(toks[1])
+ value = " ".join(toks[2:])
+ except ValueError:
+ raise SymbolResolutionError(line, f"Invalid index for {cmd}: {toks[1]}")
+
+ var_name = self._get_or_create_var_name(var_type, index, scope)
+ if value.startswith('%{') and value.endswith('}'):
+ rewritten_value, _ = self.percent_to_ident_or_func(value, section)
+ else:
+ rewritten_value = self._rewrite_inline_percents(value, section)
+ return f"{var_name} = {rewritten_value}"
for lhs_key, params in tables.OPERATOR_MAP.items():
commands = params.target if params else None
diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py
index 2878da0be2..b6dc61a90d 100644
--- a/tools/hrw4u/src/hrw_visitor.py
+++ b/tools/hrw4u/src/hrw_visitor.py
@@ -154,10 +154,16 @@ class HRWInverseVisitor(u4wrhVisitor, BaseHRWVisitor):
self.visit(line)
self._close_if_and_section()
- var_declarations = self.symbol_resolver.get_var_declarations()
- if var_declarations:
- vars_output = '\n'.join(["VARS {", *[self.format_with_indent(decl, 1) for decl in var_declarations], "}", ""])
- self.output = vars_output.split('\n') + self.output
+ txn_decls, ssn_decls = self.symbol_resolver.get_var_declarations()
+ preamble = []
+
+ if txn_decls:
+ preamble += ["VARS {"] + [self.format_with_indent(d, 1) for d in txn_decls] + ["}", ""]
+ if ssn_decls:
+ preamble += ["SESSION_VARS {"] + [self.format_with_indent(d, 1) for d in ssn_decls] + ["}", ""]
+
+ if preamble:
+ self.output = preamble + self.output
return self.output
diff --git a/tools/hrw4u/src/kg_visitor.py b/tools/hrw4u/src/kg_visitor.py
index 8b4b88929a..4f7ec6c353 100644
--- a/tools/hrw4u/src/kg_visitor.py
+++ b/tools/hrw4u/src/kg_visitor.py
@@ -242,6 +242,8 @@ class KnowledgeGraphVisitor(hrw4uVisitor, BaseHRWVisitor):
with self.debug_context("visitSection"):
if ctx.varSection():
return self.visitVarSection(ctx.varSection())
+ if ctx.sessionVarSection():
+ return self.visitSessionVarSection(ctx.sessionVarSection())
if ctx.name is None:
return
@@ -294,6 +296,25 @@ class KnowledgeGraphVisitor(hrw4uVisitor, BaseHRWVisitor):
self.current_section_id = old_section_id
+ def visitSessionVarSection(self, ctx) -> None:
+ with self.debug_context("visitSessionVarSection"):
+ vars_section_id = self._add_node(
+ "VarSection", {
+ "scope": "session",
+ "line_start": ctx.start.line if ctx.start else None,
+ "line_end": ctx.stop.line if ctx.stop else None
+ }, f"{self.filename}:SESSION_VARS")
+
+ if self.current_file_id:
+ self._add_edge(self.current_file_id, vars_section_id, "CONTAINS")
+
+ old_section_id = self.current_section_id
+ self.current_section_id = vars_section_id
+
+ self.visit(ctx.variables())
+
+ self.current_section_id = old_section_id
+
def visitVariableDecl(self, ctx) -> None:
with self.debug_context("visitVariableDecl"):
if ctx.name is None or ctx.typeName is None:
diff --git a/tools/hrw4u/src/lsp/strings.py b/tools/hrw4u/src/lsp/strings.py
index a16595da8c..e4b373cc4b 100644
--- a/tools/hrw4u/src/lsp/strings.py
+++ b/tools/hrw4u/src/lsp/strings.py
@@ -212,8 +212,9 @@ class DocumentAnalyzer:
if not stripped or stripped.startswith('//') or stripped.startswith('#'):
continue
- # Check for VARS section start
- if stripped == 'VARS {' or stripped.startswith('VARS {'):
+ # Check for VARS or SESSION_VARS section start
+ if stripped == 'VARS {' or stripped.startswith('VARS {') or \
+ stripped == 'SESSION_VARS {' or stripped.startswith('SESSION_VARS {'):
in_vars_section = True
brace_count = 1
continue
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index 7ef52fcf59..a57c6f2704 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -46,7 +46,8 @@ class SymbolResolver(SymbolResolverBase):
return params.target, params.validate
raise SymbolResolutionError(name, "Unknown operator or invalid standalone use")
- def declare_variable(self, name: str, type_name: str, explicit_slot: int | None = None) -> str:
+ def declare_variable(
+ self, name: str, type_name: str, explicit_slot: int | None = None, scope: types.VarScope = types.VarScope.TXN) -> str:
try:
var_type = types.VarType.from_str(type_name)
except ValueError as e:
@@ -54,24 +55,24 @@ class SymbolResolver(SymbolResolverBase):
error.add_note(f"Available types: {', '.join([vt.name for vt in types.VarType])}")
raise error
- # Determine slot number
+ # Determine slot number (separate slot pools per scope)
if explicit_slot is not None:
if explicit_slot < 0 or explicit_slot >= var_type.limit:
raise SymbolResolutionError(
name, f"Slot @{explicit_slot} out of range for type '{type_name}' (valid: 0-{var_type.limit-1})")
for var_name, sym in self._symbols.items():
- if sym.var_type == var_type and sym.slot == explicit_slot:
+ if sym.var_type == var_type and sym.scope == scope and sym.slot == explicit_slot:
raise SymbolResolutionError(name, f"Slot @{explicit_slot} already used by variable '{var_name}'")
slot = explicit_slot
else:
- used_slots = {sym.slot for sym in self._symbols.values() if sym.var_type == var_type}
+ used_slots = {sym.slot for sym in self._symbols.values() if sym.var_type == var_type and sym.scope == scope}
slot = next((i for i in range(var_type.limit) if i not in used_slots), None)
if slot is None:
raise SymbolResolutionError(name, f"No available slots for type '{type_name}' (max {var_type.limit})")
- symbol = types.Symbol(var_type, slot)
+ symbol = types.Symbol(var_type, slot, scope)
self._symbols[name] = symbol
return symbol.as_cond()
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 3db3e1bfab..97c28438be 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -121,15 +121,33 @@ class SuffixGroup(Enum):
f"Must be one of: {', '.join(sorted(self.value))}")
+class VarScope(Enum):
+ TXN = ("STATE", "set-state", "Transaction-scoped variable")
+ SESSION = ("SESSION", "set-session", "Session-scoped variable")
+
+ def __init__(self, cond_prefix: str, op_prefix: str, description: str) -> None:
+ self._cond_prefix = cond_prefix
+ self._op_prefix = op_prefix
+ self._description = description
+
+ @property
+ def cond_prefix(self) -> str:
+ return self._cond_prefix
+
+ @property
+ def op_prefix(self) -> str:
+ return self._op_prefix
+
+
class VarType(Enum):
- BOOL = ("bool", "FLAG", "set-state-flag", 16, "Boolean variable type - stores true/false values")
- INT8 = ("int8", "INT8", "set-state-int8", 4, "8-bit integer variable type - stores values from 0 to 255")
- INT16 = ("int16", "INT16", "set-state-int16", 1, "16-bit integer variable type - stores values from 0 to 65535")
+ BOOL = ("bool", "FLAG", "flag", 16, "Boolean variable type - stores true/false values")
+ INT8 = ("int8", "INT8", "int8", 4, "8-bit integer variable type - stores values from 0 to 255")
+ INT16 = ("int16", "INT16", "int16", 1, "16-bit integer variable type - stores values from 0 to 65535")
- def __init__(self, name: str, cond_tag: str, op_tag: str, limit: int, description: str) -> None:
+ def __init__(self, name: str, cond_tag: str, op_suffix: str, limit: int, description: str) -> None:
self._name = name
self._cond_tag = cond_tag
- self._op_tag = op_tag
+ self._op_suffix = op_suffix
self._limit = limit
self._description = description
@@ -138,8 +156,8 @@ class VarType(Enum):
return self._cond_tag
@property
- def op_tag(self) -> str:
- return self._op_tag
+ def op_suffix(self) -> str:
+ return self._op_suffix
@property
def limit(self) -> int:
@@ -165,12 +183,13 @@ class VarType(Enum):
class Symbol:
var_type: VarType
slot: int
+ scope: VarScope = VarScope.TXN
def as_cond(self) -> str:
- return f"%{{STATE-{self.var_type.cond_tag}:{self.slot}}}"
+ return f"%{{{self.scope.cond_prefix}-{self.var_type.cond_tag}:{self.slot}}}"
def as_operator(self, value: str) -> str:
- return f"{self.var_type.op_tag} {self.slot} {value}"
+ return f"{self.scope.op_prefix}-{self.var_type.op_suffix} {self.slot} {value}"
class MapParams:
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index 68182fb2b9..f3fb38c3a3 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -38,6 +38,7 @@ from hrw4u.visitor_base import BaseHRWVisitor
from hrw4u.validation import Validator
from hrw4u.procedures import resolve_use_path
from hrw4u.sandbox import SandboxConfig, SandboxDenialError
+import hrw4u.types as types
_regex_validator = Validator.regex_pattern()
@@ -91,6 +92,7 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
self._proc_call_stack: list[str] = []
self._proc_search_paths: list[Path] = list(proc_search_paths) if proc_search_paths else []
self._source_text: str = ""
+ self._current_var_scope: types.VarScope = types.VarScope.TXN
def _sandbox_check(self, ctx, check_fn) -> bool:
"""Run a sandbox check, trapping any denial error into the error collector.
@@ -725,6 +727,8 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
with self.debug_context("visitSection"):
if ctx.varSection():
return self.visitVarSection(ctx.varSection())
+ if ctx.sessionVarSection():
+ return self.visitSessionVarSection(ctx.sessionVarSection())
hook = None
with self.trap(ctx):
@@ -822,6 +826,23 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
return
if not self._sandbox_check(ctx, lambda: self._sandbox.check_language("variables")):
return
+ self._current_var_scope = types.VarScope.TXN
+ self.visit(ctx.variables())
+
+ def visitSessionVarSection(self, ctx) -> None:
+ if self.current_section is not None:
+ error = hrw4u_error(self.filename, ctx, "Session variable section must be first in a section")
+ if self.error_collector:
+ self.error_collector.add_error(error)
+ return
+ else:
+ raise error
+ with self.debug_context("visitSessionVarSection"):
+ if not self._sandbox_check(ctx, lambda: self._sandbox.check_section("SESSION_VARS")):
+ return
+ if not self._sandbox_check(ctx, lambda: self._sandbox.check_language("variables")):
+ return
+ self._current_var_scope = types.VarScope.SESSION
self.visit(ctx.variables())
def visitCommentLine(self, ctx) -> None:
@@ -924,7 +945,7 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
if '.' in name or ':' in name:
raise SymbolResolutionError("variable", f"Variable name '{name}' cannot contain '.' or ':' characters")
- symbol = self.symbol_resolver.declare_variable(name, type_name, explicit_slot)
+ symbol = self.symbol_resolver.declare_variable(name, type_name, explicit_slot, self._current_var_scope)
slot_info = f" @{explicit_slot}" if explicit_slot is not None else ""
self._dbg(f"bind `{name}' to {symbol}{slot_info}")
except Exception as e:
diff --git a/tools/hrw4u/tests/data/vars/session_assign.ast.txt b/tools/hrw4u/tests/data/vars/session_assign.ast.txt
new file mode 100644
index 0000000000..05dd8336cd
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_assign.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (varSection VARS { (variables (variablesItem (variableDecl bool_0 : bool ;))) }))) (programItem (section (sessionVarSection SESSION_VARS { (variables (variablesItem (variableDecl ssn_bool_0 : bool ;)) (variablesItem (variableDecl ssn_int8_0 : int8 ;))) }))) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor bool_0)))) (block { (blockItem (statement ssn_bool_0 = (value true) ;)) (blockItem (statement [...]
diff --git a/tools/hrw4u/tests/data/vars/session_assign.input.txt b/tools/hrw4u/tests/data/vars/session_assign.input.txt
new file mode 100644
index 0000000000..3b1dc592e0
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_assign.input.txt
@@ -0,0 +1,15 @@
+VARS {
+ bool_0: bool;
+}
+
+SESSION_VARS {
+ ssn_bool_0: bool;
+ ssn_int8_0: int8;
+}
+
+REMAP {
+ if bool_0 {
+ ssn_bool_0 = true;
+ ssn_int8_0 = 3;
+ }
+}
diff --git a/tools/hrw4u/tests/data/vars/session_assign.output.txt b/tools/hrw4u/tests/data/vars/session_assign.output.txt
new file mode 100644
index 0000000000..2cc526681d
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_assign.output.txt
@@ -0,0 +1,4 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{STATE-FLAG:0}
+ set-session-flag 0 true
+ set-session-int8 0 3
diff --git a/tools/hrw4u/tests/data/vars/session_bool.ast.txt b/tools/hrw4u/tests/data/vars/session_bool.ast.txt
new file mode 100644
index 0000000000..f418b5cb2f
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_bool.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (sessionVarSection SESSION_VARS { (variables (variablesItem (variableDecl ssn_bool_0 : bool ;))) }))) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ssn_bool_0)))) (block { (blockItem (statement inbound.req.X-suspicious = (value "true") ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/vars/session_bool.input.txt b/tools/hrw4u/tests/data/vars/session_bool.input.txt
new file mode 100644
index 0000000000..e4d335b386
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_bool.input.txt
@@ -0,0 +1,9 @@
+SESSION_VARS {
+ ssn_bool_0: bool;
+}
+
+REMAP {
+ if ssn_bool_0 {
+ inbound.req.X-suspicious = "true";
+ }
+}
diff --git a/tools/hrw4u/tests/data/vars/session_bool.output.txt b/tools/hrw4u/tests/data/vars/session_bool.output.txt
new file mode 100644
index 0000000000..c324be3f97
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_bool.output.txt
@@ -0,0 +1,3 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{SESSION-FLAG:0}
+ set-header X-suspicious "true"
diff --git a/tools/hrw4u/tests/data/vars/session_int16.ast.txt b/tools/hrw4u/tests/data/vars/session_int16.ast.txt
new file mode 100644
index 0000000000..c2a9f5f8a2
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int16.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (sessionVarSection SESSION_VARS { (variables (variablesItem (variableDecl ssn_int16_0 : int16 ;))) }))) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable ssn_int16_0) > (value 10000)))))) (block { (blockItem (statement inbound.req.X-heavy = (value "true") ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/vars/session_int16.input.txt b/tools/hrw4u/tests/data/vars/session_int16.input.txt
new file mode 100644
index 0000000000..d231f39eda
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int16.input.txt
@@ -0,0 +1,9 @@
+SESSION_VARS {
+ ssn_int16_0: int16;
+}
+
+REMAP {
+ if ssn_int16_0 > 10000 {
+ inbound.req.X-heavy = "true";
+ }
+}
diff --git a/tools/hrw4u/tests/data/vars/session_int16.output.txt b/tools/hrw4u/tests/data/vars/session_int16.output.txt
new file mode 100644
index 0000000000..90d97854b6
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int16.output.txt
@@ -0,0 +1,3 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{SESSION-INT16:0} >10000
+ set-header X-heavy "true"
diff --git a/tools/hrw4u/tests/data/vars/session_int8.ast.txt b/tools/hrw4u/tests/data/vars/session_int8.ast.txt
new file mode 100644
index 0000000000..f0b4235b86
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int8.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (sessionVarSection SESSION_VARS { (variables (variablesItem (variableDecl ssn_int8_0 : int8 ;))) }))) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable ssn_int8_0) > (value 5)))))) (block { (blockItem (statement inbound.req.X-throttle = (value "true") ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/vars/session_int8.input.txt b/tools/hrw4u/tests/data/vars/session_int8.input.txt
new file mode 100644
index 0000000000..e1ff2f0a2d
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int8.input.txt
@@ -0,0 +1,9 @@
+SESSION_VARS {
+ ssn_int8_0: int8;
+}
+
+REMAP {
+ if ssn_int8_0 > 5 {
+ inbound.req.X-throttle = "true";
+ }
+}
diff --git a/tools/hrw4u/tests/data/vars/session_int8.output.txt b/tools/hrw4u/tests/data/vars/session_int8.output.txt
new file mode 100644
index 0000000000..cbfd9271b9
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/session_int8.output.txt
@@ -0,0 +1,3 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{SESSION-INT8:0} >5
+ set-header X-throttle "true"
diff --git a/tools/hrw4u/tests/test_coverage.py b/tools/hrw4u/tests/test_coverage.py
index a0b31334d3..8af2de3dbf 100644
--- a/tools/hrw4u/tests/test_coverage.py
+++ b/tools/hrw4u/tests/test_coverage.py
@@ -366,14 +366,14 @@ class TestInverseSymbolResolver:
def test_get_var_declarations_empty(self):
r = self._resolver()
- assert r.get_var_declarations() == []
+ assert r.get_var_declarations() == ([], [])
def test_get_var_declarations_after_state_tag(self):
r = self._resolver()
r._handle_state_tag("STATE-FLAG", "0")
- decls = r.get_var_declarations()
- assert len(decls) == 1
- assert "bool" in decls[0]
+ txn_decls, ssn_decls = r.get_var_declarations()
+ assert len(txn_decls) == 1
+ assert "bool" in txn_decls[0]
def test_percent_to_ident_invalid(self):
r = self._resolver()