feat: add non-terminal Headers rule - #17
TorstenDittmann wants to merge 1 commit into
Conversation
A Headers rule carries response headers to add when it matches. It does not decide the request, so Firewall::verify() records it and keeps evaluating; matches are exposed through getMatchedNonTerminalRules(). Rule::isTerminal() marks which rules end evaluation and defaults to true, so existing rules and getLastMatchedRule() behave as before.
|
| } | ||
|
|
||
| foreach ($headers as $name => $value) { | ||
| if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+$/', $name) !== 1) { |
There was a problem hiding this comment.
Trailing newline passes validation A name such as
"X-Test " passes this check because $ can match just before a final newline. The rule then exposes a name that is not a valid HTTP token, so a consumer writing the collected headers may reject it or handle it unsafely. Anchor the match to the absolute end of the string.
| if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+$/', $name) !== 1) { | |
| if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+\z/', $name) !== 1) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Rules/Headers.php
Line: 33
Comment:
**Trailing newline passes validation** A name such as `"X-Test
"` passes this check because `$` can match just before a final newline. The rule then exposes a name that is not a valid HTTP token, so a consumer writing the collected headers may reject it or handle it unsafely. Anchor the match to the absolute end of the string.
```suggestion
if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+\z/', $name) !== 1) {
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| } | ||
|
|
||
| foreach ($headers as $name => $value) { | ||
| if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+$/', $name) !== 1) { |
There was a problem hiding this comment.
Numeric header names rejected A numeric-only name such as
"123" is a valid HTTP token, but PHP converts it to an integer array key before this check. is_string($name) therefore rejects it, preventing callers from registering that valid header name.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Rules/Headers.php
Line: 33
Comment:
**Numeric header names rejected** A numeric-only name such as `"123"` is a valid HTTP token, but PHP converts it to an integer array key before this check. `is_string($name)` therefore rejects it, preventing callers from registering that valid header name.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| $this->assertFalse($rule->isTerminal()); | ||
| $this->assertTrue((new Deny())->isTerminal()); |
There was a problem hiding this comment.
Tests assert implementation flags These assertions check
isTerminal() directly, while the firewall tests already show the observable behavior: evaluation continues after a Headers match. The repository requires tests of observable behavior rather than assertions that mirror implementation choices. Remove these flag checks before merging.
| $this->assertFalse($rule->isTerminal()); | |
| $this->assertTrue((new Deny())->isTerminal()); |
Context Used: Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/RulesTest.php
Line: 81-82
Comment:
**Tests assert implementation flags** These assertions check `isTerminal()` directly, while the firewall tests already show the observable behavior: evaluation continues after a Headers match. The repository requires tests of observable behavior rather than assertions that mirror implementation choices. Remove these flag checks before merging.
```suggestion
```
**Context Used:** Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What
Adds a rule type that carries response headers instead of deciding the request, so a consumer can attach headers (for example a security header on a path prefix) through the same rule and condition model as the other actions.
Rules\Headers: takes conditions and a header map, with actionheaders.Rule::isTerminal(): whether a match ends evaluation. Defaults totrue;Headersreturnsfalse.Firewall::verify(): a matching non-terminal rule is recorded and evaluation continues with the next rule.Firewall::getMatchedNonTerminalRules(): the non-terminal rules that matched during the lastverify(), in evaluation order.Behaviour
verify()andgetLastMatchedRule()return what they did before. The change is additive.getLastMatchedRule()stays the terminal rule that decided the request. When onlyHeadersrules match, it isnullandverify()returnsfalse, the same as when nothing matches.Headersrule placed after it is not evaluated.InvalidArgumentException, likeChallengedoes for an unknown type.The library only collects the matches; writing the headers onto a response is left to the consumer.
Testing
composer test(60 tests),composer checkandcomposer lintpass locally. New cases cover the rule itself, validation, continuing past a header match, collection order, rules after a terminal match, and the reset betweenverify()calls.