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) } }