mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 08:31:30 +00:00
clash-api: Add Clash.Meta APIs
This commit is contained in:
parent
750f87bb0a
commit
542612129d
|
@ -35,6 +35,11 @@ type OutboundGroup interface {
|
||||||
All() []string
|
All() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type URLTestGroup interface {
|
||||||
|
OutboundGroup
|
||||||
|
URLTest(ctx context.Context, url string) (map[string]uint16, error)
|
||||||
|
}
|
||||||
|
|
||||||
func OutboundTag(detour Outbound) string {
|
func OutboundTag(detour Outbound) string {
|
||||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||||
return group.Now()
|
return group.Now()
|
||||||
|
|
78
experimental/clashapi/api_meta.go
Normal file
78
experimental/clashapi/api_meta.go
Normal file
|
@ -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
experimental/clashapi/api_meta_group.go
Normal file
132
experimental/clashapi/api_meta_group.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,8 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
|
||||||
r.Mount("/profile", profileRouter())
|
r.Mount("/profile", profileRouter())
|
||||||
r.Mount("/cache", cacheRouter(router))
|
r.Mount("/cache", cacheRouter(router))
|
||||||
r.Mount("/dns", dnsRouter(router))
|
r.Mount("/dns", dnsRouter(router))
|
||||||
|
|
||||||
|
server.setupMetaAPI(r)
|
||||||
})
|
})
|
||||||
if options.ExternalUI != "" {
|
if options.ExternalUI != "" {
|
||||||
server.externalUI = C.BasePath(os.ExpandEnv(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) {
|
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})
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (s *Server) downloadExternalUI() error {
|
||||||
if s.externalUIDownloadURL != "" {
|
if s.externalUIDownloadURL != "" {
|
||||||
downloadURL = s.externalUIDownloadURL
|
downloadURL = s.externalUIDownloadURL
|
||||||
} else {
|
} 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")
|
s.logger.Info("downloading external ui")
|
||||||
var detour adapter.Outbound
|
var detour adapter.Outbound
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package trafficontrol
|
package trafficontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
||||||
|
@ -18,12 +19,15 @@ type Manager struct {
|
||||||
connections compatible.Map[string, tracker]
|
connections compatible.Map[string, tracker]
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
// process *process.Process
|
||||||
|
memory uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager() *Manager {
|
func NewManager() *Manager {
|
||||||
manager := &Manager{
|
manager := &Manager{
|
||||||
ticker: time.NewTicker(time.Second),
|
ticker: time.NewTicker(time.Second),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
|
// process: &process.Process{Pid: int32(os.Getpid())},
|
||||||
}
|
}
|
||||||
go manager.handle()
|
go manager.handle()
|
||||||
return manager
|
return manager
|
||||||
|
@ -58,10 +62,18 @@ func (m *Manager) Snapshot() *Snapshot {
|
||||||
return true
|
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{
|
return &Snapshot{
|
||||||
UploadTotal: m.uploadTotal.Load(),
|
UploadTotal: m.uploadTotal.Load(),
|
||||||
DownloadTotal: m.downloadTotal.Load(),
|
DownloadTotal: m.downloadTotal.Load(),
|
||||||
Connections: connections,
|
Connections: connections,
|
||||||
|
Memory: m.memory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,4 +112,5 @@ type Snapshot struct {
|
||||||
DownloadTotal int64 `json:"downloadTotal"`
|
DownloadTotal int64 `json:"downloadTotal"`
|
||||||
UploadTotal int64 `json:"uploadTotal"`
|
UploadTotal int64 `json:"uploadTotal"`
|
||||||
Connections []tracker `json:"connections"`
|
Connections []tracker `json:"connections"`
|
||||||
|
Memory uint64 `json:"memory"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
@ -71,7 +72,7 @@ func (s *URLTest) Start() error {
|
||||||
return s.group.Start()
|
return s.group.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s URLTest) Close() error {
|
func (s *URLTest) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
common.PtrOrNil(s.group),
|
common.PtrOrNil(s.group),
|
||||||
)
|
)
|
||||||
|
@ -85,6 +86,10 @@ func (s *URLTest) All() []string {
|
||||||
return s.tags
|
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) {
|
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
outbound := s.group.Select(network)
|
outbound := s.group.Select(network)
|
||||||
conn, err := outbound.DialContext(ctx, network, destination)
|
conn, err := outbound.DialContext(ctx, network, destination)
|
||||||
|
@ -249,8 +254,14 @@ func (g *URLTestGroup) loopCheck() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *URLTestGroup) checkOutbounds() {
|
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)
|
checked := make(map[string]bool)
|
||||||
|
result := make(map[string]uint16)
|
||||||
|
var resultAccess sync.Mutex
|
||||||
for _, detour := range g.outbounds {
|
for _, detour := range g.outbounds {
|
||||||
tag := detour.Tag()
|
tag := detour.Tag()
|
||||||
realTag := RealTag(detour)
|
realTag := RealTag(detour)
|
||||||
|
@ -269,7 +280,7 @@ func (g *URLTestGroup) checkOutbounds() {
|
||||||
b.Go(realTag, func() (any, error) {
|
b.Go(realTag, func() (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
t, err := urltest.URLTest(ctx, g.link, p)
|
t, err := urltest.URLTest(ctx, link, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logger.Debug("outbound ", tag, " unavailable: ", err)
|
g.logger.Debug("outbound ", tag, " unavailable: ", err)
|
||||||
g.history.DeleteURLTestHistory(realTag)
|
g.history.DeleteURLTestHistory(realTag)
|
||||||
|
@ -279,9 +290,13 @@ func (g *URLTestGroup) checkOutbounds() {
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Delay: t,
|
Delay: t,
|
||||||
})
|
})
|
||||||
|
resultAccess.Lock()
|
||||||
|
result[tag] = t
|
||||||
|
resultAccess.Unlock()
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
b.Wait()
|
b.Wait()
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue