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

     1  package clashapi
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/sagernet/sing-box/adapter"
    12  	"github.com/sagernet/sing-box/common/badjson"
    13  	"github.com/sagernet/sing-box/common/urltest"
    14  	C "github.com/sagernet/sing-box/constant"
    15  	"github.com/sagernet/sing-box/outbound"
    16  	"github.com/sagernet/sing/common"
    17  	F "github.com/sagernet/sing/common/format"
    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.TypeDirect:
    66  		clashType = "Direct"
    67  	case C.TypeBlock:
    68  		clashType = "Reject"
    69  	case C.TypeSocks:
    70  		clashType = "Socks"
    71  	case C.TypeHTTP:
    72  		clashType = "HTTP"
    73  	case C.TypeShadowsocks:
    74  		clashType = "Shadowsocks"
    75  	case C.TypeVMess:
    76  		clashType = "VMess"
    77  	case C.TypeTrojan:
    78  		clashType = "Trojan"
    79  	case C.TypeHysteria:
    80  		clashType = "Hysteria"
    81  	case C.TypeWireGuard:
    82  		clashType = "WireGuard"
    83  	case C.TypeShadowsocksR:
    84  		clashType = "ShadowsocksR"
    85  	case C.TypeVLESS:
    86  		clashType = "VLESS"
    87  	case C.TypeTor:
    88  		clashType = "Tor"
    89  	case C.TypeSSH:
    90  		clashType = "SSH"
    91  	case C.TypeSelector:
    92  		clashType = "Selector"
    93  	case C.TypeURLTest:
    94  		clashType = "URLTest"
    95  	default:
    96  		clashType = "Direct"
    97  	}
    98  	info.Put("type", clashType)
    99  	info.Put("name", detour.Tag())
   100  	info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP))
   101  	delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour))
   102  	if delayHistory != nil {
   103  		info.Put("history", []*urltest.History{delayHistory})
   104  	} else {
   105  		info.Put("history", []*urltest.History{})
   106  	}
   107  	if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
   108  		info.Put("now", group.Now())
   109  		info.Put("all", group.All())
   110  	}
   111  	return &info
   112  }
   113  
   114  func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
   115  	return func(w http.ResponseWriter, r *http.Request) {
   116  		var proxyMap badjson.JSONObject
   117  		outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool {
   118  			return detour.Tag() != ""
   119  		})
   120  
   121  		allProxies := make([]string, 0, len(outbounds))
   122  
   123  		for _, detour := range outbounds {
   124  			switch detour.Type() {
   125  			case C.TypeDirect, C.TypeBlock, C.TypeDNS:
   126  				continue
   127  			}
   128  			allProxies = append(allProxies, detour.Tag())
   129  		}
   130  
   131  		defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag()
   132  		if defaultTag == "" {
   133  			defaultTag = allProxies[0]
   134  		}
   135  
   136  		sort.SliceStable(allProxies, func(i, j int) bool {
   137  			return allProxies[i] == defaultTag
   138  		})
   139  
   140  		// fix clash dashboard
   141  		proxyMap.Put("GLOBAL", map[string]any{
   142  			"type":    "Fallback",
   143  			"name":    "GLOBAL",
   144  			"udp":     true,
   145  			"history": []*urltest.History{},
   146  			"all":     allProxies,
   147  			"now":     defaultTag,
   148  		})
   149  
   150  		for i, detour := range outbounds {
   151  			var tag string
   152  			if detour.Tag() == "" {
   153  				tag = F.ToString(i)
   154  			} else {
   155  				tag = detour.Tag()
   156  			}
   157  			proxyMap.Put(tag, proxyInfo(server, detour))
   158  		}
   159  		var responseMap badjson.JSONObject
   160  		responseMap.Put("proxies", &proxyMap)
   161  		response, err := responseMap.MarshalJSON()
   162  		if err != nil {
   163  			render.Status(r, http.StatusInternalServerError)
   164  			render.JSON(w, r, newError(err.Error()))
   165  			return
   166  		}
   167  		w.Write(response)
   168  	}
   169  }
   170  
   171  func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
   172  	return func(w http.ResponseWriter, r *http.Request) {
   173  		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   174  		response, err := proxyInfo(server, proxy).MarshalJSON()
   175  		if err != nil {
   176  			render.Status(r, http.StatusInternalServerError)
   177  			render.JSON(w, r, newError(err.Error()))
   178  			return
   179  		}
   180  		w.Write(response)
   181  	}
   182  }
   183  
   184  type UpdateProxyRequest struct {
   185  	Name string `json:"name"`
   186  }
   187  
   188  func updateProxy(w http.ResponseWriter, r *http.Request) {
   189  	req := UpdateProxyRequest{}
   190  	if err := render.DecodeJSON(r.Body, &req); err != nil {
   191  		render.Status(r, http.StatusBadRequest)
   192  		render.JSON(w, r, ErrBadRequest)
   193  		return
   194  	}
   195  
   196  	proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   197  	selector, ok := proxy.(*outbound.Selector)
   198  	if !ok {
   199  		render.Status(r, http.StatusBadRequest)
   200  		render.JSON(w, r, newError("Must be a Selector"))
   201  		return
   202  	}
   203  
   204  	if !selector.SelectOutbound(req.Name) {
   205  		render.Status(r, http.StatusBadRequest)
   206  		render.JSON(w, r, newError(fmt.Sprintf("Selector update error: not found")))
   207  		return
   208  	}
   209  
   210  	render.NoContent(w, r)
   211  }
   212  
   213  func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
   214  	return func(w http.ResponseWriter, r *http.Request) {
   215  		query := r.URL.Query()
   216  		url := query.Get("url")
   217  		timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
   218  		if err != nil {
   219  			render.Status(r, http.StatusBadRequest)
   220  			render.JSON(w, r, ErrBadRequest)
   221  			return
   222  		}
   223  
   224  		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
   225  		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
   226  		defer cancel()
   227  
   228  		delay, err := urltest.URLTest(ctx, url, proxy)
   229  		defer func() {
   230  			realTag := outbound.RealTag(proxy)
   231  			if err != nil {
   232  				server.urlTestHistory.DeleteURLTestHistory(realTag)
   233  			} else {
   234  				server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
   235  					Time:  time.Now(),
   236  					Delay: delay,
   237  				})
   238  			}
   239  		}()
   240  
   241  		if ctx.Err() != nil {
   242  			render.Status(r, http.StatusGatewayTimeout)
   243  			render.JSON(w, r, ErrRequestTimeout)
   244  			return
   245  		}
   246  
   247  		if err != nil || delay == 0 {
   248  			render.Status(r, http.StatusServiceUnavailable)
   249  			render.JSON(w, r, newError("An error occurred in the delay test"))
   250  			return
   251  		}
   252  
   253  		render.JSON(w, r, render.M{
   254  			"delay": delay,
   255  		})
   256  	}
   257  }