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)