Files
3x-ui-web-src/internal/web/service/geodata.go
T
Sanaei f727d04f65
CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / 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
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

157 lines
4.9 KiB
Go

package service
import (
"errors"
"os"
"strings"
"sync"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/mhsanaei/3x-ui/v3/internal/xray/geodata"
)
// GeodataTokenIssue reports a routing token the running core would reject,
// or would silently match nothing against.
type GeodataTokenIssue struct {
Token string `json:"token" example:"geosite:blabla"`
Reason string `json:"reason" example:"categoryMissing"`
File string `json:"file,omitempty" example:"geosite.dat"`
Code string `json:"code,omitempty" example:"blabla"`
}
const (
geodataReasonSyntax = "syntax"
geodataReasonFileMissing = "fileMissing"
geodataReasonCategoryMissing = "categoryMissing"
geodataReasonAttributeMissing = "attributeMissing"
geodataReasonWrongKind = "wrongKind"
)
// geodataStores keys the cache by asset directory rather than holding a single
// store, so a changed XUI_BIN_FOLDER is picked up instead of being pinned to
// whatever the first call saw.
var geodataStores sync.Map
func assetStore() *geodata.Store {
dir := assetDir()
if cached, ok := geodataStores.Load(dir); ok {
return cached.(*geodata.Store)
}
store, _ := geodataStores.LoadOrStore(dir, geodata.NewStore(dir))
return store.(*geodata.Store)
}
// assetDir resolves the folder the running core reads its databases from,
// with the same precedence the core itself uses (see ensureXrayAssetLocation
// in internal/xray). An install that points XRAY_LOCATION_ASSET at a shared
// asset directory would otherwise have the panel browsing an empty bin folder
// and reporting perfectly valid geosite:/geoip: tokens as missing.
func assetDir() string {
for _, key := range [...]string{"XRAY_LOCATION_ASSET", "xray.location.asset"} {
if dir := os.Getenv(key); dir != "" {
return dir
}
}
return config.GetBinFolderPath()
}
// GeodataService browses the geosite/geoip databases Xray resolves its
// geosite:/geoip: routing tokens against.
type GeodataService struct{}
// Files lists the databases available in the Xray asset folder.
func (s *GeodataService) Files() ([]geodata.GeoFile, error) {
return assetStore().ListFiles()
}
// Categories returns one page of a database's categories.
func (s *GeodataService) Categories(file, query string, offset, limit int) (geodata.GeoCategoryPage, error) {
return assetStore().Categories(file, query, offset, limit)
}
// Entries returns one page of the rules inside a category.
func (s *GeodataService) Entries(file, code, query string, offset, limit int) (geodata.GeoEntryPage, error) {
return assetStore().Entries(file, code, query, offset, limit)
}
// Validate reports which of the given routing tokens do not resolve against the
// databases on disk. Plain domains and CIDRs are left alone — only tokens that
// name a database are looked up.
func (s *GeodataService) Validate(isIP bool, tokens []string) []GeodataTokenIssue {
kind := geodata.KindSite
if isIP {
kind = geodata.KindIP
}
issues := make([]GeodataTokenIssue, 0)
for _, token := range tokens {
token = strings.TrimSpace(token)
if token == "" {
continue
}
reference, err := geodata.ParseReference(token, kind)
if err != nil {
reason := geodataReasonSyntax
if errors.Is(err, geodata.ErrWrongKind) {
reason = geodataReasonWrongKind
}
issues = append(issues, GeodataTokenIssue{Token: token, Reason: reason})
continue
}
if reference.File == "" {
continue
}
category, err := assetStore().Lookup(reference.File, reference.Code)
if err != nil {
issues = append(issues, GeodataTokenIssue{
Token: token,
Reason: geodataIssueReason(err),
File: reference.File,
Code: reference.Code,
})
continue
}
if missing := unknownAttributes(category, reference.Attributes); missing != "" {
issues = append(issues, GeodataTokenIssue{
Token: token,
Reason: geodataReasonAttributeMissing,
File: reference.File,
Code: missing,
})
}
}
return issues
}
// unknownAttributes returns the first attribute the category does not carry.
// The core accepts such a token, but no domain can satisfy the filter, so the
// rule silently matches nothing — worth reporting even though Xray will start.
// A leading "!" is accepted either way: some databases ship the negated key
// verbatim, and the panel must not guess which convention a database follows.
func unknownAttributes(category geodata.GeoCategory, wanted []string) string {
if len(wanted) == 0 {
return ""
}
present := make(map[string]struct{}, len(category.Attributes))
for _, attribute := range category.Attributes {
present[attribute] = struct{}{}
present[strings.TrimPrefix(attribute, "!")] = struct{}{}
}
for _, attribute := range wanted {
if _, ok := present[attribute]; ok {
continue
}
if _, ok := present[strings.TrimPrefix(attribute, "!")]; ok {
continue
}
return attribute
}
return ""
}
func geodataIssueReason(err error) string {
if errors.Is(err, geodata.ErrUnknownCategory) {
return geodataReasonCategoryMissing
}
return geodataReasonFileMissing
}