319 lines
7.4 KiB
Go
319 lines
7.4 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) {
|
||
return hostsFromRule(rule, "host", "hostregexp")
|
||
}
|
||
|
||
// HostsFromSNIRule ist das Gegenstück für TCP-Regeln: Jeder ||-Zweig muss
|
||
// durch HostSNI(...) beschränkt sein, HostSNIRegexp ist verboten. Erlaubt
|
||
// sind nur explizite Hosts – HostSNI(`*`) und Wildcards ergeben
|
||
// ErrUnconstrained.
|
||
func HostsFromSNIRule(rule string) ([]string, error) {
|
||
hosts, err := hostsFromRule(rule, "hostsni", "hostsniregexp")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, h := range hosts {
|
||
if strings.Contains(h, "*") {
|
||
return nil, fmt.Errorf("%w: %q ist kein expliziter Host", ErrUnconstrained, h)
|
||
}
|
||
}
|
||
return hosts, nil
|
||
}
|
||
|
||
// hostsFromRule parst rule; hostMatcher/regexpMatcher sind die Matcher-Namen
|
||
// in Kleinschreibung.
|
||
func hostsFromRule(rule, hostMatcher, regexpMatcher 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)
|
||
}
|
||
a := analyzer{host: hostMatcher, regexp: regexpMatcher}
|
||
constrained, hosts, err := a.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.
|
||
type analyzer struct{ host, regexp string }
|
||
|
||
func (a analyzer) analyze(n *node, positive bool) (constrained bool, hosts []string, err error) {
|
||
switch n.op {
|
||
case opCall:
|
||
switch strings.ToLower(n.name) {
|
||
case a.host:
|
||
if positive {
|
||
return true, n.args, nil
|
||
}
|
||
case a.regexp:
|
||
if positive {
|
||
return false, nil, ErrHostRegexp
|
||
}
|
||
}
|
||
return false, nil, nil
|
||
case opNot:
|
||
_, h, err := a.analyze(n.kids[0], !positive)
|
||
return false, h, err
|
||
default: // opAnd, opOr
|
||
c1, h1, err := a.analyze(n.kids[0], positive)
|
||
if err != nil {
|
||
return false, nil, err
|
||
}
|
||
c2, h2, err := a.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)
|
||
}
|
||
}
|