Added dynamic capture example

- Added dynamic capture plugin example
- Added dynamic capture plugin doc
- Removed ztnc from plugin example
This commit is contained in:
Toby Chui
2025-05-30 21:21:08 +08:00
parent a1df5a1060
commit cd822ed904
59 changed files with 1502 additions and 4134 deletions

View File

@ -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
```

View File

@ -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!
Enjoy exploring static capture in Zoraxy!

View File

@ -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`
![image-20250530205430254](img/4. Dynamic Capture Example/image-20250530205430254.png)
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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item is-active" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Compile a Plugin | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/vs2015.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
Installing Plugin
</a>
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
Enable Plugins
</a>
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
Viewing Plugin Info
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
Capture Modes
</a>
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item is-active" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
Hello World
</a>
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
RESTful Example
</a>
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="compile-a-plugin">
Compile a Plugin
</h1>
<p>
<p class="ts-text">
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.
</p>
</p>
<pre><code class="language-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
</code></pre>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
<script>
hljs.highlightAll();
</script>
</body>
</html>

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item is-active" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index
@ -275,6 +281,19 @@ pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServe
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.
</p>
</p>
<p>
<p class="ts-text">
The
<span class="ts-text is-code">
RegisterStaticCaptureHandle
</span>
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,
<span class="ts-text is-code">
/s_capture
</span>
is used for static capture endpoint.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="4-implement-handlers">
4. Implement Handlers

View File

