From 4092a7c84b623d2e567112d494bdd22c048b882e Mon Sep 17 00:00:00 2001 From: adlyq Date: Mon, 30 May 2022 21:55:09 +0800 Subject: [PATCH] feat: proxies group URLTest api --- adapter/adapter.go | 7 ++- adapter/outbound/base.go | 4 ++ adapter/outboundgroup/groupbase.go | 30 ++++++++++++ constant/adapters.go | 5 ++ constant/provider/interface.go | 10 ++-- hub/route/groups.go | 79 ++++++++++++++++++++++++++++++ hub/route/server.go | 1 + 7 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 hub/route/groups.go diff --git a/adapter/adapter.go b/adapter/adapter.go index 84895852e..b18679982 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -4,6 +4,9 @@ import ( "context" "encoding/json" "fmt" + "github.com/Dreamacro/clash/common/queue" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" "net" "net/http" "net/netip" @@ -11,10 +14,6 @@ import ( "strings" "time" - "github.com/Dreamacro/clash/common/queue" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "go.uber.org/atomic" ) diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 0d86c2da3..0666d3068 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -51,6 +51,10 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { return c, errors.New("no support") } +func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + return nil, errors.New("no support") +} + // ListenPacketContext implements C.ProxyAdapter func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { return nil, errors.New("no support") diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index a03b24f9b..4aa290c06 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -1,6 +1,8 @@ package outboundgroup import ( + "context" + "fmt" "github.com/Dreamacro/clash/adapter/outbound" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" @@ -105,6 +107,34 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { return proxies } +func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) { + var wg sync.WaitGroup + var lock sync.Mutex + mp := map[string]uint16{} + proxies := gb.GetProxies(false) + for _, proxy := range proxies { + proxy := proxy + wg.Add(1) + go func() { + delay, err := proxy.URLTest(ctx, url) + lock.Lock() + if err == nil { + mp[proxy.Name()] = delay + } + lock.Unlock() + + wg.Done() + }() + } + wg.Wait() + + if len(mp) == 0 { + return mp, fmt.Errorf("get delay: all proxies timeout") + } else { + return mp, nil + } +} + func (gb *GroupBase) onDialFailed() { if gb.failedTesting.Load() { return diff --git a/constant/adapters.go b/constant/adapters.go index 4e40f21e6..9d0894240 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -108,6 +108,11 @@ type ProxyAdapter interface { Unwrap(metadata *Metadata) Proxy } +type Group interface { + URLTest(ctx context.Context, url string) (mp map[string]uint16, err error) + GetProxies(touch bool) []Proxy +} + type DelayHistory struct { Time time.Time `json:"time"` Delay uint16 `json:"delay"` diff --git a/constant/provider/interface.go b/constant/provider/interface.go index c00a0ba2a..646e459b2 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,7 +1,7 @@ package provider import ( - "github.com/Dreamacro/clash/constant" + C "github.com/Dreamacro/clash/constant" ) // Vehicle Type @@ -65,10 +65,10 @@ type Provider interface { // ProxyProvider interface type ProxyProvider interface { Provider - Proxies() []constant.Proxy + Proxies() []C.Proxy // ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies. // Commonly used in DialContext and DialPacketConn - ProxiesWithTouch() []constant.Proxy + ProxiesWithTouch() []C.Proxy HealthCheck() Version() uint } @@ -100,7 +100,7 @@ func (rt RuleType) String() string { type RuleProvider interface { Provider Behavior() RuleType - Match(*constant.Metadata) bool + Match(*C.Metadata) bool ShouldResolveIP() bool - AsRule(adaptor string) constant.Rule + AsRule(adaptor string) C.Rule } diff --git a/hub/route/groups.go b/hub/route/groups.go new file mode 100644 index 000000000..e877bfd06 --- /dev/null +++ b/hub/route/groups.go @@ -0,0 +1,79 @@ +package route + +import ( + "context" + "github.com/Dreamacro/clash/adapter" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/tunnel" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "net/http" + "strconv" + "time" +) + +func GroupRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getGroups) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProxyByName) + r.Get("/", getGroup) + r.Get("/delay", getGroupDelay) + }) + return r +} + +func getGroups(w http.ResponseWriter, r *http.Request) { + var gs []C.Proxy + for _, p := range tunnel.Proxies() { + if _, ok := p.(*adapter.Proxy).ProxyAdapter.(C.Group); ok { + gs = append(gs, p) + } + } + render.JSON(w, r, render.M{ + "proxies": gs, + }) +} + +func getGroup(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + if _, ok := proxy.(*adapter.Proxy).ProxyAdapter.(C.Group); ok { + render.JSON(w, r, proxy) + return + } + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) +} + +func getGroupDelay(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + group, ok := proxy.(*adapter.Proxy).ProxyAdapter.(C.Group) + if !ok { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + + query := r.URL.Query() + url := query.Get("url") + timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) + defer cancel() + + dm, err := group.URLTest(ctx, url) + + if err != nil { + render.Status(r, http.StatusGatewayTimeout) + render.JSON(w, r, newError(err.Error())) + return + } + + render.JSON(w, r, dm) +} diff --git a/hub/route/server.go b/hub/route/server.go index ac12fed31..7ebc4ceb0 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -68,6 +68,7 @@ func Start(addr string, secret string) { r.Get("/version", version) r.Mount("/configs", configRouter()) r.Mount("/proxies", proxyRouter()) + r.Mount("/group", GroupRouter()) r.Mount("/rules", ruleRouter()) r.Mount("/connections", connectionRouter()) r.Mount("/providers/proxies", proxyProviderRouter())