mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-10 10:53:36 +02:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
338 lines
6.4 KiB
Markdown
338 lines
6.4 KiB
Markdown
# MaxMind
|
|
|
|
The maxmind package provides MaxMind GeoIP database integration for IP geolocation, including automatic database downloading and updates.
|
|
|
|
## Overview
|
|
|
|
The maxmind package implements MaxMind GeoIP database management, providing IP geolocation lookups for country and city information. It supports automatic database downloading, scheduled updates, and thread-safe access.
|
|
|
|
### Key Features
|
|
|
|
- MaxMind GeoIP database loading
|
|
- Automatic database downloading from MaxMind
|
|
- Scheduled updates every 24 hours
|
|
- City lookup with cache support
|
|
- IP geolocation (country, city, timezone)
|
|
- Thread-safe access
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[MaxMind Config] --> B[Load Database]
|
|
B --> C{Exists?}
|
|
C -->|No| D[Download]
|
|
C -->|Yes| E[Load]
|
|
D --> F[Extract from TarGz]
|
|
E --> G[Open Reader]
|
|
|
|
H[IP Lookup] --> I[City Lookup]
|
|
I --> J{Cache Hit?}
|
|
J -->|Yes| K[Return Cached]
|
|
J -->|No| L[Database Query]
|
|
L --> M[Cache Result]
|
|
M --> K
|
|
|
|
N[Update Scheduler] --> O[Check Daily]
|
|
O --> P{Different?}
|
|
P -->|Yes| Q[Download Update]
|
|
P -->|No| O
|
|
```
|
|
|
|
## Core Components
|
|
|
|
### MaxMind Structure
|
|
|
|
```go
|
|
type MaxMind struct {
|
|
*Config
|
|
lastUpdate time.Time
|
|
db struct {
|
|
*maxminddb.Reader
|
|
sync.RWMutex
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```go
|
|
type Config struct {
|
|
Database string // Database type (GeoLite2 or GeoIP2)
|
|
AccountID int
|
|
LicenseKey Secret
|
|
}
|
|
```
|
|
|
|
### IP Information
|
|
|
|
```go
|
|
type IPInfo struct {
|
|
IP net.IP
|
|
Str string
|
|
Country *Country
|
|
City *City
|
|
Location *Location
|
|
}
|
|
|
|
type Country struct {
|
|
IsoCode string
|
|
Name string
|
|
}
|
|
|
|
type City struct {
|
|
Country *Country
|
|
Name string
|
|
Location *Location
|
|
}
|
|
|
|
type Location struct {
|
|
TimeZone string
|
|
Latitude float64
|
|
Longitude float64
|
|
}
|
|
```
|
|
|
|
## Public API
|
|
|
|
### Initialization
|
|
|
|
```go
|
|
// LoadMaxMindDB loads or downloads the MaxMind database.
|
|
func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error
|
|
```
|
|
|
|
### Lookup
|
|
|
|
```go
|
|
// LookupCity looks up city information for an IP.
|
|
func LookupCity(info *IPInfo) (city *City, ok bool)
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic Setup
|
|
|
|
```go
|
|
maxmindCfg := &maxmind.Config{
|
|
Database: maxmind.MaxMindGeoLite,
|
|
AccountID: 123456,
|
|
LicenseKey: "your-license-key",
|
|
}
|
|
|
|
err := maxmindCfg.LoadMaxMindDB(parent)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
### IP Lookup
|
|
|
|
```go
|
|
// Create IP info
|
|
ipInfo := &maxmind.IPInfo{
|
|
IP: net.ParseIP("8.8.8.8"),
|
|
Str: "8.8.8.8",
|
|
}
|
|
|
|
// Lookup city
|
|
city, ok := maxmind.LookupCity(ipInfo)
|
|
if ok {
|
|
fmt.Printf("Country: %s\n", city.Country.IsoCode)
|
|
fmt.Printf("City: %s\n", city.Name)
|
|
fmt.Printf("Timezone: %s\n", city.Location.TimeZone)
|
|
}
|
|
```
|
|
|
|
### Database Types
|
|
|
|
```go
|
|
const (
|
|
MaxMindGeoLite = "GeoLite2-Country"
|
|
MaxMindGeoIP2 = "GeoIP2-Country"
|
|
)
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Config
|
|
participant MaxMind
|
|
participant Database
|
|
participant Cache
|
|
participant UpdateScheduler
|
|
|
|
Config->>MaxMind: LoadMaxMindDB()
|
|
MaxMind->>Database: Open()
|
|
alt Database Missing
|
|
MaxMind->>MaxMind: Download()
|
|
MaxMind->>Database: Extract & Create
|
|
end
|
|
Database-->>MaxMind: Reader
|
|
|
|
Note over MaxMind: Start Update Scheduler
|
|
|
|
loop Every 24 Hours
|
|
UpdateScheduler->>MaxMind: Check Update
|
|
MaxMind->>MaxMind: Check Last-Modified
|
|
alt Update Available
|
|
MaxMind->>MaxMind: Download
|
|
MaxMind->>Database: Replace
|
|
end
|
|
end
|
|
|
|
participant Lookup
|
|
Lookup->>MaxMind: LookupCity(ip)
|
|
MaxMind->>Cache: Check
|
|
alt Cache Hit
|
|
Cache-->>Lookup: City Info
|
|
else Cache Miss
|
|
MaxMind->>Database: Query
|
|
Database-->>MaxMind: City Info
|
|
MaxMind->>Cache: Store
|
|
MaxMind-->>Lookup: City Info
|
|
end
|
|
```
|
|
|
|
## Database Download
|
|
|
|
### Download Process
|
|
|
|
```go
|
|
func (cfg *MaxMind) download() error {
|
|
resp, err := cfg.doReq(http.MethodGet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read response
|
|
databaseGZ, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Extract from tar.gz
|
|
err = extractFileFromTarGz(databaseGZ, cfg.dbFilename(), tmpDBPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate
|
|
db, err := maxmindDBOpen(tmpDBPath)
|
|
if err != nil {
|
|
os.Remove(tmpDBPath)
|
|
return err
|
|
}
|
|
db.Close()
|
|
|
|
// Rename to final location
|
|
os.Rename(tmpDBPath, dbFile)
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Security Checks
|
|
|
|
The download process includes tar bomb protection:
|
|
|
|
```go
|
|
sumSize := int64(0)
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
sumSize += hdr.Size
|
|
if sumSize > 30*1024*1024 {
|
|
return errors.New("file size exceeds 30MB")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Update Scheduling
|
|
|
|
```go
|
|
func (cfg *MaxMind) scheduleUpdate(parent task.Parent) {
|
|
task := parent.Subtask("maxmind_schedule_update", true)
|
|
ticker := time.NewTicker(updateInterval) // 24 hours
|
|
|
|
cfg.loadLastUpdate()
|
|
cfg.update()
|
|
|
|
for {
|
|
select {
|
|
case <-task.Context().Done():
|
|
return
|
|
case <-ticker.C:
|
|
cfg.update()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Thread Safety
|
|
|
|
The database uses a read-write mutex:
|
|
|
|
```go
|
|
type MaxMind struct {
|
|
*Config
|
|
db struct {
|
|
*maxminddb.Reader
|
|
sync.RWMutex
|
|
}
|
|
}
|
|
|
|
// Lookups use RLock
|
|
func (cfg *MaxMind) lookup(ip net.IP) (*maxminddb.City, error) {
|
|
cfg.db.RLock()
|
|
defer cfg.db.RUnlock()
|
|
return cfg.db.Lookup(ip)
|
|
}
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Description |
|
|
| --------------------- | ------------------- |
|
|
| `MAXMIND_ACCOUNT_ID` | MaxMind account ID |
|
|
| `MAXMIND_LICENSE_KEY` | MaxMind license key |
|
|
|
|
### YAML Configuration
|
|
|
|
```yaml
|
|
providers:
|
|
maxmind:
|
|
database: geolite2
|
|
account_id: 123456
|
|
license_key: your-license-key
|
|
```
|
|
|
|
## Integration Points
|
|
|
|
The maxmind package integrates with:
|
|
|
|
- **ACL**: IP-based access control (country/timezone matching)
|
|
- **Config**: Configuration management
|
|
- **Logging**: Update notifications
|
|
- **City Cache**: IP geolocation caching
|
|
|
|
## Error Handling
|
|
|
|
```go
|
|
var (
|
|
ErrResponseNotOK = errors.New("response not OK")
|
|
ErrDownloadFailure = errors.New("download failure")
|
|
)
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
- 24-hour update interval reduces unnecessary downloads
|
|
- Database size ~10-30MB
|
|
- City lookup cache reduces database queries
|
|
- RLock for concurrent reads
|