main
go 142 lines 3.62 KB
Raw
1 package utils
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "net"
9 "net/http"
10 "strings"
11 "time"
12
13 "github.com/gosuda/portal-tunnel/v2/types"
14 )
15
16 var (
17 publicIPEndpoints = []string{
18 "https://api.ipify.org",
19 "https://ifconfig.me/ip",
20 "https://icanhazip.com",
21 "https://checkip.amazonaws.com",
22 }
23 publicIPv4Endpoints = []string{
24 "https://api4.ipify.org",
25 "https://ipv4.icanhazip.com",
26 "https://v4.ident.me",
27 "https://checkip.amazonaws.com",
28 }
29 )
30
31 // ResolvePublicIP attempts to determine the caller's public IP address
32 // using well-known external services. Returns empty string on failure.
33 // Best-effort with a short timeout to avoid blocking registration.
34 func ResolvePublicIP(ctx context.Context) string {
35 endpoints := append(append([]string{}, publicIPEndpoints...), publicIPv4Endpoints...)
36 ip, err := resolvePublicIP(ctx, 5*time.Second, 1500*time.Millisecond, false, endpoints...)
37 if err != nil {
38 return ""
39 }
40 return ip
41 }
42
43 func ResolvePublicIPv4(ctx context.Context) (string, error) {
44 endpoints := append(append([]string{}, publicIPv4Endpoints...), publicIPEndpoints...)
45 return resolvePublicIP(ctx, 15*time.Second, 3*time.Second, true, endpoints...)
46 }
47
48 func resolvePublicIP(ctx context.Context, totalTimeout, attemptTimeout time.Duration, requireIPv4 bool, endpoints ...string) (string, error) {
49 ctx, cancel := context.WithTimeout(ctx, totalTimeout)
50 defer cancel()
51
52 client := DefaultHTTPClient
53 headers := http.Header{"User-Agent": []string{"portal-tunnel"}}
54 var lastErr error
55
56 for _, endpoint := range endpoints {
57 if err := ctx.Err(); err != nil {
58 lastErr = err
59 break
60 }
61
62 requestTimeout := attemptTimeout
63 if deadline, ok := ctx.Deadline(); ok {
64 remaining := time.Until(deadline)
65 if remaining <= 0 {
66 lastErr = context.DeadlineExceeded
67 break
68 }
69 if requestTimeout <= 0 || requestTimeout > remaining {
70 requestTimeout = remaining
71 }
72 }
73
74 requestCtx, cancelRequest := context.WithTimeout(ctx, requestTimeout)
75 resp, err := httpDo(requestCtx, client, http.MethodGet, endpoint, nil, headers)
76 cancelRequest()
77 if err != nil {
78 lastErr = err
79 continue
80 }
81
82 limitedBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 256))
83 _ = resp.Body.Close()
84 if resp.StatusCode != http.StatusOK {
85 lastErr = errors.New(resp.Status)
86 continue
87 }
88 if readErr != nil {
89 lastErr = readErr
90 continue
91 }
92
93 candidate := SanitizeReportedIP(string(limitedBody))
94 if candidate == "" {
95 lastErr = errors.New("invalid public ip response")
96 continue
97 }
98 if requireIPv4 {
99 parsed := net.ParseIP(candidate)
100 if parsed == nil || parsed.To4() == nil {
101 lastErr = errors.New("public ip is not ipv4")
102 continue
103 }
104 }
105 return candidate, nil
106 }
107
108 if lastErr == nil {
109 lastErr = errors.New("resolve public ip failed")
110 }
111 return "", lastErr
112 }
113
114 func SanitizeReportedIP(raw string) string {
115 candidate := strings.TrimSpace(raw)
116 if candidate == "" {
117 return ""
118 }
119 if net.ParseIP(candidate) == nil {
120 return ""
121 }
122 return candidate
123 }
124
125 // FetchRelayVersion calls GET /sdk/domain on a relay and returns its release version.
126 // Returns an empty string on any error (timeout, unreachable, bad response).
127 func FetchRelayVersion(ctx context.Context, relayURL string) string {
128 client := NewHTTPClient(WithHTTPTimeout(3 * time.Second))
129 resp, err := httpDo(ctx, client, http.MethodGet, relayURL+types.PathSDKDomain, nil, nil)
130 if err != nil {
131 return ""
132 }
133 defer resp.Body.Close()
134 if resp.StatusCode != http.StatusOK {
135 return ""
136 }
137 var envelope types.APIEnvelope[types.DomainResponse]
138 if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil || !envelope.OK {
139 return ""
140 }
141 return envelope.Data.ReleaseVersion
142 }