geodata.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package service
  2. import (
  3. "errors"
  4. "os"
  5. "strings"
  6. "sync"
  7. "github.com/mhsanaei/3x-ui/v3/internal/config"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray/geodata"
  9. )
  10. // GeodataTokenIssue reports a routing token the running core would reject,
  11. // or would silently match nothing against.
  12. type GeodataTokenIssue struct {
  13. Token string `json:"token" example:"geosite:blabla"`
  14. Reason string `json:"reason" example:"categoryMissing"`
  15. File string `json:"file,omitempty" example:"geosite.dat"`
  16. Code string `json:"code,omitempty" example:"blabla"`
  17. }
  18. const (
  19. geodataReasonSyntax = "syntax"
  20. geodataReasonFileMissing = "fileMissing"
  21. geodataReasonCategoryMissing = "categoryMissing"
  22. geodataReasonAttributeMissing = "attributeMissing"
  23. geodataReasonWrongKind = "wrongKind"
  24. )
  25. // geodataStores keys the cache by asset directory rather than holding a single
  26. // store, so a changed XUI_BIN_FOLDER is picked up instead of being pinned to
  27. // whatever the first call saw.
  28. var geodataStores sync.Map
  29. func assetStore() *geodata.Store {
  30. dir := assetDir()
  31. if cached, ok := geodataStores.Load(dir); ok {
  32. return cached.(*geodata.Store)
  33. }
  34. store, _ := geodataStores.LoadOrStore(dir, geodata.NewStore(dir))
  35. return store.(*geodata.Store)
  36. }
  37. // assetDir resolves the folder the running core reads its databases from,
  38. // with the same precedence the core itself uses (see ensureXrayAssetLocation
  39. // in internal/xray). An install that points XRAY_LOCATION_ASSET at a shared
  40. // asset directory would otherwise have the panel browsing an empty bin folder
  41. // and reporting perfectly valid geosite:/geoip: tokens as missing.
  42. func assetDir() string {
  43. for _, key := range [...]string{"XRAY_LOCATION_ASSET", "xray.location.asset"} {
  44. if dir := os.Getenv(key); dir != "" {
  45. return dir
  46. }
  47. }
  48. return config.GetBinFolderPath()
  49. }
  50. // GeodataService browses the geosite/geoip databases Xray resolves its
  51. // geosite:/geoip: routing tokens against.
  52. type GeodataService struct{}
  53. // Files lists the databases available in the Xray asset folder.
  54. func (s *GeodataService) Files() ([]geodata.GeoFile, error) {
  55. return assetStore().ListFiles()
  56. }
  57. // Categories returns one page of a database's categories.
  58. func (s *GeodataService) Categories(file, query string, offset, limit int) (geodata.GeoCategoryPage, error) {
  59. return assetStore().Categories(file, query, offset, limit)
  60. }
  61. // Entries returns one page of the rules inside a category.
  62. func (s *GeodataService) Entries(file, code, query string, offset, limit int) (geodata.GeoEntryPage, error) {
  63. return assetStore().Entries(file, code, query, offset, limit)
  64. }
  65. // Validate reports which of the given routing tokens do not resolve against the
  66. // databases on disk. Plain domains and CIDRs are left alone — only tokens that
  67. // name a database are looked up.
  68. func (s *GeodataService) Validate(isIP bool, tokens []string) []GeodataTokenIssue {
  69. kind := geodata.KindSite
  70. if isIP {
  71. kind = geodata.KindIP
  72. }
  73. issues := make([]GeodataTokenIssue, 0)
  74. for _, token := range tokens {
  75. token = strings.TrimSpace(token)
  76. if token == "" {
  77. continue
  78. }
  79. reference, err := geodata.ParseReference(token, kind)
  80. if err != nil {
  81. reason := geodataReasonSyntax
  82. if errors.Is(err, geodata.ErrWrongKind) {
  83. reason = geodataReasonWrongKind
  84. }
  85. issues = append(issues, GeodataTokenIssue{Token: token, Reason: reason})
  86. continue
  87. }
  88. if reference.File == "" {
  89. continue
  90. }
  91. category, err := assetStore().Lookup(reference.File, reference.Code)
  92. if err != nil {
  93. issues = append(issues, GeodataTokenIssue{
  94. Token: token,
  95. Reason: geodataIssueReason(err),
  96. File: reference.File,
  97. Code: reference.Code,
  98. })
  99. continue
  100. }
  101. if missing := unknownAttributes(category, reference.Attributes); missing != "" {
  102. issues = append(issues, GeodataTokenIssue{
  103. Token: token,
  104. Reason: geodataReasonAttributeMissing,
  105. File: reference.File,
  106. Code: missing,
  107. })
  108. }
  109. }
  110. return issues
  111. }
  112. // unknownAttributes returns the first attribute the category does not carry.
  113. // The core accepts such a token, but no domain can satisfy the filter, so the
  114. // rule silently matches nothing — worth reporting even though Xray will start.
  115. // A leading "!" is accepted either way: some databases ship the negated key
  116. // verbatim, and the panel must not guess which convention a database follows.
  117. func unknownAttributes(category geodata.GeoCategory, wanted []string) string {
  118. if len(wanted) == 0 {
  119. return ""
  120. }
  121. present := make(map[string]struct{}, len(category.Attributes))
  122. for _, attribute := range category.Attributes {
  123. present[attribute] = struct{}{}
  124. present[strings.TrimPrefix(attribute, "!")] = struct{}{}
  125. }
  126. for _, attribute := range wanted {
  127. if _, ok := present[attribute]; ok {
  128. continue
  129. }
  130. if _, ok := present[strings.TrimPrefix(attribute, "!")]; ok {
  131. continue
  132. }
  133. return attribute
  134. }
  135. return ""
  136. }
  137. func geodataIssueReason(err error) string {
  138. if errors.Is(err, geodata.ErrUnknownCategory) {
  139. return geodataReasonCategoryMissing
  140. }
  141. return geodataReasonFileMissing
  142. }