diff --git a/.github/diy/packages/sing-box/patches/002-support-clash-proxy-providers.patch b/.github/diy/packages/sing-box/patches/002-support-clash-proxy-providers.patch deleted file mode 100644 index 639069b9c..000000000 --- a/.github/diy/packages/sing-box/patches/002-support-clash-proxy-providers.patch +++ /dev/null @@ -1,403 +0,0 @@ -diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go -index 352b289..ef07e6d 100644 ---- a/experimental/clashapi/provider.go -+++ b/experimental/clashapi/provider.go -@@ -3,43 +3,65 @@ package clashapi - import ( - "context" - "net/http" -+ "os/exec" -+ "regexp" -+ -+ "github.com/sagernet/sing-box/option" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - ) - --func proxyProviderRouter() http.Handler { -+func proxyProviderRouter(server *Server) http.Handler { - r := chi.NewRouter() -- r.Get("/", getProviders) -+ r.Get("/", getProviders(server)) - - r.Route("/{name}", func(r chi.Router) { -- r.Use(parseProviderName, findProviderByName) -- r.Get("/", getProvider) -+ r.Use(parseProviderName, findProviderByName(server)) -+ r.Get("/", getProvider(server)) - r.Put("/", updateProvider) - r.Get("/healthcheck", healthCheckProvider) - }) - return r - } - --func getProviders(w http.ResponseWriter, r *http.Request) { -- render.JSON(w, r, render.M{ -- "providers": render.M{}, -- }) -+func getProviders(server *Server) func(w http.ResponseWriter, r *http.Request) { -+ return func(w http.ResponseWriter, r *http.Request) { -+ providers := render.M{} -+ for _, provider := range server.proxyProviders { -+ if provider.Name == "" { -+ continue -+ } -+ providers[provider.Name] = proxyProviderInfo(server, provider) -+ } -+ render.JSON(w, r, render.M{ -+ "providers": providers, -+ }) -+ } - } - --func getProvider(w http.ResponseWriter, r *http.Request) { -- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) -- render.JSON(w, r, provider)*/ -- render.NoContent(w, r) -+func getProvider(server *Server) func(w http.ResponseWriter, r *http.Request) { -+ return func(w http.ResponseWriter, r *http.Request) { -+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions) -+ render.JSON(w, r, proxyProviderInfo(server, provider)) -+ } - } - - func updateProvider(w http.ResponseWriter, r *http.Request) { -- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) -- if err := provider.Update(); err != nil { -+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions) -+ if !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(provider.UpdateID) { -+ render.Status(r, http.StatusServiceUnavailable) -+ render.JSON(w, r, newError("Provider update is unavailable")) -+ return -+ } -+ -+ command := exec.Command("/bin/sh", "/etc/homeproxy/scripts/update_provider.sh", provider.UpdateID) -+ if err := command.Start(); err != nil { - render.Status(r, http.StatusServiceUnavailable) - render.JSON(w, r, newError(err.Error())) - return -- }*/ -+ } -+ _ = command.Process.Release() - render.NoContent(w, r) - } - -@@ -57,18 +79,58 @@ func parseProviderName(next http.Handler) http.Handler { - }) - } - --func findProviderByName(next http.Handler) http.Handler { -- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -- /*name := r.Context().Value(CtxKeyProviderName).(string) -- providers := tunnel.ProxyProviders() -- provider, exist := providers[name] -- if !exist {*/ -- render.Status(r, http.StatusNotFound) -- render.JSON(w, r, ErrNotFound) -- //return -- //} -+func findProviderByName(server *Server) 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) -+ for _, provider := range server.proxyProviders { -+ if provider.Name == name { -+ ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) -+ next.ServeHTTP(w, r.WithContext(ctx)) -+ return -+ } -+ } -+ render.Status(r, http.StatusNotFound) -+ render.JSON(w, r, ErrNotFound) -+ }) -+ } -+} - -- // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) -- // next.ServeHTTP(w, r.WithContext(ctx)) -- }) -+func proxyProviderInfo(server *Server, provider option.ClashProxyProviderOptions) render.M { -+ providerType := provider.Type -+ if providerType == "" { -+ providerType = "Proxy" -+ } -+ vehicleType := provider.VehicleType -+ if vehicleType == "" { -+ vehicleType = "HTTP" -+ } -+ -+ proxies := make([]any, 0, len(provider.Proxies)) -+ for _, proxyName := range provider.Proxies { -+ proxy, exists := lookupProxyByName(server, proxyName) -+ if !exists { -+ continue -+ } -+ proxies = append(proxies, proxyInfo(server, proxy)) -+ } -+ -+ info := render.M{ -+ "name": provider.Name, -+ "type": providerType, -+ "vehicleType": vehicleType, -+ "proxies": proxies, -+ } -+ if provider.UpdatedAt != "" { -+ info["updatedAt"] = provider.UpdatedAt -+ } -+ if provider.SubscriptionInfo != nil { -+ info["subscriptionInfo"] = render.M{ -+ "Upload": provider.SubscriptionInfo.Upload, -+ "Download": provider.SubscriptionInfo.Download, -+ "Total": provider.SubscriptionInfo.Total, -+ "Expire": provider.SubscriptionInfo.Expire, -+ } -+ } -+ return info - } -diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go -index ef88ff3..1f7d14d 100644 ---- a/experimental/clashapi/proxies.go -+++ b/experimental/clashapi/proxies.go -@@ -21,6 +21,50 @@ import ( - "github.com/go-chi/render" - ) - -+func proxyDisplayName(tag string) string { -+ switch tag { -+ case "GLOBAL": -+ return "GLOBAL" -+ case "main-out": -+ return "主代理" -+ case "main-udp-out": -+ return "UDP 主代理" -+ case "direct-out": -+ return "直连" -+ case "block-out": -+ return "拦截" -+ default: -+ return tag -+ } -+} -+ -+func lookupProxyByName(server *Server, name string) (adapter.Outbound, bool) { -+ proxy, exist := server.outbound.Outbound(name) -+ if exist { -+ return proxy, true -+ } -+ -+ for _, detour := range server.outbound.Outbounds() { -+ if proxyDisplayName(detour.Tag()) == name { -+ return detour, true -+ } -+ } -+ -+ for _, endpoint := range server.endpoint.Endpoints() { -+ if proxyDisplayName(endpoint.Tag()) == name { -+ return endpoint, true -+ } -+ } -+ -+ return nil, false -+} -+ -+func proxyDisplayNames(tags []string) []string { -+ return common.Map(tags, func(tag string) string { -+ return proxyDisplayName(tag) -+ }) -+} -+ - func proxyRouter(server *Server, router adapter.Router) http.Handler { - r := chi.NewRouter() - r.Get("/", getProxies(server)) -@@ -29,7 +73,7 @@ func proxyRouter(server *Server, router adapter.Router) http.Handler { - r.Use(parseProxyName, findProxyByName(server)) - r.Get("/", getProxy(server)) - r.Get("/delay", getProxyDelay(server)) -- r.Put("/", updateProxy) -+ r.Put("/", updateProxy(server)) - }) - return r - } -@@ -46,7 +90,7 @@ func findProxyByName(server *Server) 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 := server.outbound.Outbound(name) -+ proxy, exist := lookupProxyByName(server, name) - if !exist { - render.Status(r, http.StatusNotFound) - render.JSON(w, r, ErrNotFound) -@@ -68,7 +112,7 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { - clashType = C.ProxyDisplayName(detour.Type()) - } - info.Put("type", clashType) -- info.Put("name", detour.Tag()) -+ info.Put("name", proxyDisplayName(detour.Tag())) - info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP)) - delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour)) - if delayHistory != nil { -@@ -77,8 +121,8 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { - info.Put("history", []*adapter.URLTestHistory{}) - } - if group, isGroup := detour.(adapter.OutboundGroup); isGroup { -- info.Put("now", group.Now()) -- info.Put("all", group.All()) -+ info.Put("now", proxyDisplayName(group.Now())) -+ info.Put("all", proxyDisplayNames(group.All())) - } - return &info - } -@@ -102,10 +146,10 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { - case C.TypeDirect, C.TypeBlock, C.TypeDNS: - continue - } -- allProxies = append(allProxies, detour.Tag()) -+ allProxies = append(allProxies, proxyDisplayName(detour.Tag())) - } - -- defaultTag := server.outbound.Default().Tag() -+ defaultTag := proxyDisplayName(server.outbound.Default().Tag()) - - sort.SliceStable(allProxies, func(i, j int) bool { - return allProxies[i] == defaultTag -@@ -114,7 +158,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { - // fix clash dashboard - proxyMap.Put("GLOBAL", map[string]any{ - "type": "Fallback", -- "name": "GLOBAL", -+ "name": proxyDisplayName("GLOBAL"), - "udp": true, - "history": []*adapter.URLTestHistory{}, - "all": allProxies, -@@ -126,7 +170,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { - if detour.Tag() == "" { - tag = F.ToString(i) - } else { -- tag = detour.Tag() -+ tag = proxyDisplayName(detour.Tag()) - } - proxyMap.Put(tag, proxyInfo(server, detour)) - } -@@ -159,29 +203,36 @@ 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 -- } -+func updateProxy(server *Server) func(w http.ResponseWriter, r *http.Request) { -+ return func(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.(*group.Selector) -- if !ok { -- render.Status(r, http.StatusBadRequest) -- render.JSON(w, r, newError("Must be a Selector")) -- return -- } -+ proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) -+ selector, ok := proxy.(*group.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("Selector update error: not found")) -- return -- } -+ selectedName := req.Name -+ if targetProxy, exist := lookupProxyByName(server, req.Name); exist { -+ selectedName = targetProxy.Tag() -+ } -+ -+ if !selector.SelectOutbound(selectedName) { -+ render.Status(r, http.StatusBadRequest) -+ render.JSON(w, r, newError("Selector update error: not found")) -+ return -+ } - -- render.NoContent(w, r) -+ render.NoContent(w, r) -+ } - } - - func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { -diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go -index b9222ad5440..d4fd9dd3db1 100644 ---- a/experimental/clashapi/server.go -+++ b/experimental/clashapi/server.go -@@ -57,6 +57,7 @@ type Server struct { - externalUI string - externalUIDownloadURL string - externalUIDownloadDetour string -+ proxyProviders []option.ClashProxyProviderOptions - } - - func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) { -@@ -92,6 +93,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op - externalController: options.ExternalController != "", - externalUIDownloadURL: options.ExternalUIDownloadURL, - externalUIDownloadDetour: options.ExternalUIDownloadDetour, -+ proxyProviders: options.ProxyProviders, - } - //goland:noinspection GoDeprecation - //nolint:staticcheck -@@ -120,7 +122,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op - r.Mount("/proxies", proxyRouter(s, s.router)) - r.Mount("/rules", ruleRouter(s.router)) - r.Mount("/connections", connectionRouter(s.ctx, s.network, trafficManager)) -- r.Mount("/providers/proxies", proxyProviderRouter()) -+ r.Mount("/providers/proxies", proxyProviderRouter(s)) - r.Mount("/providers/rules", ruleProviderRouter()) - r.Mount("/script", scriptRouter()) - r.Mount("/profile", profileRouter()) -diff --git a/option/experimental.go b/option/experimental.go -index bf144ad4452..89f303a7b26 100644 ---- a/option/experimental.go -+++ b/option/experimental.go -@@ -34,6 +34,7 @@ type ClashAPIOptions struct { - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty" reference:"outbound"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` -+ ProxyProviders []ClashProxyProviderOptions `json:"proxy_providers,omitempty"` - AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"` - AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` - -@@ -49,6 +50,23 @@ type ClashAPIOptions struct { - StoreFakeIP bool `json:"store_fakeip,omitempty" schema:"omit"` - } - -+type ClashProxyProviderOptions struct { -+ Name string `json:"name,omitempty"` -+ Type string `json:"type,omitempty"` -+ VehicleType string `json:"vehicle_type,omitempty"` -+ Proxies []string `json:"proxies,omitempty"` -+ UpdatedAt string `json:"updated_at,omitempty"` -+ UpdateID string `json:"update_id,omitempty"` -+ SubscriptionInfo *ClashProxyProviderSubscriptionInfo `json:"subscription_info,omitempty"` -+} -+ -+type ClashProxyProviderSubscriptionInfo struct { -+ Upload uint64 `json:"upload,omitempty"` -+ Download uint64 `json:"download,omitempty"` -+ Total uint64 `json:"total,omitempty"` -+ Expire int64 `json:"expire,omitempty"` -+} -+ - type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`