293 lines
6.5 KiB
Go
293 lines
6.5 KiB
Go
package policy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// ErrSyntax: Die Traefik-Regel ist syntaktisch ungültig.
|
|
ErrSyntax = errors.New("policy: Regel-Syntax ungültig")
|
|
// ErrUnconstrained: Die Regel ist nicht in jedem Zweig auf eigene Hosts
|
|
// beschränkt und würde fremden Verkehr abgreifen (Catch-all).
|
|
ErrUnconstrained = errors.New("policy: Regel nicht auf Hosts beschränkt")
|
|
// ErrHostRegexp: HostRegexp lässt sich nicht gegen allowed_domains prüfen.
|
|
ErrHostRegexp = errors.New("policy: HostRegexp nicht erlaubt")
|
|
)
|
|
|
|
// HostsFromRule parst eine Traefik-Regel (Matcher, &&, ||, !, Klammern)
|
|
// und liefert alle positiv geforderten Hosts in Reihenfolge des Auftretens.
|
|
//
|
|
// Die Regel muss in jedem ||-Zweig durch Host(...) eingeschränkt sein:
|
|
// "Host(`a.de`) || PathPrefix(`/`)" passt sonst auf jeden Host. Negierte
|
|
// Hosts schränken nicht ein (sie schließen nur aus). Positives HostRegexp
|
|
// ist nicht erlaubt, weil sich ein Regex nicht gegen die Lizenz prüfen lässt.
|
|
func HostsFromRule(rule string) ([]string, error) {
|
|
toks, err := lex(rule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := &parser{toks: toks}
|
|
root, err := p.parseOr()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.peek().kind != tEOF {
|
|
return nil, p.errorf("unerwartetes %q", p.peek().text)
|
|
}
|
|
constrained, hosts, err := analyze(root, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !constrained {
|
|
return nil, ErrUnconstrained
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
// analyze ermittelt, ob ein Ausdruck auf Hosts beschränkt ist, und sammelt
|
|
// die Hosts aus positiven (nicht negierten) Host-Matchern.
|
|
func analyze(n *node, positive bool) (constrained bool, hosts []string, err error) {
|
|
switch n.op {
|
|
case opCall:
|
|
switch strings.ToLower(n.name) {
|
|
case "host":
|
|
if positive {
|
|
return true, n.args, nil
|
|
}
|
|
case "hostregexp":
|
|
if positive {
|
|
return false, nil, ErrHostRegexp
|
|
}
|
|
}
|
|
return false, nil, nil
|
|
case opNot:
|
|
_, h, err := analyze(n.kids[0], !positive)
|
|
return false, h, err
|
|
default: // opAnd, opOr
|
|
c1, h1, err := analyze(n.kids[0], positive)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
c2, h2, err := analyze(n.kids[1], positive)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
if n.op == opAnd {
|
|
return c1 || c2, append(h1, h2...), nil
|
|
}
|
|
return c1 && c2, append(h1, h2...), nil
|
|
}
|
|
}
|
|
|
|
// --- Lexer ---
|
|
|
|
type tokenKind int
|
|
|
|
const (
|
|
tEOF tokenKind = iota
|
|
tIdent
|
|
tString
|
|
tLParen
|
|
tRParen
|
|
tComma
|
|
tAnd
|
|
tOr
|
|
tNot
|
|
)
|
|
|
|
type token struct {
|
|
kind tokenKind
|
|
text string
|
|
pos int
|
|
}
|
|
|
|
func lex(s string) ([]token, error) {
|
|
var toks []token
|
|
for i := 0; i < len(s); {
|
|
c := s[i]
|
|
switch {
|
|
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
|
|
i++
|
|
case c == '(':
|
|
toks = append(toks, token{tLParen, "(", i})
|
|
i++
|
|
case c == ')':
|
|
toks = append(toks, token{tRParen, ")", i})
|
|
i++
|
|
case c == ',':
|
|
toks = append(toks, token{tComma, ",", i})
|
|
i++
|
|
case c == '!':
|
|
toks = append(toks, token{tNot, "!", i})
|
|
i++
|
|
case strings.HasPrefix(s[i:], "&&"):
|
|
toks = append(toks, token{tAnd, "&&", i})
|
|
i += 2
|
|
case strings.HasPrefix(s[i:], "||"):
|
|
toks = append(toks, token{tOr, "||", i})
|
|
i += 2
|
|
case c == '`':
|
|
end := strings.IndexByte(s[i+1:], '`')
|
|
if end < 0 {
|
|
return nil, fmt.Errorf("%w: nicht geschlossener String ab Position %d", ErrSyntax, i)
|
|
}
|
|
toks = append(toks, token{tString, s[i+1 : i+1+end], i})
|
|
i += end + 2
|
|
case c == '"':
|
|
var b strings.Builder
|
|
j := i + 1
|
|
for ; j < len(s) && s[j] != '"'; j++ {
|
|
if s[j] == '\\' && j+1 < len(s) {
|
|
j++
|
|
}
|
|
b.WriteByte(s[j])
|
|
}
|
|
if j >= len(s) {
|
|
return nil, fmt.Errorf("%w: nicht geschlossener String ab Position %d", ErrSyntax, i)
|
|
}
|
|
toks = append(toks, token{tString, b.String(), i})
|
|
i = j + 1
|
|
case isIdentStart(c):
|
|
j := i
|
|
for j < len(s) && (isIdentStart(s[j]) || (s[j] >= '0' && s[j] <= '9')) {
|
|
j++
|
|
}
|
|
toks = append(toks, token{tIdent, s[i:j], i})
|
|
i = j
|
|
default:
|
|
return nil, fmt.Errorf("%w: unerwartetes Zeichen %q an Position %d", ErrSyntax, c, i)
|
|
}
|
|
}
|
|
return append(toks, token{tEOF, "Ende", len(s)}), nil
|
|
}
|
|
|
|
func isIdentStart(c byte) bool {
|
|
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_'
|
|
}
|
|
|
|
// --- Parser ---
|
|
|
|
type opKind int
|
|
|
|
const (
|
|
opCall opKind = iota
|
|
opNot
|
|
opAnd
|
|
opOr
|
|
)
|
|
|
|
type node struct {
|
|
op opKind
|
|
kids []*node
|
|
name string
|
|
args []string
|
|
}
|
|
|
|
type parser struct {
|
|
toks []token
|
|
pos int
|
|
}
|
|
|
|
func (p *parser) peek() token { return p.toks[p.pos] }
|
|
|
|
func (p *parser) next() token {
|
|
t := p.toks[p.pos]
|
|
if t.kind != tEOF {
|
|
p.pos++
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (p *parser) errorf(format string, args ...any) error {
|
|
return fmt.Errorf("%w: %s an Position %d", ErrSyntax, fmt.Sprintf(format, args...), p.peek().pos)
|
|
}
|
|
|
|
func (p *parser) expect(k tokenKind, what string) error {
|
|
if p.peek().kind != k {
|
|
return p.errorf("erwartet %s, gefunden %q", what, p.peek().text)
|
|
}
|
|
p.next()
|
|
return nil
|
|
}
|
|
|
|
// parseOr: and ("||" and)*
|
|
func (p *parser) parseOr() (*node, error) {
|
|
left, err := p.parseAnd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for p.peek().kind == tOr {
|
|
p.next()
|
|
right, err := p.parseAnd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
left = &node{op: opOr, kids: []*node{left, right}}
|
|
}
|
|
return left, nil
|
|
}
|
|
|
|
// parseAnd: unary ("&&" unary)*
|
|
func (p *parser) parseAnd() (*node, error) {
|
|
left, err := p.parseUnary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for p.peek().kind == tAnd {
|
|
p.next()
|
|
right, err := p.parseUnary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
left = &node{op: opAnd, kids: []*node{left, right}}
|
|
}
|
|
return left, nil
|
|
}
|
|
|
|
// parseUnary: "!" unary | "(" or ")" | ident "(" string ("," string)* ")"
|
|
func (p *parser) parseUnary() (*node, error) {
|
|
switch p.peek().kind {
|
|
case tNot:
|
|
p.next()
|
|
kid, err := p.parseUnary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &node{op: opNot, kids: []*node{kid}}, nil
|
|
case tLParen:
|
|
p.next()
|
|
inner, err := p.parseOr()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := p.expect(tRParen, "')'"); err != nil {
|
|
return nil, err
|
|
}
|
|
return inner, nil
|
|
case tIdent:
|
|
name := p.next().text
|
|
if err := p.expect(tLParen, "'('"); err != nil {
|
|
return nil, err
|
|
}
|
|
var args []string
|
|
for {
|
|
if p.peek().kind != tString {
|
|
return nil, p.errorf("erwartet String-Argument, gefunden %q", p.peek().text)
|
|
}
|
|
args = append(args, p.next().text)
|
|
if p.peek().kind != tComma {
|
|
break
|
|
}
|
|
p.next()
|
|
}
|
|
if err := p.expect(tRParen, "')'"); err != nil {
|
|
return nil, err
|
|
}
|
|
return &node{op: opCall, name: name, args: args}, nil
|
|
default:
|
|
return nil, p.errorf("erwartet Matcher, '(' oder '!', gefunden %q", p.peek().text)
|
|
}
|
|
}
|