diff --git a/README.md b/README.md index 526a1ef..5404db0 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ make build # go build + vite build - `/proxies/extract` 页生成密钥(`pk_xxx`,存 `proxy_keys` 表,绑定地区或「全部」),删除即吊销; - `GET /v1/proxy?key=<密钥>` 免登录:随机抽 15 个存活候选**并行**协议级探活(3s 超时), - 先活先回一行 `ip:port` 纯文本;死的顺手标 `alive=0`,最多 4 批 / 20s 总超时,全死 503; + 先活先回一行 `ip:port` 纯文本(加 `&fmt=url` 返回 `scheme://ip:port`,scheme 为握手实测的 + socks5h/http/socks4);死的顺手标 `alive=0`,最多 4 批 / 20s 总超时,全死 503; - 每次成功提取 `used_count+1`、`last_used_at` 更新。 ## 系统设置 diff --git a/backend/internal/proxyx/proxyx.go b/backend/internal/proxyx/proxyx.go index 284928a..b567c25 100644 --- a/backend/internal/proxyx/proxyx.go +++ b/backend/internal/proxyx/proxyx.go @@ -271,11 +271,13 @@ 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) { +// 纯 TCP 连通不算数——必须说出协议正确的应答才算活。 +// 返回握手成功的协议名(socks5/socks4/http)与拨测耗时。 +func Check(ctx context.Context, ip string, port int, protocols string, timeout time.Duration) (string, int, error) { probes := []func(context.Context, string, int, time.Duration) error{ probeSOCKS5, probeSOCKS4, probeHTTP, } + names := []string{"socks5", "socks4", "http"} upper := strings.ToUpper(protocols) order := []int{} for i, name := range []string{"SOCKS5", "SOCKS4", "HTTP"} { @@ -290,12 +292,12 @@ func Check(ctx context.Context, ip string, port int, protocols string, timeout t for _, i := range order { start := time.Now() if err := probes[i](ctx, ip, port, timeout); err == nil { - return int(time.Since(start).Milliseconds()), nil + return names[i], int(time.Since(start).Milliseconds()), nil } else { lastErr = err } } - return 0, lastErr + return "", 0, lastErr } func dial(ctx context.Context, ip string, port int, timeout time.Duration) (net.Conn, error) { diff --git a/backend/internal/service/proxy.go b/backend/internal/service/proxy.go index dbf3f0e..f607709 100644 --- a/backend/internal/service/proxy.go +++ b/backend/internal/service/proxy.go @@ -322,7 +322,7 @@ func runProxyCheck(ctx context.Context, st *store.Store) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() - latency, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, proxyCheckTimeout) + _, latency, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, proxyCheckTimeout) if err != nil { _ = st.MarkProxyFailed(p.Id, now, p.FailCount+1) return diff --git a/backend/internal/service/proxy_routes.go b/backend/internal/service/proxy_routes.go index 33d2082..8df3840 100644 --- a/backend/internal/service/proxy_routes.go +++ b/backend/internal/service/proxy_routes.go @@ -26,9 +26,11 @@ type extractResult struct { ok bool } -// ProxyExtractHandler GET /v1/proxy?key=xxx +// ProxyExtractHandler GET /v1/proxy?key=xxx[&fmt=url] // 公开提取接口:key 落库校验,地区绑在 key 上(0=全部)。 -// 并行探测一批候选,谁先探活返回谁,纯文本 "ip:port";两批全死返回 503。 +// 并行探测一批候选,谁先探活返回谁;默认纯文本 "ip:port", +// fmt=url 时带握手实测协议返回 "scheme://ip:port"(socks5h/http/socks4)。 +// 两批全死返回 503。 func ProxyExtractHandler(st *store.Store) fun.RouteHandler { return func(rc *fun.RouteCtx) error { reqCtx := rc.RequestCtx @@ -42,6 +44,7 @@ func ProxyExtractHandler(st *store.Store) fun.RouteHandler { routeJSONErr(reqCtx, 401, "invalid key") return nil } + withScheme := strings.TrimSpace(string(reqCtx.QueryArgs().Peek("fmt"))) == "url" ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(extractDeadline)) defer cancel() @@ -55,10 +58,14 @@ func ProxyExtractHandler(st *store.Store) fun.RouteHandler { if len(items) == 0 { break } - if p, _, ok := probeBatch(ctx, st, items); ok { + if p, proto, _, ok := probeBatch(ctx, st, items); ok { st.TouchProxyApiKey(k.Id) reqCtx.SetContentType("text/plain; charset=utf-8") - reqCtx.SetBodyString(fmt.Sprintf("%s:%d\n", p.Ip, p.Port)) + if withScheme { + reqCtx.SetBodyString(fmt.Sprintf("%s://%s:%d\n", urlScheme(proto), p.Ip, p.Port)) + } else { + reqCtx.SetBodyString(fmt.Sprintf("%s:%d\n", p.Ip, p.Port)) + } return nil } } @@ -67,11 +74,21 @@ func ProxyExtractHandler(st *store.Store) fun.RouteHandler { } } +// urlScheme 握手协议名 → 代理 URL scheme:SOCKS5 用 socks5h(DNS 走代理端解析)。 +func urlScheme(proto string) string { + if proto == "socks5" { + return "socks5h" + } + return proto +} + // probeBatch 并行探测一批候选:先活先赢,取消其余;死代理顺手标记移出池。 -func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) (store.RandomProxy, int, bool) { +// 返回胜出的代理及其握手协议名(socks5/socks4/http)。 +func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) (store.RandomProxy, string, int, bool) { type hit struct { - p store.RandomProxy - lat int + p store.RandomProxy + proto string + lat int } hits := make(chan hit, 1) var wg sync.WaitGroup @@ -79,13 +96,13 @@ func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) wg.Add(1) go func(p store.RandomProxy) { defer wg.Done() - lat, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, extractProbeTO) + proto, lat, err := proxyx.Check(ctx, p.Ip, int(p.Port), p.Protocols, extractProbeTO) if err != nil { _ = st.MarkProxyFailed(p.Id, time.Now().Unix(), p.FailCount+1) return } select { - case hits <- hit{p, lat}: + case hits <- hit{p, proto, lat}: case <-ctx.Done(): } }(p) @@ -96,10 +113,10 @@ func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) case h := <-hits: now := time.Now().Unix() _ = st.MarkProxyAlive(h.p.Id, now, now, int64(h.lat)) - return h.p, h.lat, true + return h.p, h.proto, h.lat, true case <-done: - return store.RandomProxy{}, 0, false + return store.RandomProxy{}, "", 0, false case <-ctx.Done(): - return store.RandomProxy{}, 0, false + return store.RandomProxy{}, "", 0, false } } diff --git a/frontend/src/views/ProxyExtract.vue b/frontend/src/views/ProxyExtract.vue index 4aa6e65..f1161f3 100644 --- a/frontend/src/views/ProxyExtract.vue +++ b/frontend/src/views/ProxyExtract.vue @@ -129,7 +129,7 @@ function regionLabel(k: proxyKeyView): string {
使用方式
GET /api/v1/proxy?key=<密钥>
-
每次请求实时探活,返回一个可用的 ip:port(纯文本)。地区由密钥绑定,「全部地区」密钥随机返回任意国家。
+
每次请求实时探活,返回一个可用的 ip:port(纯文本);加 &fmt=url 返回带协议的 scheme://ip:port(socks5h/http/socks4)。地区由密钥绑定,「全部地区」密钥随机返回任意国家。