Files
cyi-box/backend/internal/service/proxy_routes.go
T
chiyiandDevin 2ab39ecfab proxy extract: always return scheme://ip:port
The plain ip:port output forced consumers to guess the protocol.
The probe already knows it from the handshake, so return the full
URL directly (socks5h/http/socks4).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 20:39:06 +08:00

117 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/cyi-cc/fun"
"github.com/cyi-cc/cyi-box/backend/internal/proxyx"
"github.com/cyi-cc/cyi-box/backend/internal/store"
)
const (
extractBatchSize = 15 // 每批并行探测的候选数
extractBatches = 4 // 最多取几批
extractProbeTO = 3 * time.Second // 单代理探测超时
extractDeadline = 20 * time.Second // 整个请求的最长耗时
)
type extractResult struct {
addr string
lat int
ok bool
}
// ProxyExtractHandler GET /v1/proxy?key=xxx[&fmt=url]
// 公开提取接口:key 落库校验,地区绑在 key 上(0=全部)。
// 并行探测一批候选,谁先探活返回谁;返回带握手实测协议的 "scheme://ip:port"(socks5h/http/socks4)。
// 两批全死返回 503。
func ProxyExtractHandler(st *store.Store) fun.RouteHandler {
return func(rc *fun.RouteCtx) error {
reqCtx := rc.RequestCtx
key := strings.TrimSpace(string(reqCtx.QueryArgs().Peek("key")))
if key == "" {
routeJSONErr(reqCtx, 401, "missing key")
return nil
}
k, err := st.GetProxyApiKey(key)
if err != nil {
routeJSONErr(reqCtx, 401, "invalid key")
return nil
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(extractDeadline))
defer cancel()
for b := 0; b < extractBatches; b++ {
items, err := st.RandomProxies(k.RegionID, "", "", extractBatchSize)
if err != nil {
routeJSONErr(reqCtx, 500, "query failed")
return nil
}
if len(items) == 0 {
break
}
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://%s:%d\n", urlScheme(proto), p.Ip, p.Port))
return nil
}
}
routeJSONErr(reqCtx, 503, "no alive proxy")
return nil
}
}
// urlScheme 握手协议名 → 代理 URL scheme:SOCKS5 用 socks5h(DNS 走代理端解析)。
func urlScheme(proto string) string {
if proto == "socks5" {
return "socks5h"
}
return proto
}
// probeBatch 并行探测一批候选:先活先赢,取消其余;死代理顺手标记移出池。
// 返回胜出的代理及其握手协议名(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
proto string
lat int
}
hits := make(chan hit, 1)
var wg sync.WaitGroup
for _, p := range items {
wg.Add(1)
go func(p store.RandomProxy) {
defer wg.Done()
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, proto, lat}:
case <-ctx.Done():
}
}(p)
}
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case h := <-hits:
now := time.Now().Unix()
_ = st.MarkProxyAlive(h.p.Id, now, now, int64(h.lat))
return h.p, h.proto, h.lat, true
case <-done:
return store.RandomProxy{}, "", 0, false
case <-ctx.Done():
return store.RandomProxy{}, "", 0, false
}
}