github.com/sagernet/sing-box@v1.9.0-rc.20/experimental/clashapi/proxies.go (about)

     1  package clashapi
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/sagernet/sing-box/adapter"
    12  	"github.com/sagernet/sing-box/common/urltest"
    13  	C "github.com/sagernet/sing-box/constant"
    14  	"github.com/sagernet/sing-box/outbound"
    15  	"github.com/sagernet/sing/common"
    16  	F "github.com/sagernet/sing/common/format"
    17  	"github.com/sagernet/sing/common/json/badjson"
    18  	N "github.com/sagernet/sing/common/network"
    19  
    20  	"github.com/go-chi/chi/v5"
    21  	"github.com/go-chi/render"
    22  )
    23  
    24  func proxyRouter(server *Server, router adapter.Router) http.Handler {
    25  	r := chi.NewRouter()
    26  	r.Get("/", getProxies(server, router))
    27  
    28  	r.Route("/{name}", func(r chi.Router) {
    29  		r.Use(parseProxyName, findProxyByName(router))
    30  		r.Get("/", getProxy(server))
    31  		r.Get("/delay", getProxyDelay(server))
    32  		r.Put("/", updateProxy)
    33  	})
    34  	return r
    35  }
    36  
    37  func parseProxyName(next http.Handler) http.Handler {
    38  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    39  		name := getEscapeParam(r, "name")
    40  		ctx := context.WithValue(r.Context(), CtxKeyProxyName, name)
    41  		next.ServeHTTP(w, r.WithContext(ctx))
    42  	})
    43  }
    44  
    45  func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler {
    46  	return func(next http.Handler) http.Handler {
    47  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    48  			name := r.Context().Value(CtxKeyProxyName).(string)
    49  			proxy, exist := router.Outbound(name)
    50  			if !exist {
    51  				render.Status(r, http.StatusNotFound)
    52  				render.JSON(w, r, ErrNotFound)
    53  				return
    54  			}
    55  			ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
    56  			next.ServeHTTP(w, r.WithContext(ctx))
    57  		})
    58  	}
    59  }
    60  
    61  func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
    62  	var info badjson.JSONObject
    63  	var clashType string
    64  	switch detour.Type() {
    65  	case C.TypeBlock:
    66  		clashType = "Reject"
    67  	default:
    68  		clashType = C.ProxyDisplayName(detour.Type())
    69  	}
    70  	info.Put("type", clashType)
    71  	info.Put("name", detour.Tag())
    72  	info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP))
    73  	delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour))
    74  	if delayHistory != nil {
    75  		info.Put("history", []*urltest.History{delayHistory})
    76  	} else {
    77  		info.Put("history", []*urltest.History{})
    78  	}
    79  	if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
    80  		info.Put("now", group.Now())
    81  		info.Put("all", group.All())
    82  	}
    83  	return &info
    84  }
    85  
    86  func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
    87  	return func(w http.ResponseWriter, r *http.Request) {
    88  		var proxyMap badjson.JSONObject
    89  		outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool {
    90  			return detour.Tag() != ""
    91  		})
    92  
    93  		allProxies := make([]string, 0, len(outbounds))
    94  
    95  		for _, detour := range outbounds {
    96  			switch detour.Type() {
    97  			case C.TypeDirect, C.TypeBlock, C.TypeDNS:
    98  				continue
    99  			}
   100  			allProxies = append(allProxies, detour.Tag())
   101  		}
   102  
   103  		var defaultTag string
   104  		if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
   105  			defaultTag = defaultOutbound.Tag()
   106  		} else {
   107  			defaultTag = allProxies[0]
   108  		}
   109  
   110  		sort.SliceStable(allProxies, func(i, j int) bool {
   111  			return allProxies[i] == defaultTag
   112  		})
   113  
   114  		// fix clash dashboard
   115  		proxyMap.Put("GLOBAL", map[string]any{
   116  			"type":    "Fallback",
   117  			"name":    "GLOBAL",
   118  			"udp":     true,
   119  			"history": []*urltest.History{},
   120  			"all":     allProxies,
   121  			"now":     defaultTag,
   122  		})
   123  
   124  		for i, detour := range outbounds {
   125  			var tag string
   126  			if detour.Tag() == "" {
   127  				tag = F.ToString(i)
   128  			} else {
   129  				tag = detour.Tag()
   130  			}
   131  			proxyMap.Put(tag, proxyInfo(server, detour))
   132  		}
   133  		var responseMap badjson.JSONObject
   134  		responseMap.Put("proxies", &proxyMap)
   135  		response, err := responseMap.MarshalJSON()
   136  		if err != nil {
   137  			render.Status(r, http.StatusInternalServerError)
   138  			render.JSON(w, r, newError(err.Error()))
   139  			return
   140  		}
   141  		w.Write(response)
   142  	}
   143  }
   144  
   145  func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
   146  	return func(w http.ResponseWriter, r *http.Request) {
   147  		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   148  		response, err := proxyInfo(server, proxy).MarshalJSON()
   149  		if err != nil {
   150  			render.Status(r, http.StatusInternalServerError)
   151  			render.JSON(w, r, newError(err.Error()))
   152  			return
   153  		}
   154  		w.Write(response)
   155  	}
   156  }
   157  
   158  type UpdateProxyRequest struct {
   159  	Name string `json:"name"`
   160  }
   161  
   162  func updateProxy(w http.ResponseWriter, r *http.Request) {
   163  	req := UpdateProxyRequest{}
   164  	if err := render.DecodeJSON(r.Body, &req); err != nil {
   165  		render.Status(r, http.StatusBadRequest)
   166  		render.JSON(w, r, ErrBadRequest)
   167  		return
   168  	}
   169  
   170  	proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   171  	selector, ok := proxy.(*outbound.Selector)
   172  	if !ok {
   173  		render.Status(r, http.StatusBadRequest)
   174  		render.JSON(w, r, newError("Must be a Selector"))
   175  		return
   176  	}
   177  
   178  	if !selector.SelectOutbound(req.Name) {
   179  		render.Status(r, http.StatusBadRequest)
   180  		render.JSON(w, r, newError("Selector update error: not found"))
   181  		return
   182  	}
   183  
   184  	render.NoContent(w, r)
   185  }
   186  
   187  func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
   188  	return func(w http.ResponseWriter, r *http.Request) {
   189  		query := r.URL.Query()
   190  		url := query.Get("url")
   191  		if strings.HasPrefix(url, "http://") {
   192  			url = ""
   193  		}
   194  		timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
   195  		if err != nil {
   196  			render.Status(r, http.StatusBadRequest)
   197  			render.JSON(w, r, ErrBadRequest)
   198  			return
   199  		}
   200  
   201  		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   202  		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
   203  		defer cancel()
   204  
   205  		delay, err := urltest.URLTest(ctx, url, proxy)
   206  		defer func() {
   207  			realTag := outbound.RealTag(proxy)
   208  			if err != nil {
   209  				server.urlTestHistory.DeleteURLTestHistory(realTag)
   210  			} else {
   211  				server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
   212  					Time:  time.Now(),
   213  					Delay: delay,
   214  				})
   215  			}
   216  		}()
   217  
   218  		if ctx.Err() != nil {
   219  			render.Status(r, http.StatusGatewayTimeout)
   220  			render.JSON(w, r, ErrRequestTimeout)
   221  			return
   222  		}
   223  
   224  		if err != nil || delay == 0 {
   225  			render.Status(r, http.StatusServiceUnavailable)
   226  			render.JSON(w, r, newError("An error occurred in the delay test"))
   227  			return
   228  		}
   229  
   230  		render.JSON(w, r, render.M{
   231  			"delay": delay,
   232  		})
   233  	}
   234  }