Browse Source

clash-api: Add Clash.Meta APIs

世界 2 years ago
parent
commit
542612129d

+ 5 - 0
adapter/experimental.go

@@ -35,6 +35,11 @@ type OutboundGroup interface {
 	All() []string
 }
 
+type URLTestGroup interface {
+	OutboundGroup
+	URLTest(ctx context.Context, url string) (map[string]uint16, error)
+}
+
 func OutboundTag(detour Outbound) string {
 	if group, isGroup := detour.(OutboundGroup); isGroup {
 		return group.Now()

+ 78 - 0
experimental/clashapi/api_meta.go

@@ -0,0 +1,78 @@
+package clashapi
+
+import (
+	"bytes"
+	"net/http"
+	"time"
+
+	"github.com/sagernet/sing-box/common/json"
+	"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
+	"github.com/sagernet/websocket"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-chi/render"
+)
+
+// API created by Clash.Meta
+
+func (s *Server) setupMetaAPI(r chi.Router) {
+	r.Get("/memory", memory(s.trafficManager))
+	r.Mount("/group", groupRouter(s))
+}
+
+type Memory struct {
+	Inuse   uint64 `json:"inuse"`
+	OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
+}
+
+func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var wsConn *websocket.Conn
+		if websocket.IsWebSocketUpgrade(r) {
+			var err error
+			wsConn, err = upgrader.Upgrade(w, r, nil)
+			if err != nil {
+				return
+			}
+		}
+
+		if wsConn == nil {
+			w.Header().Set("Content-Type", "application/json")
+			render.Status(r, http.StatusOK)
+		}
+
+		tick := time.NewTicker(time.Second)
+		defer tick.Stop()
+		buf := &bytes.Buffer{}
+		var err error
+		first := true
+		for range tick.C {
+			buf.Reset()
+
+			inuse := trafficManager.Snapshot().Memory
+
+			// make chat.js begin with zero
+			// this is shit var,but we need output 0 for first time
+			if first {
+				first = false
+				inuse = 0
+			}
+			if err := json.NewEncoder(buf).Encode(Memory{
+				Inuse:   inuse,
+				OSLimit: 0,
+			}); err != nil {
+				break
+			}
+			if wsConn == nil {
+				_, err = w.Write(buf.Bytes())
+				w.(http.Flusher).Flush()
+			} else {
+				err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
+			}
+
+			if err != nil {
+				break
+			}
+		}
+	}
+}

+ 132 - 0
experimental/clashapi/api_meta_group.go

@@ -0,0 +1,132 @@
+package clashapi
+
+import (
+	"context"
+	"net/http"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/badjson"
+	"github.com/sagernet/sing-box/common/urltest"
+	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/batch"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-chi/render"
+)
+
+func groupRouter(server *Server) http.Handler {
+	r := chi.NewRouter()
+	r.Get("/", getGroups(server))
+	r.Route("/{name}", func(r chi.Router) {
+		r.Use(parseProxyName, findProxyByName(server.router))
+		r.Get("/", getGroup(server))
+		r.Get("/delay", getGroupDelay(server))
+	})
+	return r
+}
+
+func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
+			_, isGroup := it.(adapter.OutboundGroup)
+			return isGroup
+		}), func(it adapter.Outbound) *badjson.JSONObject {
+			return proxyInfo(server, it)
+		})
+		render.JSON(w, r, render.M{
+			"proxies": groups,
+		})
+	}
+}
+
+func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
+		if _, ok := proxy.(adapter.OutboundGroup); ok {
+			render.JSON(w, r, proxyInfo(server, proxy))
+			return
+		}
+		render.Status(r, http.StatusNotFound)
+		render.JSON(w, r, ErrNotFound)
+	}
+}
+
+func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
+		group, ok := proxy.(adapter.OutboundGroup)
+		if !ok {
+			render.Status(r, http.StatusNotFound)
+			render.JSON(w, r, ErrNotFound)
+			return
+		}
+
+		query := r.URL.Query()
+		url := query.Get("url")
+		timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32)
+		if err != nil {
+			render.Status(r, http.StatusBadRequest)
+			render.JSON(w, r, ErrBadRequest)
+			return
+		}
+
+		ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout))
+		defer cancel()
+
+		var result map[string]uint16
+		if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup {
+			result, err = urlTestGroup.URLTest(ctx, url)
+		} else {
+			outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound {
+				itOutbound, _ := server.router.Outbound(it)
+				return itOutbound
+			}))
+			b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
+			checked := make(map[string]bool)
+			result = make(map[string]uint16)
+			var resultAccess sync.Mutex
+			for _, detour := range outbounds {
+				tag := detour.Tag()
+				realTag := outbound.RealTag(detour)
+				if checked[realTag] {
+					continue
+				}
+				checked[realTag] = true
+				p, loaded := server.router.Outbound(realTag)
+				if !loaded {
+					continue
+				}
+				b.Go(realTag, func() (any, error) {
+					t, err := urltest.URLTest(ctx, url, p)
+					if err != nil {
+						server.logger.Debug("outbound ", tag, " unavailable: ", err)
+						server.urlTestHistory.DeleteURLTestHistory(realTag)
+					} else {
+						server.logger.Debug("outbound ", tag, " available: ", t, "ms")
+						server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
+							Time:  time.Now(),
+							Delay: t,
+						})
+						resultAccess.Lock()
+						result[tag] = t
+						resultAccess.Unlock()
+					}
+					return nil, nil
+				})
+			}
+			b.Wait()
+		}
+
+		if err != nil {
+			render.Status(r, http.StatusGatewayTimeout)
+			render.JSON(w, r, newError(err.Error()))
+			return
+		}
+
+		render.JSON(w, r, result)
+	}
+}

