mirror of
https://github.com/ghostersk/gowebmail.git
synced 2026-04-17 16:46:01 +01:00
added IP Block and notification for failed logins
This commit is contained in:
97
internal/geo/geo.go
Normal file
97
internal/geo/geo.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Package geo provides IP geolocation lookup using the free ip-api.com service.
|
||||
// No API key is required. Rate limit: 45 requests/minute on the free tier.
|
||||
// Results are cached in memory to reduce API calls.
|
||||
package geo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GeoResult struct {
|
||||
CountryCode string
|
||||
Country string
|
||||
Cached bool
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
result GeoResult
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
cache = make(map[string]*cacheEntry)
|
||||
)
|
||||
|
||||
const cacheTTL = 24 * time.Hour
|
||||
|
||||
// Lookup returns the country for an IP address.
|
||||
// Returns empty strings on failure (private IPs, rate limit, etc.).
|
||||
func Lookup(ip string) GeoResult {
|
||||
// Skip private / loopback
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil || isPrivate(parsed) {
|
||||
return GeoResult{}
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if e, ok := cache[ip]; ok && time.Since(e.fetchedAt) < cacheTTL {
|
||||
mu.Unlock()
|
||||
r := e.result
|
||||
r.Cached = true
|
||||
return r
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
result := fetchFromAPI(ip)
|
||||
|
||||
mu.Lock()
|
||||
cache[ip] = &cacheEntry{result: result, fetchedAt: time.Now()}
|
||||
mu.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchFromAPI(ip string) GeoResult {
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=status,country,countryCode", ip)
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Printf("geo lookup failed for %s: %v", ip, err)
|
||||
return GeoResult{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data struct {
|
||||
Status string `json:"status"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil || data.Status != "success" {
|
||||
return GeoResult{}
|
||||
}
|
||||
return GeoResult{
|
||||
CountryCode: strings.ToUpper(data.CountryCode),
|
||||
Country: data.Country,
|
||||
}
|
||||
}
|
||||
|
||||
func isPrivate(ip net.IP) bool {
|
||||
privateRanges := []string{
|
||||
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"127.0.0.0/8", "::1/128", "fc00::/7",
|
||||
}
|
||||
for _, cidr := range privateRanges {
|
||||
_, network, _ := net.ParseCIDR(cidr)
|
||||
if network != nil && network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user