Last Update: 25/05/2025
Let start with a really simple Hello World plugin. This only function of this plugin is to print “Hello World” in the plugin web UI.
First things first, give your plugin a name. In this example, we are using the name “helloworld”.
Plugin name cannot contain space or special characters , so you must use a file name that satisfy the requirement. No worry, the plugin file name is not the same as the plugin display name in the introspect.
If your zoraxy root folder do not contains a folder named “plugins”, it might implies that your Zoraxy is freshly clone from Github. You will need to build and run it once to start working on your plugin , so if you have a newly cloned source code of Zoraxy, do the followings.
git clone https://github.com/tobychui/zoraxy
cd src
go mod tidy
go build
sudo ./zoraxy
Afterward, create a plugin folder under your Zoraxy development environment that is exactly matching your plugin name. In the above example, the folder name should be “helloworld”.
# Assume you are already inside the src/ folder
mkdir helloworld
cd ./helloworld
Similar to any Go project, you can start by creating a main.go file. Next, you would want to let the go compiler knows your plugin name so when generating a binary file, it knows what to name it. This can be done via using the go mod init command.
touch main.go
go mod init example.com/zoraxy/helloworld
ls
# After you are done, you should see the followings
# go.mod main.go
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the zoraxy_plugin Go module under src/mod/plugins/zoraxy_plugin
Copy the zoraxy_plugin folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as mod , then your copied library path should be plugins/helloword/mod/zoraxy_plugin
mkdir ./mod
cp -r "mod/plugins/zoraxy_plugin" ./mod/
ls ./mod/zoraxy_plugin/
# You should see something like this (might be different in future versions)
# dev_webserver.go dynamic_router.go embed_webserver.go README.txt static_router.go zoraxy_plugin.go
Lets create a www folder and put all our web resources, we need to create an index.html file as our plugin web ui homepage. This can be done by creating a HTML file in the www folder.
# Assuming you are currently in the src/plugins/helloworld/ folder
mkdir www
cd www
touch index.html
And here is an example index.html file that uses the Zoraxy internal resources like css and dark theme toggle mechanism. That csrf token template is not straightly needed in this example as helloworld plugin do not make any POST request to Zoraxy webmin interface, but it might come in handy later.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- CSRF token, if your plugin need to make POST request to backend -->
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/semantic/semantic.min.js"></script>
<script src="/script/utils.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/main.css">
<title>Hello World</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background:none;
}
</style>
</head>
<body>
<!-- Dark theme script must be included after body tag-->
<link rel="stylesheet" href="/darktheme.css">
<script src="/script/darktheme.js"></script>
<div style="text-align: center;">
<h1>Hello World</h1>
<p>Welcome to your first Zoraxy plugin</p>
</div>
</body>
</html>
To create a handler for introspect, you can first start your plugin with a few constants.
After you have defined these constant, we can use plugin.ServeAndRecvSpec function to handle the handshake between Zoraxy and your plugin.
const (
PLUGIN_ID = "com.example.helloworld"
UI_PATH = "/"
WEB_ROOT = "/www"
)
func main(){
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: "com.example.helloworld",
Name: "Hello World Plugin",
Author: "foobar",
AuthorContact: "admin@example.com",
Description: "A simple hello world plugin",
URL: "https://example.com",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
// As this is a utility plugin, we don't need to capture any traffic
// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
UIPath: UI_PATH,
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
}
Notes: If some post processing is needed between Introspect and Configure, you can use two seperate function to handle the first start and the second starting of your plugin. The “seperated version” of ServeAndRecvSpec is defined as ServeIntroSpect(pluginSpect *IntroSpect) and RecvConfigureSpec() (*ConfigureSpec, error) . See zoraxy_plugin.go for more information.
After that, we need to create a web server to serve our plugin UI to Zoraxy via HTTP. This can be done via the http.FileServer but for simplicity and ease of upgrade, the Zoraxy plugin library provided an easy to use embedded web FS server API for plugin developers.
To use the Zoraxy plugin embedded web server, you first need to embed your web fs into Zoraxy as such.
import (
_ "embed"
"fmt"
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
)
//go:embed www/*
var content embed.FS
Then call to the NewPluginEmbedUIRouter to create a new UI router from the embedded Fs.
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
// The router will also help to handle the termination of the plugin when
// a user wants to stop the plugin via Zoraxy Web UI
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
Here is the tricky part. since not all platform support cross process signaling, Zoraxy plugin uses HTTP request to request a plugin to shutdown. The embedWebRouter object has a function named RegisterTerminateHandler where you can easily use this function to register actions that needed to be done before shutdown.
embedWebRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
fmt.Println("Hello World Plugin Exited")
}, nil)
Notes: This is a blocking function. That is why Zoraxy has a build-in timeout context where if the terminate request takes more than 3 seconds, the plugin process will be treated as “freezed” and forcefully terminated. So please make sure the terminate handler complete its shutdown procedures within 3 seconds.
After you have created a embedded web router, you can register it to the UI PATH as follows.
// Serve the hello world page in the www folder
http.Handle(UI_PATH, embedWebRouter.Handler())
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
if err != nil {
panic(err)
}
As this is just the standard golang net/http package, you can of course add more Function Handlers to it based on your needs. There are something that you need to know about adding API endpoints, we will discuss this in the later sections.
After saving the main.go file, you can now build your plugin with go build . It should generate the plugin in your platform architecture and OS. If you are on Linux, it will be helloworld and if you are on Windows, it will be helloworld.exe .
After you are done, restart Zoraxy and enable your plugin in the Plugin List. Now you can test and debug your plugin with your HTTP Proxy Rules. All the STDOUT and STDERR of your plugin will be forwarded to the STDOUT of Zoraxy as well as the log file.
Tips
You can also enable the Developer Option - Plugin Auto Reload function if you are too lazy to restart Zoraxy everytime the plugin binary changed.
This is the full code of the helloworld plugin main.go file.
package main
import (
"embed"
_ "embed"
"fmt"
"net/http"
"strconv"
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "com.example.helloworld"
UI_PATH = "/"
WEB_ROOT = "/www"
)
//go:embed www/*
var content embed.FS
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: "com.example.helloworld",
Name: "Hello World Plugin",
Author: "foobar",
AuthorContact: "admin@example.com",
Description: "A simple hello world plugin",
URL: "https://example.com",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
// As this is a utility plugin, we don't need to capture any traffic
// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
UIPath: UI_PATH,
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
// The router will also help to handle the termination of the plugin when
// a user wants to stop the plugin via Zoraxy Web UI
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
fmt.Println("Hello World Plugin Exited")
}, nil)
// Serve the hello world page in the www folder
http.Handle(UI_PATH, embedWebRouter.Handler())
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
if err != nil {
panic(err)
}
}