diff --git a/adapters/provider/provider.go b/adapters/provider/provider.go index 1149273d..3d718cb6 100644 --- a/adapters/provider/provider.go +++ b/adapters/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "bytes" "crypto/md5" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -58,6 +59,7 @@ type Provider interface { type ProxyProvider interface { Provider Proxies() []C.Proxy + HealthCheck() } type ProxySchema struct { @@ -72,11 +74,22 @@ type ProxySetProvider struct { healthCheck *healthCheck healthCheckOption *HealthCheckOption ticker *time.Ticker + updatedAt *time.Time // mux for avoiding creating new goroutines when pulling mux sync.Mutex } +func (pp *ProxySetProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "name": pp.Name(), + "type": pp.Type().String(), + "vehicleType": pp.VehicleType().String(), + "proxies": pp.Proxies(), + "updatedAt": pp.updatedAt, + }) +} + func (pp *ProxySetProvider) Name() string { return pp.name } @@ -85,6 +98,14 @@ func (pp *ProxySetProvider) Reload() error { return nil } +func (pp *ProxySetProvider) HealthCheck() { + pp.mux.Lock() + defer pp.mux.Unlock() + if pp.healthCheck != nil { + pp.healthCheck.check() + } +} + func (pp *ProxySetProvider) Destroy() error { pp.mux.Lock() defer pp.mux.Unlock() @@ -175,6 +196,8 @@ func (pp *ProxySetProvider) pull() error { return err } + now := time.Now() + pp.updatedAt = &now pp.hash = hash pp.setProxies(proxies) @@ -210,9 +233,9 @@ func (pp *ProxySetProvider) setProxies(proxies []C.Proxy) { pp.mux.Lock() if pp.healthCheck != nil { pp.healthCheck.close() - pp.healthCheck = newHealthCheck(proxies, pp.healthCheckOption.URL, pp.healthCheckOption.Interval) - go pp.healthCheck.process() } + pp.healthCheck = newHealthCheck(proxies, pp.healthCheckOption.URL, pp.healthCheckOption.Interval) + go pp.healthCheck.process() pp.mux.Unlock() } } @@ -238,6 +261,15 @@ type CompatibleProvier struct { proxies []C.Proxy } +func (cp *CompatibleProvier) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "name": cp.Name(), + "type": cp.Type().String(), + "vehicleType": cp.VehicleType().String(), + "proxies": cp.Proxies(), + }) +} + func (cp *CompatibleProvier) Name() string { return cp.name } @@ -253,6 +285,12 @@ func (cp *CompatibleProvier) Destroy() error { return nil } +func (cp *CompatibleProvier) HealthCheck() { + if cp.healthCheck != nil { + cp.healthCheck.check() + } +} + func (cp *CompatibleProvier) Initial() error { if cp.healthCheck != nil { go cp.healthCheck.process() diff --git a/hub/route/common.go b/hub/route/common.go new file mode 100644 index 00000000..2240e891 --- /dev/null +++ b/hub/route/common.go @@ -0,0 +1,17 @@ +package route + +import ( + "net/http" + "net/url" + + "github.com/go-chi/chi" +) + +// When name is composed of a partial escape string, Golang does not unescape it +func getEscapeParam(r *http.Request, paramName string) string { + param := chi.URLParam(r, paramName) + if newParam, err := url.PathUnescape(param); err == nil { + param = newParam + } + return param +} diff --git a/hub/route/ctxkeys.go b/hub/route/ctxkeys.go index 07932971..56370192 100644 --- a/hub/route/ctxkeys.go +++ b/hub/route/ctxkeys.go @@ -1,8 +1,10 @@ package route var ( - CtxKeyProxyName = contextKey("proxy name") - CtxKeyProxy = contextKey("proxy") + CtxKeyProxyName = contextKey("proxy name") + CtxKeyProviderName = contextKey("provider name") + CtxKeyProxy = contextKey("proxy") + CtxKeyProvider = contextKey("provider") ) type contextKey string diff --git a/hub/route/provider.go b/hub/route/provider.go new file mode 100644 index 00000000..ddc2426c --- /dev/null +++ b/hub/route/provider.go @@ -0,0 +1,66 @@ +package route + +import ( + "context" + "net/http" + + "github.com/Dreamacro/clash/adapters/provider" + T "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi" + "github.com/go-chi/render" +) + +func proxyProviderRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getProviders) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProviderName, findProviderByName) + r.Get("/", getProvider) + r.Get("/healthcheck", doProviderHealthCheck) + }) + return r +} + +func getProviders(w http.ResponseWriter, r *http.Request) { + providers := T.Instance().Providers() + 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) +} + +func doProviderHealthCheck(w http.ResponseWriter, r *http.Request) { + provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + provider.HealthCheck() + render.NoContent(w, r) +} + +func parseProviderName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := getEscapeParam(r, "name") + ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProviderByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := r.Context().Value(CtxKeyProviderName).(string) + providers := T.Instance().Providers() + 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)) + }) +} diff --git a/hub/route/proxies.go b/hub/route/proxies.go index bdf4eee0..ab769f9c 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "net/url" "strconv" "time" @@ -30,13 +29,9 @@ func proxyRouter() http.Handler { return r } -// When name is composed of a partial escape string, Golang does not unescape it func parseProxyName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - name := chi.URLParam(r, "name") - if newName, err := url.PathUnescape(name); err == nil { - name = newName - } + name := getEscapeParam(r, "name") ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/hub/route/server.go b/hub/route/server.go index 560c338f..ab4e61c4 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -68,6 +68,7 @@ func Start(addr string, secret string) { r.Mount("/proxies", proxyRouter()) r.Mount("/rules", ruleRouter()) r.Mount("/connections", connectionRouter()) + r.Mount("/providers/proxies", proxyProviderRouter()) }) if uiPath != "" {