mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 17:58:45 +02: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 {
|
header X-Test-Header {
|
||||||
set header X-Remote-Type public
|
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
|
set header X-Remote-Type private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path glob(/api/admin/*) {
|
path glob(/api/admin/*) {
|
||||||
@cookie session-id {
|
cookie session-id {
|
||||||
set header X-Session-ID $cookie(session-id)
|
set header X-Session-ID $cookie(session-id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
!remote 192.168.0.0/16 {
|
!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"
|
error 403 "Access denied"
|
||||||
} elif remote 127.0.0.1 {
|
} elif remote 127.0.0.1 {
|
||||||
@header X-User-Role staff {
|
header X-User-Role staff {
|
||||||
set header X-User-Role staff
|
set header X-User-Role staff
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func TestParseBlockRules_NestedBlocks(t *testing.T) {
|
|||||||
rules := testParseRules(t, `
|
rules := testParseRules(t, `
|
||||||
header X-Test-Header {
|
header X-Test-Header {
|
||||||
set header X-Remote-Type public
|
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
|
set header X-Remote-Type private
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
@@ -226,7 +226,7 @@ func TestParseBlockRules_NestedBlocks_ElifElse(t *testing.T) {
|
|||||||
rules := testParseRules(t, `
|
rules := testParseRules(t, `
|
||||||
header X-Test-Header {
|
header X-Test-Header {
|
||||||
set header X-Mode outer
|
set header X-Mode outer
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
} elif method POST {
|
} elif method POST {
|
||||||
set header X-Mode post
|
set header X-Mode post
|
||||||
@@ -289,7 +289,7 @@ default {
|
|||||||
|
|
||||||
func TestParseBlockRules_NestedBlocks_ElifMustBeSameLine(t *testing.T) {
|
func TestParseBlockRules_NestedBlocks_ElifMustBeSameLine(t *testing.T) {
|
||||||
err := testParseRulesError(t, `header X-Test-Header {
|
err := testParseRulesError(t, `header X-Test-Header {
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
}
|
}
|
||||||
elif method POST {
|
elif method POST {
|
||||||
@@ -301,7 +301,7 @@ func TestParseBlockRules_NestedBlocks_ElifMustBeSameLine(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseBlockRules_NestedBlocks_ElseMustBeLastOnLine(t *testing.T) {
|
func TestParseBlockRules_NestedBlocks_ElseMustBeLastOnLine(t *testing.T) {
|
||||||
err := testParseRulesError(t, `header X-Test-Header {
|
err := testParseRulesError(t, `header X-Test-Header {
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
} else {
|
} else {
|
||||||
set header X-Mode other
|
set header X-Mode other
|
||||||
@@ -313,7 +313,7 @@ func TestParseBlockRules_NestedBlocks_ElseMustBeLastOnLine(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseBlockRules_NestedBlocks_MultipleElse(t *testing.T) {
|
func TestParseBlockRules_NestedBlocks_MultipleElse(t *testing.T) {
|
||||||
err := testParseRulesError(t, `header X-Test-Header {
|
err := testParseRulesError(t, `header X-Test-Header {
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
} else {
|
} else {
|
||||||
set header X-Mode other
|
set header X-Mode other
|
||||||
@@ -328,7 +328,7 @@ func TestParseBlockRules_NestedBlocks_MultipleElse(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseBlockRules_NestedBlocks_ElifMissingOnExpr(t *testing.T) {
|
func TestParseBlockRules_NestedBlocks_ElifMissingOnExpr(t *testing.T) {
|
||||||
err := testParseRulesError(t, `header X-Test-Header {
|
err := testParseRulesError(t, `header X-Test-Header {
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
} elif {
|
} elif {
|
||||||
set header X-Mode post
|
set header X-Mode post
|
||||||
@@ -337,3 +337,54 @@ func TestParseBlockRules_NestedBlocks_ElifMissingOnExpr(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "expected on-expr after 'elif'")
|
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):
|
// Syntax (within a rule do block):
|
||||||
//
|
//
|
||||||
// @<on-expr> { <do...> }
|
// <on-expr> { <do...> }
|
||||||
//
|
//
|
||||||
// Semantics:
|
// Semantics:
|
||||||
// - Evaluated in the same phase the parent rule runs.
|
// - Evaluated in the same phase the parent rule runs.
|
||||||
@@ -53,7 +53,7 @@ func (c IfBlockCommand) Phase() PhaseFlag {
|
|||||||
//
|
//
|
||||||
// Syntax (within a rule do block):
|
// 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 (`}`),
|
// NOTE: `elif`/`else` must appear on the same line as the preceding closing brace (`}`),
|
||||||
// e.g. `} elif ... {` and `} else {`.
|
// e.g. `} elif ... {` and `} else {`.
|
||||||
@@ -113,9 +113,9 @@ func skipSameLineSpace(src string, pos int) int {
|
|||||||
return pos
|
return pos
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
func parseAtBlockChain(src string, blockPos int) (CommandHandler, int, error) {
|
||||||
length := len(src)
|
length := len(src)
|
||||||
headerStart := atPos + 1
|
headerStart := blockPos
|
||||||
|
|
||||||
parseBranch := func(onExpr string, bodyStart int, bodyEnd int) (RuleOn, []CommandHandler, error) {
|
parseBranch := func(onExpr string, bodyStart int, bodyEnd int) (RuleOn, []CommandHandler, error) {
|
||||||
var on RuleOn
|
var on RuleOn
|
||||||
@@ -140,11 +140,14 @@ func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
|||||||
if herr != nil {
|
if herr != nil {
|
||||||
return nil, 0, herr
|
return nil, 0, herr
|
||||||
}
|
}
|
||||||
|
if onExpr == "" {
|
||||||
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr before '{'")
|
||||||
|
}
|
||||||
if bracePos >= length || src[bracePos] != '{' {
|
if bracePos >= length || src[bracePos] != '{' {
|
||||||
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after nested block header")
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after nested block header")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse first @<on-expr> { ... }
|
// Parse first <on-expr> { ... }
|
||||||
p := bracePos
|
p := bracePos
|
||||||
bodyStart := p + 1
|
bodyStart := p + 1
|
||||||
bodyEnd, ferr := findMatchingBrace(src, &p, bodyStart)
|
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
|
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.
|
// 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) {
|
func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
||||||
pos := 0
|
pos := 0
|
||||||
length := len(src)
|
length := len(src)
|
||||||
@@ -351,7 +400,12 @@ func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
|||||||
linePos++
|
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)
|
h, next, err := parseAtBlockChain(src, linePos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// Not a nested block; parse the rest of this line as a command.
|
||||||
lineEnd := pos
|
lineEnd = pos
|
||||||
for lineEnd < length && src[lineEnd] != '\n' {
|
for lineEnd < length && src[lineEnd] != '\n' {
|
||||||
lineEnd++
|
lineEnd++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ func TestHTTPFlow_NestedBlocks_RemoteOverride(t *testing.T) {
|
|||||||
err := parseRules(`
|
err := parseRules(`
|
||||||
header X-Test-Header {
|
header X-Test-Header {
|
||||||
set header X-Remote-Type public
|
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
|
set header X-Remote-Type private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +495,7 @@ func TestHTTPFlow_NestedBlocks_ElifElse(t *testing.T) {
|
|||||||
var rules Rules
|
var rules Rules
|
||||||
err := parseRules(`
|
err := parseRules(`
|
||||||
header X-Test-Header {
|
header X-Test-Header {
|
||||||
@method GET {
|
method GET {
|
||||||
set header X-Mode get
|
set header X-Mode get
|
||||||
} elif method POST {
|
} elif method POST {
|
||||||
set header X-Mode post
|
set header X-Mode post
|
||||||
@@ -552,7 +552,7 @@ func TestHTTPFlow_NestedBlocks_TerminatingActionStopsFlow(t *testing.T) {
|
|||||||
err := parseRules(`
|
err := parseRules(`
|
||||||
path / {
|
path / {
|
||||||
set header X-Pre pre
|
set header X-Pre pre
|
||||||
@header X-Block {
|
header X-Block {
|
||||||
error 403 "blocked"
|
error 403 "blocked"
|
||||||
}
|
}
|
||||||
set resp_header X-After should-not-run
|
set resp_header X-After should-not-run
|
||||||
@@ -593,7 +593,7 @@ func TestHTTPFlow_NestedBlocks_InResponseRule_ModifiesResponseByRequestMethod(t
|
|||||||
err := parseRules(`
|
err := parseRules(`
|
||||||
{
|
{
|
||||||
set header X-Method "should-be-overridden"
|
set header X-Method "should-be-overridden"
|
||||||
@method POST {
|
method POST {
|
||||||
set header X-Method "post"
|
set header X-Method "post"
|
||||||
} elif method GET {
|
} elif method GET {
|
||||||
set header X-Method "get"
|
set header X-Method "get"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ header Host example.com {
|
|||||||
name: "same condition with terminating handler inside if block",
|
name: "same condition with terminating handler inside if block",
|
||||||
rules: `
|
rules: `
|
||||||
header Host example.com {
|
header Host example.com {
|
||||||
@default {
|
default {
|
||||||
error 404 "not found"
|
error 404 "not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ header Host example.com {
|
|||||||
name: "same condition with terminating handler across if else block",
|
name: "same condition with terminating handler across if else block",
|
||||||
rules: `
|
rules: `
|
||||||
header Host example.com {
|
header Host example.com {
|
||||||
@method GET {
|
method GET {
|
||||||
error 404 "not found"
|
error 404 "not found"
|
||||||
} else {
|
} else {
|
||||||
redirect https://example.com
|
redirect https://example.com
|
||||||
@@ -111,7 +111,7 @@ header Host example.com {
|
|||||||
name: "same condition with non terminating if branch in if else block",
|
name: "same condition with non terminating if branch in if else block",
|
||||||
rules: `
|
rules: `
|
||||||
header Host example.com {
|
header Host example.com {
|
||||||
@method GET {
|
method GET {
|
||||||
set resp_header X-Test first
|
set resp_header X-Test first
|
||||||
} else {
|
} else {
|
||||||
error 404 "not found"
|
error 404 "not found"
|
||||||
|
|||||||
Reference in New Issue
Block a user