From ebf6ad6600743582f176830f086ccc351f67017e Mon Sep 17 00:00:00 2001 From: Joker <1465267+JokerQyou@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:29:36 +0800 Subject: [PATCH 01/16] Add Authentik forward auth support --- src/api.go | 1 + src/def.go | 4 +- src/mod/auth/sso/authentik/authentik.go | 169 ++++++++++++++++++++++++ src/mod/dynamicproxy/authProviders.go | 10 ++ src/mod/dynamicproxy/typedef.go | 5 +- src/reverseproxy.go | 3 + src/start.go | 8 ++ src/web/components/httprp.html | 7 + src/web/components/sso.html | 57 ++++++++ 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/mod/auth/sso/authentik/authentik.go diff --git a/src/api.go b/src/api.go index 1507289..889ef16 100644 --- a/src/api.go +++ b/src/api.go @@ -81,6 +81,7 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) { // Register the APIs for Authentication handlers like Authelia and OAUTH2 func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS) + authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS) } // Register the APIs for redirection rules management functions diff --git a/src/def.go b/src/def.go index 23d2ae4..f6d1aeb 100644 --- a/src/def.go +++ b/src/def.go @@ -10,6 +10,7 @@ package main import ( "embed" "flag" + "imuslab.com/zoraxy/mod/auth/sso/authentik" "net/http" "time" @@ -141,7 +142,8 @@ var ( loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing //Authentication Provider - autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication //Helper modules EmailSender *email.Sender //Email sender that handle email sending diff --git a/src/mod/auth/sso/authentik/authentik.go b/src/mod/auth/sso/authentik/authentik.go new file mode 100644 index 0000000..8c621e2 --- /dev/null +++ b/src/mod/auth/sso/authentik/authentik.go @@ -0,0 +1,169 @@ +package authentik + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strings" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/utils" +) + +type AuthentikRouterOptions struct { + UseHTTPS bool //If the Authentik server is using HTTPS + AuthentikURL string //The URL of the Authentik server + Logger *logger.Logger + Database *database.Database +} + +type AuthentikRouter struct { + options *AuthentikRouterOptions +} + +// NewAuthentikRouter creates a new AuthentikRouter object +func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter { + options.Database.NewTable("authentik") + + //Read settings from database, if exists + options.Database.Read("authentik", "authentikURL", &options.AuthentikURL) + options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS) + + return &AuthentikRouter{ + options: options, + } +} + +// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS +func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + //Return the current settings + js, _ := json.Marshal(map[string]interface{}{ + "useHTTPS": ar.options.UseHTTPS, + "authentikURL": ar.options.AuthentikURL, + }) + + utils.SendJSONResponse(w, string(js)) + return + } else if r.Method == http.MethodPost { + //Update the settings + AuthentikURL, err := utils.PostPara(r, "authentikURL") + if err != nil { + utils.SendErrorResponse(w, "authentikURL not found") + return + } + + useHTTPS, err := utils.PostBool(r, "useHTTPS") + if err != nil { + useHTTPS = false + } + + //Write changes to runtime + ar.options.AuthentikURL = AuthentikURL + ar.options.UseHTTPS = useHTTPS + + //Write changes to database + ar.options.Database.Write("authentik", "authentikURL", AuthentikURL) + ar.options.Database.Write("authentik", "useHTTPS", useHTTPS) + + utils.SendOK(w) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + +} + +// HandleAuthentikAuth is the internal handler for Authentik authentication +// Set useHTTPS to true if your Authentik server is using HTTPS +// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com +func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error { + const outpostPrefix = "outpost.goauthentik.io" + client := &http.Client{} + + if ar.options.AuthentikURL == "" { + ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil) + w.WriteHeader(500) + w.Write([]byte("500 - Internal Server Error")) + return errors.New("authentik URL not set") + } + protocol := "http" + if ar.options.UseHTTPS { + protocol = "https" + } + + authentikBaseURL := protocol + "://" + ar.options.AuthentikURL + //Remove tailing slash if any + authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/") + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + reqUrl := scheme + "://" + r.Host + r.RequestURI + // Pass request to outpost if path matches outpost prefix + if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) { + req, err := http.NewRequest(r.Method, authentikBaseURL+r.URL.Path, nil) + if err != nil { + ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err) + w.WriteHeader(401) + return errors.New("unauthorized") + } + req.Header.Set("X-Original-URL", reqUrl) + req.Header.Set("Host", r.Host) + for _, cookie := range r.Cookies() { + req.AddCookie(cookie) + } + if resp, err := client.Do(req); err != nil { + ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err) + w.WriteHeader(http.StatusInternalServerError) + return errors.New("internal server error") + } else { + defer resp.Body.Close() + for k := range resp.Header { + w.Header().Set(k, resp.Header.Get(k)) + } + w.WriteHeader(resp.StatusCode) + if _, err = io.Copy(w, resp.Body); err != nil { + ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err) + w.WriteHeader(http.StatusInternalServerError) + return errors.New("internal server error") + } + } + return nil + } + + //Make a request to Authentik to verify the request + req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil) + if err != nil { + ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err) + w.WriteHeader(401) + return errors.New("unauthorized") + } + + req.Header.Add("X-Original-URL", reqUrl) + + // Copy cookies from the incoming request + for _, cookie := range r.Cookies() { + req.AddCookie(cookie) + } + + // Making the verification request + resp, err := client.Do(req) + if err != nil { + ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err) + w.WriteHeader(401) + return errors.New("unauthorized") + } + + if resp.StatusCode != 200 { + redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + http.Redirect(w, r, redirectURL, http.StatusSeeOther) + return errors.New("unauthorized") + } + + return nil +} diff --git a/src/mod/dynamicproxy/authProviders.go b/src/mod/dynamicproxy/authProviders.go index f50cd55..d7e8190 100644 --- a/src/mod/dynamicproxy/authProviders.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -43,6 +43,12 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) return true } + } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik { + err := h.handleAuthentikAuth(w, r) + if err != nil { + h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + return true + } } //No authentication provider, do not need to handle @@ -106,3 +112,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r) } + +func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error { + return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r) +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 6761674..8a65398 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -9,6 +9,7 @@ package dynamicproxy */ import ( _ "embed" + "imuslab.com/zoraxy/mod/auth/sso/authentik" "net" "net/http" "sync" @@ -61,7 +62,8 @@ type RouterOption struct { LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target /* Authentication Providers */ - AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication /* Utilities */ Logger *logger.Logger //Logger for reverse proxy requets @@ -141,6 +143,7 @@ const ( AuthMethodBasic //Basic Auth AuthMethodAuthelia //Authelia AuthMethodOauth2 //Oauth2 + AuthMethodAuthentik ) type AuthenticationProvider struct { diff --git a/src/reverseproxy.go b/src/reverseproxy.go index e25e405..bba9515 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -115,6 +115,7 @@ func ReverseProxtInit() { WebDirectory: *path_webserver, AccessController: accessController, AutheliaRouter: autheliaRouter, + AuthentikRouter: authentikRouter, LoadBalancer: loadBalancer, Logger: SystemWideLogger, }) @@ -578,6 +579,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia } else if authProviderType == 3 { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2 + } else if authProviderType == 4 { + newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik } else { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone } diff --git a/src/start.go b/src/start.go index d237b8a..425361a 100644 --- a/src/start.go +++ b/src/start.go @@ -1,6 +1,7 @@ package main import ( + "imuslab.com/zoraxy/mod/auth/sso/authentik" "log" "net/http" "os" @@ -146,6 +147,13 @@ func startupSequence() { Database: sysdb, }) + authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{ + UseHTTPS: false, // Automatic populate in router initiation + AuthentikURL: "", // Automatic populate in router initiation + Logger: SystemWideLogger, + Database: sysdb, + }) + //Create a statistic collector statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ Database: sysdb, diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 871d0eb..ae8e65e 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -174,6 +174,7 @@ ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x2?` Authelia`:``} ${subd.AuthenticationProvider.AuthMethod == 0x3?` Oauth2`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x4?` Authentik`:``} ${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"
":""} ${subd.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:``} ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`No Special Settings`:""} @@ -382,6 +383,12 @@ +
+
+ + +
+
diff --git a/src/web/components/sso.html b/src/web/components/sso.html index af1639a..820735a 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -34,6 +34,27 @@
+
+

Authentik

+

Configuration settings for Authentik authentication provider.

+ +
+
+ + + Example: auth.example.com +
+
+
+ + + Check this if your Authentik server uses HTTPS +
+
+ +
+
+
\ No newline at end of file From 6a8057c3a703948871698a91b32c7dae95c92b77 Mon Sep 17 00:00:00 2001 From: Joker <1465267+JokerQyou@users.noreply.github.com> Date: Sun, 2 Mar 2025 00:04:08 +0800 Subject: [PATCH 02/16] fix passing wrong URI to Authentik outpost --- src/mod/auth/sso/authentik/authentik.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod/auth/sso/authentik/authentik.go b/src/mod/auth/sso/authentik/authentik.go index 8c621e2..795b0b3 100644 --- a/src/mod/auth/sso/authentik/authentik.go +++ b/src/mod/auth/sso/authentik/authentik.go @@ -106,7 +106,7 @@ func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Re reqUrl := scheme + "://" + r.Host + r.RequestURI // Pass request to outpost if path matches outpost prefix if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) { - req, err := http.NewRequest(r.Method, authentikBaseURL+r.URL.Path, nil) + req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body) if err != nil { ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err) w.WriteHeader(401) @@ -144,7 +144,7 @@ func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Re return errors.New("unauthorized") } - req.Header.Add("X-Original-URL", reqUrl) + req.Header.Set("X-Original-URL", reqUrl) // Copy cookies from the incoming request for _, cookie := range r.Cookies() { From 05f1743ecda070a9da675c230be2e18658241dfc Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Wed, 2 Apr 2025 20:11:43 +0800 Subject: [PATCH 03/16] Fixed #681 - Fixed origin is not populated in log bug --- src/def.go | 5 ++- src/mod/dynamicproxy/Server.go | 19 ++++++---- src/mod/dynamicproxy/access.go | 2 +- src/mod/dynamicproxy/authProviders.go | 16 ++++---- src/mod/dynamicproxy/dynamicproxy.go | 2 +- src/mod/dynamicproxy/proxyRequestHandler.go | 41 ++++++++++----------- src/mod/dynamicproxy/ratelimit.go | 2 +- src/mod/info/logger/trafficlog.go | 8 ++-- 8 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/def.go b/src/def.go index 6090c35..a5fba8e 100644 --- a/src/def.go +++ b/src/def.go @@ -10,10 +10,11 @@ package main import ( "embed" "flag" - "imuslab.com/zoraxy/mod/auth/sso/authentik" "net/http" "time" + "imuslab.com/zoraxy/mod/auth/sso/authentik" + "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" @@ -43,7 +44,7 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.0" + SYSTEM_VERSION = "3.2.1" DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ /* System Constants */ diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 5f942cb..7c195e6 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -48,7 +48,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { //Check if this is a redirection url if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) { statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r) - h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", "") + h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", r.Host, "") return } @@ -79,7 +79,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if sep.RequireRateLimit { err := h.handleRateLimitRouting(w, r, sep) if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307) + h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307, r.Host, "") return } } @@ -110,7 +110,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled { //Missing tailing slash. Redirect to target proxy endpoint http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect) - h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307) + h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307, r.Host, "") return } } @@ -186,6 +186,9 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) } } + //Do not log default site requests to avoid flooding the logs + //h.Parent.logRequest(r, false, 307, "root", domainOnly, "") + //No vdir match. Route via root router h.hostRequest(w, r, h.Parent.Root) case DefaultSite_Redirect: @@ -208,19 +211,19 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) } hostname := parsedURL.Hostname() if hostname == domainOnly { - h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly) + h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly, "") http.Error(w, "Loopback redirects due to invalid settings", 500) return } - h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly) + h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly, "") http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect) case DefaultSite_NotFoundPage: //Serve the not found page, use template if exists h.serve404PageWithTemplate(w, r) case DefaultSite_NoResponse: //No response. Just close the connection - h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly) + h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly, "") hijacker, ok := w.(http.Hijacker) if !ok { w.WriteHeader(http.StatusNoContent) @@ -234,11 +237,11 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) conn.Close() case DefaultSite_TeaPot: //I'm a teapot - h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly) + h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly, "") http.Error(w, "I'm a teapot", http.StatusTeapot) default: //Unknown routing option. Send empty response - h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly) + h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly, "") http.Error(w, "544 - No Route Defined", 544) } } diff --git a/src/mod/dynamicproxy/access.go b/src/mod/dynamicproxy/access.go index de8f453..1845612 100644 --- a/src/mod/dynamicproxy/access.go +++ b/src/mod/dynamicproxy/access.go @@ -23,7 +23,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r) if isBlocked { - h.Parent.logRequest(r, false, 403, blockedReason, "") + h.Parent.logRequest(r, false, 403, blockedReason, r.Host, "") } return isBlocked } diff --git a/src/mod/dynamicproxy/authProviders.go b/src/mod/dynamicproxy/authProviders.go index d7e8190..8099b55 100644 --- a/src/mod/dynamicproxy/authProviders.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -31,22 +31,23 @@ and return a boolean indicate if the request is written to http.ResponseWriter - false: the request is not handled (usually means auth ok), continue to the next handler */ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool { + requestHostname := r.Host if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic { err := h.handleBasicAuthRouting(w, r, sep) if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") return true } } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia { err := h.handleAutheliaAuth(w, r) if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") return true } } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik { err := h.handleAuthentikAuth(w, r) if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") return true } } @@ -57,11 +58,8 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt /* Basic Auth */ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - err := handleBasicAuth(w, r, pe) - if err != nil { - h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname()) - } - return err + //Wrapper for oop style + return handleBasicAuth(w, r, pe) } // Handle basic auth logic @@ -81,6 +79,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) if !ok { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.WriteHeader(401) + w.Write([]byte("401 - Unauthorized")) return errors.New("unauthorized") } @@ -100,6 +99,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) if !matchingFound { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.WriteHeader(401) + w.Write([]byte("401 - Unauthorized")) return errors.New("unauthorized") } diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 677881a..f17181e 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -155,7 +155,7 @@ func (router *Router) StartProxyService() error { if err != nil { http.ServeFile(w, r, "./web/hosterror.html") router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err) - router.logRequest(r, false, 404, "vdir-http", r.Host) + router.logRequest(r, false, 404, "vdir-http", r.Host, "") } endpointProxyRewriteRules := GetDefaultHeaderRewriteRules() diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index f4d8fea..fffc303 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -116,13 +116,13 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string { func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) - + reqHostname := r.Host /* Load balancing */ selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession) if err != nil { http.ServeFile(w, r, "./web/rperror.html") h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err) - h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname()) + h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname(), r.Host) return } @@ -144,7 +144,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe if selectedUpstream.RequireTLS { u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) } - h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain) + h.Parent.logRequest(r, true, 101, "host-websocket", reqHostname, selectedUpstream.OriginIpOrDomain) if target.HeaderRewriteRules == nil { target.HeaderRewriteRules = GetDefaultHeaderRewriteRules() @@ -161,12 +161,11 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe return } - originalHostHeader := r.Host if r.URL != nil { r.Host = r.URL.Host } else { //Fallback when the upstream proxy screw something up in the header - r.URL, _ = url.Parse(originalHostHeader) + r.URL, _ = url.Parse(reqHostname) } //Populate the user-defined headers with the values from the request @@ -188,7 +187,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe //Handle the request reverse proxy statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: selectedUpstream.OriginIpOrDomain, - OriginalHost: originalHostHeader, + OriginalHost: reqHostname, UseTLS: selectedUpstream.RequireTLS, NoCache: h.Parent.Option.NoCache, PathPrefix: "", @@ -201,28 +200,28 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe //validate the error var dnsError *net.DNSError + upstreamHostname := selectedUpstream.OriginIpOrDomain if err != nil { if errors.As(err, &dnsError) { http.ServeFile(w, r, "./web/hosterror.html") - h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname()) + h.Parent.logRequest(r, false, 404, "host-http", reqHostname, upstreamHostname) } else if errors.Is(err, context.Canceled) { //Request canceled by client, usually due to manual refresh before page load http.Error(w, "Request canceled", http.StatusRequestTimeout) - h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", r.URL.Hostname()) + h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", reqHostname, upstreamHostname) } else { http.ServeFile(w, r, "./web/rperror.html") - h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname()) + h.Parent.logRequest(r, false, 521, "host-http", reqHostname, upstreamHostname) } } - h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname()) + h.Parent.logRequest(r, true, statusCode, "host-http", reqHostname, upstreamHostname) } // Handle vdir type request func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) { rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI) r.URL, _ = url.Parse(rewriteURL) - r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) @@ -242,7 +241,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules() } - h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain) + h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain) wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ SkipTLSValidation: target.SkipCertValidations, SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility @@ -254,12 +253,12 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe return } - originalHostHeader := r.Host + reqHostname := r.Host if r.URL != nil { r.Host = r.URL.Host } else { //Fallback when the upstream proxy screw something up in the header - r.URL, _ = url.Parse(originalHostHeader) + r.URL, _ = url.Parse(reqHostname) } //Populate the user-defined headers with the values from the request @@ -282,7 +281,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe //Handle the virtual directory reverse proxy request statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: target.Domain, - OriginalHost: originalHostHeader, + OriginalHost: reqHostname, UseTLS: target.RequireTLS, PathPrefix: target.MatchingPath, UpstreamHeaders: upstreamHeaders, @@ -296,19 +295,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe if errors.As(err, &dnsError) { http.ServeFile(w, r, "./web/hosterror.html") log.Println(err.Error()) - h.Parent.logRequest(r, false, 404, "vdir-http", target.Domain) + h.Parent.logRequest(r, false, 404, "vdir-http", reqHostname, target.Domain) } else { http.ServeFile(w, r, "./web/rperror.html") log.Println(err.Error()) - h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain) + h.Parent.logRequest(r, false, 521, "vdir-http", reqHostname, target.Domain) } } - h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain) + h.Parent.logRequest(r, true, statusCode, "vdir-http", reqHostname, target.Domain) } // This logger collect data for the statistical analysis. For log to file logger, check the Logger and LogHTTPRequest handler -func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) { +func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, originalHostname string, upstreamHostname string) { if router.Option.StatisticCollector != nil { go func() { requestInfo := statistic.RequestInfo{ @@ -320,10 +319,10 @@ func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, for Referer: r.Referer(), UserAgent: r.UserAgent(), RequestURL: r.Host + r.RequestURI, - Target: target, + Target: originalHostname, } router.Option.StatisticCollector.RecordRequest(requestInfo) }() } - router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode) + router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode, originalHostname, upstreamHostname) } diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go index 40dbc9b..a809cc9 100644 --- a/src/mod/dynamicproxy/ratelimit.go +++ b/src/mod/dynamicproxy/ratelimit.go @@ -51,7 +51,7 @@ func (t *RequestCountPerIpTable) Clear() { func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { err := h.Parent.handleRateLimit(w, r, pe) if err != nil { - h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname()) + h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname(), "") } return err } diff --git a/src/mod/info/logger/trafficlog.go b/src/mod/info/logger/trafficlog.go index 640b09a..b5e555f 100644 --- a/src/mod/info/logger/trafficlog.go +++ b/src/mod/info/logger/trafficlog.go @@ -16,7 +16,7 @@ import ( // Log HTTP request. Note that this must run in go routine to prevent any blocking // in reverse proxy router -func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int) { +func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int, downstreamHostname string, upstreamHostname string) { go func() { l.ValidateAndUpdateLogFilepath() if l.logger == nil || l.file == nil { @@ -26,7 +26,9 @@ func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int clientIP := netutils.GetRequesterIP(r) requestURI := r.RequestURI statusCodeString := strconv.Itoa(statusCode) - //fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString) - l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString) + + //Pretty print for debugging + //fmt.Printf("------------\nRequest URL: %s (class: %s) \nUpstream Hostname: %s\nDownstream Hostname: %s\nStatus Code: %s\n", r.URL, reqclass, upstreamHostname, downstreamHostname, statusCodeString) + l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + downstreamHostname + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString) }() } From ac91a3fef1d7b2139f9a0e2972d3cd9e27a27ac0 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 3 Apr 2025 13:35:34 +0800 Subject: [PATCH 04/16] Fixed #201 and #608 - Added domain / host name specific statistics - Added upstream forward request count --- src/def.go | 2 +- src/mod/dynamicproxy/dpcore/dpcore.go | 13 --- src/mod/dynamicproxy/proxyRequestHandler.go | 1 + src/mod/statistic/analytic/utils.go | 10 ++ src/mod/statistic/statistic.go | 53 ++++++--- src/mod/statistic/structconv.go | 113 +++++++++----------- src/web/components/stats.html | 97 ++++++++++++++++- 7 files changed, 198 insertions(+), 91 deletions(-) diff --git a/src/def.go b/src/def.go index a5fba8e..9b717ef 100644 --- a/src/def.go +++ b/src/def.go @@ -45,7 +45,7 @@ const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" SYSTEM_VERSION = "3.2.1" - DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ + DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ /* System Constants */ TMP_FOLDER = "./tmp" diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 7eef37e..8f858e4 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -105,7 +105,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp thisTransporter := http.DefaultTransport //Hack the default transporter to handle more connections - optimalConcurrentConnection := 256 if dpcOptions.MaxConcurrentConnection > 0 { optimalConcurrentConnection = dpcOptions.MaxConcurrentConnection @@ -137,18 +136,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp } } -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - switch { - case aslash && bslash: - return a + b[1:] - case !aslash && !bslash: - return a + "/" + b - } - return a + b -} - func joinURLPath(a, b *url.URL) (path, rawpath string) { apath, bpath := a.EscapedPath(), b.EscapedPath() aslash, bslash := strings.HasSuffix(apath, "/"), strings.HasPrefix(bpath, "/") diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index fffc303..36e22df 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -320,6 +320,7 @@ func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, for UserAgent: r.UserAgent(), RequestURL: r.Host + r.RequestURI, Target: originalHostname, + Upstream: upstreamHostname, } router.Option.StatisticCollector.RecordRequest(requestInfo) }() diff --git a/src/mod/statistic/analytic/utils.go b/src/mod/statistic/analytic/utils.go index a373abb..f5b174a 100644 --- a/src/mod/statistic/analytic/utils.go +++ b/src/mod/statistic/analytic/utils.go @@ -36,6 +36,8 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti Referer: make(map[string]int), UserAgent: make(map[string]int), RequestURL: make(map[string]int), + Downstreams: make(map[string]int), + Upstreams: make(map[string]int), } for _, export := range exports { @@ -66,6 +68,14 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti for key, value := range export.RequestURL { mergedExport.RequestURL[key] += value } + + for key, value := range export.Downstreams { + mergedExport.Downstreams[key] += value + } + + for key, value := range export.Upstreams { + mergedExport.Upstreams[key] += value + } } return mergedExport diff --git a/src/mod/statistic/statistic.go b/src/mod/statistic/statistic.go index e9c7a4f..c5c8647 100644 --- a/src/mod/statistic/statistic.go +++ b/src/mod/statistic/statistic.go @@ -24,12 +24,14 @@ type DailySummary struct { ErrorRequest int64 //Invalid request of the day, including error or not found ValidRequest int64 //Valid request of the day //Type counters - ForwardTypes *sync.Map //Map that hold the forward types - RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter - RequestClientIp *sync.Map //Map that hold all unique request IPs - Referer *sync.Map //Map that store where the user was refered from - UserAgent *sync.Map //Map that store the useragent of the request - RequestURL *sync.Map //Request URL of the request object + ForwardTypes *sync.Map //Map that hold the forward types + RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter + RequestClientIp *sync.Map //Map that hold all unique request IPs + Referer *sync.Map //Map that store where the user was refered from + UserAgent *sync.Map //Map that store the useragent of the request + RequestURL *sync.Map //Request URL of the request object + DownstreamHostnames *sync.Map //Request count of downstream hostname + UpstreamHostnames *sync.Map //Forwarded request count of upstream hostname } type RequestInfo struct { @@ -42,6 +44,7 @@ type RequestInfo struct { UserAgent string //UserAgent of the downstream request RequestURL string //Request URL Target string //Target domain or hostname + Upstream string ////Upstream domain or hostname, if the request is forwarded to upstream } type CollectorOption struct { @@ -233,6 +236,24 @@ func (c *Collector) RecordRequest(ri RequestInfo) { } else { c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1) } + + //Record the downstream hostname + //This is the hostname that the user visited, not the target domain + ds, ok := c.DailySummary.DownstreamHostnames.Load(ri.Target) + if !ok { + c.DailySummary.DownstreamHostnames.Store(ri.Target, 1) + } else { + c.DailySummary.DownstreamHostnames.Store(ri.Target, ds.(int)+1) + } + + //Record the upstream hostname + //This is the selected load balancer upstream hostname or ip + us, ok := c.DailySummary.UpstreamHostnames.Load(ri.Upstream) + if !ok { + c.DailySummary.UpstreamHostnames.Store(ri.Upstream, 1) + } else { + c.DailySummary.UpstreamHostnames.Store(ri.Upstream, us.(int)+1) + } }() //ADD MORE HERE IF NEEDED @@ -271,15 +292,17 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool { func NewDailySummary() *DailySummary { return &DailySummary{ - TotalRequest: 0, - ErrorRequest: 0, - ValidRequest: 0, - ForwardTypes: &sync.Map{}, - RequestOrigin: &sync.Map{}, - RequestClientIp: &sync.Map{}, - Referer: &sync.Map{}, - UserAgent: &sync.Map{}, - RequestURL: &sync.Map{}, + TotalRequest: 0, + ErrorRequest: 0, + ValidRequest: 0, + ForwardTypes: &sync.Map{}, + RequestOrigin: &sync.Map{}, + RequestClientIp: &sync.Map{}, + Referer: &sync.Map{}, + UserAgent: &sync.Map{}, + RequestURL: &sync.Map{}, + DownstreamHostnames: &sync.Map{}, + UpstreamHostnames: &sync.Map{}, } } diff --git a/src/mod/statistic/structconv.go b/src/mod/statistic/structconv.go index 7de5c98..f36f0f8 100644 --- a/src/mod/statistic/structconv.go +++ b/src/mod/statistic/structconv.go @@ -13,6 +13,21 @@ type DailySummaryExport struct { Referer map[string]int UserAgent map[string]int RequestURL map[string]int + Downstreams map[string]int + Upstreams map[string]int +} + +func SyncMapToMapStringInt(syncMap *sync.Map) map[string]int { + result := make(map[string]int) + syncMap.Range(func(key, value interface{}) bool { + strKey, okKey := key.(string) + intValue, okValue := value.(int) + if okKey && okValue { + result[strKey] = intValue + } + return true + }) + return result } func DailySummaryToExport(summary DailySummary) DailySummaryExport { @@ -26,77 +41,53 @@ func DailySummaryToExport(summary DailySummary) DailySummaryExport { Referer: make(map[string]int), UserAgent: make(map[string]int), RequestURL: make(map[string]int), + Downstreams: make(map[string]int), + Upstreams: make(map[string]int), } - summary.ForwardTypes.Range(func(key, value interface{}) bool { - export.ForwardTypes[key.(string)] = value.(int) - return true - }) - - summary.RequestOrigin.Range(func(key, value interface{}) bool { - export.RequestOrigin[key.(string)] = value.(int) - return true - }) - - summary.RequestClientIp.Range(func(key, value interface{}) bool { - export.RequestClientIp[key.(string)] = value.(int) - return true - }) - - summary.Referer.Range(func(key, value interface{}) bool { - export.Referer[key.(string)] = value.(int) - return true - }) - - summary.UserAgent.Range(func(key, value interface{}) bool { - export.UserAgent[key.(string)] = value.(int) - return true - }) - - summary.RequestURL.Range(func(key, value interface{}) bool { - export.RequestURL[key.(string)] = value.(int) - return true - }) + export.ForwardTypes = SyncMapToMapStringInt(summary.ForwardTypes) + export.RequestOrigin = SyncMapToMapStringInt(summary.RequestOrigin) + export.RequestClientIp = SyncMapToMapStringInt(summary.RequestClientIp) + export.Referer = SyncMapToMapStringInt(summary.Referer) + export.UserAgent = SyncMapToMapStringInt(summary.UserAgent) + export.RequestURL = SyncMapToMapStringInt(summary.RequestURL) + export.Downstreams = SyncMapToMapStringInt(summary.DownstreamHostnames) + export.Upstreams = SyncMapToMapStringInt(summary.UpstreamHostnames) return export } +func MapStringIntToSyncMap(m map[string]int) *sync.Map { + syncMap := &sync.Map{} + for k, v := range m { + syncMap.Store(k, v) + } + return syncMap +} + func DailySummaryExportToSummary(export DailySummaryExport) DailySummary { summary := DailySummary{ - TotalRequest: export.TotalRequest, - ErrorRequest: export.ErrorRequest, - ValidRequest: export.ValidRequest, - ForwardTypes: &sync.Map{}, - RequestOrigin: &sync.Map{}, - RequestClientIp: &sync.Map{}, - Referer: &sync.Map{}, - UserAgent: &sync.Map{}, - RequestURL: &sync.Map{}, + TotalRequest: export.TotalRequest, + ErrorRequest: export.ErrorRequest, + ValidRequest: export.ValidRequest, + ForwardTypes: &sync.Map{}, + RequestOrigin: &sync.Map{}, + RequestClientIp: &sync.Map{}, + Referer: &sync.Map{}, + UserAgent: &sync.Map{}, + RequestURL: &sync.Map{}, + DownstreamHostnames: &sync.Map{}, + UpstreamHostnames: &sync.Map{}, } - for k, v := range export.ForwardTypes { - summary.ForwardTypes.Store(k, v) - } - - for k, v := range export.RequestOrigin { - summary.RequestOrigin.Store(k, v) - } - - for k, v := range export.RequestClientIp { - summary.RequestClientIp.Store(k, v) - } - - for k, v := range export.Referer { - summary.Referer.Store(k, v) - } - - for k, v := range export.UserAgent { - summary.UserAgent.Store(k, v) - } - - for k, v := range export.RequestURL { - summary.RequestURL.Store(k, v) - } + summary.ForwardTypes = MapStringIntToSyncMap(export.ForwardTypes) + summary.RequestOrigin = MapStringIntToSyncMap(export.RequestOrigin) + summary.RequestClientIp = MapStringIntToSyncMap(export.RequestClientIp) + summary.Referer = MapStringIntToSyncMap(export.Referer) + summary.UserAgent = MapStringIntToSyncMap(export.UserAgent) + summary.RequestURL = MapStringIntToSyncMap(export.RequestURL) + summary.DownstreamHostnames = MapStringIntToSyncMap(export.Downstreams) + summary.UpstreamHostnames = MapStringIntToSyncMap(export.Upstreams) return summary } diff --git a/src/web/components/stats.html b/src/web/components/stats.html index 4f650b0..3d78467 100644 --- a/src/web/components/stats.html +++ b/src/web/components/stats.html @@ -184,7 +184,46 @@ - + +
+
+
+

Requested Hostnames

+

Most requested hostnames from downstream

+
+
+ + + + + + + + + +
HostnameRequests
+
+
+
+
+

Forwarded Upstreams

+

The Top 100 upstreams where the requests are forwarded to

+
+
+ + + + + + + + + +
Upstream EndpointRequests
+
+
+
+

Visitor Trend Analysis

@@ -263,6 +302,22 @@ //Render Referer header renderRefererTable(data.Referer); + if (data.Downstreams == null){ + //No downstream data to show + $("#stats_downstreamTable").html("No data"); + }else{ + //Render the downstream table + renderDownstreamTable(data.Downstreams); + } + + if (data.Upstreams == null){ + //No upstream data to show + $("#stats_upstreamTable").html("No data"); + }else{ + //Render the upstream table + renderUpstreamTable(data.Upstreams); + } + //Hide the trend graphs $("#trendGraphs").hide(); }); @@ -410,6 +465,46 @@ } } + function renderDownstreamTable(downstreamList){ + const sortedEntries = Object.entries(downstreamList).sort(([, valueA], [, valueB]) => valueB - valueA); + $("#stats_downstreamTable").html(""); + let endStop = 100; + if (sortedEntries.length < 100){ + endStop = sortedEntries.length; + } + for (var i = 0; i < endStop; i++) { + let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,""); + if (sortedEntries[i][0] == ""){ + //Root + referer = `( Unknown or Hidden)`; + } + $("#stats_downstreamTable").append(` + ${referer} + ${sortedEntries[i][1]} + `); + } + } + + function renderUpstreamTable(upstreamList){ + const sortedEntries = Object.entries(upstreamList).sort(([, valueA], [, valueB]) => valueB - valueA); + $("#stats_upstreamTable").html(""); + let endStop = 100; + if (sortedEntries.length < 100){ + endStop = sortedEntries.length; + } + for (var i = 0; i < endStop; i++) { + let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,""); + if (sortedEntries[i][0] == ""){ + //Root + referer = `( Unknown or Hidden)`; + } + $("#stats_upstreamTable").append(` + ${referer} + ${sortedEntries[i][1]} + `); + } + } + function renderFileTypeGraph(requestURLs){ //Create the device chart let fileExtensions = {}; From b9b992a81726a8aec3658559fd1327109d99c097 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 6 Apr 2025 16:49:44 +0800 Subject: [PATCH 05/16] Fixed #626 - Added checks for port in hostname redirection in dpcore util --- src/mod/dynamicproxy/dpcore/dpcore.go | 1 - src/mod/dynamicproxy/dpcore/utils.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 8f858e4..eb0a489 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -339,7 +339,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } } else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" { //Back to the root of this proxy object - //fmt.Println(rrr.ProxyDomain, rrr.OriginalHost) locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation } else { //Relative path. Do not modifiy location header diff --git a/src/mod/dynamicproxy/dpcore/utils.go b/src/mod/dynamicproxy/dpcore/utils.go index ab878e2..edac154 100644 --- a/src/mod/dynamicproxy/dpcore/utils.go +++ b/src/mod/dynamicproxy/dpcore/utils.go @@ -36,6 +36,24 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b //Do not modify location header return urlString, nil } + + //Issue #626: Check if the location header is another subdomain with port + //E.g. Proxy config: blog.example.com -> 127.0.0.1:80 + //Check if it is actually redirecting to (*.)blog.example.com:8080 instead of current domain + //like Location: http://x.blog.example.com:1234/ + _, newLocationPort, err := net.SplitHostPort(u.Host) + if (newLocationPort == "80" || newLocationPort == "443") && err == nil { + //Port 80 or 443, some web server use this to switch between http and https + //E.g. http://example.com:80 -> https://example.com:443 + //E.g. http://example.com:443 -> https://example.com:80 + //That usually means the user have invalidly configured the web server to use port 80 or 443 + //for http or https. We should not modify the location header in this case. + + } else { + //Other port numbers. Do not modify location header + return urlString, nil + } + u.Host = rrr.OriginalHost if strings.Contains(rrr.ProxyDomain, "/") { From a3cccee16248bd130d5c3a6da984c197bcef3b13 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 6 Apr 2025 16:50:59 +0800 Subject: [PATCH 06/16] Added "blocks common exploits" module - Added blocks common exploits prototype - Added bot detection function (not included in dpcore yet) #615 --- src/go.mod | 18 +--- src/go.sum | 79 +--------------- src/mod/dynamicproxy/exploits/exploits.go | 108 ++++++++++++++++++++++ 3 files changed, 113 insertions(+), 92 deletions(-) create mode 100644 src/mod/dynamicproxy/exploits/exploits.go diff --git a/src/go.mod b/src/go.mod index cad79ff..e722aa9 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,11 +5,11 @@ go 1.22.0 toolchain go1.22.2 require ( + github.com/armon/go-radix v1.0.0 github.com/boltdb/bolt v1.3.1 github.com/docker/docker v27.0.0+incompatible github.com/go-acme/lego/v4 v4.21.0 github.com/go-ping/ping v1.1.0 - github.com/go-session/session v3.1.2+incompatible github.com/google/uuid v1.6.0 github.com/gorilla/sessions v1.2.2 github.com/gorilla/websocket v1.5.1 @@ -19,7 +19,6 @@ require ( github.com/shirou/gopsutil/v4 v4.25.1 github.com/syndtr/goleveldb v1.0.0 golang.org/x/net v0.33.0 - golang.org/x/sys v0.28.0 golang.org/x/text v0.21.0 ) @@ -27,30 +26,22 @@ require ( cloud.google.com/go/auth v0.13.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect - github.com/armon/go-radix v1.0.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/ebitengine/purego v0.8.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect + github.com/monperrus/crawler-user-agents v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/peterhellberg/link v1.2.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect - github.com/tidwall/buntdb v1.1.2 // indirect - github.com/tidwall/gjson v1.12.1 // indirect - github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect - github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect - github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/vultr/govultr/v3 v3.9.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect + golang.org/x/sys v0.28.0 // indirect ) require ( @@ -111,11 +102,9 @@ require ( github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/go-resty/resty/v2 v2.16.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/gofrs/uuid v4.4.0+incompatible github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -187,7 +176,6 @@ require ( github.com/transip/gotransip/v6 v6.26.0 // indirect github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect - github.com/xlzd/gotp v0.1.0 github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/src/go.sum b/src/go.sum index 6a52464..64d276a 100644 --- a/src/go.sum +++ b/src/go.sum @@ -76,15 +76,11 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0= github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -186,7 +182,6 @@ github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -202,8 +197,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o= @@ -222,16 +215,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ= -github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= -github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= -github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -242,16 +231,11 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -307,7 +291,6 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -320,7 +303,6 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= @@ -377,8 +359,6 @@ github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= @@ -395,11 +375,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= @@ -408,8 +386,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= -github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -493,11 +469,11 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monperrus/crawler-user-agents v1.1.0 h1:Xy8ZrhizT+y2FONWFFdKOP+3BhH97BDLuG7W/MswoGI= +github.com/monperrus/crawler-user-agents v1.1.0/go.mod h1:GfRyKbsbxSrRxTPYnVi4U/0stQd6BcFCxDy6i6IxQ0M= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= @@ -531,7 +507,6 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= @@ -609,10 +584,7 @@ github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -628,7 +600,6 @@ github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHei github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= @@ -661,7 +632,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -675,25 +645,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krc github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M= -github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= -github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= -github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= -github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= -github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= -github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= -github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= -github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= -github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= -github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= -github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= -github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -705,11 +656,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI= github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= -github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8= @@ -717,27 +663,12 @@ github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+H github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= -github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg= github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw= github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -783,7 +714,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -861,7 +791,6 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -903,7 +832,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -919,7 +847,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1009,7 +936,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1071,7 +997,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/mod/dynamicproxy/exploits/exploits.go b/src/mod/dynamicproxy/exploits/exploits.go new file mode 100644 index 0000000..ff5e6dd --- /dev/null +++ b/src/mod/dynamicproxy/exploits/exploits.go @@ -0,0 +1,108 @@ +package exploits + +/* + exploits.go + + This file is used to define routing rules that blocks common exploits. + +*/ + +import ( + _ "embed" + "net/http" + "regexp" + + agents "github.com/monperrus/crawler-user-agents" +) + +type Detector struct { +} + +func NewExploitDetector() *Detector { + return &Detector{} +} + +// RequestContainCommonExploits checks if the request contains common exploits +// such as SQL injection, file injection, and other common attack patterns. +func (d *Detector) RequestContainCommonExploits(r *http.Request) bool { + query := r.URL.RawQuery + userAgent := r.UserAgent() + + // Block SQL injections + sqlInjectionPatterns := []string{ + `union.*select.*\(`, + `union.*all.*select.*`, + `concat.*\(`, + } + for _, pattern := range sqlInjectionPatterns { + if match, _ := regexp.MatchString(pattern, query); match { + return true + } + } + + // Block file injections + fileInjectionPatterns := []string{ + `[a-zA-Z0-9_]=http://`, + `[a-zA-Z0-9_]=(\.\.//?)+`, + `[a-zA-Z0-9_]=/([a-z0-9_.]//?)+`, + } + for _, pattern := range fileInjectionPatterns { + if match, _ := regexp.MatchString(pattern, query); match { + return true + } + } + + // Block common exploits + commonExploitPatterns := []string{ + `(<|%3C).*script.*(>|%3E)`, + `GLOBALS(=|\[|\%[0-9A-Z]{0,2})`, + `_REQUEST(=|\[|\%[0-9A-Z]{0,2})`, + `proc/self/environ`, + `mosConfig_[a-zA-Z_]{1,21}(=|\%3D)`, + `base64_(en|de)code\(.*\)`, + } + for _, pattern := range commonExploitPatterns { + if match, _ := regexp.MatchString(pattern, query); match { + return true + } + } + + // Block spam + spamPatterns := []string{ + `\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b`, + `\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b`, + `\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b`, + `\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b`, + } + for _, pattern := range spamPatterns { + if match, _ := regexp.MatchString(pattern, query); match { + return true + } + } + + // Block user agents + userAgentPatterns := []string{ + `Indy Library`, + `libwww-perl`, + `GetRight`, + `GetWeb!`, + `Go!Zilla`, + `Download Demon`, + `Go-Ahead-Got-It`, + `TurnitinBot`, + `GrabNet`, + } + for _, pattern := range userAgentPatterns { + if match, _ := regexp.MatchString(pattern, userAgent); match { + return true + } + } + + return false +} + +// RequestIsMadeByBots checks if the request is made by bots or crawlers +func (d *Detector) RequestIsMadeByBots(r *http.Request) bool { + userAgent := r.UserAgent() + return agents.IsCrawler(userAgent) +} From ca7cd0476c06b6285de27b4cf15fa643e4b31a2a Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 6 Apr 2025 17:05:30 +0800 Subject: [PATCH 07/16] Updated location rewrite logic - Updated to a much more relax logic for handling domain with port redirection --- src/mod/dynamicproxy/dpcore/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod/dynamicproxy/dpcore/utils.go b/src/mod/dynamicproxy/dpcore/utils.go index edac154..a30f084 100644 --- a/src/mod/dynamicproxy/dpcore/utils.go +++ b/src/mod/dynamicproxy/dpcore/utils.go @@ -49,7 +49,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b //That usually means the user have invalidly configured the web server to use port 80 or 443 //for http or https. We should not modify the location header in this case. - } else { + } else if strings.Contains(u.Host, ":") && err == nil { //Other port numbers. Do not modify location header return urlString, nil } From b863a9720f2edc573566e515b6081be938a5ab81 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Mon, 7 Apr 2025 20:02:04 +0800 Subject: [PATCH 08/16] Fixed #629 - Added $remote_ip to remote port number from remote address --- src/mod/dynamicproxy/rewrite/headervars.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mod/dynamicproxy/rewrite/headervars.go b/src/mod/dynamicproxy/rewrite/headervars.go index d57c9cc..d7bd44d 100644 --- a/src/mod/dynamicproxy/rewrite/headervars.go +++ b/src/mod/dynamicproxy/rewrite/headervars.go @@ -2,6 +2,7 @@ package rewrite import ( "fmt" + "net" "net/http" "strings" ) @@ -14,6 +15,11 @@ func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string { // Request-specific variables vars["$host"] = r.Host vars["$remote_addr"] = r.RemoteAddr + remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + remoteIP = r.RemoteAddr // Fallback to the full RemoteAddr if parsing fails + } + vars["$remote_ip"] = remoteIP vars["$request_uri"] = r.RequestURI vars["$request_method"] = r.Method vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength) From e961e52dea70a4e8831e4fbb174b10df4db4480d Mon Sep 17 00:00:00 2001 From: Niklas Roth <36939232+Nirostar@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:44:11 +0200 Subject: [PATCH 09/16] Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID --- src/mod/netutils/ipmatch.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mod/netutils/ipmatch.go b/src/mod/netutils/ipmatch.go index a9733c9..d969454 100644 --- a/src/mod/netutils/ipmatch.go +++ b/src/mod/netutils/ipmatch.go @@ -58,6 +58,11 @@ func GetRequesterIP(r *http.Request) string { requesterRawIp = requesterRawIp[1 : len(requesterRawIp)-1] } + // Trim away scope ID if present (e.g. %eth0 in IPv6) + if i := strings.Index(requesterRawIp, "%"); i != -1 { + requesterRawIp = requesterRawIp[:i] + } + return requesterRawIp } From 23eeeee7015abe7b43797f7a9c1ce666994bb04d Mon Sep 17 00:00:00 2001 From: Niklas Roth <36939232+Nirostar@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:06:20 +0200 Subject: [PATCH 10/16] Move Scope ID handling into CIDR check --- src/mod/netutils/ipmatch.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mod/netutils/ipmatch.go b/src/mod/netutils/ipmatch.go index d969454..b5f6ff4 100644 --- a/src/mod/netutils/ipmatch.go +++ b/src/mod/netutils/ipmatch.go @@ -57,12 +57,7 @@ func GetRequesterIP(r *http.Request) string { //e.g. [15c4:cbb4:cc98:4291:ffc1:3a46:06a1:51a7] requesterRawIp = requesterRawIp[1 : len(requesterRawIp)-1] } - - // Trim away scope ID if present (e.g. %eth0 in IPv6) - if i := strings.Index(requesterRawIp, "%"); i != -1 { - requesterRawIp = requesterRawIp[:i] - } - + return requesterRawIp } @@ -92,6 +87,11 @@ func MatchIpWildcard(ipAddress, wildcard string) bool { // Match ip address with CIDR func MatchIpCIDR(ip string, cidr string) bool { + // Trim away scope ID if present in IP (e.g. fe80::1%eth0) + if i := strings.Index(ip, "%"); i != -1 { + ip = ip[:i] + } + // parse the CIDR string _, cidrnet, err := net.ParseCIDR(cidr) if err != nil { From b23b967165247dfe94d5dbccd0d77f2212533a7a Mon Sep 17 00:00:00 2001 From: WHFo <996579747@qq.com> Date: Fri, 11 Apr 2025 12:02:28 +0800 Subject: [PATCH 11/16] Update httprp.html Prevent the browser from filling the saved Zoraxy login account into the input searchInput --- src/web/components/httprp.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 485148f..272a420 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -48,6 +48,8 @@
+ +
@@ -782,4 +784,4 @@ filterProxyList(); }); }); - \ No newline at end of file + From 0c8dfd8aa0fdac6e8592a52a6c9db84cde1bfe3c Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sat, 12 Apr 2025 11:32:22 +0800 Subject: [PATCH 12/16] Added http proxy list link port - Added port number and http proto to http proxy list link #635 --- src/mod/netutils/ipmatch.go | 4 ++-- src/web/components/httprp.html | 21 ++++++++++++++++----- src/web/components/status.html | 1 - 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/mod/netutils/ipmatch.go b/src/mod/netutils/ipmatch.go index b5f6ff4..942bf56 100644 --- a/src/mod/netutils/ipmatch.go +++ b/src/mod/netutils/ipmatch.go @@ -57,7 +57,7 @@ func GetRequesterIP(r *http.Request) string { //e.g. [15c4:cbb4:cc98:4291:ffc1:3a46:06a1:51a7] requesterRawIp = requesterRawIp[1 : len(requesterRawIp)-1] } - + return requesterRawIp } @@ -91,7 +91,7 @@ func MatchIpCIDR(ip string, cidr string) bool { if i := strings.Index(ip, "%"); i != -1 { ip = ip[:i] } - + // parse the CIDR string _, cidrnet, err := net.ParseCIDR(cidr) if err != nil { diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 272a420..1a9d653 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -47,10 +47,10 @@ -
+
- +
@@ -144,12 +144,23 @@ if (subd.Disabled){ enableChecked = ""; } - + let httpProto = "http://"; + if ($("#tls").checkbox("is checked")) { + httpProto = "https://"; + } else { + httpProto = "http://"; + } + let hostnameRedirectPort = currentListeningPort; + if (hostnameRedirectPort == 80 || hostnameRedirectPort == 443){ + hostnameRedirectPort = ""; + }else{ + hostnameRedirectPort = ":" + hostnameRedirectPort; + } let aliasDomains = ``; if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){ aliasDomains = `Alias: `; subd.MatchingDomainAlias.forEach(alias => { - aliasDomains += `${alias}, `; + aliasDomains += `${alias}, `; }); aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator aliasDomains += `
`; @@ -157,7 +168,7 @@ $("#httpProxyList").append(` - ${subd.RootOrMatchingDomain} ${inboundTlsIcon}
+ ${subd.RootOrMatchingDomain} ${inboundTlsIcon}
${aliasDomains} diff --git a/src/web/components/status.html b/src/web/components/status.html index affaaf8..09910ea 100644 --- a/src/web/components/status.html +++ b/src/web/components/status.html @@ -524,7 +524,6 @@ $("#tls").checkbox("set checked"); }else{ $(".tlsEnabledOnly").addClass('disabled'); - $(".tlsEnabledOnly").addClass('disabled'); } //Initiate the input listener on the checkbox From 4f026e8c07a1678956fec9663423b2be7864ab9f Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 21 Apr 2025 12:57:59 +1000 Subject: [PATCH 13/16] fix(authelia): original headers This fixes the original headers imparted to authelia. --- src/mod/auth/sso/authelia/authelia.go | 54 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/mod/auth/sso/authelia/authelia.go b/src/mod/auth/sso/authelia/authelia.go index 075e97f..d86f374 100644 --- a/src/mod/auth/sso/authelia/authelia.go +++ b/src/mod/auth/sso/authelia/authelia.go @@ -3,9 +3,10 @@ package authelia import ( "encoding/json" "errors" - "fmt" + "net" "net/http" "net/url" + "strings" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/info/logger" @@ -93,25 +94,20 @@ func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Requ protocol = "https" } - autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL - //Remove tailing slash if any - if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' { - autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1] + autheliaURL := &url.URL{ + Scheme: protocol, + Host: ar.options.AutheliaURL, } //Make a request to Authelia to verify the request - req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil) + req, err := http.NewRequest("POST", autheliaURL.JoinPath("api", "verify").String(), nil) if err != nil { ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err) w.WriteHeader(401) return errors.New("unauthorized") } - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host)) + originalURL := rOriginalHeaders(r, req) // Copy cookies from the incoming request for _, cookie := range r.Cookies() { @@ -127,10 +123,42 @@ func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Requ } if resp.StatusCode != 200 { - redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method - http.Redirect(w, r, redirectURL, http.StatusSeeOther) + redirectURL := autheliaURL.JoinPath() + + query := redirectURL.Query() + + query.Set("rd", originalURL.String()) + query.Set("rm", r.Method) + + http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther) return errors.New("unauthorized") } return nil } + +func rOriginalHeaders(r, req *http.Request) *url.URL { + if r.RemoteAddr != "" { + before, _, _ := strings.Cut(r.RemoteAddr, ":") + + if ip := net.ParseIP(before); ip != nil { + req.Header.Set("X-Forwarded-For", ip.String()) + } + } + + originalURL := &url.URL{ + Scheme: "http", + Host: r.Host, + Path: r.URL.Path, + RawPath: r.URL.RawPath, + } + + if r.TLS != nil { + originalURL.Scheme = "https" + } + + req.Header.Add("X-Forwarded-Method", r.Method) + req.Header.Add("X-Original-URL", originalURL.String()) + + return originalURL +} From 6750c7fe3d5b4ae221c863cddfe9f514aa273a07 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Tue, 22 Apr 2025 07:15:30 +0800 Subject: [PATCH 14/16] Added wip plugin store - Added plugin store snippet - Added plugin list sync functions - Work in progress install / uninstall plugin function --- src/api.go | 3 + src/mod/plugins/handler.go | 2 + src/mod/plugins/plugins.go | 2 +- src/mod/plugins/store.go | 118 ++++++++++++++ src/mod/plugins/store_test.go | 52 +++++++ src/mod/plugins/typdef.go | 6 + src/start.go | 18 ++- src/web/components/plugins.html | 7 + src/web/main.css | 6 + src/web/snippet/pluginstore.html | 258 +++++++++++++++++++++++++++++++ 10 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 src/mod/plugins/store.go create mode 100644 src/mod/plugins/store_test.go create mode 100644 src/web/snippet/pluginstore.html diff --git a/src/api.go b/src/api.go index 0991823..52f6499 100644 --- a/src/api.go +++ b/src/api.go @@ -234,6 +234,9 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup) authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup) authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup) + + authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) + authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList) } // Register the APIs for Auth functions, due to scoping issue some functions are defined here diff --git a/src/mod/plugins/handler.go b/src/mod/plugins/handler.go index 8e95805..93930ab 100644 --- a/src/mod/plugins/handler.go +++ b/src/mod/plugins/handler.go @@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } + +/* Plugin Store */ diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index bb4c64b..5971ea6 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -82,7 +82,7 @@ func (m *Manager) LoadPluginsFromDisk() error { m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) // If the plugin was enabled, start it now - fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) + //fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) { err = m.StartPlugin(thisPlugin.Spec.ID) if err != nil { diff --git a/src/mod/plugins/store.go b/src/mod/plugins/store.go new file mode 100644 index 0000000..eff3fcd --- /dev/null +++ b/src/mod/plugins/store.go @@ -0,0 +1,118 @@ +package plugins + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" + "imuslab.com/zoraxy/mod/utils" +) + +/* + Plugin Store +*/ + +// See https://github.com/aroz-online/zoraxy-official-plugins/blob/main/directories/index.json for the standard format + +type Checksums struct { + LinuxAmd64 string `json:"linux_amd64"` + Linux386 string `json:"linux_386"` + LinuxArm string `json:"linux_arm"` + LinuxArm64 string `json:"linux_arm64"` + LinuxMipsle string `json:"linux_mipsle"` + LinuxRiscv64 string `json:"linux_riscv64"` + WindowsAmd64 string `json:"windows_amd64"` +} + +type DownloadablePlugin struct { + IconPath string + PluginIntroSpect zoraxy_plugin.IntroSpect //Plugin introspect information + ChecksumsSHA256 Checksums //Checksums for the plugin binary + DownloadURLs map[string]string //Download URLs for different platforms +} + +/* Plugin Store Index List Sync */ +//Update the plugin list from the plugin store URLs +func (m *Manager) UpdateDownloadablePluginList() error { + //Get downloadable plugins from each of the plugin store URLS + m.Options.DownloadablePluginCache = []*DownloadablePlugin{} + for _, url := range m.Options.PluginStoreURLs { + pluginList, err := m.getPluginListFromURL(url) + if err != nil { + return fmt.Errorf("failed to get plugin list from %s: %w", url, err) + } + m.Options.DownloadablePluginCache = append(m.Options.DownloadablePluginCache, pluginList...) + } + + m.Options.LastSuccPluginSyncTime = time.Now().Unix() + + return nil +} + +// Get the plugin list from the URL +func (m *Manager) getPluginListFromURL(url string) ([]*DownloadablePlugin, error) { + //Get the plugin list from the URL + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get plugin list from %s: %s", url, resp.Status) + } + + var pluginList []*DownloadablePlugin + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read plugin list from %s: %w", url, err) + } + content = []byte(strings.TrimSpace(string(content))) + + err = json.Unmarshal(content, &pluginList) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal plugin list from %s: %w", url, err) + } + + return pluginList, nil +} + +func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin { + //List all downloadable plugins + if len(m.Options.DownloadablePluginCache) == 0 { + return []*DownloadablePlugin{} + } + return m.Options.DownloadablePluginCache +} + +/* + Handlers for Plugin Store +*/ + +func (m *Manager) HandleListDownloadablePlugins(w http.ResponseWriter, r *http.Request) { + //List all downloadable plugins + plugins := m.ListDownloadablePlugins() + js, _ := json.Marshal(plugins) + utils.SendJSONResponse(w, string(js)) +} + +// HandleResyncPluginList is the handler for resyncing the plugin list from the plugin store URLs +func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + //Make sure this function require csrf token + utils.SendErrorResponse(w, "Method not allowed") + return + } + + //Resync the plugin list from the plugin store URLs + err := m.UpdateDownloadablePluginList() + if err != nil { + utils.SendErrorResponse(w, "Failed to resync plugin list: "+err.Error()) + return + } + utils.SendOK(w) +} diff --git a/src/mod/plugins/store_test.go b/src/mod/plugins/store_test.go new file mode 100644 index 0000000..058648a --- /dev/null +++ b/src/mod/plugins/store_test.go @@ -0,0 +1,52 @@ +package plugins + +import ( + "testing" +) + +func TestUpdateDownloadablePluginList(t *testing.T) { + mockManager := &Manager{ + Options: &ManagerOptions{ + DownloadablePluginCache: []*DownloadablePlugin{}, + PluginStoreURLs: []string{}, + }, + } + + //Inject a mock URL for testing + mockManager.Options.PluginStoreURLs = []string{"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json"} + + err := mockManager.UpdateDownloadablePluginList() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(mockManager.Options.DownloadablePluginCache) == 0 { + t.Fatalf("expected plugin cache to be updated, but it was empty") + } + + if mockManager.Options.LastSuccPluginSyncTime == 0 { + t.Fatalf("expected LastSuccPluginSyncTime to be updated, but it was not") + } +} + +func TestGetPluginListFromURL(t *testing.T) { + mockManager := &Manager{ + Options: &ManagerOptions{ + DownloadablePluginCache: []*DownloadablePlugin{}, + PluginStoreURLs: []string{}, + }, + } + + pluginList, err := mockManager.getPluginListFromURL("https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(pluginList) == 0 { + t.Fatalf("expected plugin list to be populated, but it was empty") + } + + for _, plugin := range pluginList { + t.Logf("Plugin: %+v", plugin) + } +} diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index 991651c..23ad083 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -29,10 +29,16 @@ type Plugin struct { } type ManagerOptions struct { + /* Plugins */ PluginDir string //The directory where the plugins are stored PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file + /* Plugin Downloader */ + PluginStoreURLs []string //The plugin store URLs, used to download the plugins + DownloadablePluginCache []*DownloadablePlugin //The cache for the downloadable plugins, key is the plugin ID and value is the DownloadablePlugin struct + LastSuccPluginSyncTime int64 //The last sync time for the plugin store URLs, used to check if the plugin store URLs need to be synced again + /* Runtime */ SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function diff --git a/src/start.go b/src/start.go index 2f3c79e..83ed7f1 100644 --- a/src/start.go +++ b/src/start.go @@ -1,7 +1,6 @@ package main import ( - "imuslab.com/zoraxy/mod/auth/sso/authentik" "log" "net/http" "os" @@ -10,6 +9,8 @@ import ( "strings" "time" + "imuslab.com/zoraxy/mod/auth/sso/authentik" + "github.com/gorilla/csrf" "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" @@ -322,6 +323,9 @@ func startupSequence() { ZoraxyUUID: nodeUUID, DevelopmentBuild: DEVELOPMENT_BUILD, }, + PluginStoreURLs: []string{ + "https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json", + }, Database: sysdb, Logger: SystemWideLogger, PluginGroupsConfig: CONF_PLUGIN_GROUPS, @@ -330,9 +334,19 @@ func startupSequence() { }, }) + //Sync latest plugin list from the plugin store + go func() { + err = pluginManager.UpdateDownloadablePluginList() + if err != nil { + SystemWideLogger.PrintAndLog("plugin-manager", "Failed to sync plugin list from plugin store", err) + } else { + SystemWideLogger.PrintAndLog("plugin-manager", "Plugin list synced from plugin store", nil) + } + }() + err = pluginManager.LoadPluginsFromDisk() if err != nil { - SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err) + SystemWideLogger.PrintAndLog("plugin-manager", "Failed to load plugins", err) } /* Docker UX Optimizer */ diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index bf2d9ef..bae23ba 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -185,6 +185,8 @@ +
+ diff --git a/src/web/main.css b/src/web/main.css index 713cb12..7359506 100644 --- a/src/web/main.css +++ b/src/web/main.css @@ -197,6 +197,12 @@ body.darkTheme .menubar{ max-width: calc(80% - 1em); } +@media screen and (max-width: 478px) { + .sideWrapper.extendedMode { + max-width: calc(100% - 1em); + } +} + .sideWrapper .content{ height: 100%; width: 100%; diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html new file mode 100644 index 0000000..a7a4b91 --- /dev/null +++ b/src/web/snippet/pluginstore.html @@ -0,0 +1,258 @@ + + + + + + + Plugin Store + + + + + + + + + +
+
+ +
+ +
+ +
+
+
+
+ + Advance Settings +
+
+

Plugin Store URLs

+
+
+ + +
+ +
+ +
+
+
+
+
+ +
+



+
+ + + \ No newline at end of file From ffc67ede129ed6aabfaf0eb241e8ed13c30c756b Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Thu, 24 Apr 2025 21:19:16 +0800 Subject: [PATCH 15/16] Added working plugin store prototype - Added plugin install and remove api --- src/api.go | 2 + src/mod/plugins/lifecycle.go | 7 +- src/mod/plugins/plugins.go | 58 ++++++++ src/mod/plugins/store.go | 238 +++++++++++++++++++++++++++++++ src/web/components/plugins.html | 32 ++++- src/web/snippet/pluginstore.html | 57 +++++--- 6 files changed, 362 insertions(+), 32 deletions(-) diff --git a/src/api.go b/src/api.go index 52f6499..1caedeb 100644 --- a/src/api.go +++ b/src/api.go @@ -237,6 +237,8 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList) + authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin) + authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin) } // Register the APIs for Auth functions, due to scoping issue some functions are defined here diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index e7b2161..914acf7 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -274,13 +274,10 @@ func (m *Manager) StopPlugin(pluginID string) error { } // Check if the plugin is still running -func (m *Manager) PluginStillRunning(pluginID string) bool { +func (m *Manager) PluginIsRunning(pluginID string) bool { plugin, err := m.GetPluginByID(pluginID) if err != nil { return false } - if plugin.process == nil { - return false - } - return plugin.process.ProcessState == nil + return plugin.IsRunning() } diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 5971ea6..039eaa1 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -58,6 +58,59 @@ func NewPluginManager(options *ManagerOptions) *Manager { } } +// Reload all plugins from disk +func (m *Manager) ReloadPluginFromDisk() { + //Check each of the current plugins if the directory exists + //If not, remove the plugin from the loaded plugins list + m.loadedPluginsMutex.Lock() + for pluginID, plugin := range m.LoadedPlugins { + if !utils.FileExists(plugin.RootDir) { + m.Log("Plugin directory not found, removing plugin from runtime: "+pluginID, nil) + delete(m.LoadedPlugins, pluginID) + //Remove the plugin enable state from the database + m.Options.Database.Delete("plugins", pluginID) + } + } + + m.loadedPluginsMutex.Unlock() + + //Scan the plugin directory for new plugins + foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir) + if err != nil { + m.Log("Failed to read plugin directory", err) + return + } + + for _, folder := range foldersInPluginDir { + if folder.IsDir() { + pluginPath := filepath.Join(m.Options.PluginDir, folder.Name()) + thisPlugin, err := m.LoadPluginSpec(pluginPath) + if err != nil { + m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err) + continue + } + + //Check if the plugin id is already loaded into the runtime + m.loadedPluginsMutex.RLock() + _, ok := m.LoadedPlugins[thisPlugin.Spec.ID] + m.loadedPluginsMutex.RUnlock() + if ok { + //Plugin already loaded, skip it + continue + } + + thisPlugin.RootDir = filepath.ToSlash(pluginPath) + thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy) + m.loadedPluginsMutex.Lock() + m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin + m.loadedPluginsMutex.Unlock() + m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil) + + // The default state of the plugin is disabled, so no need to start it + } + } +} + // LoadPluginsFromDisk loads all plugins from the plugin directory func (m *Manager) LoadPluginsFromDisk() error { // Load all plugins from the plugin directory @@ -258,3 +311,8 @@ func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longe }) } + +// IsRunning checks if the plugin is currently running +func (p *Plugin) IsRunning() bool { + return p.process != nil && p.process.Process != nil +} diff --git a/src/mod/plugins/store.go b/src/mod/plugins/store.go index eff3fcd..fc17833 100644 --- a/src/mod/plugins/store.go +++ b/src/mod/plugins/store.go @@ -1,10 +1,14 @@ package plugins import ( + "crypto/sha256" "encoding/json" "fmt" "io" "net/http" + "os" + "path/filepath" + "runtime" "strings" "time" @@ -89,6 +93,180 @@ func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin { return m.Options.DownloadablePluginCache } +// InstallPlugin installs the given plugin by moving it to the PluginDir. +func (m *Manager) InstallPlugin(plugin *DownloadablePlugin) error { + pluginDir := filepath.Join(m.Options.PluginDir, plugin.PluginIntroSpect.Name) + pluginFile := plugin.PluginIntroSpect.Name + if runtime.GOOS == "windows" { + pluginFile += ".exe" + } + + //Check if the plugin id already exists in runtime plugin map + if _, ok := m.LoadedPlugins[plugin.PluginIntroSpect.ID]; ok { + return fmt.Errorf("plugin already installed: %s", plugin.PluginIntroSpect.ID) + } + + // Create the plugin directory if it doesn't exist + err := os.MkdirAll(pluginDir, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create plugin directory: %w", err) + } + + // Download the plugin binary + downloadURL, ok := plugin.DownloadURLs[runtime.GOOS+"_"+runtime.GOARCH] + if !ok { + return fmt.Errorf("no download URL available for the current platform") + } + + resp, err := http.Get(downloadURL) + if err != nil { + return fmt.Errorf("failed to download plugin: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download plugin: %s", resp.Status) + } + + // Write the plugin binary to the plugin directory + pluginPath := filepath.Join(pluginDir, pluginFile) + out, err := os.Create(pluginPath) + if err != nil { + return fmt.Errorf("failed to create plugin file: %w", err) + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + out.Close() + return fmt.Errorf("failed to write plugin file: %w", err) + } + + // Make the plugin executable + err = os.Chmod(pluginPath, 0755) + if err != nil { + out.Close() + return fmt.Errorf("failed to set executable permissions: %w", err) + } + + // Verify the checksum of the downloaded plugin binary + checksums, err := plugin.ChecksumsSHA256.GetCurrentPlatformChecksum() + if err == nil { + if !verifyChecksumForFile(pluginPath, checksums) { + out.Close() + return fmt.Errorf("checksum verification failed for plugin binary") + } + } + + //Ok, also download the icon if exists + if plugin.IconPath != "" { + iconURL := strings.TrimSpace(plugin.IconPath) + if iconURL != "" { + resp, err := http.Get(iconURL) + if err != nil { + return fmt.Errorf("failed to download plugin icon: %w", err) + } + defer resp.Body.Close() + + //Save the icon to the plugin directory + iconPath := filepath.Join(pluginDir, "icon.png") + out, err := os.Create(iconPath) + if err != nil { + return fmt.Errorf("failed to create plugin icon file: %w", err) + } + defer out.Close() + + io.Copy(out, resp.Body) + } + } + //Close the plugin exeutable + out.Close() + + //Reload the plugin list + m.ReloadPluginFromDisk() + + return nil +} + +// UninstallPlugin uninstalls the plugin by removing its directory. +func (m *Manager) UninstallPlugin(pluginID string) error { + + //Stop the plugin process if it's running + plugin, ok := m.LoadedPlugins[pluginID] + if !ok { + return fmt.Errorf("plugin not found: %s", pluginID) + } + + if plugin.IsRunning() { + err := m.StopPlugin(plugin.Spec.ID) + if err != nil { + return fmt.Errorf("failed to stop plugin: %w", err) + } + } + + //Make sure the plugin process is stopped + m.Options.Logger.PrintAndLog("plugin-manager", "Removing plugin in 3 seconds...", nil) + time.Sleep(3 * time.Second) + + // Remove the plugin directory + err := os.RemoveAll(plugin.RootDir) + if err != nil { + return fmt.Errorf("failed to remove plugin directory: %w", err) + } + + //Reload the plugin list + m.ReloadPluginFromDisk() + return nil +} + +// GetCurrentPlatformChecksum returns the checksum for the current platform +func (c *Checksums) GetCurrentPlatformChecksum() (string, error) { + switch runtime.GOOS { + case "linux": + switch runtime.GOARCH { + case "amd64": + return c.LinuxAmd64, nil + case "386": + return c.Linux386, nil + case "arm": + return c.LinuxArm, nil + case "arm64": + return c.LinuxArm64, nil + case "mipsle": + return c.LinuxMipsle, nil + case "riscv64": + return c.LinuxRiscv64, nil + default: + return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH) + } + case "windows": + switch runtime.GOARCH { + case "amd64": + return c.WindowsAmd64, nil + default: + return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH) + } + default: + return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } +} + +// VerifyChecksum verifies the checksum of the downloaded plugin binary. +func verifyChecksumForFile(filePath string, checksum string) bool { + file, err := os.Open(filePath) + if err != nil { + return false + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return false + } + calculatedChecksum := fmt.Sprintf("%x", hash.Sum(nil)) + + return calculatedChecksum == checksum +} + /* Handlers for Plugin Store */ @@ -116,3 +294,63 @@ func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) } utils.SendOK(w) } + +// HandleInstallPlugin is the handler for installing a plugin +func (m *Manager) HandleInstallPlugin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + utils.SendErrorResponse(w, "Method not allowed") + return + } + + pluginID, err := utils.PostPara(r, "pluginID") + if err != nil { + utils.SendErrorResponse(w, "pluginID is required") + return + } + + // Find the plugin info from cache + var plugin *DownloadablePlugin + for _, p := range m.Options.DownloadablePluginCache { + if p.PluginIntroSpect.ID == pluginID { + plugin = p + break + } + } + + if plugin == nil { + utils.SendErrorResponse(w, "Plugin not found") + return + } + + // Install the plugin (implementation depends on your system) + err = m.InstallPlugin(plugin) + if err != nil { + utils.SendErrorResponse(w, "Failed to install plugin: "+err.Error()) + return + } + + utils.SendOK(w) +} + +// HandleUninstallPlugin is the handler for uninstalling a plugin +func (m *Manager) HandleUninstallPlugin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + utils.SendErrorResponse(w, "Method not allowed") + return + } + + pluginID, err := utils.PostPara(r, "pluginID") + if err != nil { + utils.SendErrorResponse(w, "pluginID is required") + return + } + + // Uninstall the plugin (implementation depends on your system) + err = m.UninstallPlugin(pluginID) + if err != nil { + utils.SendErrorResponse(w, "Failed to uninstall plugin: "+err.Error()) + return + } + + utils.SendOK(w) +} diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index bae23ba..9592723 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -186,7 +186,7 @@
- + diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html index a7a4b91..3f479d4 100644 --- a/src/web/snippet/pluginstore.html +++ b/src/web/snippet/pluginstore.html @@ -43,6 +43,10 @@
+
+
Experimental Feature
+

The Plugin Store is an experimental feature. Use it at your own risk.

+
`; $('#pluginList').append(item); }); + + // Reapply search filter if there's a query in the search bar + const searchQuery = document.getElementById('searchInput').value.toLowerCase(); + if (searchQuery.trim() !== '') { + searchPlugins(); + } + } /* Plugin Actions */ - function installPlugin(pluginId) { + function installPlugin(pluginId, btn=undefined) { + if (btn !== undefined) { + $(btn).addClass('loading').prop('disabled', true); + } $.cjax({ url: '/api/plugins/store/install', type: 'POST', - data: { pluginId }, + data: { "pluginID": pluginId }, success: function(data) { + if (btn !== undefined) { + $(btn).removeClass('loading').prop('disabled', false); + } if (data.error != undefined) { parent.msgbox(data.error, false); } else { parent.msgbox("Plugin installed successfully", true); initStoreList(); - } - } - }); - } - function uninstallPlugin(pluginId) { - $.cjax({ - url: '/api/plugins/store/uninstall', - type: 'POST', - data: { pluginId }, - success: function(data) { - if (data.error != undefined) { - parent.msgbox(data.error, false); - } else { - parent.msgbox("Plugin uninstalled successfully", true); - initStoreList(); + //Also reload the parent plugin list + parent.initiatePluginList(); } + }, + error: function() { + if (btn !== undefined) { + $(btn).removeClass('loading').prop('disabled', false); + } + parent.msgbox("An error occurred while installing the plugin", false); } }); } From 97817359833fb9bc0f6367be3bed44e1bbb6442b Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 27 Apr 2025 13:55:54 +0800 Subject: [PATCH 16/16] Moved dev settings to flag - Moved internal DEVELOPMENT_MODE flag to start parameters --- src/api.go | 2 +- src/def.go | 10 +++++----- src/router.go | 2 +- src/start.go | 4 ++-- src/web/snippet/pluginstore.html | 9 ++++----- src/wrappers.go | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/api.go b/src/api.go index 1caedeb..01adf20 100644 --- a/src/api.go +++ b/src/api.go @@ -324,7 +324,7 @@ func initAPIs(targetMux *http.ServeMux) { // Register the standard web services URLs var staticWebRes http.Handler - if DEVELOPMENT_BUILD { + if *development_build { staticWebRes = http.FileServer(http.Dir("web/")) } else { subFS, err := fs.Sub(webres, "web") diff --git a/src/def.go b/src/def.go index 9b717ef..7efd638 100644 --- a/src/def.go +++ b/src/def.go @@ -43,9 +43,8 @@ import ( const ( /* Build Constants */ - SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.1" - DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ + SYSTEM_NAME = "Zoraxy" + SYSTEM_VERSION = "3.2.1" /* System Constants */ TMP_FOLDER = "./tmp" @@ -101,8 +100,9 @@ var ( path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters") path_plugin = flag.String("plugin", "./plugins", "Plugin folder path") - /* Maintaince Function Flags */ - geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit") + /* Maintaince & Development Function Flags */ + geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit") + development_build = flag.Bool("dev", false, "Use external web folder for UI development") ) /* Global Variables and Handlers */ diff --git a/src/router.go b/src/router.go index 05f91dd..064ed27 100644 --- a/src/router.go +++ b/src/router.go @@ -101,7 +101,7 @@ func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath s if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" { relativeFilepath = relativeFilepath + "index.html" } - if DEVELOPMENT_BUILD { + if *development_build { //Load from disk targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/") content, err = os.ReadFile(targetFilePath) diff --git a/src/start.go b/src/start.go index 83ed7f1..0fadc05 100644 --- a/src/start.go +++ b/src/start.go @@ -100,7 +100,7 @@ func startupSequence() { }) //Create a TLS certificate manager - tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, DEVELOPMENT_BUILD, SystemWideLogger) + tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger) if err != nil { panic(err) } @@ -321,7 +321,7 @@ func startupSequence() { SystemConst: &zoraxy_plugin.RuntimeConstantValue{ ZoraxyVersion: SYSTEM_VERSION, ZoraxyUUID: nodeUUID, - DevelopmentBuild: DEVELOPMENT_BUILD, + DevelopmentBuild: *development_build, }, PluginStoreURLs: []string{ "https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json", diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html index 3f479d4..e7205f0 100644 --- a/src/web/snippet/pluginstore.html +++ b/src/web/snippet/pluginstore.html @@ -130,7 +130,8 @@ items.forEach(item => { const name = item.querySelector('.header').textContent.toLowerCase(); const description = item.querySelector('.description p').textContent.toLowerCase(); - const author = item.querySelector('.meta span:nth-child(2)').textContent.toLowerCase(); + const authorElement = item.querySelector('.plugin_author'); + const author = authorElement ? authorElement.textContent.toLowerCase() : ''; const id = item.getAttribute('plugin_id').toLowerCase(); if (name.includes(query) || description.includes(query) || author.includes(query) || id.includes(query)) { @@ -184,11 +185,9 @@ ${plugin.PluginIntroSpect.name}
-
${plugin.PluginIntroSpect.name}
+
${plugin.PluginIntroSpect.name}
- Version: ${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch} - ${plugin.PluginIntroSpect.author} - Website +

v${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch} by ${plugin.PluginIntroSpect.author}

${plugin.PluginIntroSpect.description}

diff --git a/src/wrappers.go b/src/wrappers.go index 41d8133..81783a7 100644 --- a/src/wrappers.go +++ b/src/wrappers.go @@ -345,7 +345,7 @@ func HandleZoraxyInfo(w http.ResponseWriter, r *http.Request) { info := ZoraxyInfo{ Version: SYSTEM_VERSION, NodeUUID: displayUUID, - Development: DEVELOPMENT_BUILD, + Development: *development_build, BootTime: displayBootTime, EnableSshLoopback: displayAllowSSHLB, }