diff --git a/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md b/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md
new file mode 100644
index 0000000..49fbc2c
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md
@@ -0,0 +1,15 @@
+# Compile a Plugin
+
+A plugin is basically a go program with a HTTP Server / Listener. The steps required to build a plugin is identical as building a ordinary go program.
+
+```bash
+# Assuming you are currently inside the root folder of your plugin
+go mod tidy
+go build
+
+# Validate if the plugin is correctly build using -introspect flag
+./{{your_plugin_name}} -introspect
+
+# You should see your plugin information printed to STDOUT as JSON string
+```
+
diff --git a/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md b/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
index 166af0b..c7549a6 100644
--- a/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
+++ b/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
@@ -76,7 +76,7 @@ pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServe
The `SetDefaultHandler` is used to handle exceptions where a request is forwarded to your plugin but it cannot be handled by any of your registered path handlers. This is usually an implementation bug on the plugin side and you can add some help message or debug log to this function if needed.
-
+The `RegisterStaticCaptureHandle` is used to register the static capture ingress endpoint, so Zoraxy knows where to forward the HTTP request when it thinks your plugin shall be the one handling the request. In this example, `/s_capture` is used for static capture endpoint.
---
@@ -261,4 +261,5 @@ Request URI: /test_b
---
-Enjoy exploring static capture in Zoraxy!
\ No newline at end of file
+Enjoy exploring static capture in Zoraxy!
+
diff --git a/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md b/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md
new file mode 100644
index 0000000..329e72c
--- /dev/null
+++ b/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md
@@ -0,0 +1,352 @@
+# Dynamic Capture Example
+Last Update: 29/05/2025
+
+---
+
+
+This example demonstrates how to use dynamic capture in Zoraxy plugins. Dynamic capture allows you to intercept requests based on real-time conditions, so you can program your plugin in a way that it can decided if it want to handle the request or not.
+
+**Notes: This example assumes you have already read Hello World and Stataic Capture Example.**
+
+Lets dive in!
+
+---
+
+## 1. Create the plugin folder structure
+
+Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
+
+---
+
+## 2. Define Introspect
+
+The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin.
+
+```go
+runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+```
+
+Note the `DynamicCaptureSniff` and `DynamicCaptureIngress`. These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests.
+
+---
+
+## 3. Register Dynamic Capture Handlers
+
+Dynamic capture handlers are used to process requests that match specific conditions.
+
+```go
+pathRouter := plugin.NewPathRouter()
+pathRouter.SetDebugPrintMode(true)
+
+pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ fmt.Println("Accepting request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultAccpet
+ }
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+})
+
+pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+})
+```
+
+The `RegisterDynamicSniffHandler` evaluates incoming requests, while the `RegisterDynamicCaptureHandle` processes the intercepted requests.
+
+### Sniffing Logic
+
+If a module registered a dynamic capture path, Zoraxy will forward the request headers as `DynamicSniffForwardRequest` (`dsfr`) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and "think" if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
+
+```go
+type DynamicSniffForwardRequest struct {
+ Method string `json:"method"`
+ Hostname string `json:"hostname"`
+ URL string `json:"url"`
+ Header map[string][]string `json:"header"`
+ RemoteAddr string `json:"remote_addr"`
+ Host string `json:"host"`
+ RequestURI string `json:"request_uri"`
+ Proto string `json:"proto"`
+ ProtoMajor int `json:"proto_major"`
+ ProtoMinor int `json:"proto_minor"`
+}
+```
+
+You can also use the `GetRequest()` function to get the `*http.Request` object or `GetRequestUUID()` to get a `string` value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
+
+**Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled.**
+
+In the sniffing stage, you can choose to either return `ControlStatusCode_CAPTURED`, where Zoraxy will forward the request to your plugin `DynamicCaptureIngress` endpoint, or `ControlStatusCode_UNHANDLED`, where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server.
+
+### Capture Handling
+
+The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the ` http.Request` by writing to the `http.ResponseWriter`.
+
+If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the `X-Zoraxy-Requestid` header in the HTTP request. This is the same UUID as the one you get from `dsfr.GetRequestUUID()` in the sniffing stage if they are the same request object on Zoraxy side.
+
+The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
+
+```html
+Request URI: /foobar/test
+Request Method: GET
+Request Headers:
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Encoding: gzip, deflate, br, zstd
+(more fileds)
+X-Forwarded-For: 127.0.0.1
+X-Forwarded-Proto: https
+X-Real-Ip: 127.0.0.1
+X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
+```
+
+You can extract the `X-Zoraxy-Requestid` value from the request header and do your matching for implementing your function if needed.
+
+---
+
+## 4. Render Debug UI
+
+This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here.
+
+```go
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI 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")
+}
+```
+
+
+
+---
+
+## 5. Full Code
+
+Here is the complete code for the dynamic capture example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+
+ plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
+ UI_PATH = "/debug"
+ STATIC_CAPTURE_INGRESS = "/s_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: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Dynamic Captures
+ */
+ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ //In this example, we want to capture all URI
+ //that start with /foobar and forward it to the dynamic capture handler
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ reqUUID := dsfr.GetRequestUUID()
+ fmt.Println("Accepting request with UUID: " + reqUUID)
+
+ // Print all the values of the request
+ fmt.Println("Method:", dsfr.Method)
+ fmt.Println("Hostname:", dsfr.Hostname)
+ fmt.Println("URL:", dsfr.URL)
+ fmt.Println("Header:")
+ for key, values := range dsfr.Header {
+ for _, value := range values {
+ fmt.Printf(" %s: %s\n", key, value)
+ }
+ }
+ fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
+ fmt.Println("Host:", dsfr.Host)
+ fmt.Println("RequestURI:", dsfr.RequestURI)
+ fmt.Println("Proto:", dsfr.Proto)
+ fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
+ fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
+
+ // We want to handle this request, reply with aSniffResultAccept
+ return plugin.SniffResultAccpet
+ }
+
+ // If the request URI does not match, we skip this request
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+ })
+ pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ // This is the dynamic capture handler where it actually captures and handle the request
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!"))
+
+ // Print all the request info to the response writer
+ w.Write([]byte("\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+ })
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI 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")
+}
+
+```
+
+---
+
+## 6. Expected Output
+
+To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the "debug" tag and assigning it to a localhost loopback HTTP proxy rule.
+
+When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
+
+If everything is correctly setup, you should see the following page when requesting any URL with prefix `(your_HTTP_proxy_rule_hostname)/foobar`
+
+
+
+
+
+Example terminal output for requesting `/foobar/*`:
+
+```
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
+[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: "Windows"
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
+```
+
+---
+
+Now you know how to develop a plugin in Zoraxy that handles special routings!
\ No newline at end of file
diff --git a/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png b/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png
new file mode 100644
index 0000000..3c264f2
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png differ
diff --git a/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html b/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
index cebfa6b..a0057cd 100644
--- a/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
+++ b/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/1. Introduction/2. Getting Started.html b/docs/plugins/html/1. Introduction/2. Getting Started.html
index c15e0be..bce7229 100644
--- a/docs/plugins/html/1. Introduction/2. Getting Started.html
+++ b/docs/plugins/html/1. Introduction/2. Getting Started.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/1. Introduction/3. Installing Plugin.html b/docs/plugins/html/1. Introduction/3. Installing Plugin.html
index 3636ba4..c213ed7 100644
--- a/docs/plugins/html/1. Introduction/3. Installing Plugin.html
+++ b/docs/plugins/html/1. Introduction/3. Installing Plugin.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/1. Introduction/4. Enable Plugins.html b/docs/plugins/html/1. Introduction/4. Enable Plugins.html
index fa637f4..071895f 100644
--- a/docs/plugins/html/1. Introduction/4. Enable Plugins.html
+++ b/docs/plugins/html/1. Introduction/4. Enable Plugins.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html b/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
index d354dda..1f6b3cf 100644
--- a/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
+++ b/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/1. Plugin Architecture.html b/docs/plugins/html/2. Architecture/1. Plugin Architecture.html
index 7a6c02d..9ab5cb5 100644
--- a/docs/plugins/html/2. Architecture/1. Plugin Architecture.html
+++ b/docs/plugins/html/2. Architecture/1. Plugin Architecture.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/2. Introspect.html b/docs/plugins/html/2. Architecture/2. Introspect.html
index 5b6904d..af8afdd 100644
--- a/docs/plugins/html/2. Architecture/2. Introspect.html
+++ b/docs/plugins/html/2. Architecture/2. Introspect.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/3. Configure.html b/docs/plugins/html/2. Architecture/3. Configure.html
index 12b26c9..5287c12 100644
--- a/docs/plugins/html/2. Architecture/3. Configure.html
+++ b/docs/plugins/html/2. Architecture/3. Configure.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/4. Capture Modes.html b/docs/plugins/html/2. Architecture/4. Capture Modes.html
index 9420a3c..6de5a9f 100644
--- a/docs/plugins/html/2. Architecture/4. Capture Modes.html
+++ b/docs/plugins/html/2. Architecture/4. Capture Modes.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/5. Plugin UI.html b/docs/plugins/html/2. Architecture/5. Plugin UI.html
index 6b81c60..88d8f1d 100644
--- a/docs/plugins/html/2. Architecture/5. Plugin UI.html
+++ b/docs/plugins/html/2. Architecture/5. Plugin UI.html
@@ -123,6 +123,9 @@
Plugin UI
+
+ Compile a Plugin
+
Basic Examples
@@ -138,6 +141,9 @@
Static Capture Example
+
+ Dynamic Capture Example
+
index
diff --git a/docs/plugins/html/2. Architecture/6. Compile a Plugin.html b/docs/plugins/html/2. Architecture/6. Compile a Plugin.html
new file mode 100644
index 0000000..2198d06
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/6. Compile a Plugin.html
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+ A plugin is basically a go program with a HTTP Server / Listener. The steps required to build a plugin is identical as building a ordinary go program.
+
+ Compile a Plugin
+
+
+ # Assuming you are currently inside the root folder of your plugin
+go mod tidy
+go build
+
+# Validate if the plugin is correctly build using -introspect flag
+./{{your_plugin_name}} -introspect
+
+# You should see your plugin information printed to STDOUT as JSON string
+
+
+
+
+ The + + RegisterStaticCaptureHandle + + is used to register the static capture ingress endpoint, so Zoraxy knows where to forward the HTTP request when it thinks your plugin shall be the one handling the request. In this example, + + /s_capture + + is used for static capture endpoint. +
++
+ Last Update: 29/05/2025 +
+ + ++
+ This example demonstrates how to use dynamic capture in Zoraxy plugins. Dynamic capture allows you to intercept requests based on real-time conditions, so you can program your plugin in a way that it can decided if it want to handle the request or not. +
+ ++
+ + Notes: This example assumes you have already read Hello World and Stataic Capture Example. + +
+ ++
+ Lets dive in! +
+ + ++
+ Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details. +
+ + ++
+ The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin. +
+ +runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+
+ +
+ Note the + + DynamicCaptureSniff + + and + + DynamicCaptureIngress + + . These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests. +
+ + ++
+ Dynamic capture handlers are used to process requests that match specific conditions. +
+ +pathRouter := plugin.NewPathRouter()
+pathRouter.SetDebugPrintMode(true)
+
+pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ fmt.Println("Accepting request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultAccpet
+ }
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+})
+
+pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+})
+
+ +
+ The + + RegisterDynamicSniffHandler + + evaluates incoming requests, while the + + RegisterDynamicCaptureHandle + + processes the intercepted requests. +
+ ++ If a module registered a dynamic capture path, Zoraxy will forward the request headers as + + DynamicSniffForwardRequest + + ( + + dsfr + + ) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and “think” if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it. +
+type DynamicSniffForwardRequest struct {
+ Method string `json:"method"`
+ Hostname string `json:"hostname"`
+ URL string `json:"url"`
+ Header map[string][]string `json:"header"`
+ RemoteAddr string `json:"remote_addr"`
+ Host string `json:"host"`
+ RequestURI string `json:"request_uri"`
+ Proto string `json:"proto"`
+ ProtoMajor int `json:"proto_major"`
+ ProtoMinor int `json:"proto_minor"`
+}
+
+ +
+ You can also use the + + GetRequest() + + function to get the + + *http.Request + + object or + + GetRequestUUID() + + to get a + + string + + value that is a UUID corresponding to this request for later matching with the incoming, forwarded request. +
+ ++
+ + Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled. + +
+ ++
+ In the sniffing stage, you can choose to either return + + ControlStatusCode_CAPTURED + + , where Zoraxy will forward the request to your plugin + + DynamicCaptureIngress + + endpoint, or + + ControlStatusCode_UNHANDLED + + , where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server. +
+ ++
+ The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the + + http.Request + + by writing to the + + http.ResponseWriter + + . +
+ ++
+ If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the + + X-Zoraxy-Requestid + + header in the HTTP request. This is the same UUID as the one you get from + + dsfr.GetRequestUUID() + + in the sniffing stage if they are the same request object on Zoraxy side. +
+ ++
+ The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these. +
+ +Request URI: /foobar/test
+Request Method: GET
+Request Headers:
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Encoding: gzip, deflate, br, zstd
+(more fileds)
+X-Forwarded-For: 127.0.0.1
+X-Forwarded-Proto: https
+X-Real-Ip: 127.0.0.1
+X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
+
+ +
+ You can extract the + + X-Zoraxy-Requestid + + value from the request header and do your matching for implementing your function if needed. +
+ + ++
+ This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here. +
+ +func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI 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")
+}
+
+
+ +
+ Here is the complete code for the dynamic capture example: +
+ +package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+
+ plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
+ UI_PATH = "/debug"
+ STATIC_CAPTURE_INGRESS = "/s_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: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Dynamic Captures
+ */
+ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ //In this example, we want to capture all URI
+ //that start with /foobar and forward it to the dynamic capture handler
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ reqUUID := dsfr.GetRequestUUID()
+ fmt.Println("Accepting request with UUID: " + reqUUID)
+
+ // Print all the values of the request
+ fmt.Println("Method:", dsfr.Method)
+ fmt.Println("Hostname:", dsfr.Hostname)
+ fmt.Println("URL:", dsfr.URL)
+ fmt.Println("Header:")
+ for key, values := range dsfr.Header {
+ for _, value := range values {
+ fmt.Printf(" %s: %s\n", key, value)
+ }
+ }
+ fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
+ fmt.Println("Host:", dsfr.Host)
+ fmt.Println("RequestURI:", dsfr.RequestURI)
+ fmt.Println("Proto:", dsfr.Proto)
+ fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
+ fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
+
+ // We want to handle this request, reply with aSniffResultAccept
+ return plugin.SniffResultAccpet
+ }
+
+ // If the request URI does not match, we skip this request
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+ })
+ pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ // This is the dynamic capture handler where it actually captures and handle the request
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!"))
+
+ // Print all the request info to the response writer
+ w.Write([]byte("\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+ })
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI 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")
+}
+
+
+
+ + To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the “debug” tag and assigning it to a localhost loopback HTTP proxy rule. +
++
+ When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler. +
+ ++
+ If everything is correctly setup, you should see the following page when requesting any URL with prefix + + (your_HTTP_proxy_rule_hostname)/foobar + +
+ ++
+
+ Example terminal output for requesting + + /foobar/* + + : +
+ +[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
+[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: "Windows"
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
+
+
+ +
+ Now you know how to develop a plugin in Zoraxy that handles special routings! +
+ +- -
IPv4 Auto-Assign | -
---|
Manual IP Range Configuration. The IP range must be within the selected CIDR range.
-
Use Utilities > IP to CIDR tool
if you are not too familiar with CIDR notations.
To join this network using command line, type sudo zerotier-cli join
on your device terminal
Auth | -Address | -Name | -Managed IP | -Authorized Since | -Version | -Remove | -
---|---|---|---|---|---|---|
Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.
- - -Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region
-Network ID | -Name | -Description | -Subnet (Assign Range) | -Nodes | -Actions | -
---|---|---|---|---|---|
No Global Area Network Found on this host | -