reader.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. package geodata
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "net/netip"
  7. "os"
  8. "sort"
  9. "strings"
  10. "google.golang.org/protobuf/encoding/protowire"
  11. )
  12. // ErrUnrecognized reports a file that parses as neither database layout, which
  13. // in practice means a truncated download or an unrelated file renamed to .dat.
  14. var ErrUnrecognized = errors.New("file is not a geosite or geoip database")
  15. const (
  16. fieldListEntry = 1
  17. fieldEntryCode = 1
  18. fieldEntryPayload = 2
  19. fieldDomainType = 1
  20. fieldDomainValue = 2
  21. fieldDomainAttr = 3
  22. fieldAttrKey = 1
  23. fieldCIDRAddress = 1
  24. fieldCIDRPrefixLen = 2
  25. )
  26. const (
  27. domainTypeSubstr = 0
  28. domainTypeRegex = 1
  29. domainTypeFull = 3
  30. )
  31. type categoryScan struct {
  32. kind GeoKind
  33. categories []GeoCategory
  34. spans map[string][]byteSpan
  35. usable int
  36. }
  37. // byteSpan locates one category's record inside the database file, so a page of
  38. // its rules can be read without pulling the whole file into memory again.
  39. type byteSpan struct {
  40. offset int64
  41. length int64
  42. }
  43. // scanIndex walks the database once and reports every category with its entry
  44. // count and attribute keys, holding nothing else in memory.
  45. func scanIndex(data []byte, kind GeoKind) (*categoryScan, error) {
  46. scan := &categoryScan{kind: kind, spans: make(map[string][]byteSpan)}
  47. byCode := make(map[string]int)
  48. err := eachListEntry(data, func(entry []byte, span byteSpan) error {
  49. count := 0
  50. attributes := make(map[string]struct{})
  51. code, err := walkEntry(entry, func(payload []byte) error {
  52. if kind == KindSite {
  53. value, attrs, err := domainValue(payload)
  54. if err != nil {
  55. return err
  56. }
  57. if len(value) == 0 {
  58. return nil
  59. }
  60. for _, attr := range attrs {
  61. attributes[attr] = struct{}{}
  62. }
  63. } else {
  64. _, ok, err := cidrBytes(payload)
  65. if err != nil {
  66. return err
  67. }
  68. if !ok {
  69. return nil
  70. }
  71. }
  72. count++
  73. return nil
  74. })
  75. if err != nil || code == "" {
  76. return err
  77. }
  78. scan.usable += count
  79. scan.spans[code] = append(scan.spans[code], span)
  80. if position, seen := byCode[code]; seen {
  81. scan.categories[position].Entries += count
  82. scan.categories[position].Attributes = mergeAttributes(scan.categories[position].Attributes, attributes)
  83. return nil
  84. }
  85. byCode[code] = len(scan.categories)
  86. scan.categories = append(scan.categories, GeoCategory{
  87. Code: code,
  88. Entries: count,
  89. Attributes: mergeAttributes(nil, attributes),
  90. })
  91. return nil
  92. })
  93. if err != nil {
  94. return nil, err
  95. }
  96. sort.Slice(scan.categories, func(i, j int) bool { return scan.categories[i].Code < scan.categories[j].Code })
  97. return scan, nil
  98. }
  99. // scanEntries walks the database once and materialises only the requested page
  100. // of one category, so browsing a category with hundreds of thousands of rules
  101. // costs no more than browsing a small one.
  102. func scanEntries(records [][]byte, kind GeoKind, code, query string, offset, limit int) (GeoEntryPage, error) {
  103. page := GeoEntryPage{Items: []GeoEntry{}}
  104. matched := 0
  105. for _, entry := range records {
  106. // Values stay as raw bytes until a row is known to belong on the
  107. // requested page: turning all 170k rules of a category into strings
  108. // to serve one screenful is what made this expensive.
  109. if _, err := walkEntry(entry, func(payload []byte) error {
  110. var raw []byte
  111. var ok bool
  112. var err error
  113. if kind == KindSite {
  114. raw, _, err = domainValue(payload)
  115. if err != nil {
  116. return err
  117. }
  118. ok = len(raw) > 0
  119. } else {
  120. raw, ok, err = cidrBytes(payload)
  121. if err != nil {
  122. return err
  123. }
  124. }
  125. if !ok {
  126. return nil
  127. }
  128. if query != "" && !containsFold(raw, query) {
  129. return nil
  130. }
  131. if matched >= offset && len(page.Items) < limit {
  132. if kind == KindSite {
  133. page.Items = append(page.Items, GeoEntry{Kind: domainKind(payload), Value: string(raw)})
  134. } else {
  135. page.Items = append(page.Items, GeoEntry{Kind: "cidr", Value: string(raw)})
  136. }
  137. }
  138. matched++
  139. return nil
  140. }); err != nil {
  141. return GeoEntryPage{}, err
  142. }
  143. }
  144. page.Total = matched
  145. return page, nil
  146. }
  147. // detectKind reports which layout the file uses. The two share a wire layout
  148. // whose field types disagree, so decoding one as the other yields no usable
  149. // values at all — the count of readable entries is what tells them apart. The
  150. // file name only picks which layout to try first, so the common case scans once.
  151. func detectKind(data []byte, name string) (GeoKind, *categoryScan, error) {
  152. first, second := KindSite, KindIP
  153. if strings.Contains(strings.ToLower(name), "ip") {
  154. first, second = KindIP, KindSite
  155. }
  156. var firstErr error
  157. for _, kind := range [...]GeoKind{first, second} {
  158. scan, err := scanIndex(data, kind)
  159. if err != nil {
  160. if firstErr == nil {
  161. firstErr = err
  162. }
  163. continue
  164. }
  165. if scan.usable > 0 {
  166. return kind, scan, nil
  167. }
  168. }
  169. if firstErr != nil {
  170. // A truncated download is the common case here, and it reads very
  171. // differently to the user than "this is not a geo database at all".
  172. return "", nil, fmt.Errorf("%w: %w", ErrUnrecognized, firstErr)
  173. }
  174. return "", nil, ErrUnrecognized
  175. }
  176. // readSpans reads only the recorded slices of the file, so serving a page of a
  177. // category costs its own record rather than the whole database. The handle is
  178. // opened through an os.Root for the same reason readDatabase is.
  179. func readSpans(dir, name string, spans []byteSpan) ([][]byte, error) {
  180. root, err := os.OpenRoot(dir)
  181. if err != nil {
  182. return nil, err
  183. }
  184. defer root.Close()
  185. file, err := root.Open(name)
  186. if err != nil {
  187. return nil, err
  188. }
  189. defer file.Close()
  190. records := make([][]byte, 0, len(spans))
  191. for _, span := range spans {
  192. if span.length <= 0 || span.length > MaxFileSize {
  193. return nil, ErrUnrecognized
  194. }
  195. record := make([]byte, span.length)
  196. if _, err := file.ReadAt(record, span.offset); err != nil {
  197. return nil, err
  198. }
  199. records = append(records, record)
  200. }
  201. return records, nil
  202. }
  203. // readDatabase reads one database through an os.Root rooted at the asset
  204. // directory. Going through the root rather than a joined path means the file
  205. // name — which arrives from an HTTP request — never becomes a path this code
  206. // resolves itself: a symlink planted in the folder, or swapped in between the
  207. // check and the read, cannot pull in a file from elsewhere on disk.
  208. func readDatabase(dir, name string) ([]byte, error) {
  209. root, err := os.OpenRoot(dir)
  210. if err != nil {
  211. return nil, err
  212. }
  213. defer root.Close()
  214. file, err := root.Open(name)
  215. if err != nil {
  216. return nil, err
  217. }
  218. defer file.Close()
  219. info, err := file.Stat()
  220. if err != nil {
  221. return nil, err
  222. }
  223. if !info.Mode().IsRegular() {
  224. return nil, ErrInvalidName
  225. }
  226. if info.Size() > MaxFileSize {
  227. return nil, ErrFileTooLarge
  228. }
  229. return io.ReadAll(io.LimitReader(file, MaxFileSize))
  230. }
  231. func eachListEntry(data []byte, visit func(entry []byte, span byteSpan) error) error {
  232. total := int64(len(data))
  233. for len(data) > 0 {
  234. consumedSoFar := total - int64(len(data))
  235. number, wireType, consumed := protowire.ConsumeTag(data)
  236. if consumed < 0 {
  237. return protowire.ParseError(consumed)
  238. }
  239. data = data[consumed:]
  240. if number == fieldListEntry && wireType == protowire.BytesType {
  241. entry, size := protowire.ConsumeBytes(data)
  242. if size < 0 {
  243. return protowire.ParseError(size)
  244. }
  245. span := byteSpan{offset: consumedSoFar + int64(consumed) + int64(size) - int64(len(entry)), length: int64(len(entry))}
  246. if err := visit(entry, span); err != nil {
  247. return err
  248. }
  249. data = data[size:]
  250. continue
  251. }
  252. size := protowire.ConsumeFieldValue(number, wireType, data)
  253. if size < 0 {
  254. return protowire.ParseError(size)
  255. }
  256. data = data[size:]
  257. }
  258. return nil
  259. }
  260. // walkEntry reports a record's category code and hands each rule to visit.
  261. // The rules are not collected into a slice first: a single category can hold
  262. // a hundred thousand of them, and that slice was the bulk of what serving one
  263. // page allocated.
  264. func walkEntry(entry []byte, visit func(payload []byte) error) (string, error) {
  265. code := ""
  266. for len(entry) > 0 {
  267. number, wireType, consumed := protowire.ConsumeTag(entry)
  268. if consumed < 0 {
  269. return "", protowire.ParseError(consumed)
  270. }
  271. entry = entry[consumed:]
  272. switch {
  273. case number == fieldEntryCode && wireType == protowire.BytesType:
  274. value, size := protowire.ConsumeBytes(entry)
  275. if size < 0 {
  276. return "", protowire.ParseError(size)
  277. }
  278. code = strings.ToLower(string(value))
  279. entry = entry[size:]
  280. case number == fieldEntryPayload && wireType == protowire.BytesType:
  281. payload, size := protowire.ConsumeBytes(entry)
  282. if size < 0 {
  283. return "", protowire.ParseError(size)
  284. }
  285. if visit != nil {
  286. if err := visit(payload); err != nil {
  287. return "", err
  288. }
  289. }
  290. entry = entry[size:]
  291. default:
  292. size := protowire.ConsumeFieldValue(number, wireType, entry)
  293. if size < 0 {
  294. return "", protowire.ParseError(size)
  295. }
  296. entry = entry[size:]
  297. }
  298. }
  299. return code, nil
  300. }
  301. func containsFold(haystack []byte, needle string) bool {
  302. return strings.Contains(strings.ToLower(string(haystack)), needle)
  303. }
  304. func domainValue(payload []byte) ([]byte, []string, error) {
  305. var value []byte
  306. var attributes []string
  307. for len(payload) > 0 {
  308. number, wireType, consumed := protowire.ConsumeTag(payload)
  309. if consumed < 0 {
  310. return nil, nil, protowire.ParseError(consumed)
  311. }
  312. payload = payload[consumed:]
  313. switch {
  314. case number == fieldDomainValue && wireType == protowire.BytesType:
  315. raw, size := protowire.ConsumeBytes(payload)
  316. if size < 0 {
  317. return nil, nil, protowire.ParseError(size)
  318. }
  319. value = raw
  320. payload = payload[size:]
  321. case number == fieldDomainAttr && wireType == protowire.BytesType:
  322. raw, size := protowire.ConsumeBytes(payload)
  323. if size < 0 {
  324. return nil, nil, protowire.ParseError(size)
  325. }
  326. if key := attributeKey(raw); key != "" {
  327. attributes = append(attributes, key)
  328. }
  329. payload = payload[size:]
  330. default:
  331. size := protowire.ConsumeFieldValue(number, wireType, payload)
  332. if size < 0 {
  333. return nil, nil, protowire.ParseError(size)
  334. }
  335. payload = payload[size:]
  336. }
  337. }
  338. return value, attributes, nil
  339. }
  340. func attributeKey(attribute []byte) string {
  341. for len(attribute) > 0 {
  342. number, wireType, consumed := protowire.ConsumeTag(attribute)
  343. if consumed < 0 {
  344. return ""
  345. }
  346. attribute = attribute[consumed:]
  347. if number == fieldAttrKey && wireType == protowire.BytesType {
  348. raw, size := protowire.ConsumeBytes(attribute)
  349. if size < 0 {
  350. return ""
  351. }
  352. return strings.ToLower(string(raw))
  353. }
  354. size := protowire.ConsumeFieldValue(number, wireType, attribute)
  355. if size < 0 {
  356. return ""
  357. }
  358. attribute = attribute[size:]
  359. }
  360. return ""
  361. }
  362. // domainKind maps a domain's match type. proto3 omits zero values, so a domain
  363. // with no type field on the wire is a Substr (keyword) rule, not a domain one.
  364. func domainKind(payload []byte) string {
  365. matchType := uint64(domainTypeSubstr)
  366. for len(payload) > 0 {
  367. number, wireType, consumed := protowire.ConsumeTag(payload)
  368. if consumed < 0 {
  369. break
  370. }
  371. payload = payload[consumed:]
  372. if number == fieldDomainType && wireType == protowire.VarintType {
  373. raw, size := protowire.ConsumeVarint(payload)
  374. if size < 0 {
  375. break
  376. }
  377. matchType = raw
  378. break
  379. }
  380. size := protowire.ConsumeFieldValue(number, wireType, payload)
  381. if size < 0 {
  382. break
  383. }
  384. payload = payload[size:]
  385. }
  386. switch matchType {
  387. case domainTypeFull:
  388. return "full"
  389. case domainTypeRegex:
  390. return "regexp"
  391. case domainTypeSubstr:
  392. return "keyword"
  393. default:
  394. return "domain"
  395. }
  396. }
  397. // cidrBytes renders one CIDR. proto3 omits zero values, so a missing prefix
  398. // field means /0 — a default route, which a hand-built ext: database may well
  399. // contain — and must not be read as "no prefix given".
  400. func cidrBytes(payload []byte) ([]byte, bool, error) {
  401. var address []byte
  402. prefix := uint64(0)
  403. for len(payload) > 0 {
  404. number, wireType, consumed := protowire.ConsumeTag(payload)
  405. if consumed < 0 {
  406. return nil, false, protowire.ParseError(consumed)
  407. }
  408. payload = payload[consumed:]
  409. switch {
  410. case number == fieldCIDRAddress && wireType == protowire.BytesType:
  411. raw, size := protowire.ConsumeBytes(payload)
  412. if size < 0 {
  413. return nil, false, protowire.ParseError(size)
  414. }
  415. address = raw
  416. payload = payload[size:]
  417. case number == fieldCIDRPrefixLen && wireType == protowire.VarintType:
  418. raw, size := protowire.ConsumeVarint(payload)
  419. if size < 0 {
  420. return nil, false, protowire.ParseError(size)
  421. }
  422. prefix = raw
  423. payload = payload[size:]
  424. default:
  425. size := protowire.ConsumeFieldValue(number, wireType, payload)
  426. if size < 0 {
  427. return nil, false, protowire.ParseError(size)
  428. }
  429. payload = payload[size:]
  430. }
  431. }
  432. addr, ok := netip.AddrFromSlice(address)
  433. if !ok || prefix > uint64(addr.BitLen()) {
  434. return nil, false, nil
  435. }
  436. return []byte(netip.PrefixFrom(addr, int(prefix)).String()), true, nil
  437. }
  438. // mergeAttributes always returns a non-nil slice: the JSON contract declares
  439. // attributes as an array, and a nil slice would marshal to null and break
  440. // clients validating against it.
  441. func mergeAttributes(existing []string, attributes map[string]struct{}) []string {
  442. if len(attributes) == 0 {
  443. if existing == nil {
  444. return []string{}
  445. }
  446. return existing
  447. }
  448. merged := make(map[string]struct{}, len(existing)+len(attributes))
  449. for _, attribute := range existing {
  450. merged[attribute] = struct{}{}
  451. }
  452. for attribute := range attributes {
  453. merged[attribute] = struct{}{}
  454. }
  455. out := make([]string, 0, len(merged))
  456. for attribute := range merged {
  457. out = append(out, attribute)
  458. }
  459. sort.Strings(out)
  460. return out
  461. }