// Package hostparse извлекает имена хостов из правил роутеров Traefik // и сопоставляет их с разрешёнными DNS-зонами. package hostparse import ( "strings" ) // Result — результат разбора правила роутера. type Result struct { // Hosts — валидные нормализованные (lower-case, без точки в конце) имена из Host(...). Hosts []string // Wildcards — хосты вида *.example.com (не поддерживаются). Wildcards []string // Invalid — значения Host(...), не являющиеся корректным DNS-именем. Invalid []string // RegexpCount — число HostRegexp(...) в правиле (игнорируются). RegexpCount int } // Parse разбирает rule и возвращает все «положительные» Host(...). // Host внутри отрицания (!Host(...), !(...)) пропускается. // Поддерживаются строки в обратных кавычках и двойных кавычках. func Parse(rule string) Result { var res Result seen := map[string]struct{}{} s := []rune(rule) n := len(s) i := 0 // стек скобок: true — группа под отрицанием var stack []bool negDepth := 0 pendingNot := false skipSpaces := func() { for i < n && isSpace(s[i]) { i++ } } for i < n { c := s[i] switch { case isSpace(c): i++ case c == '!': pendingNot = true i++ case c == '`' || c == '"': _, next := readString(s, i) i = next pendingNot = false case c == '(': neg := pendingNot stack = append(stack, neg) if neg { negDepth++ } pendingNot = false i++ case c == ')': if len(stack) > 0 { if stack[len(stack)-1] { negDepth-- } stack = stack[:len(stack)-1] } pendingNot = false i++ case isIdentStart(c): start := i for i < n && isIdentPart(s[i]) { i++ } ident := string(s[start:i]) skipSpaces() negated := pendingNot || negDepth > 0 pendingNot = false if i >= n || s[i] != '(' { continue } switch ident { case "Host": i++ // '(' args, next := readArgs(s, i) i = next if negated { continue } for _, a := range args { classify(a, &res, seen) } case "HostRegexp": i++ _, next := readArgs(s, i) i = next if !negated { res.RegexpCount++ } default: // прочие функции: обычный вход в скобку, строки внутри пропускаются основным циклом stack = append(stack, negated) if negated { negDepth++ } i++ } default: i++ } } return res } func classify(raw string, res *Result, seen map[string]struct{}) { h := Normalize(raw) if h == "" { res.Invalid = append(res.Invalid, raw) return } if strings.HasPrefix(h, "*.") || h == "*" { res.Wildcards = append(res.Wildcards, h) return } if !ValidHostname(h) { res.Invalid = append(res.Invalid, raw) return } if _, ok := seen[h]; ok { return } seen[h] = struct{}{} res.Hosts = append(res.Hosts, h) } // Normalize приводит имя к нижнему регистру и убирает пробелы и точку в конце. func Normalize(name string) string { name = strings.TrimSpace(name) name = strings.TrimSuffix(name, ".") return strings.ToLower(name) } // ValidHostname проверяет, что имя — корректный ASCII hostname (LDH, допускается '_'). func ValidHostname(h string) bool { if h == "" || len(h) > 253 { return false } for _, label := range strings.Split(h, ".") { if label == "" || len(label) > 63 { return false } if label[0] == '-' || label[len(label)-1] == '-' { return false } for _, r := range label { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-', r == '_': default: return false } } } return true } // readArgs читает список строк до закрывающей ')' (начиная сразу после '('). func readArgs(s []rune, i int) ([]string, int) { var args []string n := len(s) for i < n { c := s[i] switch { case c == ')': return args, i + 1 case c == '`' || c == '"': v, next := readString(s, i) args = append(args, v) i = next default: i++ } } return args, i } // readString читает строку в кавычках начиная с s[i] (кавычка), возвращает значение и позицию после неё. func readString(s []rune, i int) (string, int) { q := s[i] i++ var b strings.Builder for i < len(s) { c := s[i] if q == '"' && c == '\\' && i+1 < len(s) { b.WriteRune(s[i+1]) i += 2 continue } if c == q { return b.String(), i + 1 } b.WriteRune(c) i++ } return b.String(), i } func isSpace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' || r == '\r' } func isIdentStart(r rune) bool { return r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') } func isIdentPart(r rune) bool { return isIdentStart(r) || (r >= '0' && r <= '9') }