From 39d6d16c2a4301d293a66369704d01a162b5acd0 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 2 Mar 2025 09:15:50 +0800 Subject: [PATCH] Updated plugin interface - Updated plugin interface to support static path routing - Added autosave for statistic data (workaround for #561) --- .gitignore | 1 + example/plugins/build_all.sh | 12 +- example/plugins/debugger/main.go | 56 ++++-- .../mod/zoraxy_plugin/embed_webserver.go | 22 +++ .../mod/zoraxy_plugin/static_router.go | 103 +++++++++++ .../mod/zoraxy_plugin/zoraxy_plugin.go | 53 ++---- example/plugins/helloworld/main.go | 2 +- .../{ => mod}/zoraxy_plugin/README.txt | 0 .../zoraxy_plugin/embed_webserver.go | 0 .../mod/zoraxy_plugin/static_router.go | 103 +++++++++++ .../{ => mod}/zoraxy_plugin/zoraxy_plugin.go | 29 +-- example/plugins/static_capture/go.mod | 3 + example/plugins/static_capture/main.go | 104 +++++++++++ .../mod/zoraxy_plugin/README.txt | 19 ++ .../mod/zoraxy_plugin/embed_webserver.go | 128 +++++++++++++ .../mod/zoraxy_plugin/static_router.go | 103 +++++++++++ .../mod/zoraxy_plugin/zoraxy_plugin.go | 175 ++++++++++++++++++ example/plugins/static_capture/ui_info.go | 26 +++ .../ztnc/mod/zoraxy_plugin/static_router.go | 103 +++++++++++ .../ztnc/mod/zoraxy_plugin/zoraxy_plugin.go | 29 +-- src/def.go | 33 ++-- src/go.mod | 1 + src/go.sum | 2 + src/mod/dynamicproxy/Server.go | 9 +- src/mod/dynamicproxy/typedef.go | 2 + src/mod/plugins/forwarder.go | 26 --- src/mod/plugins/lifecycle.go | 4 + src/mod/plugins/plugins.go | 87 +++++++++ src/mod/plugins/static_forwarder.go | 82 ++++++++ src/mod/plugins/static_router.go | 47 +++++ src/mod/plugins/typdef.go | 11 +- src/mod/plugins/uirouter.go | 3 + .../plugins/zoraxy_plugin/static_router.go | 103 +++++++++++ .../plugins/zoraxy_plugin/zoraxy_plugin.go | 29 +-- src/mod/statistic/statistic.go | 32 +++- src/reverseproxy.go | 5 +- 36 files changed, 1398 insertions(+), 149 deletions(-) create mode 100644 example/plugins/debugger/mod/zoraxy_plugin/static_router.go rename example/plugins/helloworld/{ => mod}/zoraxy_plugin/README.txt (100%) rename example/plugins/helloworld/{ => mod}/zoraxy_plugin/embed_webserver.go (100%) create mode 100644 example/plugins/helloworld/mod/zoraxy_plugin/static_router.go rename example/plugins/helloworld/{ => mod}/zoraxy_plugin/zoraxy_plugin.go (82%) create mode 100644 example/plugins/static_capture/go.mod create mode 100644 example/plugins/static_capture/main.go create mode 100644 example/plugins/static_capture/mod/zoraxy_plugin/README.txt create mode 100644 example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go create mode 100644 example/plugins/static_capture/mod/zoraxy_plugin/static_router.go create mode 100644 example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go create mode 100644 example/plugins/static_capture/ui_info.go create mode 100644 example/plugins/ztnc/mod/zoraxy_plugin/static_router.go delete mode 100644 src/mod/plugins/forwarder.go create mode 100644 src/mod/plugins/static_forwarder.go create mode 100644 src/mod/plugins/static_router.go create mode 100644 src/mod/plugins/zoraxy_plugin/static_router.go diff --git a/.gitignore b/.gitignore index 36003b0..5c9767d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ src/log/ # plugins example/plugins/ztnc/ztnc.db example/plugins/ztnc/authtoken.secret +example/plugins/ztnc/ztnc.db.lock diff --git a/example/plugins/build_all.sh b/example/plugins/build_all.sh index 76d3792..7fabafb 100644 --- a/example/plugins/build_all.sh +++ b/example/plugins/build_all.sh @@ -1,6 +1,16 @@ #!/bin/bash +# This script builds all the plugins in the current directory + +echo "Copying zoraxy_plugin to all mods" +for dir in ./*; do + if [ -d "$dir" ]; then + cp -r ../mod/plugins/zoraxy_plugin "$dir/mod" + fi +done + # Iterate over all directories in the current directory +echo "Running go mod tidy and go build for all directories" for dir in */; do if [ -d "$dir" ]; then echo "Processing directory: $dir" @@ -19,4 +29,4 @@ for dir in */; do fi done -echo "Build process completed for all directories." \ No newline at end of file +echo "Build process completed for all directories." diff --git a/example/plugins/debugger/main.go b/example/plugins/debugger/main.go index 4e1b15d..84631cc 100644 --- a/example/plugins/debugger/main.go +++ b/example/plugins/debugger/main.go @@ -9,8 +9,9 @@ import ( ) const ( - PLUGIN_ID = "org.aroz.zoraxy.debugger" - UI_PATH = "/debug" + PLUGIN_ID = "org.aroz.zoraxy.debugger" + UI_PATH = "/debug" + STATIC_CAPTURE_INGRESS = "/s_capture" ) func main() { @@ -28,15 +29,15 @@ func main() { VersionMinor: 0, VersionPatch: 0, - GlobalCapturePaths: []plugin.CaptureRule{ + StaticCapturePaths: []plugin.StaticCaptureRule{ { - CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule - IncludeSubPaths: true, + CapturePath: "/test_a", + }, + { + CapturePath: "/test_b", }, }, - GlobalCaptureIngress: "", - AlwaysCapturePaths: []plugin.CaptureRule{}, - AlwaysCaptureIngress: "", + StaticCaptureIngress: "/s_capture", UIPath: UI_PATH, @@ -50,21 +51,42 @@ func main() { panic(err) } - // Register the shutdown handler - plugin.RegisterShutdownHandler(func() { - // Do cleanup here if needed - fmt.Println("Debugger Terminated") - }) + //Create a path handler for the capture paths + pathRouter := plugin.NewPathRouter() + pathRouter.SetDebugPrintMode(true) + pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA)) + pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB)) + pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //In theory this should never be called + //but just in case the request is not captured by the path handlers + //this will be the fallback handler + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String())) + })) + pathRouter.RegisterHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux) http.HandleFunc(UI_PATH+"/", RenderDebugUI) - http.HandleFunc("/gcapture", HandleIngressCapture) fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) } // Handle the captured request -func HandleIngressCapture(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "Capture request received") +func HandleCaptureA(w http.ResponseWriter, r *http.Request) { + for key, values := range r.Header { + for _, value := range values { + fmt.Printf("%s: %s\n", key, value) + } + } w.Header().Set("Content-Type", "text/html") - w.Write([]byte("This request is captured by the debugger")) + w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String())) +} + +func HandleCaptureB(w http.ResponseWriter, r *http.Request) { + for key, values := range r.Header { + for _, value := range values { + fmt.Printf("%s: %s\n", key, value) + } + } + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String())) } diff --git a/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go index d9b3fde..c529e99 100644 --- a/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go +++ b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" "time" ) @@ -15,6 +16,8 @@ type PluginUiRouter struct { TargetFs *embed.FS //The embed.FS where the UI files are stored TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + + terminateHandler func() //The handler to be called when the plugin is terminated } // NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS @@ -104,3 +107,22 @@ func (p *PluginUiRouter) Handler() http.Handler { p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) }) } + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/static_router.go b/example/plugins/debugger/mod/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..13b07ff --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/static_router.go @@ -0,0 +1,103 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.ServeHTTP(w, r) + })) +} + +func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go index f3865ea..2cf494e 100644 --- a/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go @@ -4,9 +4,7 @@ import ( "encoding/json" "fmt" "os" - "os/signal" "strings" - "syscall" ) /* @@ -24,9 +22,9 @@ const ( PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore ) -type CaptureRule struct { - CapturePath string `json:"capture_path"` - IncludeSubPaths bool `json:"include_sub_paths"` +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded } type ControlStatusCode int @@ -74,23 +72,24 @@ type IntroSpect struct { */ /* - Global Capture Settings - - Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on - This captures the whole traffic of Zoraxy + Static Capture Settings + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible */ - GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin - GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) /* - Always Capture Settings + Dynamic Capture Settings - Once the plugin is enabled on a given HTTP Proxy rule, - these always applies + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible */ - AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) - AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI @@ -174,25 +173,3 @@ func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { ServeIntroSpect(pluginSpect) return RecvConfigureSpec() } - -/* - -Shutdown handler - -This function will register a shutdown handler for the plugin -The shutdown callback will be called when the plugin is shutting down -You can use this to clean up resources like closing database connections -*/ - -func RegisterShutdownHandler(shutdownCallback func()) { - // Set up a channel to receive OS signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Start a goroutine to listen for signals - go func() { - <-sigChan - shutdownCallback() - os.Exit(0) - }() -} diff --git a/example/plugins/helloworld/main.go b/example/plugins/helloworld/main.go index 74188cf..4e94fea 100644 --- a/example/plugins/helloworld/main.go +++ b/example/plugins/helloworld/main.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" - plugin "example.com/zoraxy/helloworld/zoraxy_plugin" + plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin" ) const ( diff --git a/example/plugins/helloworld/zoraxy_plugin/README.txt b/example/plugins/helloworld/mod/zoraxy_plugin/README.txt similarity index 100% rename from example/plugins/helloworld/zoraxy_plugin/README.txt rename to example/plugins/helloworld/mod/zoraxy_plugin/README.txt diff --git a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go b/example/plugins/helloworld/mod/zoraxy_plugin/embed_webserver.go similarity index 100% rename from example/plugins/helloworld/zoraxy_plugin/embed_webserver.go rename to example/plugins/helloworld/mod/zoraxy_plugin/embed_webserver.go diff --git a/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go b/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..13b07ff --- /dev/null +++ b/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go @@ -0,0 +1,103 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.ServeHTTP(w, r) + })) +} + +func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/helloworld/mod/zoraxy_plugin/zoraxy_plugin.go similarity index 82% rename from example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go rename to example/plugins/helloworld/mod/zoraxy_plugin/zoraxy_plugin.go index b316e6d..2cf494e 100644 --- a/example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/helloworld/mod/zoraxy_plugin/zoraxy_plugin.go @@ -22,9 +22,9 @@ const ( PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore ) -type CaptureRule struct { - CapturePath string `json:"capture_path"` - IncludeSubPaths bool `json:"include_sub_paths"` +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded } type ControlStatusCode int @@ -72,23 +72,24 @@ type IntroSpect struct { */ /* - Global Capture Settings - - Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on - This captures the whole traffic of Zoraxy + Static Capture Settings + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible */ - GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin - GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) /* - Always Capture Settings + Dynamic Capture Settings - Once the plugin is enabled on a given HTTP Proxy rule, - these always applies + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible */ - AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) - AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI diff --git a/example/plugins/static_capture/go.mod b/example/plugins/static_capture/go.mod new file mode 100644 index 0000000..25a7842 --- /dev/null +++ b/example/plugins/static_capture/go.mod @@ -0,0 +1,3 @@ +module aroz.org/zoraxy/example/static_capture + +go 1.23.6 diff --git a/example/plugins/static_capture/main.go b/example/plugins/static_capture/main.go new file mode 100644 index 0000000..09ac268 --- /dev/null +++ b/example/plugins/static_capture/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + plugin "aroz.org/zoraxy/example/static_capture/mod/zoraxy_plugin" +) + +const ( + PLUGIN_ID = "org.aroz.zoraxy.static_capture" + UI_PATH = "/ui" + STATIC_CAPTURE_INGRESS = "/static_capture" +) + +func main() { + // Serve the plugin intro spect + // This will print the plugin intro spect and exit if the -introspect flag is provided + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: PLUGIN_ID, + Name: "Static Capture Example", + Author: "aroz.org", + AuthorContact: "https://aroz.org", + Description: "An example plugin implementing static capture", + URL: "https://zoraxy.aroz.org", + Type: plugin.PluginType_Router, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + /* + Static Capture Settings + + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible + + In this example, we will capture two paths + /test_a and /test_b. Once this plugin is enabled on a HTTP proxy rule, let say + https://example.com the plugin will capture all requests to + https://example.com/test_a and https://example.com/test_b + and reverse proxy it to the StaticCaptureIngress path of your plugin like + /static_capture/test_a and /static_capture/test_b + */ + StaticCapturePaths: []plugin.StaticCaptureRule{ + { + CapturePath: "/test_a/", + }, + { + CapturePath: "/test_b/", + }, + }, + StaticCaptureIngress: STATIC_CAPTURE_INGRESS, + + UIPath: UI_PATH, + }) + if err != nil { + //Terminate or enter standalone mode here + panic(err) + } + + /* + Static Capture Router + + The plugin library already provided a path router to handle the static capture + paths. The path router will capture the requests to the specified paths and + restore the original request path before forwarding it to the handler. + + In this example, we will create a path router to handle the two capture paths + /test_a and /test_b. The path router will capture the requests to these paths + and print the request headers to the console. + */ + pathRouter := plugin.NewPathRouter() + pathRouter.SetDebugPrintMode(true) + pathRouter.RegisterPathHandler("/test_a/", http.HandlerFunc(HandleCaptureA)) + pathRouter.RegisterPathHandler("/test_b/", http.HandlerFunc(HandleCaptureB)) + pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //In theory this should never be called + //but just in case the request is not captured by the path handlers + //like if you have forgotten to register the handler for the capture path + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String())) + })) + //Lastly, register the path routing to the default http mux (or the mux you are using) + pathRouter.RegisterHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux) + + //Create a path handler for the UI + http.HandleFunc(UI_PATH+"/", RenderDebugUI) + fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) + http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) +} + +// Handle the captured request +func HandleCaptureA(w http.ResponseWriter, r *http.Request) { + fmt.Println("Request captured by A handler") + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String())) +} + +func HandleCaptureB(w http.ResponseWriter, r *http.Request) { + fmt.Println("Request captured by B handler") + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String())) +} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/README.txt b/example/plugins/static_capture/mod/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/static_capture/mod/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..c529e99 --- /dev/null +++ b/example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,128 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui + + terminateHandler func() //The handler to be called when the plugin is terminated +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} + +// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter +// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager +// if mux is nil, the handler will be registered to http.DefaultServeMux +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + //Make sure the response is sent before the plugin is terminated + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go b/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..13b07ff --- /dev/null +++ b/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go @@ -0,0 +1,103 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.ServeHTTP(w, r) + })) +} + +func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..2cf494e --- /dev/null +++ b/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,175 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Static Capture Settings + + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible + */ + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) + + /* + Dynamic Capture Settings + + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible + */ + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} diff --git a/example/plugins/static_capture/ui_info.go b/example/plugins/static_capture/ui_info.go new file mode 100644 index 0000000..18c307b --- /dev/null +++ b/example/plugins/static_capture/ui_info.go @@ -0,0 +1,26 @@ +package main + +import ( + _ "embed" + "fmt" + "net/http" + "sort" +) + +// Render the debug UI +func RenderDebugUI(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "**static_capture Debug Interface**\n\n[Recv Headers] \n") + + headerKeys := make([]string, 0, len(r.Header)) + for name := range r.Header { + headerKeys = append(headerKeys, name) + } + sort.Strings(headerKeys) + for _, name := range headerKeys { + values := r.Header[name] + for _, value := range values { + fmt.Fprintf(w, "%s: %s\n", name, value) + } + } + w.Header().Set("Content-Type", "text/html") +} diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go b/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..13b07ff --- /dev/null +++ b/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go @@ -0,0 +1,103 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.ServeHTTP(w, r) + })) +} + +func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go index b316e6d..2cf494e 100644 --- a/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go +++ b/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go @@ -22,9 +22,9 @@ const ( PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore ) -type CaptureRule struct { - CapturePath string `json:"capture_path"` - IncludeSubPaths bool `json:"include_sub_paths"` +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded } type ControlStatusCode int @@ -72,23 +72,24 @@ type IntroSpect struct { */ /* - Global Capture Settings - - Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on - This captures the whole traffic of Zoraxy + Static Capture Settings + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible */ - GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin - GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) /* - Always Capture Settings + Dynamic Capture Settings - Once the plugin is enabled on a given HTTP Proxy rule, - these always applies + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible */ - AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) - AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI diff --git a/src/def.go b/src/def.go index 9e355c9..7bbff5d 100644 --- a/src/def.go +++ b/src/def.go @@ -23,7 +23,6 @@ import ( "imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/email" "imuslab.com/zoraxy/mod/forwardproxy" - "imuslab.com/zoraxy/mod/ganserv" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logviewer" @@ -43,23 +42,24 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.1.9" - DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ + SYSTEM_VERSION = "3.2.0" + DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ /* System Constants */ - TMP_FOLDER = "./tmp" - WEBSERV_DEFAULT_PORT = 5487 - MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */ - MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway" - MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org" - MDNS_IDENTIFY_VENDOR = "imuslab.com" - MDNS_SCAN_TIMEOUT = 30 /* Seconds */ - MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */ - GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */ - ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json" - CSRF_COOKIENAME = "zoraxy_csrf" - LOG_PREFIX = "zr" - LOG_EXTENSION = ".log" + TMP_FOLDER = "./tmp" + WEBSERV_DEFAULT_PORT = 5487 + MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */ + MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway" + MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org" + MDNS_IDENTIFY_VENDOR = "imuslab.com" + MDNS_SCAN_TIMEOUT = 30 /* Seconds */ + MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */ + GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */ + ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json" + CSRF_COOKIENAME = "zoraxy_csrf" + LOG_PREFIX = "zr" + LOG_EXTENSION = ".log" + STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */ /* Configuration Folder Storage Path Constants */ CONF_HTTP_PROXY = "./conf/proxy" @@ -132,7 +132,6 @@ var ( statisticCollector *statistic.Collector //Collecting statistic from visitors uptimeMonitor *uptime.Monitor //Uptime monitor service worker mdnsScanner *mdns.MDNSHost //mDNS discovery services - ganManager *ganserv.NetworkManager //Global Area Network Manager webSshManager *sshprox.Manager //Web SSH connection service streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew diff --git a/src/go.mod b/src/go.mod index 76762b1..cad79ff 100644 --- a/src/go.mod +++ b/src/go.mod @@ -27,6 +27,7 @@ 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 diff --git a/src/go.sum b/src/go.sum index d1a5dde..6a52464 100644 --- a/src/go.sum +++ b/src/go.sum @@ -88,6 +88,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG 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= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index c1be285..59b7ac3 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -23,6 +23,7 @@ import ( - Rate Limitor - SSO Auth - Basic Auth + - Plugin Router - Vitrual Directory Proxy - Subdomain Proxy - Root router (default site router) @@ -83,13 +84,19 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - //Validate basic auth + //Validate auth (basic auth or SSO auth) respWritten := handleAuthProviderRouting(sep, w, r, h) if respWritten { //Request handled by subroute return } + //Plugin routing + if h.Parent.Option.PluginManager.HandleRoute(w, r, sep.Tags) { + //Request handled by subroute + return + } + //Check if any virtual directory rules matches proxyingPath := strings.TrimSpace(r.RequestURI) targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath) diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 6761674..580098f 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -22,6 +22,7 @@ import ( "imuslab.com/zoraxy/mod/dynamicproxy/rewrite" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/plugins" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/tlscert" ) @@ -59,6 +60,7 @@ type RouterOption struct { StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors WebDirectory string //The static web server directory containing the templates folder LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target + PluginManager *plugins.Manager //Plugin manager for handling plugin routing /* Authentication Providers */ AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication diff --git a/src/mod/plugins/forwarder.go b/src/mod/plugins/forwarder.go deleted file mode 100644 index 5089b27..0000000 --- a/src/mod/plugins/forwarder.go +++ /dev/null @@ -1,26 +0,0 @@ -package plugins - -import "net/http" - -/* - Forwarder.go - - This file handles the dynamic proxy routing forwarding - request to plugin capture path that handles the matching - request path registered when the plugin started -*/ - -func (m *Manager) GetHandlerPlugins(w http.ResponseWriter, r *http.Request) { - -} - -func (m *Manager) GetHandlerPluginsSubsets(w http.ResponseWriter, r *http.Request) { - -} - -func (p *Plugin) HandlePluginRoute(w http.ResponseWriter, r *http.Request) { - //Find the plugin that matches the request path - //If no plugin found, return 404 - //If found, forward the request to the plugin - -} diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index 88c5b23..e2462e8 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -92,6 +92,9 @@ func (m *Manager) StartPlugin(pluginID string) error { // Store the cmd object so it can be accessed later for stopping the plugin plugin.(*Plugin).process = cmd plugin.(*Plugin).Enabled = true + + //Create a new static forwarder router for each of the static capture paths + plugin.(*Plugin).StartAllStaticPathRouters() return nil } @@ -198,6 +201,7 @@ func (m *Manager) StopPlugin(pluginID string) error { //Remove the UI proxy thisPlugin.uiProxy = nil plugin.(*Plugin).Enabled = false + plugin.(*Plugin).StopAllStaticPathRouters() return nil } diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 5be4af4..21799b6 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -11,10 +11,16 @@ package plugins import ( "errors" + "fmt" + "net/http" + "net/url" "os" "path/filepath" + "strconv" + "strings" "sync" + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/utils" ) @@ -33,6 +39,7 @@ func NewPluginManager(options *ManagerOptions) *Manager { return &Manager{ LoadedPlugins: sync.Map{}, + TagPluginMap: sync.Map{}, Options: options, } } @@ -54,6 +61,7 @@ func (m *Manager) LoadPluginsFromDisk() error { continue } thisPlugin.RootDir = filepath.ToSlash(pluginPath) + thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy) m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin) m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) @@ -67,6 +75,9 @@ func (m *Manager) LoadPluginsFromDisk() error { } } + //Generate the static forwarder radix tree + m.UpdateTagsToTree() + return nil } @@ -86,6 +97,8 @@ func (m *Manager) EnablePlugin(pluginID string) error { return err } m.Options.Database.Write("plugins", pluginID, true) + //Generate the static forwarder radix tree + m.UpdateTagsToTree() return nil } @@ -96,6 +109,8 @@ func (m *Manager) DisablePlugin(pluginID string) error { if err != nil { return err } + //Generate the static forwarder radix tree + m.UpdateTagsToTree() return nil } @@ -121,6 +136,16 @@ func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) { return plugins, nil } +// Log a message with the plugin name +func (m *Manager) LogForPlugin(p *Plugin, message string, err error) { + processID := -1 + if p.process != nil && p.process.Process != nil { + // Get the process ID of the plugin + processID = p.process.Process.Pid + } + m.Log("["+p.Spec.Name+":"+strconv.Itoa(processID)+"] "+message, err) +} + // Terminate all plugins and exit func (m *Manager) Close() { m.LoadedPlugins.Range(func(key, value interface{}) bool { @@ -134,3 +159,65 @@ func (m *Manager) Close() { //Wait until all loaded plugin process are terminated m.BlockUntilAllProcessExited() } + +/* Plugin Functions */ +func (m *Plugin) StartAllStaticPathRouters() { + // Create a dpcore object for each of the static capture paths of the plugin + for _, captureRule := range m.Spec.StaticCapturePaths { + //Make sure the captureRule consists / prefix and no trailing / + if captureRule.CapturePath == "" { + continue + } + if !strings.HasPrefix(captureRule.CapturePath, "/") { + captureRule.CapturePath = "/" + captureRule.CapturePath + } + captureRule.CapturePath = strings.TrimSuffix(captureRule.CapturePath, "/") + + // Create a new dpcore object to forward the traffic to the plugin + targetURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(m.AssignedPort) + m.Spec.StaticCaptureIngress) + if err != nil { + fmt.Println("Failed to parse target URL: "+targetURL.String(), err) + continue + } + thisRouter := dpcore.NewDynamicProxyCore(targetURL, captureRule.CapturePath, &dpcore.DpcoreOptions{}) + m.staticRouteProxy[captureRule.CapturePath] = thisRouter + } +} + +func (m *Plugin) StopAllStaticPathRouters() { + +} + +func (p *Plugin) HandleRoute(w http.ResponseWriter, r *http.Request, longestPrefix string) { + longestPrefix = strings.TrimSuffix(longestPrefix, "/") + targetRouter := p.staticRouteProxy[longestPrefix] + if targetRouter == nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + fmt.Println("Error: target router not found for prefix", longestPrefix) + return + } + + originalRequestURI := r.RequestURI + + //Rewrite the request path to the plugin UI path + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, longestPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + r.URL, _ = url.Parse(rewrittenURL) + + targetRouter.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + UseTLS: false, + OriginalHost: r.Host, + ProxyDomain: "127.0.0.1:" + strconv.Itoa(p.AssignedPort), + NoCache: true, + PathPrefix: longestPrefix, + UpstreamHeaders: [][]string{ + {"X-Zoraxy-Capture", longestPrefix}, + {"X-Zoraxy-URI", originalRequestURI}, + }, + }) + +} diff --git a/src/mod/plugins/static_forwarder.go b/src/mod/plugins/static_forwarder.go new file mode 100644 index 0000000..0f33c06 --- /dev/null +++ b/src/mod/plugins/static_forwarder.go @@ -0,0 +1,82 @@ +package plugins + +import ( + "errors" + "sync" + + "github.com/armon/go-radix" +) + +/* + Static Forwarder + + This file handles the dynamic proxy routing forwarding + request to plugin capture path that handles the matching + request path registered when the plugin started +*/ + +func (m *Manager) UpdateTagsToTree() { + //build the tag to plugin pointer sync.Map + m.TagPluginMap = sync.Map{} + for tag, pluginIds := range m.Options.PluginGroups { + tree := m.GetForwarderRadixTreeFromPlugins(pluginIds) + m.TagPluginMap.Store(tag, tree) + } +} + +// GenerateForwarderRadixTree generates the radix tree for static forwarders +func (m *Manager) GetForwarderRadixTreeFromPlugins(pluginIds []string) *radix.Tree { + // Create a new radix tree + r := radix.New() + + // Iterate over the loaded plugins and insert their paths into the radix tree + m.LoadedPlugins.Range(func(key, value interface{}) bool { + plugin := value.(*Plugin) + if !plugin.Enabled { + //Ignore disabled plugins + return true + } + + // Check if the plugin ID is in the list of plugin IDs + includeThisPlugin := false + for _, id := range pluginIds { + if plugin.Spec.ID == id { + includeThisPlugin = true + } + } + if !includeThisPlugin { + return true + } + + //For each of the plugin, insert the requested static capture paths + if len(plugin.Spec.StaticCapturePaths) > 0 { + for _, captureRule := range plugin.Spec.StaticCapturePaths { + _, ok := r.Get(captureRule.CapturePath) + m.LogForPlugin(plugin, "Assigned static capture path: "+captureRule.CapturePath, nil) + if !ok { + //If the path does not exist, create a new list + newPluginList := make([]*Plugin, 0) + newPluginList = append(newPluginList, plugin) + r.Insert(captureRule.CapturePath, newPluginList) + } else { + //The path has already been assigned to another plugin + pluginList, _ := r.Get(captureRule.CapturePath) + //pluginList = append(pluginList.([]*Plugin), plugin) + //r.Insert(captureRule.CapturePath, pluginList) + + //Warn the path is already assigned to another plugin + if plugin.Spec.ID == pluginList.([]*Plugin)[0].Spec.ID { + m.Log("Duplicate path register for plugin: "+plugin.Spec.Name+" ("+plugin.Spec.ID+")", errors.New("duplcated path: "+captureRule.CapturePath)) + continue + } + incompatiblePluginAInfo := pluginList.([]*Plugin)[0].Spec.Name + " (" + pluginList.([]*Plugin)[0].Spec.ID + ")" + incompatiblePluginBInfo := plugin.Spec.Name + " (" + plugin.Spec.ID + ")" + m.Log("Incompatible plugins: "+incompatiblePluginAInfo+" and "+incompatiblePluginBInfo, errors.New("incompatible plugins found for path: "+captureRule.CapturePath)) + } + } + } + return true + }) + + return r +} diff --git a/src/mod/plugins/static_router.go b/src/mod/plugins/static_router.go new file mode 100644 index 0000000..0417389 --- /dev/null +++ b/src/mod/plugins/static_router.go @@ -0,0 +1,47 @@ +package plugins + +import ( + "net/http" + "sync" + + "github.com/armon/go-radix" +) + +// HandleRoute handles the request to the plugin +// return true if the request is handled by the plugin +func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []string) bool { + if len(tags) == 0 { + return false + } + + //For each tag, check if the request path matches the static capture path + var wg sync.WaitGroup //Wait group for the goroutines + var handler []*Plugin //The handler for the request, can be multiple plugins + var longestPrefixAcrossAlltags string = "" //The longest prefix across all tags + for _, tag := range tags { + wg.Add(1) + go func(thisTag string) { + defer wg.Done() + //Get the radix tree for the tag + tree, ok := m.TagPluginMap.Load(thisTag) + if !ok { + return + } + //Check if the request path matches the static capture path + longestPrefix, pluginList, ok := tree.(*radix.Tree).LongestPrefix(r.URL.Path) + if ok { + if longestPrefix > longestPrefixAcrossAlltags { + longestPrefixAcrossAlltags = longestPrefix + handler = pluginList.([]*Plugin) + } + } + }(tag) + } + wg.Wait() + if len(handler) > 0 { + //Handle the request + handler[0].HandleRoute(w, r, longestPrefixAcrossAlltags) + return true + } + return false +} diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index 240742b..d99caa4 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -21,13 +21,15 @@ type Plugin struct { Enabled bool //Whether the plugin is enabled //Runtime - AssignedPort int //The assigned port for the plugin - uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI - process *exec.Cmd //The process of the plugin + AssignedPort int //The assigned port for the plugin + uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI + staticRouteProxy map[string]*dpcore.ReverseProxy //Storing longest prefix => dpcore map for static route + process *exec.Cmd //The process of the plugin } type ManagerOptions struct { - PluginDir string + 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 SystemConst *zoraxyPlugin.RuntimeConstantValue Database *database.Database Logger *logger.Logger @@ -36,5 +38,6 @@ type ManagerOptions struct { type Manager struct { LoadedPlugins sync.Map //Storing *Plugin + TagPluginMap sync.Map //Storing *radix.Tree for each plugin tag Options *ManagerOptions } diff --git a/src/mod/plugins/uirouter.go b/src/mod/plugins/uirouter.go index d2ac1c9..2dee458 100644 --- a/src/mod/plugins/uirouter.go +++ b/src/mod/plugins/uirouter.go @@ -38,6 +38,9 @@ func (m *Manager) HandlePluginUI(pluginID string, w http.ResponseWriter, r *http rewrittenURL := r.RequestURI rewrittenURL = strings.TrimPrefix(rewrittenURL, matchingPath) rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } r.URL, _ = url.Parse(rewrittenURL) //Call the plugin UI handler diff --git a/src/mod/plugins/zoraxy_plugin/static_router.go b/src/mod/plugins/zoraxy_plugin/static_router.go new file mode 100644 index 0000000..13b07ff --- /dev/null +++ b/src/mod/plugins/zoraxy_plugin/static_router.go @@ -0,0 +1,103 @@ +package zoraxy_plugin + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +type PathRouter struct { + enableDebugPrint bool + pathHandlers map[string]http.Handler + defaultHandler http.Handler +} + +// NewPathRouter creates a new PathRouter +func NewPathRouter() *PathRouter { + return &PathRouter{ + enableDebugPrint: false, + pathHandlers: make(map[string]http.Handler), + } +} + +// RegisterPathHandler registers a handler for a path +func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { + path = strings.TrimSuffix(path, "/") + p.pathHandlers[path] = handler +} + +// RemovePathHandler removes a handler for a path +func (p *PathRouter) RemovePathHandler(path string) { + delete(p.pathHandlers, path) +} + +// SetDefaultHandler sets the default handler for the router +// This handler will be called if no path handler is found +func (p *PathRouter) SetDefaultHandler(handler http.Handler) { + p.defaultHandler = handler +} + +// SetDebugPrintMode sets the debug print mode +func (p *PathRouter) SetDebugPrintMode(enable bool) { + p.enableDebugPrint = enable +} + +func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p.ServeHTTP(w, r) + })) +} + +func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + capturePath := r.Header.Get("X-Zoraxy-Capture") + if capturePath != "" { + if p.enableDebugPrint { + fmt.Printf("Using capture path: %s\n", capturePath) + } + originalURI := r.Header.Get("X-Zoraxy-Uri") + r.URL.Path = originalURI + if handler, ok := p.pathHandlers[capturePath]; ok { + handler.ServeHTTP(w, r) + return + } + } + p.defaultHandler.ServeHTTP(w, r) +} + +func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { + if p.enableDebugPrint { + fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) + keys := make([]string, 0, len(r.Header)) + for key := range r.Header { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + for _, value := range r.Header[key] { + fmt.Printf("%s: %s\n", key, value) + } + } + + fmt.Printf("\n\n**Request Details**\n\n") + fmt.Printf("Method: %s\n", r.Method) + fmt.Printf("URL: %s\n", r.URL.String()) + fmt.Printf("Proto: %s\n", r.Proto) + fmt.Printf("Host: %s\n", r.Host) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + fmt.Printf("ContentLength: %d\n", r.ContentLength) + fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) + fmt.Printf("Close: %v\n", r.Close) + fmt.Printf("Form: %v\n", r.Form) + fmt.Printf("PostForm: %v\n", r.PostForm) + fmt.Printf("MultipartForm: %v\n", r.MultipartForm) + fmt.Printf("Trailer: %v\n", r.Trailer) + fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("RequestURI: %s\n", r.RequestURI) + + } +} diff --git a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go index b316e6d..2cf494e 100644 --- a/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go +++ b/src/mod/plugins/zoraxy_plugin/zoraxy_plugin.go @@ -22,9 +22,9 @@ const ( PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore ) -type CaptureRule struct { - CapturePath string `json:"capture_path"` - IncludeSubPaths bool `json:"include_sub_paths"` +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` + //To be expanded } type ControlStatusCode int @@ -72,23 +72,24 @@ type IntroSpect struct { */ /* - Global Capture Settings - - Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on - This captures the whole traffic of Zoraxy + Static Capture Settings + Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule + This is faster than dynamic capture, but less flexible */ - GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin - GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details + StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) /* - Always Capture Settings + Dynamic Capture Settings - Once the plugin is enabled on a given HTTP Proxy rule, - these always applies + Once plugin is enabled, these rules will be captured and forward to plugin sniff + if the plugin sniff returns 280, the traffic will be captured + otherwise, the traffic will be forwarded to the next plugin + This is slower than static capture, but more flexible */ - AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) - AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler) /* UI Path for your plugin */ UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI diff --git a/src/mod/statistic/statistic.go b/src/mod/statistic/statistic.go index eb58175..e9c7a4f 100644 --- a/src/mod/statistic/statistic.go +++ b/src/mod/statistic/statistic.go @@ -50,6 +50,8 @@ type CollectorOption struct { type Collector struct { rtdataStopChan chan bool + autoSaveTicker *time.Ticker + autSaveStop chan bool DailySummary *DailySummary Option *CollectorOption } @@ -78,6 +80,35 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) { return &thisCollector, nil } +// Set the autosave duration, the collector will save the daily summary to database +// set saveInterval to 0 to disable autosave +func (c *Collector) SetAutoSave(saveInterval int) { + //Stop the current ticker if exists + if c.autSaveStop != nil { + c.autSaveStop <- true + } + + if saveInterval == 0 { + return + } + + c.autSaveStop = make(chan bool) + ticker := time.NewTicker(time.Duration(saveInterval) * time.Second) + c.autoSaveTicker = ticker + + go func() { + for { + select { + case <-ticker.C: + c.SaveSummaryOfDay() + case <-c.autSaveStop: + ticker.Stop() + return + } + } + }() +} + // Write the current in-memory summary to database file func (c *Collector) SaveSummaryOfDay() { //When it is called in 0:00am, make sure it is stored as yesterday key @@ -122,7 +153,6 @@ func (c *Collector) Close() { //Write the buffered data into database c.SaveSummaryOfDay() - } // Main function to record all the inbound traffics diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 459c49e..6dabdbc 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -108,6 +108,7 @@ func ReverseProxtInit() { NoCache: developmentMode, ListenOnPort80: listenOnPort80, ForceHttpsRedirect: forceHttpsRedirect, + /* Routing Service Managers */ TlsManager: tlsCertManager, RedirectRuleTable: redirectTable, GeodbStore: geodbStore, @@ -116,7 +117,9 @@ func ReverseProxtInit() { AccessController: accessController, AutheliaRouter: autheliaRouter, LoadBalancer: loadBalancer, - Logger: SystemWideLogger, + PluginManager: pluginManager, + /* Utilities */ + Logger: SystemWideLogger, }) if err != nil { SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err)