213 lines
5.1 KiB
Go
213 lines
5.1 KiB
Go
// 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') }
|