@ -0,0 +1,673 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Dynamic Capture Example | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/vs2015.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
Installing Plugin
</a>
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
Enable Plugins
</a>
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
Viewing Plugin Info
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
Capture Modes
</a>
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
Hello World
</a>
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
RESTful Example
</a>
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item is-active" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="dynamic-capture-example">
Dynamic Capture Example
</h1>
<p>
<p class="ts-text">
Last Update: 29/05/2025
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<p>
<p class="ts-text">
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.
</p>
</p>
<p>
<p class="ts-text">
<span class="ts-text is-heavy">
Notes: This example assumes you have already read Hello World and Stataic Capture Example.
</span>
</p>
</p>
<p>
<p class="ts-text">
Lets dive in!
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="1-create-the-plugin-folder-structure">
1. Create the plugin folder structure
</h2>
<p>
<p class="ts-text">
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.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="2-define-introspect">
2. Define Introspect
</h2>
<p>
<p class="ts-text">
The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin.
</p>
</p>
<pre><code class="language-go">runtimeCfg, err := plugin.ServeAndRecvSpec(&amp;plugin.IntroSpect{
ID: &quot;org.aroz.zoraxy.dynamic-capture-example&quot;,
Name: &quot;Dynamic Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: &quot;/d_sniff&quot;,
DynamicCaptureIngress: &quot;/d_capture&quot;,
UIPath: UI_PATH,
})
if err != nil {
panic(err)
}
</code></pre>
<p>
<p class="ts-text">
Note the
<span class="ts-text is-code">
DynamicCaptureSniff
</span>
and
<span class="ts-text is-code">
DynamicCaptureIngress
</span>
. 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.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="3-register-dynamic-capture-handlers">
3. Register Dynamic Capture Handlers
</h2>
<p>
<p class="ts-text">
Dynamic capture handlers are used to process requests that match specific conditions.
</p>
</p>
<pre><code class="language-go">pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
pathRouter.RegisterDynamicSniffHandler(&quot;/d_sniff&quot;, http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
if strings.HasPrefix(dsfr.RequestURI, &quot;/foobar&quot;) {
fmt.Println(&quot;Accepting request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultAccpet
}
fmt.Println(&quot;Skipping request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle(&quot;/d_capture&quot;, http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(&quot;Welcome to the dynamic capture handler!\n\nRequest Info:\n&quot;))
w.Write([]byte(&quot;Request URI: &quot; + r.RequestURI + &quot;\n&quot;))
w.Write([]byte(&quot;Request Method: &quot; + r.Method + &quot;\n&quot;))
w.Write([]byte(&quot;Request Headers:\n&quot;))
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(&quot;%s: %s\n&quot;, key, value)))
}
}
})
</code></pre>
<p>
<p class="ts-text">
The
<span class="ts-text is-code">
RegisterDynamicSniffHandler
</span>
evaluates incoming requests, while the
<span class="ts-text is-code">
RegisterDynamicCaptureHandle
</span>
processes the intercepted requests.
</p>
</p>
<h3 id="sniffing-logic">
Sniffing Logic
</h3>
<p>
If a module registered a dynamic capture path, Zoraxy will forward the request headers as
<span class="ts-text is-code">
DynamicSniffForwardRequest
</span>
(
<span class="ts-text is-code">
dsfr
</span>
) 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 &ldquo;think&rdquo; if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
</p>
<pre><code class="language-go">type DynamicSniffForwardRequest struct {
Method string `json:&quot;method&quot;`
Hostname string `json:&quot;hostname&quot;`
URL string `json:&quot;url&quot;`
Header map[string][]string `json:&quot;header&quot;`
RemoteAddr string `json:&quot;remote_addr&quot;`
Host string `json:&quot;host&quot;`
RequestURI string `json:&quot;request_uri&quot;`
Proto string `json:&quot;proto&quot;`
ProtoMajor int `json:&quot;proto_major&quot;`
ProtoMinor int `json:&quot;proto_minor&quot;`
}
</code></pre>
<p>
<p class="ts-text">
You can also use the
<span class="ts-text is-code">
GetRequest()
</span>
function to get the
<span class="ts-text is-code">
*http.Request
</span>
object or
<span class="ts-text is-code">
GetRequestUUID()
</span>
to get a
<span class="ts-text is-code">
string
</span>
value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
</p>
</p>
<p>
<p class="ts-text">
<span class="ts-text is-heavy">
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.
</span>
</p>
</p>
<p>
<p class="ts-text">
In the sniffing stage, you can choose to either return
<span class="ts-text is-code">
ControlStatusCode_CAPTURED
</span>
, where Zoraxy will forward the request to your plugin
<span class="ts-text is-code">
DynamicCaptureIngress
</span>
endpoint, or
<span class="ts-text is-code">
ControlStatusCode_UNHANDLED
</span>
, 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.
</p>
</p>
<h3 id="capture-handling">
Capture Handling
</h3>
<p>
<p class="ts-text">
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
<span class="ts-text is-code">
http.Request
</span>
by writing to the
<span class="ts-text is-code">
http.ResponseWriter
</span>
.
</p>
</p>
<p>
<p class="ts-text">
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
<span class="ts-text is-code">
X-Zoraxy-Requestid
</span>
header in the HTTP request. This is the same UUID as the one you get from
<span class="ts-text is-code">
dsfr.GetRequestUUID()
</span>
in the sniffing stage if they are the same request object on Zoraxy side.
</p>
</p>
<p>
<p class="ts-text">
The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
</p>
</p>
<pre><code class="language-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
</code></pre>
<p>
<p class="ts-text">
You can extract the
<span class="ts-text is-code">
X-Zoraxy-Requestid
</span>
value from the request header and do your matching for implementing your function if needed.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="4-render-debug-ui">
4. Render Debug UI
</h2>
<p>
<p class="ts-text">
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.
</p>
</p>
<pre><code class="language-go">func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
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, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="5-full-code">
5. Full Code
</h2>
<p>
<p class="ts-text">
Here is the complete code for the dynamic capture example:
</p>
</p>
<pre><code class="language-go">package main
import (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;sort&quot;
&quot;strconv&quot;
&quot;strings&quot;
plugin &quot;example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin&quot;
)
const (
PLUGIN_ID = &quot;org.aroz.zoraxy.dynamic-capture-example&quot;
UI_PATH = &quot;/debug&quot;
STATIC_CAPTURE_INGRESS = &quot;/s_capture&quot;
)
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(&amp;plugin.IntroSpect{
ID: &quot;org.aroz.zoraxy.dynamic-capture-example&quot;,
Name: &quot;Dynamic Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: &quot;/d_sniff&quot;,
DynamicCaptureIngress: &quot;/d_capture&quot;,
UIPath: UI_PATH,
/*
SubscriptionPath: &quot;/subept&quot;,
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(&quot;/d_sniff&quot;, 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, &quot;/foobar&quot;) {
reqUUID := dsfr.GetRequestUUID()
fmt.Println(&quot;Accepting request with UUID: &quot; + reqUUID)
// Print all the values of the request
fmt.Println(&quot;Method:&quot;, dsfr.Method)
fmt.Println(&quot;Hostname:&quot;, dsfr.Hostname)
fmt.Println(&quot;URL:&quot;, dsfr.URL)
fmt.Println(&quot;Header:&quot;)
for key, values := range dsfr.Header {
for _, value := range values {
fmt.Printf(&quot; %s: %s\n&quot;, key, value)
}
}
fmt.Println(&quot;RemoteAddr:&quot;, dsfr.RemoteAddr)
fmt.Println(&quot;Host:&quot;, dsfr.Host)
fmt.Println(&quot;RequestURI:&quot;, dsfr.RequestURI)
fmt.Println(&quot;Proto:&quot;, dsfr.Proto)
fmt.Println(&quot;ProtoMajor:&quot;, dsfr.ProtoMajor)
fmt.Println(&quot;ProtoMinor:&quot;, 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(&quot;Skipping request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle(&quot;/d_capture&quot;, 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(&quot;Welcome to the dynamic capture handler!&quot;))
// Print all the request info to the response writer
w.Write([]byte(&quot;\n\nRequest Info:\n&quot;))
w.Write([]byte(&quot;Request URI: &quot; + r.RequestURI + &quot;\n&quot;))
w.Write([]byte(&quot;Request Method: &quot; + r.Method + &quot;\n&quot;))
w.Write([]byte(&quot;Request Headers:\n&quot;))
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(&quot;%s: %s\n&quot;, key, value)))
}
}
})
http.HandleFunc(UI_PATH+&quot;/&quot;, RenderDebugUI)
fmt.Println(&quot;Dynamic capture example started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
http.ListenAndServe(&quot;127.0.0.1:&quot;+strconv.Itoa(runtimeCfg.Port), nil)
}
// Render the debug UI
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
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, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="6-expected-output">
6. Expected Output
</h2>
<p>
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 &ldquo;debug&rdquo; tag and assigning it to a localhost loopback HTTP proxy rule.
</p>
<p>
<p class="ts-text">
When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
</p>
</p>
<p>
<p class="ts-text">
If everything is correctly setup, you should see the following page when requesting any URL with prefix
<span class="ts-text is-code">
(your_HTTP_proxy_rule_hostname)/foobar
</span>
</p>
</p>
<p>
<div class="ts-image is-rounded" style="max-width: 800px">
<img src="img/4. Dynamic Capture Example/image-20250530205430254.png" alt="image-20250530205430254" />
</div>
</p>
<p>
<p class="ts-text">
Example terminal output for requesting
<span class="ts-text is-code">
/foobar/*
</span>
:
</p>
</p>
<pre><span class="ts-text is-code">[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: &quot;Chromium&quot;;v=&quot;136&quot;, &quot;Microsoft Edge&quot;;v=&quot;136&quot;, &quot;Not.A/Brand&quot;;v=&quot;99&quot;
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: &quot;Windows&quot;
[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
</span></pre>
<div class="ts-divider has-top-spaced-large"></div>
<p>
<p class="ts-text">
Now you know how to develop a plugin in Zoraxy that handles special routings!
</p>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
<script>
hljs.highlightAll();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -123,6 +123,9 @@
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
Plugin UI
</a>
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
Compile a Plugin
</a>
</div>
<a class="item">
Basic Examples
@ -138,6 +141,9 @@
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
Static Capture Example
</a>
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
Dynamic Capture Example
</a>
</div>
<a class="item is-active" href="/html/index.html">
index

View File

@ -64,6 +64,11 @@
"filename": "2. Architecture/5. Plugin UI.md",
"title": "Plugin UI",
"type": "file"
},
{
"filename": "2. Architecture/6. Compile a Plugin.md",
"title": "Compile a Plugin",
"type": "file"
}
]
},
@ -86,6 +91,11 @@
"filename": "3. Basic Examples/3. Static Capture Example.md",
"title": "Static Capture Example",
"type": "file"
},
{
"filename": "3. Basic Examples/4. Dynamic Capture Example.md",
"title": "Dynamic Capture Example",
"type": "file"
}
]
},