Files
OpenFlare/pkg/geoip/geoip.go
T
ryan 699e95f12c perfsprint 18→0:strconv.Itoa/FormatInt/FormatUint/FormatBool 替代 fmt.Sprintf、无动词 fmt.Errorf→errors.New、纯字符串拼接。全部语义等价(已核对 diff)。修正 fixer 遗留的 import 问题(引入 goimports 统一整理)。
Result: {"status":"keep","total_issues":56,"golint_canonicalheader":8,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":3,"golint_wastedassign":7,"golint_total":55,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":47}
2026-08-16 17:34:44 +08:00

261 lines
6.2 KiB
Go

// Package geoip resolves geographic information for IP addresses.
package geoip
import (
"errors"
"fmt"
"log/slog"
"net"
"strings"
"sync"
"time"
"unicode"
ristretto "github.com/dgraph-io/ristretto/v2"
)
// CurrentProvider is the active GeoIP backend used by package-level helpers.
var CurrentProvider Service
var geoCache *providerCache
var providerMutex sync.RWMutex
var providerFactory = newProvider
// Supported GeoIP provider identifiers.
const (
// ProviderDisabled disables GeoIP lookups.
ProviderDisabled = "disabled"
ProviderMaxMind = "mmdb"
ProviderIPAPI = "ip-api"
ProviderGeoJS = "geojs"
ProviderIPInfo = "ipinfo"
defaultGeoCacheDuration = 48 * time.Hour
isoCountryCodeLength = 2
geoipDataDirPerm = 0o750
)
// GeoInfo contains normalized geographic metadata for an IP address.
type GeoInfo struct {
ISOCode string
Name string
Latitude *float64
Longitude *float64
}
func init() {
CurrentProvider = &EmptyProvider{}
geoCache = newProviderCache(defaultGeoCacheDuration)
}
// Service defines the core GeoIP lookup operations implemented by providers.
type Service interface {
Name() string
GetGeoInfo(ip net.IP) (*GeoInfo, error)
UpdateDatabase() error
Close() error
}
type cachedGeoInfo struct {
info *GeoInfo
expiresAt time.Time
}
type providerCache struct {
items *ristretto.Cache[string, cachedGeoInfo]
duration time.Duration
}
func newProviderCache(duration time.Duration) *providerCache {
items, err := ristretto.NewCache(&ristretto.Config[string, cachedGeoInfo]{
NumCounters: 1e5,
MaxCost: 2e4,
BufferItems: 64,
})
if err != nil {
panic(err)
}
return &providerCache{
items: items,
duration: duration,
}
}
func (c *providerCache) Get(key string) (*GeoInfo, bool) {
entry, ok := c.items.Get(key)
if !ok {
return nil, false
}
if time.Now().After(entry.expiresAt) {
c.items.Del(key)
return nil, false
}
return entry.info, true
}
func (c *providerCache) Set(key string, info *GeoInfo) {
c.items.Set(key, cachedGeoInfo{
info: info,
expiresAt: time.Now().Add(c.duration),
}, 1)
c.items.Wait()
}
func (c *providerCache) Flush() {
c.items.Clear()
}
// GetRegionUnicodeEmoji returns the regional indicator emoji for a two-letter ISO code.
func GetRegionUnicodeEmoji(isoCode string) string {
if len(isoCode) != isoCountryCodeLength {
return ""
}
isoCode = strings.ToUpper(isoCode)
if !unicode.IsLetter(rune(isoCode[0])) || !unicode.IsLetter(rune(isoCode[1])) {
return ""
}
rune1 := 0x1F1E6 + (rune(isoCode[0]) - 'A')
rune2 := 0x1F1E6 + (rune(isoCode[1]) - 'A')
return string(rune1) + string(rune2)
}
// InitGeoIP configures the active GeoIP provider.
func InitGeoIP(provider string) {
providerName := normalizeProvider(provider)
nextProvider, err := providerFactory(providerName)
if err != nil {
slog.Error("initialize GeoIP provider failed", "provider", providerName, "error", err)
nextProvider = &EmptyProvider{}
}
setProvider(nextProvider)
if providerName == ProviderDisabled {
slog.Info("GeoIP provider disabled")
return
}
slog.Info("GeoIP provider configured", "provider", CurrentProvider.Name())
}
// GetGeoInfo looks up geographic information for ip using the active provider.
func GetGeoInfo(ip net.IP) (*GeoInfo, error) {
if ip == nil {
return nil, errors.New("IP address cannot be nil")
}
provider := getProvider()
cacheKey := provider.Name() + ":" + ip.String()
if cachedInfo, found := geoCache.Get(cacheKey); found {
return cachedInfo, nil
}
info, err := provider.GetGeoInfo(ip)
if err == nil && info != nil {
geoCache.Set(cacheKey, info)
}
return info, err
}
// LookupGeoInfoWithProvider looks up geographic information using a temporary provider.
func LookupGeoInfoWithProvider(providerName string, ip net.IP) (*GeoInfo, error) {
if ip == nil {
return nil, errors.New("IP address cannot be nil")
}
provider, err := providerFactory(normalizeProvider(providerName))
if err != nil {
return nil, err
}
defer func() {
if closeErr := provider.Close(); closeErr != nil {
slog.Warn("close temporary GeoIP provider failed", "provider", provider.Name(), "error", closeErr)
}
}()
return provider.GetGeoInfo(ip)
}
// UpdateDatabase refreshes the active provider database and clears cached lookups.
func UpdateDatabase() error {
err := getProvider().UpdateDatabase()
if err == nil {
geoCache.Flush()
slog.Info("GeoIP cache cleared due to database update.")
}
return err
}
// IsValidProvider reports whether provider names a supported GeoIP backend.
func IsValidProvider(provider string) bool {
switch normalizeProvider(provider) {
case ProviderDisabled, ProviderMaxMind, ProviderIPAPI, ProviderGeoJS, ProviderIPInfo:
return true
default:
return false
}
}
func normalizeProvider(provider string) string {
normalized := strings.TrimSpace(strings.ToLower(provider))
if normalized == "" {
return ProviderDisabled
}
return normalized
}
func newProvider(provider string) (Service, error) {
switch provider {
case ProviderDisabled:
return &EmptyProvider{}, nil
case ProviderMaxMind:
return NewMaxMindGeoIPService()
case ProviderIPAPI:
return NewIPAPIService()
case ProviderGeoJS:
return NewGeoJSService()
case ProviderIPInfo:
return NewIPInfoService()
default:
return nil, fmt.Errorf("unsupported GeoIP provider %q", provider)
}
}
func setProvider(provider Service) {
providerMutex.Lock()
previous := CurrentProvider
CurrentProvider = provider
providerMutex.Unlock()
geoCache.Flush()
if previous != nil && previous != provider {
if err := previous.Close(); err != nil {
slog.Warn("close previous GeoIP provider failed", "error", err)
}
}
}
func getProvider() Service {
providerMutex.RLock()
defer providerMutex.RUnlock()
if CurrentProvider == nil {
return &EmptyProvider{}
}
return CurrentProvider
}
func float64Pointer(value float64) *float64 {
return &value
}
// ProviderFactoryForTest returns the provider factory used by package helpers.
func ProviderFactoryForTest() func(string) (Service, error) {
return providerFactory
}
// SetProviderFactoryForTest replaces the provider factory for tests.
func SetProviderFactoryForTest(factory func(string) (Service, error)) {
if factory == nil {
providerFactory = newProvider
return
}
providerFactory = factory
}