From ac283332e3980e8d32cba3ba80f08851109fa9db Mon Sep 17 00:00:00 2001 From: 0xffffharry <95022881+0xffffharry@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:05:49 +0800 Subject: [PATCH] Add RuleSet ClashAPI (Rule Provider) --- adapter/router.go | 11 +++- experimental/clashapi/ruleprovider.go | 79 +++++++++++++++++++-------- experimental/clashapi/server.go | 2 +- route/router.go | 10 +++- route/rule_set_local.go | 15 ++++- route/rule_set_remote.go | 15 ++++- 6 files changed, 101 insertions(+), 31 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 5828ab35..6a4b0b19 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -4,10 +4,11 @@ import ( "context" "net/http" "net/netip" + "time" "github.com/sagernet/sing-box/common/geoip" - "github.com/sagernet/sing-dns" - "github.com/sagernet/sing-tun" + dns "github.com/sagernet/sing-dns" + tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -32,6 +33,7 @@ type Router interface { LoadGeosite(code string) (Rule, error) RuleSet(tag string) (RuleSet, bool) + RuleSets() []RuleSet Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) @@ -87,6 +89,8 @@ type DNSRule interface { } type RuleSet interface { + Tag() string + Type() string StartContext(ctx context.Context, startContext RuleSetStartContext) error PostStart() error Metadata() RuleSetMetadata @@ -97,6 +101,9 @@ type RuleSet interface { type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool + RuleNum int + LastUpdated time.Time + Format string } type RuleSetStartContext interface { diff --git a/experimental/clashapi/ruleprovider.go b/experimental/clashapi/ruleprovider.go index 4a410854..11d05c6c 100644 --- a/experimental/clashapi/ruleprovider.go +++ b/experimental/clashapi/ruleprovider.go @@ -1,34 +1,49 @@ package clashapi import ( + "context" "net/http" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/json/badjson" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func ruleProviderRouter() http.Handler { +func ruleProviderRouter(router adapter.Router) http.Handler { r := chi.NewRouter() - r.Get("/", getRuleProviders) + r.Get("/", getRuleProviders(router)) r.Route("/{name}", func(r chi.Router) { - r.Use(parseProviderName, findRuleProviderByName) + r.Use(parseProviderName, findRuleProviderByName(router)) r.Get("/", getRuleProvider) r.Put("/", updateRuleProvider) }) return r } -func getRuleProviders(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, render.M{ - "providers": []string{}, - }) +func getRuleProviders(router adapter.Router) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ruleSets := router.RuleSets() + if len(ruleSets) == 0 { + render.JSON(w, r, render.M{ + "providers": []string{}, + }) + } + m := render.M{} + for _, ruleSet := range ruleSets { + m[ruleSet.Tag()] = ruleProviderInfo(ruleSet) + } + render.JSON(w, r, render.M{ + "providers": m, + }) + } } func getRuleProvider(w http.ResponseWriter, r *http.Request) { - // provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) - // render.JSON(w, r, provider) - render.NoContent(w, r) + ruleSet := r.Context().Value(CtxKeyProvider).(adapter.RuleSet) + render.JSON(w, r, ruleProviderInfo(ruleSet)) } func updateRuleProvider(w http.ResponseWriter, r *http.Request) { @@ -41,18 +56,36 @@ func updateRuleProvider(w http.ResponseWriter, r *http.Request) { render.NoContent(w, r) } -func findRuleProviderByName(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - /*name := r.Context().Value(CtxKeyProviderName).(string) - providers := tunnel.RuleProviders() - provider, exist := providers[name] - if !exist {*/ - render.Status(r, http.StatusNotFound) - render.JSON(w, r, ErrNotFound) - //return - //} +func findRuleProviderByName(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(CtxKeyProviderName).(string) + ruleSet, exist := router.RuleSet(name) + if !exist { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } - // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) - // next.ServeHTTP(w, r.WithContext(ctx)) - }) + ctx := context.WithValue(r.Context(), CtxKeyProvider, ruleSet) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func ruleProviderInfo(ruleSet adapter.RuleSet) *badjson.JSONObject { + var info badjson.JSONObject + info.Put("name", ruleSet.Tag()) + info.Put("type", "Rule") + if ruleSet.Type() == "remote" { + info.Put("vehicleType", "HTTP") + } else { + info.Put("vehicleType", "File") + } + metadata := ruleSet.Metadata() + info.Put("format", metadata.Format) + info.Put("behavior", "sing") + info.Put("ruleCount", metadata.RuleNum) + info.Put("updatedAt", metadata.LastUpdated) + return &info } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 1eec8448..b16ffa2e 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -107,7 +107,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/rules", ruleRouter(router)) r.Mount("/connections", connectionRouter(router, trafficManager)) r.Mount("/providers/proxies", proxyProviderRouter()) - r.Mount("/providers/rules", ruleProviderRouter()) + r.Mount("/providers/rules", ruleProviderRouter(router)) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) r.Mount("/cache", cacheRouter(ctx)) diff --git a/route/router.go b/route/router.go index b7aee514..58026e46 100644 --- a/route/router.go +++ b/route/router.go @@ -26,10 +26,10 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/transport/fakeip" - "github.com/sagernet/sing-dns" + dns "github.com/sagernet/sing-dns" mux "github.com/sagernet/sing-mux" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-vmess" + tun "github.com/sagernet/sing-tun" + vmess "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" @@ -713,6 +713,10 @@ func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { return ruleSet, loaded } +func (r *Router) RuleSets() []adapter.RuleSet { + return r.ruleSets +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if r.pauseManager.IsDevicePaused() { return E.New("reject connection to ", metadata.Destination, " while device paused") diff --git a/route/rule_set_local.go b/route/rule_set_local.go index b6424ab3..e96a2d27 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -3,6 +3,7 @@ package route import ( "context" "os" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" @@ -15,6 +16,7 @@ import ( var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { + tag string rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata } @@ -53,7 +55,18 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS var metadata adapter.RuleSetMetadata metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) - return &LocalRuleSet{rules, metadata}, nil + metadata.RuleNum = len(rules) + metadata.LastUpdated = time.Now() + metadata.Format = options.Format + return &LocalRuleSet{options.Tag, rules, metadata}, nil +} + +func (s *LocalRuleSet) Tag() string { + return s.tag +} + +func (s *LocalRuleSet) Type() string { + return "local" } func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index eb6e4f9f..735b034a 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -59,6 +59,14 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. } } +func (s *RemoteRuleSet) Tag() string { + return s.options.Tag +} + +func (s *RemoteRuleSet) Type() string { + return "remote" +} + func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { for _, rule := range s.rules { if rule.Match(metadata) { @@ -117,7 +125,10 @@ func (s *RemoteRuleSet) PostStart() error { } func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { - return s.metadata + metadata := s.metadata + metadata.LastUpdated = s.lastUpdated + metadata.Format = s.options.Format + return metadata } func (s *RemoteRuleSet) loadBytes(content []byte) error { @@ -152,6 +163,8 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { } s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + s.metadata.RuleNum = len(rules) + s.lastUpdated = time.Now() s.rules = rules return nil }