Files

143 lines
4.4 KiB
Go

package geodata
import (
"errors"
"strings"
)
// DefaultSiteFile and DefaultIPFile are the databases the geosite: and geoip:
// shorthands expand to. They are xray-core's file names, inlined so this
// package no longer links the core library just for two constants.
const (
DefaultSiteFile = "geosite.dat"
DefaultIPFile = "geoip.dat"
)
var (
// ErrInvalidToken reports a routing token that names a database but does not
// resolve to a file and category.
ErrInvalidToken = errors.New("invalid geodata routing token")
// ErrWrongKind reports a token carrying the other rule kind's prefix, such
// as geoip: typed into a domain field.
ErrWrongKind = errors.New("geodata token belongs to the other rule kind")
)
var (
sitePrefixes = []string{"ext:", "ext-domain:", "ext-site:"}
ipPrefixes = []string{"ext:", "ext-ip:"}
)
// Reference is the database file and category a routing token points at.
// An empty File means a plain domain or CIDR, which needs no database.
type Reference struct {
File string
Code string
Attributes []string
Reverse bool
}
// ParseReference resolves a routing token the way xray-core does, expanding the
// geosite:/geoip: shorthands to their ext: form. The core's own parser is not
// reusable here: it opens the database from disk as part of parsing, which
// would both duplicate this package's cache and fail whenever the file is
// merely absent — exactly the case the panel needs to report.
func ParseReference(token string, kind GeoKind) (Reference, error) {
token = strings.TrimSpace(token)
if token == "" {
return Reference{}, ErrInvalidToken
}
var reference Reference
if kind == KindIP {
// The core strips one "!" before the prefix and another before the code,
// each flipping the match, so "!!geoip:cn" is an ordinary geoip:cn.
token, reference.Reverse = cutNegation(token)
}
shorthand, defaultFile, prefixes := "geosite:", DefaultSiteFile, sitePrefixes
if kind == KindIP {
shorthand, defaultFile, prefixes = "geoip:", DefaultIPFile, ipPrefixes
}
if rest, found := strings.CutPrefix(token, shorthand); found {
token = "ext:" + defaultFile + ":" + rest
}
// A geoip: token in a domain field (or the reverse) parses as a plain
// domain and would be waved through, yet the core cannot resolve it as one.
// The field knows its own kind, so say so instead.
if strings.HasPrefix(token, otherShorthand(kind)) {
return Reference{}, ErrWrongKind
}
rest, matched := "", false
for _, prefix := range prefixes {
if trimmed, found := strings.CutPrefix(token, prefix); found {
rest, matched = trimmed, true
break
}
}
if !matched {
return Reference{}, nil
}
if rest == "" {
return Reference{}, ErrInvalidToken
}
file, code, found := strings.Cut(rest, ":")
if !found || file == "" {
return Reference{}, ErrInvalidToken
}
if kind == KindIP {
var negated bool
code, negated = cutNegation(code)
reference.Reverse = reference.Reverse != negated
}
reference.File = file
// Attribute filters exist for domain rules only; in an ip rule the core
// treats "@" as part of the category code and fails to resolve it, so the
// panel must not quietly strip it either.
// Whitespace inside the token is significant: the core matches the code and
// the attributes verbatim, so "geosite: cn" and "geosite:cn@ ads" are its
// problems to report, not ours to silently repair. Only the case is folded,
// which the core does too.
if kind == KindSite {
parts := strings.Split(code, "@")
code = parts[0]
for _, attribute := range parts[1:] {
// The core rejects an empty attribute outright ("geosite:cn@"),
// so accepting it here would hide a config it will not start with.
if attribute == "" {
return Reference{}, ErrInvalidToken
}
reference.Attributes = append(reference.Attributes, strings.ToLower(attribute))
}
}
reference.Code = strings.ToLower(code)
if reference.Code == "" {
return Reference{}, ErrInvalidToken
}
return reference, nil
}
// cutNegation strips leading "!" markers, reporting whether an odd number of
// them was present — the core folds a double negation back into a plain match.
func cutNegation(value string) (string, bool) {
negated := false
for {
rest, found := strings.CutPrefix(value, "!")
if !found {
return value, negated
}
value = rest
negated = !negated
}
}
func otherShorthand(kind GeoKind) string {
if kind == KindIP {
return "geosite:"
}
return "geoip:"
}