package clashapi import ( "context" "fmt" "net" "net/http" "net/url" "strconv" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/badjson" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "sort" ) func proxyRouter(server *Server, router adapter.Router) http.Handler { r := chi.NewRouter() r.Get("/", getProxies(server, router)) r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProxyByName(router)) r.Get("/", getProxy(server)) r.Get("/delay", getProxyDelay(server)) r.Put("/", updateProxy) }) return r } func parseProxyName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := getEscapeParam(r, "name") ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) next.ServeHTTP(w, r.WithContext(ctx)) }) } func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) proxy, exist := router.Outbound(name) if !exist { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) return } ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { var info badjson.JSONObject var clashType string var isSelector bool switch detour.Type() { case C.TypeDirect: clashType = "Direct" case C.TypeBlock: clashType = "Reject" case C.TypeSocks: clashType = "Socks" case C.TypeHTTP: clashType = "Http" case C.TypeShadowsocks: clashType = "Shadowsocks" case C.TypeVMess: clashType = "Vmess" case C.TypeSelector: clashType = "Selector" isSelector = true default: clashType = "Unknown" } info.Put("type", clashType) info.Put("name", detour.Tag()) info.Put("udp", common.Contains(detour.Network(), C.NetworkUDP)) var delayHistory *DelayHistory var loaded bool if isSelector { selector := detour.(*outbound.Selector) info.Put("now", selector.Now()) info.Put("all", selector.All()) delayHistory, loaded = server.delayHistory[selector.Now()] } else { delayHistory, loaded = server.delayHistory[detour.Tag()] } if loaded { info.Put("history", []*DelayHistory{delayHistory}) } else { info.Put("history", []*DelayHistory{}) } return &info } func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var proxyMap badjson.JSONObject outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool { return detour.Tag() != "" }) allProxies := make([]string, 0, len(outbounds)) for _, detour := range outbounds { switch detour.Type() { case C.TypeDirect, C.TypeBlock: continue } allProxies = append(allProxies, detour.Tag()) } defaultTag := router.DefaultOutbound(C.NetworkTCP).Tag() if defaultTag == "" { defaultTag = allProxies[0] } sort.Slice(allProxies, func(i, j int) bool { return allProxies[i] == defaultTag }) // fix clash dashboard proxyMap.Put("GLOBAL", map[string]any{ "type": "Fallback", "name": "GLOBAL", "udp": true, "history": []*DelayHistory{}, "all": allProxies, "now": defaultTag, }) for i, detour := range outbounds { var tag string if detour.Tag() == "" { tag = F.ToString(i) } else { tag = detour.Tag() } proxyMap.Put(tag, proxyInfo(server, detour)) } var responseMap badjson.JSONObject responseMap.Put("proxies", &proxyMap) response, err := responseMap.MarshalJSON() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } w.Write(response) } } func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) response, err := proxyInfo(server, proxy).MarshalJSON() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } w.Write(response) } } type UpdateProxyRequest struct { Name string `json:"name"` } func updateProxy(w http.ResponseWriter, r *http.Request) { req := UpdateProxyRequest{} if err := render.DecodeJSON(r.Body, &req); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) selector, ok := proxy.(*outbound.Selector) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) return } if !selector.SelectOutbound(req.Name) { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(fmt.Sprintf("Selector update error: not found"))) return } render.NoContent(w, r) } func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() url := query.Get("url") timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) defer cancel() delay, err := URLTest(ctx, url, proxy) if ctx.Err() != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, ErrRequestTimeout) return } if err != nil || delay == 0 { render.Status(r, http.StatusServiceUnavailable) render.JSON(w, r, newError("An error occurred in the delay test")) return } server.delayHistory[proxy.Tag()] = &DelayHistory{ Time: time.Now(), Delay: delay, } render.JSON(w, r, render.M{ "delay": delay, }) } } func URLTest(ctx context.Context, link string, detour adapter.Outbound) (t uint16, err error) { linkURL, err := url.Parse(link) if err != nil { return } hostname := linkURL.Hostname() port := linkURL.Port() if port == "" { switch linkURL.Scheme { case "http": port = "80" case "https": port = "443" } } start := time.Now() instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port)) if err != nil { return } defer instance.Close() req, err := http.NewRequest(http.MethodHead, link, nil) if err != nil { return } req = req.WithContext(ctx) transport := &http.Transport{ Dial: func(string, string) (net.Conn, error) { return instance, nil }, // from http.DefaultTransport MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } client := http.Client{ Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return } resp.Body.Close() t = uint16(time.Since(start) / time.Millisecond) return }