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 }