github.com/sagernet/sing-box@v1.9.0-rc.20/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/sagernet/sing-box/adapter"
    12  	"github.com/sagernet/sing-box/common/urltest"
    13  	"github.com/sagernet/sing-box/outbound"
    14  	"github.com/sagernet/sing/common"
    15  	"github.com/sagernet/sing/common/batch"
    16  	"github.com/sagernet/sing/common/json/badjson"
    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)
    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  }