2025-10-10 07:13:36 -07:00

180 lines
4.7 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) {
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
}
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"`
Loc string `json:"loc"`
}