mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
89 lines
3.0 KiB
Go
89 lines
3.0 KiB
Go
package exportproxy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const ipInfoURL = "https://ipinfo.io/json"
|
|
|
|
type PublicIPInfo struct {
|
|
IP string `json:"ip"`
|
|
CountryCode string `json:"country_code"`
|
|
Region string `json:"region"`
|
|
City string `json:"city"`
|
|
Organization string `json:"organization,omitempty"`
|
|
}
|
|
|
|
// LookupPublicIP sends the lookup through the same marked, interface-bound
|
|
// dialer and isolated DNS resolver as Export Proxy. It therefore reports the
|
|
// modem's roaming exit rather than the host or browser's default connection.
|
|
func LookupPublicIP(ctx context.Context, networkInterface string) (PublicIPInfo, error) {
|
|
networkInterface = strings.TrimSpace(networkInterface)
|
|
if networkInterface == "" {
|
|
return PublicIPInfo{}, errors.New("cellular network interface is required")
|
|
}
|
|
if err := platformSupported(); err != nil {
|
|
return PublicIPInfo{}, err
|
|
}
|
|
dialer := boundDialer(networkInterface)
|
|
resolver := boundResolver(networkInterface)
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, _, address string) (net.Conn, error) {
|
|
return dialTarget(ctx, address, &dialer, resolver)
|
|
},
|
|
DisableKeepAlives: true,
|
|
ResponseHeaderTimeout: 12 * time.Second,
|
|
}
|
|
defer transport.CloseIdleConnections()
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, ipInfoURL, nil)
|
|
if err != nil {
|
|
return PublicIPInfo{}, err
|
|
}
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("User-Agent", "VoCat/1.0")
|
|
response, err := transport.RoundTrip(request)
|
|
if err != nil {
|
|
return PublicIPInfo{}, fmt.Errorf("query ipinfo.io through %s: %w", networkInterface, err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4<<10))
|
|
return PublicIPInfo{}, fmt.Errorf("ipinfo.io returned HTTP %d", response.StatusCode)
|
|
}
|
|
return decodePublicIPInfo(io.LimitReader(response.Body, 64<<10))
|
|
}
|
|
|
|
func decodePublicIPInfo(reader io.Reader) (PublicIPInfo, error) {
|
|
var response struct {
|
|
IP string `json:"ip"`
|
|
Country string `json:"country"`
|
|
Region string `json:"region"`
|
|
City string `json:"city"`
|
|
Org string `json:"org"`
|
|
}
|
|
if err := json.NewDecoder(reader).Decode(&response); err != nil {
|
|
return PublicIPInfo{}, fmt.Errorf("decode ipinfo.io response: %w", err)
|
|
}
|
|
response.IP = strings.TrimSpace(response.IP)
|
|
response.Country = strings.ToUpper(strings.TrimSpace(response.Country))
|
|
if net.ParseIP(response.IP) == nil {
|
|
return PublicIPInfo{}, errors.New("ipinfo.io response contained no valid IP address")
|
|
}
|
|
if len(response.Country) != 2 {
|
|
return PublicIPInfo{}, errors.New("ipinfo.io response contained no valid country code")
|
|
}
|
|
return PublicIPInfo{
|
|
IP: response.IP, CountryCode: response.Country,
|
|
Region: strings.TrimSpace(response.Region), City: strings.TrimSpace(response.City),
|
|
Organization: strings.TrimSpace(response.Org),
|
|
}, nil
|
|
}
|