Files
cyi-box/backend/internal/service/proxy_routes.go
T
chiyiandDevin 834ecacdb9 cyi-box: Memphis-style toolbox admin platform
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>
2026-10-01 01:58:47 +08:00

106 lines
2.7 KiB
Go

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
// 公开提取接口:key 落库校验,地区绑在 key 上(0=全部)。
// 并行探测一批候选,谁先探活返回谁,纯文本 "ip:port";两批全死返回 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, _, 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))
return nil
}
}
routeJSONErr(reqCtx, 503, "no alive proxy")
return nil
}
}
// probeBatch 并行探测一批候选:先活先赢,取消其余;死代理顺手标记移出池。
func probeBatch(ctx context.Context, st *store.Store, items []store.RandomProxy) (store.RandomProxy, int, bool) {
type hit struct {
p store.RandomProxy
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()
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 <-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.lat, true
case <-done:
return store.RandomProxy{}, 0, false
case <-ctx.Done():
return store.RandomProxy{}, 0, false
}
}