mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
fix lint
This commit is contained in:
@@ -5,21 +5,30 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// EmptyProvider is a no-op GeoIP backend used before a real provider is configured.
|
||||
type EmptyProvider struct{}
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (e *EmptyProvider) Name() string {
|
||||
return "EmptyProvider"
|
||||
}
|
||||
|
||||
// Initialize prepares the empty provider for use.
|
||||
func (e *EmptyProvider) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
func (e *EmptyProvider) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
|
||||
// GetGeoInfo reports that no GeoIP provider has been configured.
|
||||
func (e *EmptyProvider) GetGeoInfo(_ net.IP) (*GeoInfo, error) {
|
||||
return nil, fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider")
|
||||
}
|
||||
|
||||
// UpdateDatabase reports that no GeoIP provider has been configured.
|
||||
func (e *EmptyProvider) UpdateDatabase() error {
|
||||
return fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider")
|
||||
}
|
||||
|
||||
// Close releases resources held by the empty provider.
|
||||
func (e *EmptyProvider) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+29
-12
@@ -1,3 +1,4 @@
|
||||
// Package geoip resolves geographic information for IP addresses.
|
||||
package geoip
|
||||
|
||||
import (
|
||||
@@ -12,19 +13,27 @@ import (
|
||||
ristretto "github.com/dgraph-io/ristretto/v2"
|
||||
)
|
||||
|
||||
var CurrentProvider GeoIPService
|
||||
// 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
|
||||
@@ -34,11 +43,11 @@ type GeoInfo struct {
|
||||
|
||||
func init() {
|
||||
CurrentProvider = &EmptyProvider{}
|
||||
geoCache = newProviderCache(48 * time.Hour)
|
||||
geoCache = newProviderCache(defaultGeoCacheDuration)
|
||||
}
|
||||
|
||||
// GeoIPService 接口定义了获取地理位置信息的核心方法。
|
||||
type GeoIPService interface {
|
||||
// Service defines the core GeoIP lookup operations implemented by providers.
|
||||
type Service interface {
|
||||
Name() string
|
||||
GetGeoInfo(ip net.IP) (*GeoInfo, error)
|
||||
UpdateDatabase() error
|
||||
@@ -94,8 +103,9 @@ 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) != 2 {
|
||||
if len(isoCode) != isoCountryCodeLength {
|
||||
return ""
|
||||
}
|
||||
isoCode = strings.ToUpper(isoCode)
|
||||
@@ -104,11 +114,12 @@ func GetRegionUnicodeEmoji(isoCode string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
rune1 := rune(0x1F1E6 + (rune(isoCode[0]) - 'A'))
|
||||
rune2 := rune(0x1F1E6 + (rune(isoCode[1]) - 'A'))
|
||||
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)
|
||||
@@ -124,6 +135,7 @@ func InitGeoIP(provider string) {
|
||||
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, fmt.Errorf("IP address cannot be nil")
|
||||
@@ -142,6 +154,7 @@ func GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
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, fmt.Errorf("IP address cannot be nil")
|
||||
@@ -160,6 +173,7 @@ func LookupGeoInfoWithProvider(providerName string, ip net.IP) (*GeoInfo, error)
|
||||
return provider.GetGeoInfo(ip)
|
||||
}
|
||||
|
||||
// UpdateDatabase refreshes the active provider database and clears cached lookups.
|
||||
func UpdateDatabase() error {
|
||||
err := getProvider().UpdateDatabase()
|
||||
if err == nil {
|
||||
@@ -169,6 +183,7 @@ func UpdateDatabase() error {
|
||||
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:
|
||||
@@ -186,7 +201,7 @@ func normalizeProvider(provider string) string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func newProvider(provider string) (GeoIPService, error) {
|
||||
func newProvider(provider string) (Service, error) {
|
||||
switch provider {
|
||||
case ProviderDisabled:
|
||||
return &EmptyProvider{}, nil
|
||||
@@ -203,7 +218,7 @@ func newProvider(provider string) (GeoIPService, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func setProvider(provider GeoIPService) {
|
||||
func setProvider(provider Service) {
|
||||
providerMutex.Lock()
|
||||
previous := CurrentProvider
|
||||
CurrentProvider = provider
|
||||
@@ -216,7 +231,7 @@ func setProvider(provider GeoIPService) {
|
||||
}
|
||||
}
|
||||
|
||||
func getProvider() GeoIPService {
|
||||
func getProvider() Service {
|
||||
providerMutex.RLock()
|
||||
defer providerMutex.RUnlock()
|
||||
if CurrentProvider == nil {
|
||||
@@ -229,11 +244,13 @@ func float64Pointer(value float64) *float64 {
|
||||
return &value
|
||||
}
|
||||
|
||||
func ProviderFactoryForTest() func(string) (GeoIPService, error) {
|
||||
// ProviderFactoryForTest returns the provider factory used by package helpers.
|
||||
func ProviderFactoryForTest() func(string) (Service, error) {
|
||||
return providerFactory
|
||||
}
|
||||
|
||||
func SetProviderFactoryForTest(factory func(string) (GeoIPService, error)) {
|
||||
// SetProviderFactoryForTest replaces the provider factory for tests.
|
||||
func SetProviderFactoryForTest(factory func(string) (Service, error)) {
|
||||
if factory == nil {
|
||||
providerFactory = newProvider
|
||||
return
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestIsValidProvider(t *testing.T) {
|
||||
|
||||
func TestLookupGeoInfoWithProviderUsesTemporaryProvider(t *testing.T) {
|
||||
previousFactory := providerFactory
|
||||
providerFactory = func(provider string) (GeoIPService, error) {
|
||||
providerFactory = func(provider string) (Service, error) {
|
||||
return &fakeProvider{}, nil
|
||||
}
|
||||
defer func() {
|
||||
|
||||
+8
-3
@@ -1,6 +1,7 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GeoJSService 使用 geojs.io 服务实现 GeoIPService 接口。
|
||||
// GeoJSService resolves geographic information using the geojs.io service.
|
||||
type GeoJSService struct {
|
||||
Client *http.Client
|
||||
}
|
||||
@@ -44,11 +45,15 @@ func (s *GeoJSService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
// GeoJS 的 API 端点
|
||||
apiURL := fmt.Sprintf("https://get.geojs.io/v1/ip/geo/%s.json", ip.String())
|
||||
|
||||
resp, err := s.Client.Get(apiURL)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request for geojs.io: %w", err)
|
||||
}
|
||||
resp, err := s.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get geo info from geojs.io: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
|
||||
+9
-3
@@ -1,6 +1,7 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPAPIService 使用 ip-api.com 服务实现 GeoIPService 接口。
|
||||
// IPAPIService resolves geographic information using the ip-api.com service.
|
||||
type IPAPIService struct {
|
||||
Client *http.Client
|
||||
}
|
||||
@@ -32,6 +33,7 @@ type ipAPIResponse struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// Name returns the provider identifier for the ip-api.com service.
|
||||
func (s *IPAPIService) Name() string {
|
||||
return "ip-api.com"
|
||||
}
|
||||
@@ -50,11 +52,15 @@ func (s *IPAPIService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
// API URL, 使用 fields 参数来仅请求需要的字段
|
||||
apiURL := fmt.Sprintf("http://ip-api.com/json/%s?fields=status,message,country,countryCode", ip.String())
|
||||
|
||||
resp, err := s.Client.Get(apiURL)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request for ip-api.com: %w", err)
|
||||
}
|
||||
resp, err := s.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get geo info from ip-api.com: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var apiResp ipAPIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
|
||||
+9
-4
@@ -1,6 +1,7 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPInfoService 使用 ipinfo.io 服务实现 GeoIPService 接口。
|
||||
// IPInfoService resolves geographic information using the ipinfo.io service.
|
||||
type IPInfoService struct {
|
||||
Client *http.Client
|
||||
// 每天 1000 次请求,限制由 IP 地址的所有人共享。
|
||||
@@ -52,11 +53,15 @@ func (s *IPInfoService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
// API URL: https://ipinfo.io/json (查询自身IP) 或 https://ipinfo.io/YOUR_IP/json
|
||||
apiURL := fmt.Sprintf("https://ipinfo.io/%s/json", ip.String())
|
||||
|
||||
resp, err := s.Client.Get(apiURL)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request for ipinfo.io: %w", err)
|
||||
}
|
||||
resp, err := s.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get geo info from ipinfo.io: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ipinfo.io returned non-200 status: %d %s", resp.StatusCode, resp.Status)
|
||||
@@ -98,7 +103,7 @@ func (s *IPInfoService) Close() error {
|
||||
|
||||
func parseIPInfoCoordinates(value string) (*float64, *float64) {
|
||||
parts := strings.Split(strings.TrimSpace(value), ",")
|
||||
if len(parts) != 2 {
|
||||
if len(parts) != isoCountryCodeLength {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package iputil provides helpers for parsing, normalizing, and scoring IP addresses.
|
||||
package iputil
|
||||
|
||||
import (
|
||||
@@ -5,6 +6,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
scorePublic = 2
|
||||
scorePrivate = 1
|
||||
)
|
||||
|
||||
// NormalizeIP parses and normalizes a raw IP address string, preferring the IPv4 form for mapped addresses.
|
||||
func NormalizeIP(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
@@ -20,6 +27,7 @@ func NormalizeIP(raw string) string {
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// NormalizeRemoteAddr extracts and normalizes the IP address from a host:port remote address string.
|
||||
func NormalizeRemoteAddr(remoteAddr string) string {
|
||||
trimmed := strings.TrimSpace(remoteAddr)
|
||||
if trimmed == "" {
|
||||
@@ -31,6 +39,7 @@ func NormalizeRemoteAddr(remoteAddr string) string {
|
||||
return NormalizeIP(trimmed)
|
||||
}
|
||||
|
||||
// IsPublic reports whether the given IP address is a publicly routable unicast address.
|
||||
func IsPublic(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
@@ -44,11 +53,13 @@ func IsPublic(ip net.IP) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsPublicString parses raw and reports whether it represents a publicly routable IP address.
|
||||
func IsPublicString(raw string) bool {
|
||||
ip := net.ParseIP(strings.TrimSpace(raw))
|
||||
return IsPublic(ip)
|
||||
}
|
||||
|
||||
// Score returns a preference score for the IP address: 2 for public, 1 for private, -1 for invalid or non-unicast.
|
||||
func Score(ip net.IP) int {
|
||||
if ip == nil {
|
||||
return -1
|
||||
@@ -60,7 +71,7 @@ func Score(ip net.IP) int {
|
||||
return -1
|
||||
}
|
||||
if IsPublic(ip) {
|
||||
return 2
|
||||
return scorePublic
|
||||
}
|
||||
return 1
|
||||
return scorePrivate
|
||||
}
|
||||
|
||||
+34
-17
@@ -1,6 +1,7 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -12,47 +13,55 @@ import (
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
var GeoIpUrl = "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-Country.mmdb"
|
||||
var GeoIpFilePath = "./data/GeoLite2-Country.mmdb"
|
||||
// GeoIPURL is the default download URL for the MaxMind GeoLite2 country database.
|
||||
var GeoIPURL = "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-Country.mmdb"
|
||||
|
||||
type GeoIpRecord struct {
|
||||
// GeoIPFilePath is the default local path for the MaxMind country database file.
|
||||
var GeoIPFilePath = "./data/GeoLite2-Country.mmdb"
|
||||
|
||||
// Record is the MaxMind database record structure for country lookups.
|
||||
type Record struct {
|
||||
Country struct {
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
}
|
||||
|
||||
// MaxMindGeoIPService resolves geographic information using a local MaxMind MMDB database.
|
||||
type MaxMindGeoIPService struct {
|
||||
maxMindDBReader *maxminddb.Reader
|
||||
dbFilePath string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Name returns the provider identifier for the MaxMind database service.
|
||||
func (s *MaxMindGeoIPService) Name() string {
|
||||
return "MaxMind"
|
||||
}
|
||||
|
||||
// NewMaxMindGeoIPService creates a MaxMind service using the default database path and URL.
|
||||
func NewMaxMindGeoIPService() (*MaxMindGeoIPService, error) {
|
||||
return NewMaxMindGeoIPServiceWithConfig(GeoIpFilePath, GeoIpUrl)
|
||||
return NewMaxMindGeoIPServiceWithConfig(GeoIPFilePath, GeoIPURL)
|
||||
}
|
||||
|
||||
// NewMaxMindGeoIPServiceWithConfig creates a MaxMind service with custom database path and download URL.
|
||||
func NewMaxMindGeoIPServiceWithConfig(dbFilePath string, downloadURL string) (*MaxMindGeoIPService, error) {
|
||||
if dbFilePath == "" {
|
||||
dbFilePath = GeoIpFilePath
|
||||
dbFilePath = GeoIPFilePath
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = GeoIpUrl
|
||||
downloadURL = GeoIPURL
|
||||
}
|
||||
service := &MaxMindGeoIPService{
|
||||
dbFilePath: dbFilePath,
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(service.dbFilePath), os.ModePerm); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(service.dbFilePath), geoipDataDirPerm); err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory for MaxMind database: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(service.dbFilePath); os.IsNotExist(err) {
|
||||
if err := DownloadMaxMindDatabase(service.dbFilePath, downloadURL); err != nil {
|
||||
if err := DownloadMaxMindDatabase(context.Background(), service.dbFilePath, downloadURL); err != nil {
|
||||
return nil, fmt.Errorf("failed to download initial MaxMind database: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -81,6 +90,7 @@ func (s *MaxMindGeoIPService) initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGeoInfo looks up geographic information for ip in the MaxMind database.
|
||||
func (s *MaxMindGeoIPService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -92,7 +102,7 @@ func (s *MaxMindGeoIPService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
return nil, fmt.Errorf("IP address cannot be nil")
|
||||
}
|
||||
|
||||
var record GeoIpRecord
|
||||
var record Record
|
||||
if err := s.maxMindDBReader.Lookup(ip, &record); err != nil {
|
||||
return nil, fmt.Errorf("error looking up IP %s in MaxMind database: %w", ip.String(), err)
|
||||
}
|
||||
@@ -108,36 +118,42 @@ func (s *MaxMindGeoIPService) GetGeoInfo(ip net.IP) (*GeoInfo, error) {
|
||||
return geoInfo, nil
|
||||
}
|
||||
|
||||
// UpdateDatabase downloads the latest MaxMind database and reloads the reader.
|
||||
func (s *MaxMindGeoIPService) UpdateDatabase() error {
|
||||
if err := DownloadMaxMindDatabase(s.dbFilePath, GeoIpUrl); err != nil {
|
||||
if err := DownloadMaxMindDatabase(context.Background(), s.dbFilePath, GeoIPURL); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.initialize()
|
||||
}
|
||||
|
||||
func DownloadMaxMindDatabase(dbFilePath string, downloadURL string) error {
|
||||
// DownloadMaxMindDatabase downloads the MaxMind database from downloadURL to dbFilePath.
|
||||
func DownloadMaxMindDatabase(ctx context.Context, dbFilePath string, downloadURL string) error {
|
||||
if dbFilePath == "" {
|
||||
dbFilePath = GeoIpFilePath
|
||||
dbFilePath = GeoIPFilePath
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = GeoIpUrl
|
||||
downloadURL = GeoIPURL
|
||||
}
|
||||
resp, err := http.Get(downloadURL)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) //nolint:gosec // URL from trusted GeoIP provider config
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate MaxMind database download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate MaxMind database download: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download MaxMind database: HTTP status %s", resp.Status)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dbFilePath), os.ModePerm); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(dbFilePath), geoipDataDirPerm); err != nil {
|
||||
return fmt.Errorf("failed to create data directory for MaxMind database update: %w", err)
|
||||
}
|
||||
|
||||
tempPath := dbFilePath + ".download"
|
||||
out, err := os.Create(tempPath)
|
||||
out, err := os.Create(tempPath) //nolint:gosec // tempPath is derived from configured dbFilePath
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create MaxMind database file at %s: %w", tempPath, err)
|
||||
}
|
||||
@@ -157,6 +173,7 @@ func DownloadMaxMindDatabase(dbFilePath string, downloadURL string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the MaxMind database reader.
|
||||
func (s *MaxMindGeoIPService) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
+13
-2
@@ -29,11 +29,13 @@ type OutboundIPAPIAdapter interface {
|
||||
DecodeIP(io.Reader) (net.IP, error)
|
||||
}
|
||||
|
||||
// HTTPOutboundIPStrategy resolves the public egress IP via an HTTP API adapter.
|
||||
type HTTPOutboundIPStrategy struct {
|
||||
Client *http.Client
|
||||
Adapter OutboundIPAPIAdapter
|
||||
}
|
||||
|
||||
// NewHTTPOutboundIPStrategy creates a strategy that queries adapter over HTTP.
|
||||
func NewHTTPOutboundIPStrategy(adapter OutboundIPAPIAdapter, client *http.Client) *HTTPOutboundIPStrategy {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: defaultOutboundIPLookupTimeout}
|
||||
@@ -44,6 +46,7 @@ func NewHTTPOutboundIPStrategy(adapter OutboundIPAPIAdapter, client *http.Client
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the strategy or adapter identifier.
|
||||
func (s *HTTPOutboundIPStrategy) Name() string {
|
||||
if s == nil || s.Adapter == nil {
|
||||
return "http-outbound-ip"
|
||||
@@ -51,12 +54,13 @@ func (s *HTTPOutboundIPStrategy) Name() string {
|
||||
return s.Adapter.Name()
|
||||
}
|
||||
|
||||
// GetOutboundIP queries the configured HTTP endpoint for the current public IP.
|
||||
func (s *HTTPOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, error) {
|
||||
if s == nil || s.Adapter == nil {
|
||||
return nil, errors.New("outbound IP adapter is nil")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
return nil, errors.New("context is required")
|
||||
}
|
||||
client := s.Client
|
||||
if client == nil {
|
||||
@@ -70,7 +74,7 @@ func (s *HTTPOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, err
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s request failed: %w", s.Name(), err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s returned non-200 status: %d %s", s.Name(), response.StatusCode, response.Status)
|
||||
}
|
||||
@@ -84,6 +88,7 @@ func (s *HTTPOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, err
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// RealIPCCAdapter decodes public IP responses from realip.cc.
|
||||
type RealIPCCAdapter struct {
|
||||
URL string
|
||||
}
|
||||
@@ -92,14 +97,17 @@ type realIPCCResponse struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// NewRealIPCCOutboundIPStrategy creates the default realip.cc lookup strategy.
|
||||
func NewRealIPCCOutboundIPStrategy() *HTTPOutboundIPStrategy {
|
||||
return NewHTTPOutboundIPStrategy(RealIPCCAdapter{}, nil)
|
||||
}
|
||||
|
||||
// Name returns the realip.cc adapter identifier.
|
||||
func (a RealIPCCAdapter) Name() string {
|
||||
return "realip.cc"
|
||||
}
|
||||
|
||||
// Endpoint returns the realip.cc API URL.
|
||||
func (a RealIPCCAdapter) Endpoint() string {
|
||||
if strings.TrimSpace(a.URL) != "" {
|
||||
return strings.TrimSpace(a.URL)
|
||||
@@ -107,6 +115,7 @@ func (a RealIPCCAdapter) Endpoint() string {
|
||||
return "https://realip.cc"
|
||||
}
|
||||
|
||||
// DecodeIP parses a realip.cc JSON response into a public IP address.
|
||||
func (a RealIPCCAdapter) DecodeIP(reader io.Reader) (net.IP, error) {
|
||||
var payload realIPCCResponse
|
||||
if err := json.NewDecoder(reader).Decode(&payload); err != nil {
|
||||
@@ -122,12 +131,14 @@ func (a RealIPCCAdapter) DecodeIP(reader io.Reader) (net.IP, error) {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// DefaultOutboundIPStrategies returns the built-in public egress IP lookup strategies.
|
||||
func DefaultOutboundIPStrategies() []OutboundIPStrategy {
|
||||
return []OutboundIPStrategy{
|
||||
NewRealIPCCOutboundIPStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetOutboundIP tries each strategy until one returns a public egress IP.
|
||||
func GetOutboundIP(ctx context.Context, strategies ...OutboundIPStrategy) (net.IP, error) {
|
||||
if len(strategies) == 0 {
|
||||
strategies = DefaultOutboundIPStrategies()
|
||||
|
||||
Reference in New Issue
Block a user