release: prepare v0.1.132
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// Package ipgeo provides best-effort IP geolocation lookup for audit display.
|
||||
package ipgeo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
IPAddress string
|
||||
Country string
|
||||
CountryCode string
|
||||
Region string
|
||||
City string
|
||||
Location string
|
||||
}
|
||||
|
||||
func Lookup(ctx context.Context, rawIP string) (*Info, error) {
|
||||
ip := strings.TrimSpace(rawIP)
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return nil, fmt.Errorf("invalid ip")
|
||||
}
|
||||
if isLocalIP(parsed) {
|
||||
return &Info{IPAddress: ip}, nil
|
||||
}
|
||||
|
||||
endpoint := "http://ip-api.com/json/" + url.PathEscape(ip) + "?lang=zh-CN&fields=status,message,country,countryCode,regionName,region,city,query"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("lookup failed: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Query string `json:"query"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Region string `json:"region"`
|
||||
RegionName string `json:"regionName"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.ToLower(body.Status) != "success" {
|
||||
if body.Message == "" {
|
||||
body.Message = "ip lookup failed"
|
||||
}
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
|
||||
region := strings.TrimSpace(body.RegionName)
|
||||
if region == "" {
|
||||
region = strings.TrimSpace(body.Region)
|
||||
}
|
||||
info := &Info{
|
||||
IPAddress: firstNonEmpty(body.Query, ip),
|
||||
Country: strings.TrimSpace(body.Country),
|
||||
CountryCode: strings.TrimSpace(body.CountryCode),
|
||||
Region: region,
|
||||
City: strings.TrimSpace(body.City),
|
||||
}
|
||||
info.Location = formatLocation(info.Country, info.Region, info.City)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func isLocalIP(ip net.IP) bool {
|
||||
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified()
|
||||
}
|
||||
|
||||
func formatLocation(parts ...string) string {
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := make(map[string]struct{}, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(part)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, part)
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user