shanshanzhong 15f4e69dc3
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m3s
feat(server): 添加服务器地理位置信息字段
为服务器模型添加经度、纬度及中心点坐标字段,并在相关逻辑中处理这些字段
同时修复服务器用户列表缓存功能
2025-11-03 23:50:23 -08:00

200 lines
5.4 KiB
Go

package ip
import (
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"go.uber.org/zap"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd"
"github.com/pkg/errors"
)
// GetIP parses the input domain or IP address and returns the IP address.
func GetIP(input string) ([]string, error) {
// Check if the input is already a valid IP address.
if net.ParseIP(input) != nil {
return []string{input}, nil
}
// Use net.LookupIP to resolve the domain name.
ips, err := net.LookupIP(input)
if err != nil {
return nil, err
}
// Convert IP addresses to string format.
var result []string
for _, ip := range ips {
result = append(result, ip.String())
}
return result, nil
}
const (
ipapi = "ipapi.co"
ipbase = "api.ipbase.com"
ipwhois = "ipwhois.app"
ipinfo = "ipinfo.io"
)
var (
queryUrls = map[string]bool{
ipbase: true,
ipapi: true,
ipwhois: true,
ipinfo: true,
}
)
// GetRegionByIp queries the geolocation of an IP address using supported services.
func GetRegionByIp(ip string) (*GeoLocationResponse, error) {
// 如果是域名,先解析成 IP
if net.ParseIP(ip) == nil {
ips, err := GetIP(ip)
if err != nil || len(ips) == 0 {
return nil, errors.Wrap(err, "无法解析域名为IP")
}
ip = ips[0] // 取第一个解析到的IP
}
for service, enabled := range queryUrls {
if enabled {
response, err := fetchGeolocation(service, ip)
if err != nil {
zap.S().Errorf("Failed to fetch geolocation from %s: %v", service, err)
continue
}
if response.Country == "" {
continue
}
response.LatitudeCenter, response.LongitudeCenter, _ = GetCapitalCoordinates(fmt.Sprintf("%s,%s", response.Country, response.City))
if response.LatitudeCenter == "" || response.LongitudeCenter == "" {
response.LatitudeCenter = response.Latitude
response.LongitudeCenter = response.Longitude
}
return response, nil
}
}
return nil, errors.New("unable to fetch geolocation for the IP")
}
// fetchGeolocation sends a request to the specified service to retrieve geolocation data.
func fetchGeolocation(service, ip string) (*GeoLocationResponse, error) {
var apiURL string
// Construct the API URL based on the service.
switch service {
case ipinfo:
apiURL = fmt.Sprintf("https://ipinfo.io/%s/json", ip)
case ipapi:
apiURL = fmt.Sprintf("https://ipapi.co/%s/json", ip)
case ipbase:
apiURL = fmt.Sprintf("https://api.ipbase.com/v1/json/%s", ip)
case ipwhois:
apiURL = fmt.Sprintf("https://ipwhois.app/json/%s", ip)
default:
return nil, fmt.Errorf("unsupported service: %s", service)
}
// Create the HTTP request.
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
setHeaders(req, service)
// Create the HTTP client and send the request.
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
// Decompress the response body based on Content-Encoding.
body, err := decompressResponse(resp)
if err != nil {
return nil, err
}
// Parse the JSON response into GeoLocationResponse.
var location GeoLocationResponse
if err := json.Unmarshal(body, &location); err != nil {
return nil, fmt.Errorf("failed to parse response: %v", err)
}
// Ensure compatibility between country fields.
if location.Country == "" {
location.Country = location.CountryName
}
if location.Loc != "" && strings.Contains(location.Loc, ",") {
loc := strings.Split(location.Loc, ",")
location.Latitude = loc[0]
location.Longitude = loc[1]
}
return &location, nil
}
// setHeaders sets the necessary headers for the HTTP request.
func setHeaders(req *http.Request, host string) {
req.Header.Set("Host", host)
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0")
req.Header.Set("Accept", "application/json, text/html, application/xhtml+xml, */*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Upgrade-Insecure-Requests", "1")
}
// decompressResponse decompresses the HTTP response body based on its Content-Encoding.
func decompressResponse(resp *http.Response) ([]byte, error) {
var reader io.ReadCloser
var err error
switch resp.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
case "br":
reader = io.NopCloser(brotli.NewReader(resp.Body))
case "zstd":
decoder, zstdErr := zstd.NewReader(resp.Body)
if zstdErr != nil {
return nil, fmt.Errorf("failed to create zstd decoder: %v", zstdErr)
}
defer decoder.Close()
return io.ReadAll(decoder)
default:
reader = resp.Body
}
if err != nil {
return nil, fmt.Errorf("failed to create reader: %v", err)
}
defer reader.Close()
return io.ReadAll(reader)
}
// GeoLocationResponse represents the geolocation data returned by the API.
type GeoLocationResponse struct {
Country string `json:"country"`
CountryName string `json:"country_name"`
Region string `json:"region"`
City string `json:"city"`
Latitude string `json:"latitude"`
Longitude string `json:"longitude"`
LatitudeCenter string `json:"latitude_center"`
LongitudeCenter string `json:"longitude_center"`
Loc string `json:"loc"`
}