Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway (Epay-compatible + CDK stock), license/card system (per-app appid, domain binding), AI memory bank with built-in MCP server (/mcp, FTS5 search, signed read links), server SSH monitor + terminal + SFTP, proxy pool with protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL autocomplete + import/export), disk share, vault (AES-GCM + TOTP), bookmarks, dev tools. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
433 lines
16 KiB
Go
433 lines
16 KiB
Go
package proxyx
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
SourceURL = "https://proxy5.net/api/free-proxies.php"
|
|
pageURL = "https://proxy5.net/cn/free-proxy"
|
|
// version window used by the site's own JS: floor(now / 30min)
|
|
versionWindowSec = 1800
|
|
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
|
)
|
|
|
|
type Remote struct {
|
|
IP string `json:"ip_address"`
|
|
Port int `json:"port"`
|
|
Protocols []string `json:"protocols"`
|
|
Anonymity string `json:"anonymity"`
|
|
Country string `json:"country"`
|
|
CountryCode string `json:"country_code"`
|
|
CountrySlug string `json:"country_slug"`
|
|
Region string `json:"region"`
|
|
Latency int `json:"latency"`
|
|
Speed int `json:"download_speed"`
|
|
Uptime int `json:"uptime"`
|
|
Working bool `json:"is_working"`
|
|
}
|
|
|
|
type payload struct {
|
|
CreatedAt string `json:"created_at_utc"`
|
|
Count int `json:"count"`
|
|
Results []Remote `json:"results"`
|
|
}
|
|
|
|
var client = newClient()
|
|
|
|
func newClient() *http.Client {
|
|
jar, _ := cookiejar.New(nil)
|
|
return &http.Client{
|
|
Jar: jar,
|
|
Timeout: 90 * time.Second,
|
|
}
|
|
}
|
|
|
|
func version() string {
|
|
return fmt.Sprint(time.Now().Unix() / versionWindowSec)
|
|
}
|
|
|
|
func get(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", ua)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Referer", pageURL)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16))
|
|
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
|
}
|
|
|
|
// Fetch pulls the full proxy catalog. If the API answers with a Cloudflare
|
|
// challenge it warms up the cookie jar by loading the listing page once, then retries.
|
|
func Fetch(ctx context.Context) ([]Remote, string, error) {
|
|
u := SourceURL + "?v=" + version()
|
|
body, err := get(ctx, u)
|
|
var p payload
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &p)
|
|
}
|
|
if err != nil {
|
|
if _, warmErr := get(ctx, pageURL); warmErr == nil {
|
|
if body, err = get(ctx, u); err == nil {
|
|
err = json.Unmarshal(body, &p)
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
out := p.Results[:0]
|
|
for _, r := range p.Results {
|
|
if r.IP != "" && r.Port > 0 {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out, p.CreatedAt, nil
|
|
}
|
|
|
|
// ---- rola-ip 源 ----
|
|
|
|
const rolaURL = "https://rola-ip.co/proxy-api/api/v1/proxies"
|
|
const rolaReferer = "https://rola-ip.co/zh/tools/free-proxy-list/"
|
|
const rolaPageSize = 500
|
|
const rolaMaxPages = 40
|
|
|
|
type rolaRow struct {
|
|
IP string `json:"ip"`
|
|
Port int `json:"port"`
|
|
Protocols []string `json:"protocols"`
|
|
Code string `json:"code"`
|
|
Country string `json:"country"`
|
|
Anonymity string `json:"anonymity"`
|
|
Latency *float64 `json:"latency"`
|
|
Speed *float64 `json:"speed"`
|
|
Uptime *float64 `json:"uptime"`
|
|
}
|
|
|
|
type rolaPayload struct {
|
|
Data []rolaRow `json:"data"`
|
|
Pagination struct {
|
|
Page int `json:"page"`
|
|
Total int `json:"total"`
|
|
TotalPages int `json:"totalPages"`
|
|
} `json:"pagination"`
|
|
}
|
|
|
|
func rolaAnonymity(a string) string {
|
|
switch strings.ToLower(a) {
|
|
case "elite":
|
|
return "Elite"
|
|
case "anonymous":
|
|
return "Anonymous"
|
|
case "transparent":
|
|
return "Transparent"
|
|
}
|
|
return a
|
|
}
|
|
|
|
// FetchRola 拉取 rola-ip 免费代理列表:公开前缀 /proxy-api,500/页翻到底。
|
|
func FetchRola(ctx context.Context) ([]Remote, error) {
|
|
out := []Remote{}
|
|
for page := 1; page <= rolaMaxPages; page++ {
|
|
u := fmt.Sprintf("%s?page=%d&pageSize=%d", rolaURL, page, rolaPageSize)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", ua)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Referer", rolaReferer)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
var p rolaPayload
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range p.Data {
|
|
if r.IP == "" || r.Port <= 0 {
|
|
continue
|
|
}
|
|
protos := make([]string, 0, len(r.Protocols))
|
|
for _, pr := range r.Protocols {
|
|
protos = append(protos, strings.ToUpper(pr))
|
|
}
|
|
var lat, spd, upt int
|
|
if r.Latency != nil {
|
|
lat = int(*r.Latency)
|
|
}
|
|
if r.Speed != nil {
|
|
spd = int(*r.Speed)
|
|
}
|
|
if r.Uptime != nil {
|
|
upt = int(*r.Uptime)
|
|
}
|
|
out = append(out, Remote{
|
|
IP: r.IP, Port: r.Port, Protocols: protos,
|
|
Anonymity: rolaAnonymity(r.Anonymity),
|
|
Country: r.Country, CountryCode: strings.ToUpper(r.Code),
|
|
Region: Continent(r.Code),
|
|
Latency: lat, Speed: spd, Uptime: upt,
|
|
})
|
|
}
|
|
if page >= p.Pagination.TotalPages || len(p.Data) == 0 {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(300 * time.Millisecond):
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Continent 国家码 → 大洲(与 proxy5 的 region 字段取值对齐)
|
|
func Continent(code string) string {
|
|
if c, ok := continents[strings.ToUpper(code)]; ok {
|
|
return c
|
|
}
|
|
return "Other"
|
|
}
|
|
|
|
var continents = map[string]string{
|
|
// Asia
|
|
"AF": "Asia", "AM": "Asia", "AZ": "Asia", "BH": "Asia", "BD": "Asia",
|
|
"BN": "Asia", "KH": "Asia", "CN": "Asia", "HK": "Asia", "MO": "Asia",
|
|
"TW": "Asia", "CY": "Asia", "GE": "Asia", "IN": "Asia", "ID": "Asia",
|
|
"IR": "Asia", "IQ": "Asia", "IL": "Asia", "JP": "Asia", "JO": "Asia",
|
|
"KZ": "Asia", "KW": "Asia", "KG": "Asia", "LA": "Asia", "LB": "Asia",
|
|
"MY": "Asia", "MV": "Asia", "MN": "Asia", "MM": "Asia", "NP": "Asia",
|
|
"KP": "Asia", "OM": "Asia", "PK": "Asia", "PS": "Asia", "PH": "Asia",
|
|
"QA": "Asia", "SA": "Asia", "SG": "Asia", "KR": "Asia", "LK": "Asia",
|
|
"SY": "Asia", "TJ": "Asia", "TH": "Asia", "TL": "Asia", "TR": "Asia",
|
|
"TM": "Asia", "AE": "Asia", "UZ": "Asia", "VN": "Asia", "YE": "Asia",
|
|
// Europe
|
|
"AL": "Europe", "AD": "Europe", "AT": "Europe", "BY": "Europe", "BE": "Europe",
|
|
"BA": "Europe", "BG": "Europe", "HR": "Europe", "CZ": "Europe", "DK": "Europe",
|
|
"EE": "Europe", "FI": "Europe", "FR": "Europe", "DE": "Europe", "GR": "Europe",
|
|
"HU": "Europe", "IS": "Europe", "IE": "Europe", "IT": "Europe", "LV": "Europe",
|
|
"LI": "Europe", "LT": "Europe", "LU": "Europe", "MT": "Europe", "MD": "Europe",
|
|
"MC": "Europe", "ME": "Europe", "NL": "Europe", "MK": "Europe", "NO": "Europe",
|
|
"PL": "Europe", "PT": "Europe", "RO": "Europe", "RU": "Europe", "SM": "Europe",
|
|
"RS": "Europe", "SK": "Europe", "SI": "Europe", "ES": "Europe", "SE": "Europe",
|
|
"CH": "Europe", "UA": "Europe", "GB": "Europe", "XK": "Europe", "VA": "Europe",
|
|
// Americas
|
|
"AG": "Americas", "AR": "Americas", "BS": "Americas", "BB": "Americas", "BZ": "Americas",
|
|
"BO": "Americas", "BR": "Americas", "CA": "Americas", "CL": "Americas", "CO": "Americas",
|
|
"CR": "Americas", "CU": "Americas", "DM": "Americas", "DO": "Americas", "EC": "Americas",
|
|
"SV": "Americas", "GD": "Americas", "GT": "Americas", "GY": "Americas", "HT": "Americas",
|
|
"HN": "Americas", "JM": "Americas", "MX": "Americas", "NI": "Americas", "PA": "Americas",
|
|
"PY": "Americas", "PE": "Americas", "PR": "Americas", "KN": "Americas", "LC": "Americas",
|
|
"VC": "Americas", "SR": "Americas", "TT": "Americas", "US": "Americas", "UY": "Americas",
|
|
"VE": "Americas", "VG": "Americas", "AI": "Americas", "AW": "Americas", "BM": "Americas",
|
|
// Africa
|
|
"DZ": "Africa", "AO": "Africa", "BJ": "Africa", "BW": "Africa", "BF": "Africa",
|
|
"BI": "Africa", "CM": "Africa", "CF": "Africa", "TD": "Africa", "KM": "Africa",
|
|
"CG": "Africa", "CD": "Africa", "CI": "Africa", "DJ": "Africa", "EG": "Africa",
|
|
"GQ": "Africa", "ER": "Africa", "SZ": "Africa", "ET": "Africa", "GA": "Africa",
|
|
"GM": "Africa", "GH": "Africa", "GN": "Africa", "GW": "Africa", "KE": "Africa",
|
|
"LS": "Africa", "LR": "Africa", "LY": "Africa", "MG": "Africa", "MW": "Africa",
|
|
"ML": "Africa", "MR": "Africa", "MU": "Africa", "MA": "Africa", "MZ": "Africa",
|
|
"NA": "Africa", "NE": "Africa", "NG": "Africa", "RW": "Africa", "ST": "Africa",
|
|
"SN": "Africa", "SC": "Africa", "SL": "Africa", "SO": "Africa", "ZA": "Africa",
|
|
"SS": "Africa", "SD": "Africa", "TZ": "Africa", "TG": "Africa", "TN": "Africa",
|
|
"UG": "Africa", "ZM": "Africa", "ZW": "Africa",
|
|
// Pacific
|
|
"AU": "Pacific", "FJ": "Pacific", "KI": "Pacific", "MH": "Pacific", "FM": "Pacific",
|
|
"NR": "Pacific", "NZ": "Pacific", "PW": "Pacific", "PG": "Pacific", "WS": "Pacific",
|
|
"SB": "Pacific", "TO": "Pacific", "TV": "Pacific", "VU": "Pacific", "GU": "Pacific",
|
|
}
|
|
|
|
// probeTarget 测活拨测目标:公网稳定 HTTP 站点
|
|
const probeHost = "www.gstatic.com"
|
|
const probePort = 80
|
|
|
|
// Check 协议感知测活:按 protocols 依次尝试 SOCKS5/SOCKS4/HTTP 握手,
|
|
// 纯 TCP 连通不算数——必须说出协议正确的应答才算活。返回拨测耗时。
|
|
func Check(ctx context.Context, ip string, port int, protocols string, timeout time.Duration) (int, error) {
|
|
probes := []func(context.Context, string, int, time.Duration) error{
|
|
probeSOCKS5, probeSOCKS4, probeHTTP,
|
|
}
|
|
upper := strings.ToUpper(protocols)
|
|
order := []int{}
|
|
for i, name := range []string{"SOCKS5", "SOCKS4", "HTTP"} {
|
|
if strings.Contains(upper, name) {
|
|
order = append(order, i)
|
|
}
|
|
}
|
|
if len(order) == 0 {
|
|
order = []int{0, 1, 2}
|
|
}
|
|
var lastErr error
|
|
for _, i := range order {
|
|
start := time.Now()
|
|
if err := probes[i](ctx, ip, port, timeout); err == nil {
|
|
return int(time.Since(start).Milliseconds()), nil
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
}
|
|
return 0, lastErr
|
|
}
|
|
|
|
func dial(ctx context.Context, ip string, port int, timeout time.Duration) (net.Conn, error) {
|
|
d := net.Dialer{Timeout: timeout}
|
|
return d.DialContext(ctx, "tcp", net.JoinHostPort(ip, fmt.Sprint(port)))
|
|
}
|
|
|
|
func probeSOCKS5(ctx context.Context, ip string, port int, timeout time.Duration) error {
|
|
conn, err := dial(ctx, ip, port, timeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(timeout))
|
|
// greeting: VER=5, 1 method, no-auth
|
|
if _, err := conn.Write([]byte{0x05, 0x01, 0x00}); err != nil {
|
|
return err
|
|
}
|
|
buf := make([]byte, 2)
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
return err
|
|
}
|
|
if buf[0] != 0x05 || buf[1] == 0xff {
|
|
return fmt.Errorf("socks5 greeting rejected: %x", buf)
|
|
}
|
|
// CONNECT domain:port
|
|
req := append([]byte{0x05, 0x01, 0x00, 0x03, byte(len(probeHost))}, []byte(probeHost)...)
|
|
req = append(req, byte(probePort>>8), byte(probePort))
|
|
if _, err := conn.Write(req); err != nil {
|
|
return err
|
|
}
|
|
// reply: VER REP RSV ATYP ... — 收到合法的 socks5 应答头即算活
|
|
head := make([]byte, 5)
|
|
if _, err := io.ReadFull(conn, head); err != nil {
|
|
return err
|
|
}
|
|
if head[0] != 0x05 {
|
|
return fmt.Errorf("bad socks5 reply: %x", head[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func probeSOCKS4(ctx context.Context, ip string, port int, timeout time.Duration) error {
|
|
conn, err := dial(ctx, ip, port, timeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(timeout))
|
|
// CONNECT 1.1.1.1:80
|
|
req := []byte{0x04, 0x01, 0x00, 0x50, 1, 1, 1, 1, 0x00}
|
|
if _, err := conn.Write(req); err != nil {
|
|
return err
|
|
}
|
|
buf := make([]byte, 8)
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
return err
|
|
}
|
|
if buf[0] != 0x00 && buf[0] != 0x04 {
|
|
return fmt.Errorf("bad socks4 reply: %x", buf[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func probeHTTP(ctx context.Context, ip string, port int, timeout time.Duration) error {
|
|
conn, err := dial(ctx, ip, port, timeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(timeout))
|
|
req := fmt.Sprintf("GET http://%s/generate_204 HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", probeHost, probeHost)
|
|
if _, err := conn.Write([]byte(req)); err != nil {
|
|
return err
|
|
}
|
|
buf := make([]byte, 5)
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
return err
|
|
}
|
|
if string(buf) != "HTTP/" {
|
|
return fmt.Errorf("bad http reply: %q", string(buf))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ZhName maps an ISO-like country code to a Chinese display name.
|
|
func ZhName(code string) string {
|
|
if n, ok := zhNames[strings.ToUpper(code)]; ok {
|
|
return n
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var zhNames = map[string]string{
|
|
"AF": "阿富汗", "AL": "阿尔巴尼亚", "DZ": "阿尔及利亚", "AD": "安道尔", "AO": "安哥拉",
|
|
"AG": "安提瓜和巴布达", "AR": "阿根廷", "AM": "亚美尼亚", "AU": "澳大利亚", "AT": "奥地利",
|
|
"AZ": "阿塞拜疆", "BS": "巴哈马", "BH": "巴林", "BD": "孟加拉国", "BB": "巴巴多斯",
|
|
"BY": "白俄罗斯", "BE": "比利时", "BZ": "伯利兹", "BJ": "贝宁", "BO": "玻利维亚",
|
|
"BA": "波黑", "BW": "博茨瓦纳", "BR": "巴西", "BN": "文莱", "BG": "保加利亚",
|
|
"BF": "布基纳法索", "BI": "布隆迪", "KH": "柬埔寨", "CM": "喀麦隆", "CA": "加拿大",
|
|
"CF": "中非", "TD": "乍得", "CL": "智利", "CN": "中国", "HK": "中国香港",
|
|
"MO": "中国澳门", "TW": "中国台湾", "CO": "哥伦比亚", "KM": "科摩罗", "CG": "刚果",
|
|
"CD": "刚果(金)", "CR": "哥斯达黎加", "CI": "科特迪瓦", "HR": "克罗地亚", "CU": "古巴",
|
|
"CY": "塞浦路斯", "CZ": "捷克", "DK": "丹麦", "DJ": "吉布提", "DM": "多米尼克",
|
|
"DO": "多米尼加", "EC": "厄瓜多尔", "EG": "埃及", "SV": "萨尔瓦多", "GQ": "赤道几内亚",
|
|
"ER": "厄立特里亚", "EE": "爱沙尼亚", "SZ": "斯威士兰", "ET": "埃塞俄比亚", "FJ": "斐济",
|
|
"FI": "芬兰", "FR": "法国", "GA": "加蓬", "GM": "冈比亚", "GE": "格鲁吉亚",
|
|
"DE": "德国", "GH": "加纳", "GR": "希腊", "GD": "格林纳达", "GT": "危地马拉",
|
|
"GN": "几内亚", "GW": "几内亚比绍", "GY": "圭亚那", "HT": "海地", "HN": "洪都拉斯",
|
|
"HU": "匈牙利", "IS": "冰岛", "IN": "印度", "ID": "印度尼西亚", "IR": "伊朗",
|
|
"IQ": "伊拉克", "IE": "爱尔兰", "IL": "以色列", "IT": "意大利", "JM": "牙买加",
|
|
"JP": "日本", "JO": "约旦", "KZ": "哈萨克斯坦", "KE": "肯尼亚", "KI": "基里巴斯",
|
|
"KP": "朝鲜", "KR": "韩国", "KW": "科威特", "KG": "吉尔吉斯斯坦", "LA": "老挝",
|
|
"LV": "拉脱维亚", "LB": "黎巴嫩", "LS": "莱索托", "LR": "利比里亚", "LY": "利比亚",
|
|
"LI": "列支敦士登", "LT": "立陶宛", "LU": "卢森堡", "MG": "马达加斯加", "MW": "马拉维",
|
|
"MY": "马来西亚", "MV": "马尔代夫", "ML": "马里", "MT": "马耳他", "MH": "马绍尔群岛",
|
|
"MR": "毛里塔尼亚", "MU": "毛里求斯", "MX": "墨西哥", "FM": "密克罗尼西亚", "MD": "摩尔多瓦",
|
|
"MC": "摩纳哥", "MN": "蒙古", "ME": "黑山", "MA": "摩洛哥", "MZ": "莫桑比克",
|
|
"MM": "缅甸", "NA": "纳米比亚", "NR": "瑙鲁", "NP": "尼泊尔", "NL": "荷兰",
|
|
"NZ": "新西兰", "NI": "尼加拉瓜", "NE": "尼日尔", "NG": "尼日利亚", "MK": "北马其顿",
|
|
"NO": "挪威", "OM": "阿曼", "PK": "巴基斯坦", "PW": "帕劳", "PA": "巴拿马",
|
|
"PG": "巴布亚新几内亚", "PY": "巴拉圭", "PE": "秘鲁", "PH": "菲律宾", "PL": "波兰",
|
|
"PT": "葡萄牙", "QA": "卡塔尔", "RO": "罗马尼亚", "RU": "俄罗斯", "RW": "卢旺达",
|
|
"WS": "萨摩亚", "SM": "圣马力诺", "ST": "圣多美和普林西比", "SA": "沙特阿拉伯", "SN": "塞内加尔",
|
|
"RS": "塞尔维亚", "SC": "塞舌尔", "SL": "塞拉利昂", "SG": "新加坡", "SK": "斯洛伐克",
|
|
"SI": "斯洛文尼亚", "SB": "所罗门群岛", "SO": "索马里", "ZA": "南非", "SS": "南苏丹",
|
|
"ES": "西班牙", "LK": "斯里兰卡", "SD": "苏丹", "SR": "苏里南", "SE": "瑞典",
|
|
"CH": "瑞士", "SY": "叙利亚", "TJ": "塔吉克斯坦", "TZ": "坦桑尼亚", "TH": "泰国",
|
|
"TL": "东帝汶", "TG": "多哥", "TO": "汤加", "TT": "特立尼达和多巴哥", "TN": "突尼斯",
|
|
"TR": "土耳其", "TM": "土库曼斯坦", "TV": "图瓦卢", "UG": "乌干达", "UA": "乌克兰",
|
|
"AE": "阿联酋", "GB": "英国", "US": "美国", "UY": "乌拉圭", "UZ": "乌兹别克斯坦",
|
|
"VU": "瓦努阿图", "VE": "委内瑞拉", "VN": "越南", "YE": "也门", "ZM": "赞比亚",
|
|
"ZW": "津巴布韦", "PS": "巴勒斯坦", "XK": "科索沃", "PR": "波多黎各", "VG": "英属维尔京群岛",
|
|
}
|