feat: add proxy geo location
This commit is contained in:
@@ -218,6 +218,11 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
|
|||||||
LatencyMs: p.LatencyMs,
|
LatencyMs: p.LatencyMs,
|
||||||
LatencyStatus: p.LatencyStatus,
|
LatencyStatus: p.LatencyStatus,
|
||||||
LatencyMessage: p.LatencyMessage,
|
LatencyMessage: p.LatencyMessage,
|
||||||
|
IPAddress: p.IPAddress,
|
||||||
|
Country: p.Country,
|
||||||
|
CountryCode: p.CountryCode,
|
||||||
|
Region: p.Region,
|
||||||
|
City: p.City,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ type ProxyWithAccountCount struct {
|
|||||||
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
||||||
LatencyStatus string `json:"latency_status,omitempty"`
|
LatencyStatus string `json:"latency_status,omitempty"`
|
||||||
LatencyMessage string `json:"latency_message,omitempty"`
|
LatencyMessage string `json:"latency_message,omitempty"`
|
||||||
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyAccountSummary struct {
|
type ProxyAccountSummary struct {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
@@ -35,7 +36,7 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultIPInfoURL = "https://ipinfo.io/json"
|
defaultIPInfoURL = "http://ip-api.com/json/?lang=zh-CN"
|
||||||
defaultProxyProbeTimeout = 30 * time.Second
|
defaultProxyProbeTimeout = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,10 +79,14 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipInfo struct {
|
var ipInfo struct {
|
||||||
IP string `json:"ip"`
|
Status string `json:"status"`
|
||||||
City string `json:"city"`
|
Message string `json:"message"`
|
||||||
Region string `json:"region"`
|
Query string `json:"query"`
|
||||||
Country string `json:"country"`
|
City string `json:"city"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
RegionName string `json:"regionName"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
@@ -92,11 +97,22 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
|
|||||||
if err := json.Unmarshal(body, &ipInfo); err != nil {
|
if err := json.Unmarshal(body, &ipInfo); err != nil {
|
||||||
return nil, latencyMs, fmt.Errorf("failed to parse response: %w", err)
|
return nil, latencyMs, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
|
if strings.ToLower(ipInfo.Status) != "success" {
|
||||||
|
if ipInfo.Message == "" {
|
||||||
|
ipInfo.Message = "ip-api request failed"
|
||||||
|
}
|
||||||
|
return nil, latencyMs, fmt.Errorf("ip-api request failed: %s", ipInfo.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
region := ipInfo.RegionName
|
||||||
|
if region == "" {
|
||||||
|
region = ipInfo.Region
|
||||||
|
}
|
||||||
return &service.ProxyExitInfo{
|
return &service.ProxyExitInfo{
|
||||||
IP: ipInfo.IP,
|
IP: ipInfo.Query,
|
||||||
City: ipInfo.City,
|
City: ipInfo.City,
|
||||||
Region: ipInfo.Region,
|
Region: region,
|
||||||
Country: ipInfo.Country,
|
Country: ipInfo.Country,
|
||||||
|
CountryCode: ipInfo.CountryCode,
|
||||||
}, latencyMs, nil
|
}, latencyMs, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type ProxyProbeServiceSuite struct {
|
|||||||
func (s *ProxyProbeServiceSuite) SetupTest() {
|
func (s *ProxyProbeServiceSuite) SetupTest() {
|
||||||
s.ctx = context.Background()
|
s.ctx = context.Background()
|
||||||
s.prober = &proxyProbeService{
|
s.prober = &proxyProbeService{
|
||||||
ipInfoURL: "http://ipinfo.test/json",
|
ipInfoURL: "http://ip-api.test/json/?lang=zh-CN",
|
||||||
allowPrivateHosts: true,
|
allowPrivateHosts: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
|
|||||||
s.setupProxyServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
s.setupProxyServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
seen <- r.RequestURI
|
seen <- r.RequestURI
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_, _ = io.WriteString(w, `{"ip":"1.2.3.4","city":"c","region":"r","country":"cc"}`)
|
_, _ = io.WriteString(w, `{"status":"success","query":"1.2.3.4","city":"c","regionName":"r","country":"cc","countryCode":"CC"}`)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
info, latencyMs, err := s.prober.ProbeProxy(s.ctx, s.proxySrv.URL)
|
info, latencyMs, err := s.prober.ProbeProxy(s.ctx, s.proxySrv.URL)
|
||||||
@@ -64,11 +64,12 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
|
|||||||
require.Equal(s.T(), "c", info.City)
|
require.Equal(s.T(), "c", info.City)
|
||||||
require.Equal(s.T(), "r", info.Region)
|
require.Equal(s.T(), "r", info.Region)
|
||||||
require.Equal(s.T(), "cc", info.Country)
|
require.Equal(s.T(), "cc", info.Country)
|
||||||
|
require.Equal(s.T(), "CC", info.CountryCode)
|
||||||
|
|
||||||
// Verify proxy received the request
|
// Verify proxy received the request
|
||||||
select {
|
select {
|
||||||
case uri := <-seen:
|
case uri := <-seen:
|
||||||
require.Contains(s.T(), uri, "ipinfo.test", "expected request to go through proxy")
|
require.Contains(s.T(), uri, "ip-api.test", "expected request to go through proxy")
|
||||||
default:
|
default:
|
||||||
require.Fail(s.T(), "expected proxy to receive request")
|
require.Fail(s.T(), "expected proxy to receive request")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,21 +236,23 @@ type ProxyBatchDeleteSkipped struct {
|
|||||||
|
|
||||||
// ProxyTestResult represents the result of testing a proxy
|
// ProxyTestResult represents the result of testing a proxy
|
||||||
type ProxyTestResult struct {
|
type ProxyTestResult struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||||
IPAddress string `json:"ip_address,omitempty"`
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
City string `json:"city,omitempty"`
|
City string `json:"city,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyExitInfo represents proxy exit information from ipinfo.io
|
// ProxyExitInfo represents proxy exit information from ip-api.com
|
||||||
type ProxyExitInfo struct {
|
type ProxyExitInfo struct {
|
||||||
IP string
|
IP string
|
||||||
City string
|
City string
|
||||||
Region string
|
Region string
|
||||||
Country string
|
Country string
|
||||||
|
CountryCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyExitInfoProber tests proxy connectivity and retrieves exit information
|
// ProxyExitInfoProber tests proxy connectivity and retrieves exit information
|
||||||
@@ -1340,19 +1342,25 @@ func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestR
|
|||||||
|
|
||||||
latency := latencyMs
|
latency := latencyMs
|
||||||
s.saveProxyLatency(ctx, id, &ProxyLatencyInfo{
|
s.saveProxyLatency(ctx, id, &ProxyLatencyInfo{
|
||||||
Success: true,
|
Success: true,
|
||||||
LatencyMs: &latency,
|
LatencyMs: &latency,
|
||||||
Message: "Proxy is accessible",
|
Message: "Proxy is accessible",
|
||||||
UpdatedAt: time.Now(),
|
IPAddress: exitInfo.IP,
|
||||||
|
Country: exitInfo.Country,
|
||||||
|
CountryCode: exitInfo.CountryCode,
|
||||||
|
Region: exitInfo.Region,
|
||||||
|
City: exitInfo.City,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
})
|
})
|
||||||
return &ProxyTestResult{
|
return &ProxyTestResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "Proxy is accessible",
|
Message: "Proxy is accessible",
|
||||||
LatencyMs: latencyMs,
|
LatencyMs: latencyMs,
|
||||||
IPAddress: exitInfo.IP,
|
IPAddress: exitInfo.IP,
|
||||||
City: exitInfo.City,
|
City: exitInfo.City,
|
||||||
Region: exitInfo.Region,
|
Region: exitInfo.Region,
|
||||||
Country: exitInfo.Country,
|
Country: exitInfo.Country,
|
||||||
|
CountryCode: exitInfo.CountryCode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,10 +1380,15 @@ func (s *adminServiceImpl) probeProxyLatency(ctx context.Context, proxy *Proxy)
|
|||||||
|
|
||||||
latency := latencyMs
|
latency := latencyMs
|
||||||
s.saveProxyLatency(ctx, proxy.ID, &ProxyLatencyInfo{
|
s.saveProxyLatency(ctx, proxy.ID, &ProxyLatencyInfo{
|
||||||
Success: true,
|
Success: true,
|
||||||
LatencyMs: &latency,
|
LatencyMs: &latency,
|
||||||
Message: "Proxy is accessible",
|
Message: "Proxy is accessible",
|
||||||
UpdatedAt: time.Now(),
|
IPAddress: exitInfo.IP,
|
||||||
|
Country: exitInfo.Country,
|
||||||
|
CountryCode: exitInfo.CountryCode,
|
||||||
|
Region: exitInfo.Region,
|
||||||
|
City: exitInfo.City,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,6 +1469,11 @@ func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []Pro
|
|||||||
proxies[i].LatencyStatus = "failed"
|
proxies[i].LatencyStatus = "failed"
|
||||||
}
|
}
|
||||||
proxies[i].LatencyMessage = info.Message
|
proxies[i].LatencyMessage = info.Message
|
||||||
|
proxies[i].IPAddress = info.IPAddress
|
||||||
|
proxies[i].Country = info.Country
|
||||||
|
proxies[i].CountryCode = info.CountryCode
|
||||||
|
proxies[i].Region = info.Region
|
||||||
|
proxies[i].City = info.City
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ type ProxyWithAccountCount struct {
|
|||||||
LatencyMs *int64
|
LatencyMs *int64
|
||||||
LatencyStatus string
|
LatencyStatus string
|
||||||
LatencyMessage string
|
LatencyMessage string
|
||||||
|
IPAddress string
|
||||||
|
Country string
|
||||||
|
CountryCode string
|
||||||
|
Region string
|
||||||
|
City string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyAccountSummary struct {
|
type ProxyAccountSummary struct {
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProxyLatencyInfo struct {
|
type ProxyLatencyInfo struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
LatencyMs *int64 `json:"latency_ms,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyLatencyCache interface {
|
type ProxyLatencyCache interface {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export async function testProxy(id: number): Promise<{
|
|||||||
city?: string
|
city?: string
|
||||||
region?: string
|
region?: string
|
||||||
country?: string
|
country?: string
|
||||||
|
country_code?: string
|
||||||
}> {
|
}> {
|
||||||
const { data } = await apiClient.post<{
|
const { data } = await apiClient.post<{
|
||||||
success: boolean
|
success: boolean
|
||||||
@@ -135,6 +136,7 @@ export async function testProxy(id: number): Promise<{
|
|||||||
city?: string
|
city?: string
|
||||||
region?: string
|
region?: string
|
||||||
country?: string
|
country?: string
|
||||||
|
country_code?: string
|
||||||
}>(`/admin/proxies/${id}/test`)
|
}>(`/admin/proxies/${id}/test`)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1634,6 +1634,7 @@ export default {
|
|||||||
name: 'Name',
|
name: 'Name',
|
||||||
protocol: 'Protocol',
|
protocol: 'Protocol',
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
location: 'Location',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
accounts: 'Accounts',
|
accounts: 'Accounts',
|
||||||
latency: 'Latency',
|
latency: 'Latency',
|
||||||
|
|||||||
@@ -1719,6 +1719,7 @@ export default {
|
|||||||
name: '名称',
|
name: '名称',
|
||||||
protocol: '协议',
|
protocol: '协议',
|
||||||
address: '地址',
|
address: '地址',
|
||||||
|
location: '地理位置',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
accounts: '账号数',
|
accounts: '账号数',
|
||||||
latency: '延迟',
|
latency: '延迟',
|
||||||
|
|||||||
@@ -367,6 +367,11 @@ export interface Proxy {
|
|||||||
latency_ms?: number
|
latency_ms?: number
|
||||||
latency_status?: 'success' | 'failed'
|
latency_status?: 'success' | 'failed'
|
||||||
latency_message?: string
|
latency_message?: string
|
||||||
|
ip_address?: string
|
||||||
|
country?: string
|
||||||
|
country_code?: string
|
||||||
|
region?: string
|
||||||
|
city?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,21 @@
|
|||||||
<code class="code text-xs">{{ row.host }}:{{ row.port }}</code>
|
<code class="code text-xs">{{ row.host }}:{{ row.port }}</code>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-location="{ row }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
v-if="row.country_code"
|
||||||
|
:src="flagUrl(row.country_code)"
|
||||||
|
:alt="row.country || row.country_code"
|
||||||
|
class="h-4 w-6 rounded-sm"
|
||||||
|
/>
|
||||||
|
<span v-if="formatLocation(row)" class="text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{{ formatLocation(row) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-sm text-gray-400">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-account_count="{ row, value }">
|
<template #cell-account_count="{ row, value }">
|
||||||
<button
|
<button
|
||||||
v-if="(value || 0) > 0"
|
v-if="(value || 0) > 0"
|
||||||
@@ -665,6 +680,7 @@ const columns = computed<Column[]>(() => [
|
|||||||
{ key: 'name', label: t('admin.proxies.columns.name'), sortable: true },
|
{ key: 'name', label: t('admin.proxies.columns.name'), sortable: true },
|
||||||
{ key: 'protocol', label: t('admin.proxies.columns.protocol'), sortable: true },
|
{ key: 'protocol', label: t('admin.proxies.columns.protocol'), sortable: true },
|
||||||
{ key: 'address', label: t('admin.proxies.columns.address'), sortable: false },
|
{ key: 'address', label: t('admin.proxies.columns.address'), sortable: false },
|
||||||
|
{ key: 'location', label: t('admin.proxies.columns.location'), sortable: false },
|
||||||
{ key: 'account_count', label: t('admin.proxies.columns.accounts'), sortable: true },
|
{ key: 'account_count', label: t('admin.proxies.columns.accounts'), sortable: true },
|
||||||
{ key: 'latency', label: t('admin.proxies.columns.latency'), sortable: false },
|
{ key: 'latency', label: t('admin.proxies.columns.latency'), sortable: false },
|
||||||
{ key: 'status', label: t('admin.proxies.columns.status'), sortable: true },
|
{ key: 'status', label: t('admin.proxies.columns.status'), sortable: true },
|
||||||
@@ -1058,20 +1074,47 @@ const handleUpdateProxy = async () => {
|
|||||||
|
|
||||||
const applyLatencyResult = (
|
const applyLatencyResult = (
|
||||||
proxyId: number,
|
proxyId: number,
|
||||||
result: { success: boolean; latency_ms?: number; message?: string }
|
result: {
|
||||||
|
success: boolean
|
||||||
|
latency_ms?: number
|
||||||
|
message?: string
|
||||||
|
ip_address?: string
|
||||||
|
country?: string
|
||||||
|
country_code?: string
|
||||||
|
region?: string
|
||||||
|
city?: string
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const target = proxies.value.find((proxy) => proxy.id === proxyId)
|
const target = proxies.value.find((proxy) => proxy.id === proxyId)
|
||||||
if (!target) return
|
if (!target) return
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
target.latency_status = 'success'
|
target.latency_status = 'success'
|
||||||
target.latency_ms = result.latency_ms
|
target.latency_ms = result.latency_ms
|
||||||
|
target.ip_address = result.ip_address
|
||||||
|
target.country = result.country
|
||||||
|
target.country_code = result.country_code
|
||||||
|
target.region = result.region
|
||||||
|
target.city = result.city
|
||||||
} else {
|
} else {
|
||||||
target.latency_status = 'failed'
|
target.latency_status = 'failed'
|
||||||
target.latency_ms = undefined
|
target.latency_ms = undefined
|
||||||
|
target.ip_address = undefined
|
||||||
|
target.country = undefined
|
||||||
|
target.country_code = undefined
|
||||||
|
target.region = undefined
|
||||||
|
target.city = undefined
|
||||||
}
|
}
|
||||||
target.latency_message = result.message
|
target.latency_message = result.message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatLocation = (proxy: Proxy) => {
|
||||||
|
const parts = [proxy.country, proxy.city].filter(Boolean) as string[]
|
||||||
|
return parts.join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagUrl = (code: string) =>
|
||||||
|
`https://unpkg.com/flag-icons/flags/4x3/${code.toLowerCase()}.svg`
|
||||||
|
|
||||||
const startTestingProxy = (proxyId: number) => {
|
const startTestingProxy = (proxyId: number) => {
|
||||||
testingProxyIds.value = new Set([...testingProxyIds.value, proxyId])
|
testingProxyIds.value = new Set([...testingProxyIds.value, proxyId])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user