| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- package clashapi
- import (
- "context"
- "fmt"
- "net"
- "net/http"
- "net/url"
- "strconv"
- "time"
- "github.com/sagernet/sing-box/adapter"
- "github.com/sagernet/sing-box/common/badjson"
- C "github.com/sagernet/sing-box/constant"
- "github.com/sagernet/sing-box/outbound"
- "github.com/sagernet/sing/common"
- F "github.com/sagernet/sing/common/format"
- M "github.com/sagernet/sing/common/metadata"
- "github.com/go-chi/chi/v5"
- "github.com/go-chi/render"
- "sort"
- )
- func proxyRouter(server *Server, router adapter.Router) http.Handler {
- r := chi.NewRouter()
- r.Get("/", getProxies(server, router))
- r.Route("/{name}", func(r chi.Router) {
- r.Use(parseProxyName, findProxyByName(router))
- r.Get("/", getProxy(server))
- r.Get("/delay", getProxyDelay(server))
- r.Put("/", updateProxy)
- })
- return r
- }
- func parseProxyName(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- name := getEscapeParam(r, "name")
- ctx := context.WithValue(r.Context(), CtxKeyProxyName, name)
- next.ServeHTTP(w, r.WithContext(ctx))
- })
- }
- func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler {
- return func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- name := r.Context().Value(CtxKeyProxyName).(string)
- proxy, exist := router.Outbound(name)
- if !exist {
- render.Status(r, http.StatusNotFound)
- render.JSON(w, r, ErrNotFound)
- return
- }
- ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
- next.ServeHTTP(w, r.WithContext(ctx))
- })
- }
- }
- func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
- var info badjson.JSONObject
- var clashType string
- var isSelector bool
- switch detour.Type() {
- case C.TypeDirect:
- clashType = "Direct"
- case C.TypeBlock:
- clashType = "Reject"
- case C.TypeSocks:
- clashType = "Socks"
- case C.TypeHTTP:
- clashType = "Http"
- case C.TypeShadowsocks:
- clashType = "Shadowsocks"
- case C.TypeVMess:
- clashType = "Vmess"
- case C.TypeSelector:
- clashType = "Selector"
- isSelector = true
- default:
- clashType = "Unknown"
- }
- info.Put("type", clashType)
- info.Put("name", detour.Tag())
- info.Put("udp", common.Contains(detour.Network(), C.NetworkUDP))
- var delayHistory *DelayHistory
- var loaded bool
- if isSelector {
- selector := detour.(*outbound.Selector)
- info.Put("now", selector.Now())
- info.Put("all", selector.All())
- delayHistory, loaded = server.delayHistory[selector.Now()]
- } else {
- delayHistory, loaded = server.delayHistory[detour.Tag()]
- }
- if loaded {
- info.Put("history", []*DelayHistory{delayHistory})
- } else {
- info.Put("history", []*DelayHistory{})
- }
- return &info
- }
- func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- var proxyMap badjson.JSONObject
- outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool {
- return detour.Tag() != ""
- })
- allProxies := make([]string, 0, len(outbounds))
- for _, detour := range outbounds {
- switch detour.Type() {
- case C.TypeDirect, C.TypeBlock:
- continue
- }
- allProxies = append(allProxies, detour.Tag())
- }
- defaultTag := router.DefaultOutbound(C.NetworkTCP).Tag()
- if defaultTag == "" {
- defaultTag = allProxies[0]
- }
- sort.Slice(allProxies, func(i, j int) bool {
- return allProxies[i] == defaultTag
- })
- // fix clash dashboard
- proxyMap.Put("GLOBAL", map[string]any{
- "type": "Fallback",
- "name": "GLOBAL",
- "udp": true,
- "history": []*DelayHistory{},
- "all": allProxies,
- "now": defaultTag,
- })
- for i, detour := range outbounds {
- var tag string
- if detour.Tag() == "" {
- tag = F.ToString(i)
- } else {
- tag = detour.Tag()
- }
- proxyMap.Put(tag, proxyInfo(server, detour))
- }
- var responseMap badjson.JSONObject
- responseMap.Put("proxies", &proxyMap)
- response, err := responseMap.MarshalJSON()
- if err != nil {
- render.Status(r, http.StatusInternalServerError)
- render.JSON(w, r, newError(err.Error()))
- return
- }
- w.Write(response)
- }
- }
- func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
- response, err := proxyInfo(server, proxy).MarshalJSON()
- if err != nil {
- render.Status(r, http.StatusInternalServerError)
- render.JSON(w, r, newError(err.Error()))
- return
- }
- w.Write(response)
- }
- }
- type UpdateProxyRequest struct {
- Name string `json:"name"`
- }
- func updateProxy(w http.ResponseWriter, r *http.Request) {
- req := UpdateProxyRequest{}
- if err := render.DecodeJSON(r.Body, &req); err != nil {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, ErrBadRequest)
- return
- }
- proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
- selector, ok := proxy.(*outbound.Selector)
- if !ok {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, newError("Must be a Selector"))
- return
- }
- if !selector.SelectOutbound(req.Name) {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, newError(fmt.Sprintf("Selector update error: not found")))
- return
- }
- render.NoContent(w, r)
- }
- func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- query := r.URL.Query()
- url := query.Get("url")
- timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
- if err != nil {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, ErrBadRequest)
- return
- }
- proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
- ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
- defer cancel()
- delay, err := URLTest(ctx, url, proxy)
- if ctx.Err() != nil {
- render.Status(r, http.StatusGatewayTimeout)
- render.JSON(w, r, ErrRequestTimeout)
- return
- }
- if err != nil || delay == 0 {
- render.Status(r, http.StatusServiceUnavailable)
- render.JSON(w, r, newError("An error occurred in the delay test"))
- return
- }
- server.delayHistory[proxy.Tag()] = &DelayHistory{
- Time: time.Now(),
- Delay: delay,
- }
- render.JSON(w, r, render.M{
- "delay": delay,
- })
- }
- }
- func URLTest(ctx context.Context, link string, detour adapter.Outbound) (t uint16, err error) {
- linkURL, err := url.Parse(link)
- if err != nil {
- return
- }
- hostname := linkURL.Hostname()
- port := linkURL.Port()
- if port == "" {
- switch linkURL.Scheme {
- case "http":
- port = "80"
- case "https":
- port = "443"
- }
- }
- start := time.Now()
- instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port))
- if err != nil {
- return
- }
- defer instance.Close()
- req, err := http.NewRequest(http.MethodHead, link, nil)
- if err != nil {
- return
- }
- req = req.WithContext(ctx)
- transport := &http.Transport{
- Dial: func(string, string) (net.Conn, error) {
- return instance, nil
- },
- // from http.DefaultTransport
- MaxIdleConns: 100,
- IdleConnTimeout: 90 * time.Second,
- TLSHandshakeTimeout: 10 * time.Second,
- ExpectContinueTimeout: 1 * time.Second,
- }
- client := http.Client{
- Transport: transport,
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- },
- }
- defer client.CloseIdleConnections()
- resp, err := client.Do(req)
- if err != nil {
- return
- }
- resp.Body.Close()
- t = uint16(time.Since(start) / time.Millisecond)
- return
- }
|