mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-19 16:21:43 +01:00
refactor(rules): simplify nested block detection by removing @ prefix requirement
Changes the nested block syntax detection from requiring `@`
as the first non-space character on a line to using a line-ending brace heuristic.
The parser now recognizes nested blocks when a line ends with an unquoted `{`,
simplifying the syntax and removing the mandatory `@` prefix while maintaining the same functionality.
This commit is contained in:
@@ -11,22 +11,22 @@ default {
|
||||
|
||||
header X-Test-Header {
|
||||
set header X-Remote-Type public
|
||||
@remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
set header X-Remote-Type private
|
||||
}
|
||||
}
|
||||
|
||||
path glob(/api/admin/*) {
|
||||
@cookie session-id {
|
||||
cookie session-id {
|
||||
set header X-Session-ID $cookie(session-id)
|
||||
}
|
||||
}
|
||||
|
||||
!remote 192.168.0.0/16 {
|
||||
@!header X-User-Role admin & !header X-User-Role user {
|
||||
!header X-User-Role admin & !header X-User-Role user {
|
||||
error 403 "Access denied"
|
||||
} elif remote 127.0.0.1 {
|
||||
@header X-User-Role staff {
|
||||
header X-User-Role staff {
|
||||
set header X-User-Role staff
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -176,7 +176,7 @@ func TestParseBlockRules_NestedBlocks(t *testing.T) {
|
||||
rules := testParseRules(t, `
|
||||
header X-Test-Header {
|
||||
set header X-Remote-Type public
|
||||
@remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
set header X-Remote-Type private
|
||||
}
|
||||
}`)
|
||||
@@ -226,7 +226,7 @@ func TestParseBlockRules_NestedBlocks_ElifElse(t *testing.T) {
|
||||
rules := testParseRules(t, `
|
||||
header X-Test-Header {
|
||||
set header X-Mode outer
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
} elif method POST {
|
||||
set header X-Mode post
|
||||
@@ -289,7 +289,7 @@ default {
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_ElifMustBeSameLine(t *testing.T) {
|
||||
err := testParseRulesError(t, `header X-Test-Header {
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
}
|
||||
elif method POST {
|
||||
@@ -301,7 +301,7 @@ func TestParseBlockRules_NestedBlocks_ElifMustBeSameLine(t *testing.T) {
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_ElseMustBeLastOnLine(t *testing.T) {
|
||||
err := testParseRulesError(t, `header X-Test-Header {
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
} else {
|
||||
set header X-Mode other
|
||||
@@ -313,7 +313,7 @@ func TestParseBlockRules_NestedBlocks_ElseMustBeLastOnLine(t *testing.T) {
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_MultipleElse(t *testing.T) {
|
||||
err := testParseRulesError(t, `header X-Test-Header {
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
} else {
|
||||
set header X-Mode other
|
||||
@@ -328,7 +328,7 @@ func TestParseBlockRules_NestedBlocks_MultipleElse(t *testing.T) {
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_ElifMissingOnExpr(t *testing.T) {
|
||||
err := testParseRulesError(t, `header X-Test-Header {
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
} elif {
|
||||
set header X-Mode post
|
||||
@@ -337,3 +337,54 @@ func TestParseBlockRules_NestedBlocks_ElifMissingOnExpr(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "expected on-expr after 'elif'")
|
||||
}
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_LineEndingBraceHeuristic(t *testing.T) {
|
||||
rules := testParseRules(t, `{
|
||||
set header X-Literal "{"
|
||||
}`)
|
||||
require.Len(t, rules, 1)
|
||||
require.Len(t, rules[0].Do.pre, 1)
|
||||
_, ok := rules[0].Do.pre[0].(Handler)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_LineEndingBraceWithTrailingSpaces(t *testing.T) {
|
||||
rules := testParseRules(t, `header X-Test-Header {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
}
|
||||
}`)
|
||||
require.Len(t, rules, 1)
|
||||
require.Len(t, rules[0].Do.pre, 1)
|
||||
ifCmd, ok := rules[0].Do.pre[0].(IfBlockCommand)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "method GET", ifCmd.On.raw)
|
||||
}
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_LineEndingBraceWithTrailingComment(t *testing.T) {
|
||||
rules := testParseRules(t, `header X-Test-Header {
|
||||
method GET { // GET branch
|
||||
set header X-Mode get
|
||||
} else { # fallback branch
|
||||
set header X-Mode other
|
||||
}
|
||||
}`)
|
||||
require.Len(t, rules, 1)
|
||||
require.Len(t, rules[0].Do.pre, 1)
|
||||
|
||||
ifCmd, ok := rules[0].Do.pre[0].(IfElseBlockCommand)
|
||||
require.True(t, ok)
|
||||
require.Len(t, ifCmd.Ifs, 1)
|
||||
assert.Equal(t, "method GET", ifCmd.Ifs[0].On.raw)
|
||||
require.NotNil(t, ifCmd.Else)
|
||||
}
|
||||
|
||||
func TestParseBlockRules_NestedBlocks_LineEndingBraceInterpretsAsBlock(t *testing.T) {
|
||||
err := testParseRulesError(t, `{
|
||||
set header X-Bad {
|
||||
set header X-Test fail
|
||||
}
|
||||
}`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid `rule.on` target")
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
//
|
||||
// Syntax (within a rule do block):
|
||||
//
|
||||
// @<on-expr> { <do...> }
|
||||
// <on-expr> { <do...> }
|
||||
//
|
||||
// Semantics:
|
||||
// - Evaluated in the same phase the parent rule runs.
|
||||
@@ -53,7 +53,7 @@ func (c IfBlockCommand) Phase() PhaseFlag {
|
||||
//
|
||||
// Syntax (within a rule do block):
|
||||
//
|
||||
// @<on-expr> { <do...> } elif <on-expr> { <do...> } ... else { <do...> }
|
||||
// <on-expr> { <do...> } elif <on-expr> { <do...> } ... else { <do...> }
|
||||
//
|
||||
// NOTE: `elif`/`else` must appear on the same line as the preceding closing brace (`}`),
|
||||
// e.g. `} elif ... {` and `} else {`.
|
||||
@@ -113,9 +113,9 @@ func skipSameLineSpace(src string, pos int) int {
|
||||
return pos
|
||||
}
|
||||
|
||||
func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
||||
func parseAtBlockChain(src string, blockPos int) (CommandHandler, int, error) {
|
||||
length := len(src)
|
||||
headerStart := atPos + 1
|
||||
headerStart := blockPos
|
||||
|
||||
parseBranch := func(onExpr string, bodyStart int, bodyEnd int) (RuleOn, []CommandHandler, error) {
|
||||
var on RuleOn
|
||||
@@ -140,11 +140,14 @@ func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
||||
if herr != nil {
|
||||
return nil, 0, herr
|
||||
}
|
||||
if onExpr == "" {
|
||||
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr before '{'")
|
||||
}
|
||||
if bracePos >= length || src[bracePos] != '{' {
|
||||
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after nested block header")
|
||||
}
|
||||
|
||||
// Parse first @<on-expr> { ... }
|
||||
// Parse first <on-expr> { ... }
|
||||
p := bracePos
|
||||
bodyStart := p + 1
|
||||
bodyEnd, ferr := findMatchingBrace(src, &p, bodyStart)
|
||||
@@ -288,10 +291,56 @@ func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
||||
return IfBlockCommand{On: ifs[0].On, Do: ifs[0].Do}, p, nil
|
||||
}
|
||||
|
||||
// parseDoWithBlocks parses a do-body containing plain command lines and nested @-blocks.
|
||||
func lineEndsWithUnquotedOpenBrace(src string, lineStart int, lineEnd int) bool {
|
||||
quote := byte(0)
|
||||
lastSignificant := byte(0)
|
||||
atLineStart := true
|
||||
prevIsSpace := true
|
||||
|
||||
for i := lineStart; i < lineEnd; i++ {
|
||||
c := src[i]
|
||||
if quote != 0 {
|
||||
if c == '\\' && i+1 < lineEnd {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if c == quote {
|
||||
quote = 0
|
||||
}
|
||||
atLineStart = false
|
||||
prevIsSpace = false
|
||||
continue
|
||||
}
|
||||
if quoteChars[c] {
|
||||
quote = c
|
||||
atLineStart = false
|
||||
prevIsSpace = false
|
||||
continue
|
||||
}
|
||||
if c == '#' && (atLineStart || prevIsSpace) {
|
||||
break
|
||||
}
|
||||
if c == '/' && i+1 < lineEnd {
|
||||
n := rune(src[i+1])
|
||||
if (atLineStart || prevIsSpace) && (n == '/' || n == '*') {
|
||||
break
|
||||
}
|
||||
}
|
||||
if unicode.IsSpace(rune(c)) {
|
||||
prevIsSpace = true
|
||||
continue
|
||||
}
|
||||
lastSignificant = c
|
||||
atLineStart = false
|
||||
prevIsSpace = false
|
||||
}
|
||||
return quote == 0 && lastSignificant == '{'
|
||||
}
|
||||
|
||||
// parseDoWithBlocks parses a do-body containing plain command lines and nested blocks.
|
||||
// It returns the outer command handlers and the require phase.
|
||||
//
|
||||
// A nested block is only recognized when '@' is the first non-space character on a line.
|
||||
// A nested block is recognized when a line ends with an unquoted '{' (ignoring trailing whitespace).
|
||||
func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
||||
pos := 0
|
||||
length := len(src)
|
||||
@@ -351,7 +400,12 @@ func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
||||
linePos++
|
||||
}
|
||||
|
||||
if linePos < length && src[linePos] == '@' {
|
||||
lineEnd := linePos
|
||||
for lineEnd < length && src[lineEnd] != '\n' {
|
||||
lineEnd++
|
||||
}
|
||||
|
||||
if linePos < length && lineEndsWithUnquotedOpenBrace(src, linePos, lineEnd) {
|
||||
h, next, err := parseAtBlockChain(src, linePos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -363,7 +417,7 @@ func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
||||
}
|
||||
|
||||
// Not a nested block; parse the rest of this line as a command.
|
||||
lineEnd := pos
|
||||
lineEnd = pos
|
||||
for lineEnd < length && src[lineEnd] != '\n' {
|
||||
lineEnd++
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ func TestHTTPFlow_NestedBlocks_RemoteOverride(t *testing.T) {
|
||||
err := parseRules(`
|
||||
header X-Test-Header {
|
||||
set header X-Remote-Type public
|
||||
@remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
remote 127.0.0.1 | remote 192.168.0.0/16 {
|
||||
set header X-Remote-Type private
|
||||
}
|
||||
}
|
||||
@@ -495,7 +495,7 @@ func TestHTTPFlow_NestedBlocks_ElifElse(t *testing.T) {
|
||||
var rules Rules
|
||||
err := parseRules(`
|
||||
header X-Test-Header {
|
||||
@method GET {
|
||||
method GET {
|
||||
set header X-Mode get
|
||||
} elif method POST {
|
||||
set header X-Mode post
|
||||
@@ -552,7 +552,7 @@ func TestHTTPFlow_NestedBlocks_TerminatingActionStopsFlow(t *testing.T) {
|
||||
err := parseRules(`
|
||||
path / {
|
||||
set header X-Pre pre
|
||||
@header X-Block {
|
||||
header X-Block {
|
||||
error 403 "blocked"
|
||||
}
|
||||
set resp_header X-After should-not-run
|
||||
@@ -593,7 +593,7 @@ func TestHTTPFlow_NestedBlocks_InResponseRule_ModifiesResponseByRequestMethod(t
|
||||
err := parseRules(`
|
||||
{
|
||||
set header X-Method "should-be-overridden"
|
||||
@method POST {
|
||||
method POST {
|
||||
set header X-Method "post"
|
||||
} elif method GET {
|
||||
set header X-Method "get"
|
||||
|
||||
@@ -79,7 +79,7 @@ header Host example.com {
|
||||
name: "same condition with terminating handler inside if block",
|
||||
rules: `
|
||||
header Host example.com {
|
||||
@default {
|
||||
default {
|
||||
error 404 "not found"
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ header Host example.com {
|
||||
name: "same condition with terminating handler across if else block",
|
||||
rules: `
|
||||
header Host example.com {
|
||||
@method GET {
|
||||
method GET {
|
||||
error 404 "not found"
|
||||
} else {
|
||||
redirect https://example.com
|
||||
@@ -111,7 +111,7 @@ header Host example.com {
|
||||
name: "same condition with non terminating if branch in if else block",
|
||||
rules: `
|
||||
header Host example.com {
|
||||
@method GET {
|
||||
method GET {
|
||||
set resp_header X-Test first
|
||||
} else {
|
||||
error 404 "not found"
|
||||
|
||||
Reference in New Issue
Block a user