Files

319 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}