1
0

geolite.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package geolite
  2. import (
  3. "bytes"
  4. _ "embed"
  5. "fmt"
  6. "io"
  7. "net"
  8. "sync"
  9. "github.com/oschwald/geoip2-golang"
  10. "github.com/ulikunitz/xz"
  11. "github.com/uozi-tech/cosy/geoip"
  12. )
  13. //go:embed GeoLite2-City.mmdb.xz
  14. var embeddedCityDB []byte
  15. type IPLocation struct {
  16. RegionCode string `json:"region_code"`
  17. Province string `json:"province"`
  18. City string `json:"city"`
  19. }
  20. type Service struct {
  21. cityDB *geoip2.Reader
  22. }
  23. var (
  24. instance *Service
  25. once sync.Once
  26. initErr error
  27. )
  28. func GetService() (*Service, error) {
  29. once.Do(func() {
  30. instance = &Service{}
  31. initErr = instance.init()
  32. })
  33. return instance, initErr
  34. }
  35. func (s *Service) init() error {
  36. // Load embedded compressed database
  37. if len(embeddedCityDB) > 0 {
  38. if err := s.loadEmbeddedCityDB(); err != nil {
  39. return fmt.Errorf("failed to load embedded city database: %v", err)
  40. }
  41. return nil
  42. }
  43. return fmt.Errorf("no embedded GeoLite2 City database available")
  44. }
  45. func (s *Service) loadEmbeddedCityDB() error {
  46. // Decompress the embedded database
  47. xzReader, err := xz.NewReader(bytes.NewReader(embeddedCityDB))
  48. if err != nil {
  49. return fmt.Errorf("failed to create xz reader: %v", err)
  50. }
  51. decompressedData, err := io.ReadAll(xzReader)
  52. if err != nil {
  53. return fmt.Errorf("failed to decompress database: %v", err)
  54. }
  55. // Create geoip2 reader from decompressed data
  56. cityDB, err := geoip2.FromBytes(decompressedData)
  57. if err != nil {
  58. return fmt.Errorf("failed to create geoip2 reader: %v", err)
  59. }
  60. s.cityDB = cityDB
  61. return nil
  62. }
  63. func (s *Service) Search(ipStr string) (*IPLocation, error) {
  64. if s.cityDB == nil {
  65. return nil, fmt.Errorf("no databases loaded")
  66. }
  67. ip := net.ParseIP(ipStr)
  68. if ip == nil {
  69. return nil, fmt.Errorf("invalid IP address: %s", ipStr)
  70. }
  71. loc := &IPLocation{}
  72. // Use cosy geoip for country code
  73. loc.RegionCode = geoip.ParseIP(ipStr)
  74. // Use city database for detailed information
  75. if record, err := s.cityDB.City(ip); err == nil {
  76. // Override country code from city database if cosy didn't provide it
  77. if loc.RegionCode == "" {
  78. loc.RegionCode = record.Country.IsoCode
  79. }
  80. if len(record.Subdivisions) > 0 {
  81. loc.Province = record.Subdivisions[0].Names["en"]
  82. }
  83. loc.City = record.City.Names["en"]
  84. // Get Chinese names for Chinese regions
  85. if loc.RegionCode == "CN" || loc.RegionCode == "HK" ||
  86. loc.RegionCode == "MO" || loc.RegionCode == "TW" {
  87. if len(record.Subdivisions) > 0 {
  88. if cnRegion := record.Subdivisions[0].Names["zh-CN"]; cnRegion != "" {
  89. loc.Province = cnRegion
  90. }
  91. } else {
  92. // If it's a Chinese IP but has no province, mark it as "其它"
  93. loc.Province = "其它"
  94. }
  95. if cnCity := record.City.Names["zh-CN"]; cnCity != "" {
  96. loc.City = cnCity
  97. }
  98. loc.RegionCode = "CN"
  99. }
  100. return loc, nil
  101. }
  102. // If city database lookup fails, return minimal info with country code
  103. if loc.RegionCode != "" {
  104. return loc, nil
  105. }
  106. return nil, fmt.Errorf("no location data found for IP: %s", ipStr)
  107. }
  108. func (s *Service) SearchWithISO(ipStr string) (*IPLocation, error) {
  109. // This method specifically returns English names and ISO codes
  110. if s.cityDB == nil {
  111. return nil, fmt.Errorf("no databases loaded")
  112. }
  113. ip := net.ParseIP(ipStr)
  114. if ip == nil {
  115. return nil, fmt.Errorf("invalid IP address: %s", ipStr)
  116. }
  117. loc := &IPLocation{}
  118. // Use cosy geoip for country code
  119. loc.RegionCode = geoip.ParseIP(ipStr)
  120. // Use city database for detailed information
  121. if record, err := s.cityDB.City(ip); err == nil {
  122. // Override country code from city database if cosy didn't provide it
  123. if loc.RegionCode == "" {
  124. loc.RegionCode = record.Country.IsoCode
  125. }
  126. if len(record.Subdivisions) > 0 {
  127. loc.RegionCode = record.Subdivisions[0].Names["en"]
  128. }
  129. loc.RegionCode = record.City.Names["en"]
  130. return loc, nil
  131. }
  132. // If city database lookup fails, return minimal info with country code
  133. if loc.RegionCode != "" {
  134. return loc, nil
  135. }
  136. return nil, fmt.Errorf("no location data found for IP: %s", ipStr)
  137. }
  138. func (s *Service) Close() {
  139. if s.cityDB != nil {
  140. s.cityDB.Close()
  141. s.cityDB = nil
  142. }
  143. }
  144. func IsChineseIP(loc *IPLocation) bool {
  145. return loc != nil && (loc.RegionCode == "CN" ||
  146. loc.RegionCode == "HK" ||
  147. loc.RegionCode == "MO" ||
  148. loc.RegionCode == "TW")
  149. }
  150. func IsChineseRegion(regionCode string) bool {
  151. chineseRegionCodes := []string{"CN", "HK", "MO", "TW"}
  152. for _, region := range chineseRegionCodes {
  153. if regionCode == region {
  154. return true
  155. }
  156. }
  157. return false
  158. }