|
|
@@ -0,0 +1,132 @@
|
|
|
+package clashapi
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "net/http"
|
|
|
+ "strconv"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/sagernet/sing-box/adapter"
|
|
|
+ "github.com/sagernet/sing-box/common/badjson"
|
|
|
+ "github.com/sagernet/sing-box/common/urltest"
|
|
|
+ "github.com/sagernet/sing-box/outbound"
|
|
|
+ "github.com/sagernet/sing/common"
|
|
|
+ "github.com/sagernet/sing/common/batch"
|
|
|
+
|
|
|
+ "github.com/go-chi/chi/v5"
|
|
|
+ "github.com/go-chi/render"
|
|
|
+)
|
|
|
+
|
|
|
+func groupRouter(server *Server) http.Handler {
|
|
|
+ r := chi.NewRouter()
|
|
|
+ r.Get("/", getGroups(server))
|
|
|
+ r.Route("/{name}", func(r chi.Router) {
|
|
|
+ r.Use(parseProxyName, findProxyByName(server.router))
|
|
|
+ r.Get("/", getGroup(server))
|
|
|
+ r.Get("/delay", getGroupDelay(server))
|
|
|
+ })
|
|
|
+ return r
|
|
|
+}
|
|
|
+
|
|
|
+func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ return func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
|
|
|
+ _, isGroup := it.(adapter.OutboundGroup)
|
|
|
+ return isGroup
|
|
|
+ }), func(it adapter.Outbound) *badjson.JSONObject {
|
|
|
+ return proxyInfo(server, it)
|
|
|
+ })
|
|
|
+ render.JSON(w, r, render.M{
|
|
|
+ "proxies": groups,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ return func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
|
|
|
+ if _, ok := proxy.(adapter.OutboundGroup); ok {
|
|
|
+ render.JSON(w, r, proxyInfo(server, proxy))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ render.Status(r, http.StatusNotFound)
|
|
|
+ render.JSON(w, r, ErrNotFound)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ return func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
|
|
|
+ group, ok := proxy.(adapter.OutboundGroup)
|
|
|
+ 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(r.Context(), time.Millisecond*time.Duration(timeout))
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ var result map[string]uint16
|
|
|
+ if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup {
|
|
|
+ result, err = urlTestGroup.URLTest(ctx, url)
|
|
|
+ } else {
|
|
|
+ outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound {
|
|
|
+ itOutbound, _ := server.router.Outbound(it)
|
|
|
+ return itOutbound
|
|
|
+ }))
|
|
|
+ b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
|
|
|
+ checked := make(map[string]bool)
|
|
|
+ result = make(map[string]uint16)
|
|
|
+ var resultAccess sync.Mutex
|
|
|
+ for _, detour := range outbounds {
|
|
|
+ tag := detour.Tag()
|
|
|
+ realTag := outbound.RealTag(detour)
|
|
|
+ if checked[realTag] {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ checked[realTag] = true
|
|
|
+ p, loaded := server.router.Outbound(realTag)
|
|
|
+ if !loaded {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ b.Go(realTag, func() (any, error) {
|
|
|
+ t, err := urltest.URLTest(ctx, url, p)
|
|
|
+ if err != nil {
|
|
|
+ server.logger.Debug("outbound ", tag, " unavailable: ", err)
|
|
|
+ server.urlTestHistory.DeleteURLTestHistory(realTag)
|
|
|
+ } else {
|
|
|
+ server.logger.Debug("outbound ", tag, " available: ", t, "ms")
|
|
|
+ server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
|
|
|
+ Time: time.Now(),
|
|
|
+ Delay: t,
|
|
|
+ })
|
|
|
+ resultAccess.Lock()
|
|
|
+ result[tag] = t
|
|
|
+ resultAccess.Unlock()
|
|
|
+ }
|
|
|
+ return nil, nil
|
|
|
+ })
|
|
|
+ }
|
|
|
+ b.Wait()
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ render.Status(r, http.StatusGatewayTimeout)
|
|
|
+ render.JSON(w, r, newError(err.Error()))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ render.JSON(w, r, result)
|
|
|
+ }
|
|
|
+}
|