+ 3 - 1
experimental/clashapi/server.go

@@ -109,6 +109,8 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
 		r.Mount("/profile", profileRouter())
 		r.Mount("/cache", cacheRouter(router))
 		r.Mount("/dns", dnsRouter(router))
+
+		server.setupMetaAPI(r)
 	})
 	if options.ExternalUI != "" {
 		server.externalUI = C.BasePath(os.ExpandEnv(options.ExternalUI))
@@ -406,5 +408,5 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht
 }
 
 func version(w http.ResponseWriter, r *http.Request) {
-	render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true})
+	render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true})
 }

+ 1 - 1
experimental/clashapi/server_resources.go

@@ -40,7 +40,7 @@ func (s *Server) downloadExternalUI() error {
 	if s.externalUIDownloadURL != "" {
 		downloadURL = s.externalUIDownloadURL
 	} else {
-		downloadURL = "https://github.com/Dreamacro/clash-dashboard/archive/refs/heads/gh-pages.zip"
+		downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"
 	}
 	s.logger.Info("downloading external ui")
 	var detour adapter.Outbound

+ 13 - 0
experimental/clashapi/trafficontrol/manager.go

@@ -1,6 +1,7 @@
 package trafficontrol
 
 import (
+	"runtime"
 	"time"
 
 	"github.com/sagernet/sing-box/experimental/clashapi/compatible"
@@ -18,12 +19,15 @@ type Manager struct {
 	connections compatible.Map[string, tracker]
 	ticker      *time.Ticker
 	done        chan struct{}
+	// process     *process.Process
+	memory uint64
 }
 
 func NewManager() *Manager {
 	manager := &Manager{
 		ticker: time.NewTicker(time.Second),
 		done:   make(chan struct{}),
+		// process: &process.Process{Pid: int32(os.Getpid())},
 	}
 	go manager.handle()
 	return manager
@@ -58,10 +62,18 @@ func (m *Manager) Snapshot() *Snapshot {
 		return true
 	})
 
+	//if memoryInfo, err := m.process.MemoryInfo(); err == nil {
+	//	m.memory = memoryInfo.RSS
+	//} else {
+	var memStats runtime.MemStats
+	runtime.ReadMemStats(&memStats)
+	m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
+
 	return &Snapshot{
 		UploadTotal:   m.uploadTotal.Load(),
 		DownloadTotal: m.downloadTotal.Load(),
 		Connections:   connections,
+		Memory:        m.memory,
 	}
 }
 
@@ -100,4 +112,5 @@ type Snapshot struct {
 	DownloadTotal int64     `json:"downloadTotal"`
 	UploadTotal   int64     `json:"uploadTotal"`
 	Connections   []tracker `json:"connections"`
+	Memory        uint64    `json:"memory"`
 }

+ 18 - 3
outbound/urltest.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"net"
 	"sort"
+	"sync"
 	"time"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -71,7 +72,7 @@ func (s *URLTest) Start() error {
 	return s.group.Start()
 }
 
-func (s URLTest) Close() error {
+func (s *URLTest) Close() error {
 	return common.Close(
 		common.PtrOrNil(s.group),
 	)
@@ -85,6 +86,10 @@ func (s *URLTest) All() []string {
 	return s.tags
 }
 
+func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16, error) {
+	return s.group.URLTest(ctx, link)
+}
+
 func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	outbound := s.group.Select(network)
 	conn, err := outbound.DialContext(ctx, network, destination)
@@ -249,8 +254,14 @@ func (g *URLTestGroup) loopCheck() {
 }
 
 func (g *URLTestGroup) checkOutbounds() {
-	b, _ := batch.New(context.Background(), batch.WithConcurrencyNum[any](10))
+	_, _ = g.URLTest(context.Background(), g.link)
+}
+
+func (g *URLTestGroup) URLTest(ctx context.Context, link string) (map[string]uint16, error) {
+	b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
 	checked := make(map[string]bool)
+	result := make(map[string]uint16)
+	var resultAccess sync.Mutex
 	for _, detour := range g.outbounds {
 		tag := detour.Tag()
 		realTag := RealTag(detour)
@@ -269,7 +280,7 @@ func (g *URLTestGroup) checkOutbounds() {
 		b.Go(realTag, func() (any, error) {
 			ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
 			defer cancel()
-			t, err := urltest.URLTest(ctx, g.link, p)
+			t, err := urltest.URLTest(ctx, link, p)
 			if err != nil {
 				g.logger.Debug("outbound ", tag, " unavailable: ", err)
 				g.history.DeleteURLTestHistory(realTag)
@@ -279,9 +290,13 @@ func (g *URLTestGroup) checkOutbounds() {
 					Time:  time.Now(),
 					Delay: t,
 				})
+				resultAccess.Lock()
+				result[tag] = t
+				resultAccess.Unlock()
 			}
 			return nil, nil
 		})
 	}
 	b.Wait()
+	return result, nil
 }