| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- 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
- }
|