Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
144 lines
4.3 KiB
Go
144 lines
4.3 KiB
Go
package geodata
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
xraygeodata "github.com/xtls/xray-core/common/geodata"
|
|
)
|
|
|
|
// DefaultSiteFile and DefaultIPFile are the databases the geosite: and geoip:
|
|
// shorthands expand to.
|
|
const (
|
|
DefaultSiteFile = xraygeodata.DefaultGeoSiteDat
|
|
DefaultIPFile = xraygeodata.DefaultGeoIPDat
|
|
)
|
|
|
|
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:"
|
|
}
|