server läuft

This commit is contained in:
br committed 2026-10-01 23:52:15 +02:00
1 parent c8df9d7651
commit 42f0c0accb
21 files changed
+4185 -10

No files matched your search

+292
View File
@@ -0,0 +1,292 @@
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)
}
}