token.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package geodata
  2. import (
  3. "errors"
  4. "strings"
  5. xraygeodata "github.com/xtls/xray-core/common/geodata"
  6. )
  7. // DefaultSiteFile and DefaultIPFile are the databases the geosite: and geoip:
  8. // shorthands expand to.
  9. const (
  10. DefaultSiteFile = xraygeodata.DefaultGeoSiteDat
  11. DefaultIPFile = xraygeodata.DefaultGeoIPDat
  12. )
  13. var (
  14. // ErrInvalidToken reports a routing token that names a database but does not
  15. // resolve to a file and category.
  16. ErrInvalidToken = errors.New("invalid geodata routing token")
  17. // ErrWrongKind reports a token carrying the other rule kind's prefix, such
  18. // as geoip: typed into a domain field.
  19. ErrWrongKind = errors.New("geodata token belongs to the other rule kind")
  20. )
  21. var (
  22. sitePrefixes = []string{"ext:", "ext-domain:", "ext-site:"}
  23. ipPrefixes = []string{"ext:", "ext-ip:"}
  24. )
  25. // Reference is the database file and category a routing token points at.
  26. // An empty File means a plain domain or CIDR, which needs no database.
  27. type Reference struct {
  28. File string
  29. Code string
  30. Attributes []string
  31. Reverse bool
  32. }
  33. // ParseReference resolves a routing token the way xray-core does, expanding the
  34. // geosite:/geoip: shorthands to their ext: form. The core's own parser is not
  35. // reusable here: it opens the database from disk as part of parsing, which
  36. // would both duplicate this package's cache and fail whenever the file is
  37. // merely absent — exactly the case the panel needs to report.
  38. func ParseReference(token string, kind GeoKind) (Reference, error) {
  39. token = strings.TrimSpace(token)
  40. if token == "" {
  41. return Reference{}, ErrInvalidToken
  42. }
  43. var reference Reference
  44. if kind == KindIP {
  45. // The core strips one "!" before the prefix and another before the code,
  46. // each flipping the match, so "!!geoip:cn" is an ordinary geoip:cn.
  47. token, reference.Reverse = cutNegation(token)
  48. }
  49. shorthand, defaultFile, prefixes := "geosite:", DefaultSiteFile, sitePrefixes
  50. if kind == KindIP {
  51. shorthand, defaultFile, prefixes = "geoip:", DefaultIPFile, ipPrefixes
  52. }
  53. if rest, found := strings.CutPrefix(token, shorthand); found {
  54. token = "ext:" + defaultFile + ":" + rest
  55. }
  56. // A geoip: token in a domain field (or the reverse) parses as a plain
  57. // domain and would be waved through, yet the core cannot resolve it as one.
  58. // The field knows its own kind, so say so instead.
  59. if strings.HasPrefix(token, otherShorthand(kind)) {
  60. return Reference{}, ErrWrongKind
  61. }
  62. rest, matched := "", false
  63. for _, prefix := range prefixes {
  64. if trimmed, found := strings.CutPrefix(token, prefix); found {
  65. rest, matched = trimmed, true
  66. break
  67. }
  68. }
  69. if !matched {
  70. return Reference{}, nil
  71. }
  72. if rest == "" {
  73. return Reference{}, ErrInvalidToken
  74. }
  75. file, code, found := strings.Cut(rest, ":")
  76. if !found || file == "" {
  77. return Reference{}, ErrInvalidToken
  78. }
  79. if kind == KindIP {
  80. var negated bool
  81. code, negated = cutNegation(code)
  82. reference.Reverse = reference.Reverse != negated
  83. }
  84. reference.File = file
  85. // Attribute filters exist for domain rules only; in an ip rule the core
  86. // treats "@" as part of the category code and fails to resolve it, so the
  87. // panel must not quietly strip it either.
  88. // Whitespace inside the token is significant: the core matches the code and
  89. // the attributes verbatim, so "geosite: cn" and "geosite:cn@ ads" are its
  90. // problems to report, not ours to silently repair. Only the case is folded,
  91. // which the core does too.
  92. if kind == KindSite {
  93. parts := strings.Split(code, "@")
  94. code = parts[0]
  95. for _, attribute := range parts[1:] {
  96. // The core rejects an empty attribute outright ("geosite:cn@"),
  97. // so accepting it here would hide a config it will not start with.
  98. if attribute == "" {
  99. return Reference{}, ErrInvalidToken
  100. }
  101. reference.Attributes = append(reference.Attributes, strings.ToLower(attribute))
  102. }
  103. }
  104. reference.Code = strings.ToLower(code)
  105. if reference.Code == "" {
  106. return Reference{}, ErrInvalidToken
  107. }
  108. return reference, nil
  109. }
  110. // cutNegation strips leading "!" markers, reporting whether an odd number of
  111. // them was present — the core folds a double negation back into a plain match.
  112. func cutNegation(value string) (string, bool) {
  113. negated := false
  114. for {
  115. rest, found := strings.CutPrefix(value, "!")
  116. if !found {
  117. return value, negated
  118. }
  119. value = rest
  120. negated = !negated
  121. }
  122. }
  123. func otherShorthand(kind GeoKind) string {
  124. if kind == KindIP {
  125. return "geosite:"
  126. }
  127. return "geoip:"
  128. }