github.com/inazumav/sing-box@v0.0.0-20230926072359-ab51429a14f1/experimental/clashapi/proxies.go (about)

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