From 0ec07e573ef9d41c3400779c9be79a108dc3483c Mon Sep 17 00:00:00 2001 From: PuerNya Date: Tue, 13 Aug 2024 06:42:47 +0800 Subject: [PATCH] add rule-provider clash-api (cherry picked from commit e6e574a8e868a630fe17ce6cab05a68fe96653c1) --- adapter/router.go | 7 +++ experimental/clashapi/ruleprovider.go | 91 ++++++++++++++++++--------- experimental/clashapi/server.go | 2 +- option/rule_set.go | 2 +- route/router.go | 4 ++ route/rule_abstract.go | 18 ++++-- route/rule_headless.go | 13 ++++ route/rule_set_abstract.go | 25 +++++++- route/rule_set_local.go | 7 ++- route/rule_set_remote.go | 10 +++ 10 files changed, 142 insertions(+), 37 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 619c1110..8649061b 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/netip" + "time" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" @@ -34,6 +35,7 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSets() []RuleSet RuleSet(tag string) (RuleSet, bool) NeedWIFIState() bool @@ -76,6 +78,7 @@ func RouterFromContext(ctx context.Context) Router { type HeadlessRule interface { Match(metadata *InboundContext) bool + RuleCount() uint64 String() string } @@ -98,6 +101,10 @@ type DNSRule interface { type RuleSet interface { Name() string + Type() string + Format() string + UpdatedTime() time.Time + Update(ctx context.Context) error StartContext(ctx context.Context, startContext RuleSetStartContext) error PostStart() error Metadata() RuleSetMetadata diff --git a/experimental/clashapi/ruleprovider.go b/experimental/clashapi/ruleprovider.go index 4a410854..f618b19d 100644 --- a/experimental/clashapi/ruleprovider.go +++ b/experimental/clashapi/ruleprovider.go @@ -1,58 +1,93 @@ package clashapi import ( + "context" "net/http" + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" + "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 ruleSetInfo(ruleSet adapter.RuleSet) *badjson.JSONObject { + var info badjson.JSONObject + info.Put("name", ruleSet.Name()) + info.Put("type", "Rule") + info.Put("vehicleType", strings.ToUpper(ruleSet.Type())) + info.Put("behavior", strings.ToUpper(ruleSet.Format())) + info.Put("ruleCount", ruleSet.RuleCount()) + info.Put("updatedAt", ruleSet.UpdatedTime().Format("2006-01-02T15:04:05.999999999-07:00")) + return &info +} + +func getRuleProviders(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + providerMap := render.M{} + for i, ruleSet := range router.RuleSets() { + var tag string + if ruleSet.Name() == "" { + tag = F.ToString(i) + } else { + tag = ruleSet.Name() + } + providerMap[tag] = ruleSetInfo(ruleSet) + } + render.JSON(w, r, render.M{ + "providers": providerMap, + }) + } } 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) + response, err := ruleSetInfo(ruleSet).MarshalJSON() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + w.Write(response) } func updateRuleProvider(w http.ResponseWriter, r *http.Request) { - /*provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) - if err := provider.Update(); err != nil { - render.Status(r, http.StatusServiceUnavailable) + ruleSet := r.Context().Value(CtxKeyProvider).(adapter.RuleSet) + err := ruleSet.Update(r.Context()) + if err != nil { + render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return - }*/ + } 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 - //} - - // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) - // next.ServeHTTP(w, r.WithContext(ctx)) - }) +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) + provider, 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)) + }) + } } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 889d191e..ba5ab905 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -113,7 +113,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/option/rule_set.go b/option/rule_set.go index f7730dfc..e9802f18 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -57,7 +57,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { return E.New("unknown rule-set format: " + r.Format) } } else { - r.Format = "" + r.Format = C.RuleSetFormatSource r.Path = "" } var v any diff --git a/route/router.go b/route/router.go index dc579800..4f7bea40 100644 --- a/route/router.go +++ b/route/router.go @@ -784,6 +784,10 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSets() []adapter.RuleSet { + return r.ruleSets +} + func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { ruleSet, loaded := r.ruleSetMap[tag] return ruleSet, loaded diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 9ef2e932..06af8f61 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -19,6 +19,7 @@ type abstractDefaultRule struct { destinationPortItems []RuleItem allItems []RuleItem ruleSetItem RuleItem + ruleCount uint64 invert bool outbound string } @@ -27,6 +28,10 @@ func (r *abstractDefaultRule) Type() string { return C.RuleTypeDefault } +func (r *abstractDefaultRule) RuleCount() uint64 { + return r.ruleCount +} + func (r *abstractDefaultRule) Start() error { for _, item := range r.allItems { if starter, isStarter := item.(interface { @@ -163,16 +168,21 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.HeadlessRule - mode string - invert bool - outbound string + rules []adapter.HeadlessRule + mode string + invert bool + outbound string + ruleCount uint64 } func (r *abstractLogicalRule) Type() string { return C.RuleTypeLogical } +func (r *abstractLogicalRule) RuleCount() uint64 { + return r.ruleCount +} + func (r *abstractLogicalRule) UpdateGeosite() error { for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { rule, loaded := it.(adapter.Rule) diff --git a/route/rule_headless.go b/route/rule_headless.go index 23a98c72..4e1c4b12 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -159,6 +159,18 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } + switch true { + case len(rule.allItems) == len(rule.destinationAddressItems)+len(rule.destinationIPCIDRItems): + rule.ruleCount = uint64(len(rule.destinationAddressItems) + len(rule.destinationIPCIDRItems)) + case len(rule.allItems) == len(rule.sourceAddressItems): + rule.ruleCount = uint64(len(rule.sourceAddressItems)) + case len(rule.allItems) == len(rule.sourcePortItems): + rule.ruleCount = uint64(len(rule.sourcePortItems)) + case len(rule.allItems) == len(rule.destinationPortItems): + rule.ruleCount = uint64(len(rule.destinationPortItems)) + default: + rule.ruleCount = 1 + } return rule, nil } @@ -190,5 +202,6 @@ func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadles } r.rules[i] = rule } + r.ruleCount = 1 return r, nil } diff --git a/route/rule_set_abstract.go b/route/rule_set_abstract.go index 49bae13e..bb65a7eb 100644 --- a/route/rule_set_abstract.go +++ b/route/rule_set_abstract.go @@ -26,9 +26,11 @@ type abstractRuleSet struct { router adapter.Router logger logger.ContextLogger tag string + sType string path string format string rules []adapter.HeadlessRule + ruleCount uint64 metadata adapter.RuleSetMetadata lastUpdated time.Time refs atomic.Int32 @@ -38,6 +40,22 @@ func (s *abstractRuleSet) Name() string { return s.tag } +func (s *abstractRuleSet) Type() string { + return s.sType +} + +func (s *abstractRuleSet) Format() string { + return s.format +} + +func (s *abstractRuleSet) RuleCount() uint64 { + return s.ruleCount +} + +func (s *abstractRuleSet) UpdatedTime() time.Time { + return s.lastUpdated +} + func (s *abstractRuleSet) String() string { return strings.Join(F.MapToString(s.rules), " ") } @@ -129,18 +147,21 @@ func (s *abstractRuleSet) loadBytes(content []byte) error { func (s *abstractRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { rules := make([]adapter.HeadlessRule, len(headlessRules)) - var err error + var ruleCount uint64 for i, ruleOptions := range headlessRules { - rules[i], err = NewHeadlessRule(s.router, ruleOptions) + rule, err := NewHeadlessRule(s.router, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } + rules[i] = rule + ruleCount += rule.RuleCount() } var metadata adapter.RuleSetMetadata metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) s.rules = rules + s.ruleCount = ruleCount s.metadata = metadata return nil } diff --git a/route/rule_set_local.go b/route/rule_set_local.go index f7cb5edd..8917cb27 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -28,6 +28,8 @@ func NewLocalRuleSet(ctx context.Context, router adapter.Router, logger logger.C router: router, logger: logger, tag: options.Tag, + sType: options.Type, + format: options.Format, }, } if options.Type == C.RuleSetTypeInline { @@ -41,7 +43,6 @@ func NewLocalRuleSet(ctx context.Context, router adapter.Router, logger logger.C return ruleSet, nil } ruleSet.path = options.Path - ruleSet.format = options.Format path, err := ruleSet.getPath(options.Path) if err != nil { return nil, err @@ -89,6 +90,10 @@ func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { } +func (s *LocalRuleSet) Update(ctx context.Context) error { + return nil +} + func (s *LocalRuleSet) Close() error { s.rules = nil return common.Close(common.PtrOrNil(s.watcher)) diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 11e475f8..d6575fd8 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -157,6 +157,16 @@ func (s *RemoteRuleSet) update() { } } +func (s *RemoteRuleSet) Update(ctx context.Context) error { + err := s.fetchOnce(log.ContextWithNewID(ctx), nil) + if err != nil { + return err + } else if s.refs.Load() == 0 { + s.rules = nil + } + return nil +} + func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error { s.logger.DebugContext(ctx, "updating rule-set ", s.tag, " from URL: ", s.options.URL) var httpClient *http.Client