github.com/inazumav/sing-box@v0.0.0-20230926072359-ab51429a14f1/experimental/clashapi/api_meta_group.go (about) 1 package clashapi 2 3 import ( 4 "context" 5 "net/http" 6 "strconv" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/inazumav/sing-box/adapter" 12 "github.com/inazumav/sing-box/common/badjson" 13 "github.com/inazumav/sing-box/common/urltest" 14 "github.com/inazumav/sing-box/outbound" 15 "github.com/sagernet/sing/common" 16 "github.com/sagernet/sing/common/batch" 17 18 "github.com/go-chi/chi/v5" 19 "github.com/go-chi/render" 20 ) 21 22 func groupRouter(server *Server) http.Handler { 23 r := chi.NewRouter() 24 r.Get("/", getGroups(server)) 25 r.Route("/{name}", func(r chi.Router) { 26 r.Use(parseProxyName, findProxyByName(server.router)) 27 r.Get("/", getGroup(server)) 28 r.Get("/delay", getGroupDelay(server)) 29 }) 30 return r 31 } 32 33 func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { 34 return func(w http.ResponseWriter, r *http.Request) { 35 groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool { 36 _, isGroup := it.(adapter.OutboundGroup) 37 return isGroup 38 }), func(it adapter.Outbound) *badjson.JSONObject { 39 return proxyInfo(server, it) 40 }) 41 render.JSON(w, r, render.M{ 42 "proxies": groups, 43 }) 44 } 45 } 46 47 func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) { 48 return func(w http.ResponseWriter, r *http.Request) { 49 proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) 50 if _, ok := proxy.(adapter.OutboundGroup); ok { 51 render.JSON(w, r, proxyInfo(server, proxy)) 52 return 53 } 54 render.Status(r, http.StatusNotFound) 55 render.JSON(w, r, ErrNotFound) 56 } 57 } 58 59 func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { 60 return func(w http.ResponseWriter, r *http.Request) { 61 proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) 62 group, ok := proxy.(adapter.OutboundGroup) 63 if !ok { 64 render.Status(r, http.StatusNotFound) 65 render.JSON(w, r, ErrNotFound) 66 return 67 } 68 69 query := r.URL.Query() 70 url := query.Get("url") 71 if strings.HasPrefix(url, "http://") { 72 url = "" 73 } 74 timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) 75 if err != nil { 76 render.Status(r, http.StatusBadRequest) 77 render.JSON(w, r, ErrBadRequest) 78 return 79 } 80 81 ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout)) 82 defer cancel() 83 84 var result map[string]uint16 85 if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup { 86 result, err = urlTestGroup.URLTest(ctx, url) 87 } else { 88 outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound { 89 itOutbound, _ := server.router.Outbound(it) 90 return itOutbound 91 })) 92 b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) 93 checked := make(map[string]bool) 94 result = make(map[string]uint16) 95 var resultAccess sync.Mutex 96 for _, detour := range outbounds { 97 tag := detour.Tag() 98 realTag := outbound.RealTag(detour) 99 if checked[realTag] { 100 continue 101 } 102 checked[realTag] = true 103 p, loaded := server.router.Outbound(realTag) 104 if !loaded { 105 continue 106 } 107 b.Go(realTag, func() (any, error) { 108 t, err := urltest.URLTest(ctx, url, p) 109 if err != nil { 110 server.logger.Debug("outbound ", tag, " unavailable: ", err) 111 server.urlTestHistory.DeleteURLTestHistory(realTag) 112 } else { 113 server.logger.Debug("outbound ", tag, " available: ", t, "ms") 114 server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{ 115 Time: time.Now(), 116 Delay: t, 117 }) 118 resultAccess.Lock() 119 result[tag] = t 120 resultAccess.Unlock() 121 } 122 return nil, nil 123 }) 124 } 125 b.Wait() 126 } 127 128 if err != nil { 129 render.Status(r, http.StatusGatewayTimeout) 130 render.JSON(w, r, newError(err.Error())) 131 return 132 } 133 134 render.JSON(w, r, result) 135 } 136 }