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 }