Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
475650de0d | ||
![]() |
b19867865c | ||
![]() |
df636c9f76 | ||
![]() |
e6b2cf09d7 | ||
![]() |
bf0df928c7 | ||
![]() |
eec6cec0db | ||
![]() |
0215171646 | ||
![]() |
cd822ed904 | ||
![]() |
a1df5a1060 | ||
![]() |
54c130165a | ||
![]() |
ddb1a8773e | ||
![]() |
29daa4402d | ||
![]() |
a85bf82c3e | ||
![]() |
c56e317bfd | ||
![]() |
b4c2f6bf13 | ||
![]() |
9d2b8f224c | ||
![]() |
877692695e | ||
![]() |
b9c609e413 | ||
![]() |
7426cc2bb1 | ||
![]() |
bd71335f47 | ||
![]() |
8bcc8e7095 | ||
![]() |
b1824a66a3 | ||
![]() |
cc6501db12 | ||
![]() |
70d95bd4e4 | ||
![]() |
b590e15ef2 | ||
![]() |
b25f8aab3e | ||
![]() |
c0578a33b6 | ||
![]() |
55a525106a | ||
![]() |
e3b68b9aad | ||
![]() |
3f1c50c009 | ||
![]() |
8f046a0b47 | ||
![]() |
a98d86a303 | ||
![]() |
73e6530862 | ||
![]() |
0c753ae531 | ||
![]() |
6353cc532a | ||
![]() |
e049761f36 | ||
![]() |
4dc7175588 |
3
.github/workflows/docker.yml
vendored
@ -31,8 +31,7 @@ jobs:
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
cp -lr $GITHUB_WORKSPACE/example/ $GITHUB_WORKSPACE/docker/
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
|
1
.gitignore
vendored
@ -50,3 +50,4 @@ src/log/
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
example/plugins/ztnc/ztnc.db.lock
|
||||
*.exe
|
36
README.md
@ -13,22 +13,24 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
- Basic Auth
|
||||
- Alias Hostnames
|
||||
- Custom Headers
|
||||
- Load Balancing
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (and SAN certs)
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- Stream Proxy (TCP & UDP)
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Plugin System
|
||||
- Utilities
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- Wake-On-Lan
|
||||
- Debug Forward Proxy
|
||||
- IP Scanner
|
||||
- Port Scanner
|
||||
- Others
|
||||
- Basic single-admin management mode
|
||||
- External permission management system for easy system integration
|
||||
@ -107,6 +109,8 @@ Usage of zoraxy:
|
||||
If web server is enabled by default (default true)
|
||||
-default_inbound_port int
|
||||
Default web server listening port (default 443)
|
||||
-dev
|
||||
Use external web folder for UI development
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-earlyrenew int
|
||||
@ -164,19 +168,7 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
|
||||
|
||||
## Global Area Network Controller
|
||||
|
||||
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
||||
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
|
||||
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
```
|
||||
|
||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||
|
||||
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
Moved to official plugin repo, see [ztnc](https://github.com/aroz-online/zoraxy-official-plugins/tree/main/src/ztnc) plugin
|
||||
|
||||
## Web SSH
|
||||
|
||||
@ -199,10 +191,20 @@ Loopback web SSH connections, by default, are disabled. This means that if you a
|
||||
|
||||
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
|
||||
|
||||
- Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
- Forward Auth [@james-d-elliott](https://github.com/james-d-elliott)
|
||||
|
||||
- (Legacy) Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
|
||||
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
|
||||
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
### Looking for Maintainer
|
||||
|
||||
- ACME DNS Challenge Module
|
||||
- Logging (including analysis & attack prevention) Module
|
||||
|
||||
Thank you so much for your contributions!
|
||||
|
||||
## Sponsor This Project
|
||||
@ -210,7 +212,7 @@ Thank you so much for your contributions!
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
- [PassiveLemon (Docker compatibility maintainer)](https://github.com/PassiveLemon)
|
||||
|
||||
## License
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
## Build Zoraxy
|
||||
FROM docker.io/golang:bookworm AS build-zoraxy
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
@ -15,39 +15,54 @@ RUN go mod tidy &&\
|
||||
|
||||
|
||||
## Build ZeroTier
|
||||
FROM docker.io/golang:bookworm AS build-zerotier
|
||||
FROM docker.io/rust:1.79-alpine AS build-zerotier
|
||||
|
||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
||||
RUN apk add --update --no-cache curl make gcc g++ linux-headers openssl-dev nano
|
||||
|
||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||
cd ZeroTierOne-* &&\
|
||||
make &&\
|
||||
cd ZeroTierOne-*/zeroidc &&\
|
||||
cargo update -p getrandom &&\
|
||||
cd .. &&\
|
||||
make -f make-linux.mk &&\
|
||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
|
||||
FROM docker.io/golang:bookworm
|
||||
## Fetch plugin
|
||||
FROM docker.io/golang:alpine AS fetch-plugin
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
RUN apk add --update --no-cache git
|
||||
|
||||
WORKDIR /opt/zoraxy/
|
||||
|
||||
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
|
||||
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
|
||||
## Main
|
||||
FROM docker.io/golang:alpine
|
||||
|
||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
||||
COPY --chmod=700 ./example/plugins/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/plugin/
|
||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
echo "tun" | tee -a /etc/modules
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
@ -72,6 +87,8 @@ ENV WEBROOT="./www"
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||
|
||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
||||
|
@ -19,6 +19,7 @@ Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Cha
|
||||
docker run -d \
|
||||
--name zoraxy \
|
||||
--restart unless-stopped \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
@ -47,6 +48,8 @@ services:
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
FASTGEOIP: "true"
|
||||
```
|
||||
@ -68,6 +71,11 @@ services:
|
||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||
|
||||
### Extra Hosts
|
||||
| Host | Details |
|
||||
|:-|:-|
|
||||
| `host.docker.internal:host-gateway` | Resolves host.docker.internal to the host’s gateway IP on the Docker bridge network, allowing containers to access services running on the host machine. |
|
||||
|
||||
### Environment
|
||||
|
||||
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
|
||||
@ -95,6 +103,20 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|
||||
> [!IMPORTANT]
|
||||
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
||||
|
||||
### ZeroTier
|
||||
|
||||
If you are running with ZeroTier, make sure to add the following flags to ensure ZeroTier functionality:
|
||||
|
||||
`--cap_add NET_ADMIN` and `--device /dev/net/tun:/dev/net/tun`
|
||||
|
||||
Or for Docker Compose:
|
||||
```
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
```
|
||||
|
||||
### Plugins
|
||||
|
||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods..."
|
||||
for dir in "$1"/*; do
|
||||
|
@ -12,5 +12,7 @@ services:
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
FASTGEOIP: "true"
|
||||
|
@ -457,7 +457,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<i class="divider"> </i>
|
||||
<a class="section externallink" href="" target="_blank">
|
||||
<a class="section externallink" href="https://zoraxy.aroz.org/plugins/html/" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="green code icon"></i>
|
||||
<div class="content" i18n>
|
||||
|
BIN
docs/plugins/assets/banner.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
docs/plugins/assets/banner.psd
Normal file
BIN
docs/plugins/assets/logo.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/plugins/assets/logo_white.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
51
docs/plugins/assets/theme.js
Normal file
@ -0,0 +1,51 @@
|
||||
/* Things to do before body loads */
|
||||
function restoreDarkMode(){
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
$("html").addClass("is-dark");
|
||||
$("html").removeClass("is-white");
|
||||
} else {
|
||||
$("html").removeClass("is-dark");
|
||||
$("html").addClass("is-white");
|
||||
}
|
||||
}
|
||||
restoreDarkMode();
|
||||
|
||||
function updateElementToTheme(isDarkTheme=false){
|
||||
if (!isDarkTheme){
|
||||
let whiteSrc = $("#sysicon").attr("white_src");
|
||||
$("#sysicon").attr("src", whiteSrc);
|
||||
$("#darkModeToggle").html(`<span class="ts-icon is-sun-icon"></span>`);
|
||||
|
||||
// Update the rendering text color in the garphs
|
||||
if (typeof(changeScaleTextColor) != "undefined"){
|
||||
changeScaleTextColor("black");
|
||||
}
|
||||
|
||||
}else{
|
||||
let darkSrc = $("#sysicon").attr("dark_src");
|
||||
$("#sysicon").attr("src", darkSrc);
|
||||
$("#darkModeToggle").html(`<span class="ts-icon is-moon-icon"></span>`);
|
||||
|
||||
// Update the rendering text color in the garphs
|
||||
if (typeof(changeScaleTextColor) != "undefined"){
|
||||
changeScaleTextColor("white");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Things to do after body loads */
|
||||
$(document).ready(function(){
|
||||
$("#darkModeToggle").on("click", function() {
|
||||
$("html").toggleClass("is-dark");
|
||||
$("html").toggleClass("is-white");
|
||||
if ($("html").hasClass("is-dark")) {
|
||||
localStorage.setItem("darkMode", "enabled");
|
||||
updateElementToTheme(true);
|
||||
} else {
|
||||
localStorage.setItem("darkMode", "disabled");
|
||||
updateElementToTheme(false);
|
||||
}
|
||||
});
|
||||
|
||||
updateElementToTheme(localStorage.getItem("darkMode") === "enabled");
|
||||
});
|
280
docs/plugins/build.go
Normal file
@ -0,0 +1,280 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/yosssi/gohtml"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
Filename string `json:"filename"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func build() {
|
||||
rootDir := "./docs"
|
||||
outputFile := "./index.json"
|
||||
|
||||
type Folder struct {
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Files []interface{} `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
var buildTree func(path string, d fs.DirEntry) interface{}
|
||||
buildTree = func(path string, d fs.DirEntry) interface{} {
|
||||
relativePath, _ := filepath.Rel(rootDir, path)
|
||||
relativePath = filepath.ToSlash(relativePath)
|
||||
var title string
|
||||
if d.IsDir() {
|
||||
title = filepath.Base(relativePath)
|
||||
} else {
|
||||
title = strings.TrimSuffix(filepath.Base(relativePath), filepath.Ext(relativePath))
|
||||
}
|
||||
|
||||
//Strip the leader numbers from the title, e.g. 1. Introduction -> Introduction
|
||||
if strings.Contains(title, ".") {
|
||||
parts := strings.SplitN(title, ".", 2)
|
||||
if len(parts) > 1 {
|
||||
title = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
// Remove leading numbers and dots
|
||||
title = strings.TrimLeft(title, "0123456789. ")
|
||||
// Remove leading spaces
|
||||
title = strings.TrimLeft(title, " ")
|
||||
|
||||
if d.IsDir() {
|
||||
folder := Folder{
|
||||
Title: title,
|
||||
Path: relativePath,
|
||||
Type: "folder",
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == "img" || entry.Name() == "assets" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/img/") || strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/assets/") {
|
||||
continue
|
||||
}
|
||||
child := buildTree(filepath.Join(path, entry.Name()), entry)
|
||||
if child != nil {
|
||||
folder.Files = append(folder.Files, child)
|
||||
}
|
||||
}
|
||||
return folder
|
||||
} else {
|
||||
ext := filepath.Ext(relativePath)
|
||||
if ext != ".md" && ext != ".html" && ext != ".txt" {
|
||||
return nil
|
||||
}
|
||||
return FileInfo{
|
||||
Filename: relativePath,
|
||||
Title: title,
|
||||
Type: "file",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootInfo, err := os.Stat(rootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rootFolder := buildTree(rootDir, fs.FileInfoToDirEntry(rootInfo))
|
||||
jsonData, err := json.MarshalIndent(rootFolder, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
/* For debug purposes, print the JSON structure */
|
||||
err = os.WriteFile(outputFile, jsonData, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
/* For each file in the folder structure, convert markdown to HTML */
|
||||
htmlOutputDir := "./html"
|
||||
os.RemoveAll(htmlOutputDir) // Clear previous HTML output
|
||||
err = os.MkdirAll(htmlOutputDir, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var processFiles func(interface{})
|
||||
processFiles = func(node interface{}) {
|
||||
switch n := node.(type) {
|
||||
case FileInfo:
|
||||
if filepath.Ext(n.Filename) == ".md" {
|
||||
inputPath := filepath.Join(rootDir, n.Filename)
|
||||
outputPath := filepath.Join(htmlOutputDir, strings.TrimSuffix(n.Filename, ".md")+".html")
|
||||
|
||||
// Ensure the output directory exists
|
||||
err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Read the markdown file
|
||||
mdContent, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Convert markdown to HTML
|
||||
docContent := mdToHTML(mdContent)
|
||||
docContent, err = optimizeCss(docContent)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Load the HTML template
|
||||
templateBytes, err := os.ReadFile("template/documents.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Generate the side menu HTML
|
||||
sideMenuHTML, err := generateSideMenu(string(jsonData), n.Title)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateBody := string(templateBytes)
|
||||
// Replace placeholders in the template
|
||||
htmlContent := strings.ReplaceAll(templateBody, "{{title}}", n.Title+" | Zoraxy Documentation")
|
||||
htmlContent = strings.ReplaceAll(htmlContent, "{{content}}", string(docContent))
|
||||
htmlContent = strings.ReplaceAll(htmlContent, "{{sideMenu}}", sideMenuHTML)
|
||||
htmlContent = strings.ReplaceAll(htmlContent, "{{root_url}}", *root_url)
|
||||
//Add more if needed
|
||||
|
||||
//Beautify the HTML content
|
||||
htmlContent = gohtml.Format(htmlContent)
|
||||
|
||||
// Write the HTML file
|
||||
err = os.WriteFile(outputPath, []byte(htmlContent), 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Check if the .md file directory have an ./img or ./assets folder. If yes, copy the contents to the output directory
|
||||
imgDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "img")
|
||||
assetsDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "assets")
|
||||
if _, err := os.Stat(imgDir); !os.IsNotExist(err) {
|
||||
err = filepath.Walk(imgDir, func(srcPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(imgDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(filepath.Dir(outputPath), "img", relPath)
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destPath, os.ModePerm)
|
||||
}
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destPath, data, 0644)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(assetsDir); !os.IsNotExist(err) {
|
||||
err = filepath.Walk(assetsDir, func(srcPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(assetsDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(filepath.Dir(outputPath), "assets", relPath)
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destPath, os.ModePerm)
|
||||
}
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destPath, data, 0644)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Generated HTML:", outputPath)
|
||||
}
|
||||
case Folder:
|
||||
for _, child := range n.Files {
|
||||
processFiles(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles(rootFolder)
|
||||
copyOtherRes()
|
||||
}
|
||||
|
||||
func copyOtherRes() {
|
||||
srcDir := "./assets"
|
||||
destDir := "./html/assets"
|
||||
|
||||
err := filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(destDir, relPath)
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destPath, os.ModePerm)
|
||||
}
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destPath, data, 0644)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func mdToHTML(md []byte) []byte {
|
||||
// create markdown parser with extensions
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
p := parser.NewWithExtensions(extensions)
|
||||
doc := p.Parse(md)
|
||||
|
||||
// create HTML renderer with extensions
|
||||
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||
opts := html.RendererOptions{
|
||||
Flags: htmlFlags,
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
return markdown.Render(doc, renderer)
|
||||
}
|
4
docs/plugins/build.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the Go program with the specified arguments
|
||||
./docs.exe -m=build -root=plugins/html/
|
130
docs/plugins/cssOptimizer.go
Normal file
@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func optimizeCss(htmlContent []byte) ([]byte, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(htmlContent)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originalHTMLContent := string(htmlContent)
|
||||
// Replace img elements
|
||||
|
||||
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||
//For each of the image element, replace the parent from p to div
|
||||
originalParent, err := s.Parent().Html()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting parent HTML:", err)
|
||||
return
|
||||
}
|
||||
|
||||
src, exists := s.Attr("src")
|
||||
if !exists {
|
||||
fmt.Println("No src attribute found for img element")
|
||||
return
|
||||
}
|
||||
encodedSrc := (&url.URL{Path: src}).String()
|
||||
|
||||
//Patch the bug in the parser that converts " />" to "/>"
|
||||
originalParent = strings.ReplaceAll(originalParent, "/>", " />")
|
||||
fmt.Println("<div class=\"ts-image is-rounded\"><img src=\"./" + encodedSrc + "\"></div>")
|
||||
//Replace the img with ts-image
|
||||
originalHTMLContent = strings.Replace(originalHTMLContent, originalParent, "<div class=\"ts-image is-rounded\" style=\"max-width: 800px\">"+originalParent+"</div>", 1)
|
||||
})
|
||||
|
||||
// Add "ts-text" class to each p element
|
||||
doc.Find("p").Each(func(i int, s *goquery.Selection) {
|
||||
class, exists := s.Attr("class")
|
||||
var newClass string
|
||||
if exists {
|
||||
newClass = fmt.Sprintf("%s ts-text", class)
|
||||
} else {
|
||||
newClass = "ts-text"
|
||||
}
|
||||
|
||||
originalParagraph, _ := s.Html()
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, originalParagraph, fmt.Sprintf("<p class=\"%s\">%s</p>", newClass, originalParagraph))
|
||||
})
|
||||
|
||||
//Replace hr with ts-divider
|
||||
// Replace hr elements outside of code blocks
|
||||
doc.Find("hr").Each(func(i int, s *goquery.Selection) {
|
||||
parent := s.Parent()
|
||||
if parent.Is("code") {
|
||||
// Skip <hr> inside <code> blocks
|
||||
return
|
||||
}
|
||||
|
||||
// Replace <hr> with <div class="ts-divider"></div>
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<hr>", "<div class=\"ts-divider has-top-spaced-large\"></div>")
|
||||
})
|
||||
|
||||
// Add ts-table to all table elements
|
||||
doc.Find("table").Each(func(i int, s *goquery.Selection) {
|
||||
class, exists := s.Attr("class")
|
||||
var newClass string
|
||||
if exists {
|
||||
newClass = fmt.Sprintf("%s ts-table", class)
|
||||
} else {
|
||||
newClass = "ts-table"
|
||||
}
|
||||
|
||||
originalTable, _ := s.Html()
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, originalTable, fmt.Sprintf("<table class=\"%s\">%s</table>", newClass, originalTable))
|
||||
})
|
||||
|
||||
// Replace <ul> <ol> and <li>
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<ul>", "<div class=\"ts-list is-unordered\">")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</ul>", "</div>")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<ol>", "<div class=\"ts-list is-ordered\">")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</ol>", "</div>")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<li>", "<div class=\"item\">")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</li>", "</div>")
|
||||
|
||||
// Replace <strong> with <span class="ts-text is-heavy"></span>
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<strong>", "<span class=\"ts-text is-heavy\">")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</strong>", "</span>")
|
||||
|
||||
// Replace <code> without class with <span class="ts-text is-code">
|
||||
for {
|
||||
startIndex := strings.Index(originalHTMLContent, "<code>")
|
||||
if startIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
endIndex := strings.Index(originalHTMLContent[startIndex+6:], "</code>")
|
||||
if endIndex == -1 {
|
||||
break
|
||||
}
|
||||
endIndex += startIndex + 6
|
||||
|
||||
codeSegment := originalHTMLContent[startIndex : endIndex+7] // Include </code>
|
||||
if !strings.Contains(codeSegment, "class=") {
|
||||
replacement := strings.Replace(codeSegment, "<code>", "<span class=\"ts-text is-code\">", 1)
|
||||
replacement = strings.Replace(replacement, "</code>", "</span>", 1)
|
||||
originalHTMLContent = strings.Replace(originalHTMLContent, codeSegment, replacement, 1)
|
||||
} else {
|
||||
// Skip if <code> already has a class
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Replace blockquote to <div class="ts-quote">
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<blockquote>", "<div class=\"ts-quote\">")
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</blockquote>", "</div>")
|
||||
|
||||
/*
|
||||
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "language-xml", "")
|
||||
// Remove class attribute from <code> inside <pre>
|
||||
re := regexp.MustCompile(`<pre><code class="[^"]*">`)
|
||||
originalHTMLContent = re.ReplaceAllString(originalHTMLContent, "<pre><code>")
|
||||
*/
|
||||
return []byte(originalHTMLContent), err
|
||||
}
|
12
docs/plugins/dev.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#/bin/bash
|
||||
go build
|
||||
# Run the Go program with the specified arguments
|
||||
./docs.exe -m=build
|
||||
|
||||
echo "Running docs in development mode..."
|
||||
./docs.exe
|
||||
|
||||
# After the docs web server mode terminate, rebuild it with root = plugins/html/
|
||||
./docs.exe -m=build -root=plugins/html/
|
||||
|
||||
# The doc should always be ready to push to release branch
|
1
docs/plugins/diagrams/dynamic_capture.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2025-05-26T13:15:22.444Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.4.2 Chrome/78.0.3904.130 Electron/7.1.4 Safari/537.36" etag="PlPASUA5DkVflfK71JS_" version="12.4.2" type="device" pages="1"><diagram id="fpBU8wsgIdeXEMTqvbYu" name="Page-1">5VrRcps4FP0az2Qf1gMIAX5MnCbNbHcn00xn277syCBjpRhRIcdOvn4lkG2Q1CZxMSGpHzxwBZI49xxd3QsjMF1uLhkqFn/TBGcjz0k2I3A+8rzQn4h/abivDRAGtSFlJKlN7t5wQx6wMjrKuiIJLlsXckozToq2MaZ5jmPesiHG6Lp92Zxm7VELlGLDcBOjzLT+SxK+qK2RF+7t7zFJF9uR3UA98BJtL1ZPUi5QQtcNE3g3AlNGKa+PlpspziR2W1zq+y5+0LqbGMM5f8oNX+6Xk9ldzsLJZXobx5+vspvwT9XLHcpW6oG/UoY292rK/H6LA6OrPMGyK2cEztYLwvFNgWLZuhaOF7YFX2bizBWH5tS242DG8aZhUlO9xHSJORPDOtvWiYJN8caN1Pl67wV3C+2i4QFf2ZByfLrreo+NOFDwPAMqz4DqOlulJH9xqHxvaFABAyoDJJwnp1Ke4izOUFmSuI0L3hD+WUI4hursS6PlfKPQrU7u1UmCykWF+0+BLemKxfhxTeCktTKY8DfghRZ0tzaGM8TJXXs9sUGuRrimRMx4510Qtr3ru5rX6udRdzX1r3W0m5DqKPC0jjhiKeZGRxUDdo99OCn8V02KYFCk8MPJGEbO/ue3XCtCwbjR6LiHMUaM8qKMcYPHKdP3Yuv5sA01sCy2FseD4FiLrRv9urB+CMxA+O4Fjh3151Na6wj2S+kBMtoPoR2SRxh9PEJPhhwpaoK8rkgRTva/txgotsM1GHOVc0bLQqaEOneELnibLSgjaS6pJJyNmTBI9RCRBJ6qhiVJEnn7GcMleUCzqivJmUI+UvWQ8GwEzyVxRHcrTss6k5W9M8qFB6nsJ+hGskDb8e9yzAanQgundLd0lxuZydGbi0F66ADOoRtxLZgBfUd/bLWY6dlNTuZzkqcS7TwpdiMPRDYioexGNy5o6yYKLKGuV+E8ISvqf0PQRgn4lg0BtIB0vNKLuW3C43Q8km65KCV3X5ytnazq7Y3Ybl1o4B70Ss7QgH2aEWxZG8oFKuThapmdxpw20f2AZji7FlApLc8o53QpLshkwxmKv6UVwac0k/eJ3sC8+lk8xKnGdrriGcnxdFf8dTqSgFZzsSR5oM8tsfcb5HhawdfXV5Mnh1fdeT2neJ6ZvnwqSs4wWr740h44bWwGUCo2t+7PZvbxkr2BSCOAPSRxPx+k75QOmG+mRD9EqGUzrlz/BgK+C7SV6qXTOPCEFze9L1nQa7PdVnC17EaPV3AFv0Gy68H2XtTX0Ty04OpHPa8jZoJ1QdkasaQC6XsFH0rE+iA/LBhSRtHIf2FH7y6idmLn2yq9vWYYoINS79ClZChgcujGVqsbQb0AdewXuGZIPkFxjAs5x3ktqj+GJJvOykaOthiGPZaN/rq6/e/7x3+uPn0oNmw9+fhwPvvaxQcWc5JljZw7gThKfGEXmQr9hhstkTcDwb5wranKAu1QYhbs6uWGJjs9F+pOdlZfmwHs5EpEJ2f79ZGMYitcyhkvUJ5kJE8HpcJ98DIU9yTy7IKX5l1LdexowcvqGNi5COdRjOPYJsJZBGW0fg0iDMeaow4t5ATaB0vweFtHq3/NovMJ0YR3W71fHKbugm505080d9o2jTZCHaA7cbr/HrZ24/6jYvDufw==</diagram></mxfile>
|
BIN
docs/plugins/diagrams/dynamic_capture.png
Normal file
After Width: | Height: | Size: 190 KiB |
1
docs/plugins/diagrams/plugin_workflow.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2025-05-25T13:00:38.901Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.4.2 Chrome/78.0.3904.130 Electron/7.1.4 Safari/537.36" etag="lsRaVkYmhHZrtifUUFnd" version="12.4.2" type="device" pages="1"><diagram id="aAlAxoldjFgNxtltmMrS" name="Page-1">7Vpbc5s4FP41nuk+lEGI62PtdDfbzLaZOpnN7suOahSsXYy8Qm7s/PpKRoCRcEwTcNykfvCgIyHQdy76zhEjOFmsf2NoOf+DxjgdOXa8HsGzkeMEbiT+pWBTCDzPLwQJI3EhArVgSu6xEtpKuiIxzhsDOaUpJ8umcEazDM94Q4YYo3fNYbc0bT51iRJsCKYzlJrSP0nM54U0dIJafo5JMi+fDHy14AUqB6uV5HMU07sdEXw/ghNGKS+uFusJTiV2JS7Ffb/u6a1ejOGMd7nBTa5BHF3Azx/t65sPk4uLm9m/bx2lja8oXakVq7flmxICRldZjOUs9giO7+aE4+kSzWTvndC5kM35IhUtIC5vSZpOaEqZaGc0E4PGMcrn29tlv3ocZhyv9y4EVPAIs8J0gTnbiCGlTXnFHcqinEgBfFfrx3fVmPmObkI1DimTSKqZa9TEhQLue0AMfngQYdgRROANhqL3w6MIgq4ogqFQBAaIf1OG1punQdkDVKCMgyVW0MSqGrOLlTuYwRlQXaarhGRC9pmuOGbPjhmEp4YZNDC7XuacYbR4drC84NTAcg8HNJzF7yRLEa1ZivKczJq4tAQsHBuM5SBEOxB4LQiUMoZTxMnX5vRtsKgnXFIiHly7eKhpINCQzemKzbC6a5eqHJjIi7SJOGIJ5sZEWy1Vy3684jrsRC9KcTCMLN+L6l/Qjx4PzHtstXbgui9Krb4j4Pft6gf60erD0x5bqX1z7x52IqDTwhZyDVrUDgfLUKKnW/6pGzuImnuGYz86aMHmRDpXH9iggUnhn92iYahZdOR1smjPG8iggUne77d5zj/LLYe3fs84o9OlLAlJzuen4o3GXwSl9xN59WYqtCi7LhFD4pUw+8UAWcDFm0iilCSZdBABm0gP4FiCSmYofac6FiSO5e1jhnNyj75sp5IqWkpr2YLgjUfemdSKmG7FaV5UvOTsjHLhAFTOA/tRW6WTKuS3qK3NJ+FQgQiYCcSLi0RQY69GgtGZBushzT9yJDITGCLdKldu9ebD9NNH2U+Q+J9enX26vjopNxJEvp8NPdJKjm2pZdhiUc5gftR33XaATQJ63WiPsVP3V+Gxnx5txOLZ5kYak20BLyolf0lYLeBUgrO1ArpobXZbl5iR7SajhKcewUAYWk7UVCaMLOdxYQyGwArsffmCsBLLtr26W6MMA8e40iL2EokJzW5JsmJYkonXSiT0AAhBi2sHxyQSTg9E4tW6dlhXZLSY7drSG3d+fj8+b2uPAbYV7ji9toyhfT40bKc8JLGVOx/04FzwoP+wdrzUcuLU3dPbNujmFt4oPu3SnEj23tKMT9UrlynkU3ze1cgngFZL+hC1beiONVTe55iVjOrQ5qfq9qgOgvAEVAdNMlaft+H/VziX8eMcZXFKsuSnHqU2mhzMdVt2XXhkLb6GBD5yrZ1Di0grCLqO37q3fXcdXTvddWF41G0Qmun9+dXV5RY/5Y4vjsgaoTH0u8XFwQ7UzQ3txSvBDXQ22HFzGuygvs86QdUo8gjvQBbRNRg6dhEOHlhGURR6ttNGPZzpX4p1jYuufWCigeOiax7AVC6ZL2mW41PyyZ6Kd26wB/RG8e6oTtlnhj+QU57KEYOrnwx0/9JGC8WBLIUYtbpjed4DjOT0PO9tX5wEajrwK/o+gPOJZv3NeaG4+sN9+P4b</diagram></mxfile>
|
BIN
docs/plugins/diagrams/plugin_workflow.png
Normal file
After Width: | Height: | Size: 194 KiB |
1
docs/plugins/diagrams/static_capture.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2025-05-26T13:04:13.815Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.4.2 Chrome/78.0.3904.130 Electron/7.1.4 Safari/537.36" etag="gzmRxnqbWDJ6DOZzUBFr" version="12.4.2" type="device" pages="1"><diagram id="fpBU8wsgIdeXEMTqvbYu" name="Page-1">5VrRjqM2FP2aSNOHRoAxhMeZzO52pW010qjqbt884AFawMg4k2S/vjaYALa3k0kDpJk8RHABY849x9fHsADrfPeJojL5lUQ4WzhWtFuA+4XjBBbk/yKwbwKe7zSBmKZRE7K7wGP6HcugJaObNMLV4ERGSMbSchgMSVHgkA1iiFKyHZ72TLLhXUsUYy3wGKJMj/6RRixpoivH7+K/4DRO2jvbXtAcyVF7snySKkER2fZC4MMCrCkhrNnKd2ucCexaXJrrPv7g6KFjFBfsmAu+7fPg6aWgfvAp/isMv37OHv2fZSsvKNvIB/6TULTbyy6zfYsDJZsiwqIpawHutknK8GOJQnF0yxPPYwnLM75n803ZKKYM737YW/uAAecOJjlmlN/Wai8IJGySN/ZK7m+7LNgttEkvA66MIZn4+NB0hw3fkPC8ASpHg+oh28RpMTtUrnNpUAENKg0kXES3Qp58L8xQVaXhEBe8S9lXAeESyr1vvSP3O4luvbOXOxGqkhr3DlgcafJWYOW9Ihsa4tdEosPfgxca0G1jFGeIpS/Dbpggl3d4ICnv4CG7wB9m17WVrDXdl1f19a80dOhQOxI7SkMM0RgzraGaAYfHPp0U7nWRwpuTFK4fLOHK6n7uILW8FCx7By37NMbwu8zKGNt7nTJTD7aOC4dQA8Nga0g88MYabO3VfxfWsbqYi++OZ5lRfzullYbgtJS+QEa7PjRD8gqjxyN08L+qFA1jLrpS+EH3u8ZC0d6ux5jPBaOkKoUlVLnDpcKGbEFZGheCSjy3mPKAEFTKTeCtPJCnUSQuv6O4Sr+jp7opwZlSPFL9kPBuAe8FcXhzG0aqxsmK1ilhPINEtOOdR7JAmfEfPGaPU76BU2pazueNdHN0dTVILR3AOnUirhQzoM7ox1aLbs++pJXoIHmuU8ZRCvlGiEq2objOAUuqS9IRd5jnmc2tVkMhrTy99pk4NZ6SjrBJ088QwJCwrmGGAA0gjbcWo8+j8DJeLkRaPqIoN6zKTM3WEYb5w0Ax2zDva7CvsxQXeo2tElSKzU2e3YaM9NH9gp5w9sChklp+IoyRnJ+QiQN3KPw7rgm+Jpm4jrcGnuufIUOMKGwnG5alBV4fVoOtM0lAWYQxuD4w5RzZeQemT1kBdtXR5Oh6qyZvYs/n6H7m97JiFKN89qHds4bYXMDasT6XfzOzJ3N/c0nDgxO4un+/ydQeD+ivqng7KVfLblmn/goKvg2UkWpuXweOeJMz+ZAFnSHbTSuwhtnoeCuw4B24XwcOVwldFc2jRxXF/brBxOOIbrBufiOig8I0dO5XeN6fZh9Uep4XnslUwNUSDP2ca1rxNS35jjfOwOtXUDAUEFTBPH46qzSkvgsZW0Cmdxit6b62gmwrWFuzK0W34FenFLXWwFOdn7pkC9WiNbZUdJd+c+FVZqQ3FNAwSQsm1c05Xyr2TKW19FuPafaVx4rtiE9OmrI9n+dUUnpyAVNmgFB9kTL2h0i6k7zhqilJUeH5tXgO8akrZ4ZFnUltpPsODJJWa/xzyWM1sTx0z39l8nCGL/1gO904vzz4bveBd5Og7it58OEf</diagram></mxfile>
|
BIN
docs/plugins/diagrams/static_capture.png
Normal file
After Width: | Height: | Size: 171 KiB |
@ -0,0 +1,43 @@
|
||||
# What is Zoraxy Plugin?
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
|
||||
|
||||
Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not **as the plugin library and interface are open source under the LGPL license**.
|
||||
|
||||
There are two primary types of plugins:
|
||||
- **Router plugins**: Involved with connections from HTTP proxy rules.
|
||||
- **Utility plugins**: Provide user interfaces for various network features that operate independently of the Zoraxy core.
|
||||
|
||||
---
|
||||
|
||||
## How plugins are distributed & installed
|
||||
Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as `linux_amd64_foobar`, `windows_amd64_foobar.exe`, or `linux_arm64_foobar`.
|
||||
|
||||
To manually install a plugin for testing, place the binary file into the `/plugins/{plugin_name}/` folder within your Zoraxy installation directory.
|
||||
|
||||
> **Warning:** The binary name inside the folder must match the plugin folder name. For example, the binary should be named `foobar` (or `foobar.exe` on Windows) if placed in the `/plugins/foobar/` folder. Avoid using names like `foobar_plugin.exe`.
|
||||
|
||||
For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
|
||||
|
||||
---
|
||||
|
||||
## Plugin vs Pull Request
|
||||
The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
|
||||
|
||||
- Designed to handle features that are challenging to integrate directly into the Zoraxy core.
|
||||
- Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
|
||||
- Allows for frequent updates to specific code components without impacting the core's stability or causing downtime.
|
||||
|
||||
---
|
||||
|
||||
### When should you add a core PR or a plugin?
|
||||
In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
|
||||
|
||||
- **Core PR**: If the feature is relevant to most users and enhances Zoraxy's core functionality, consider submitting a core Pull Request (PR).
|
||||
- **Plugin**: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
|
||||
|
||||
The decision depends on the feature's general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
|
62
docs/plugins/docs/1. Introduction/2. Getting Started.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Getting Started
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
To start developing plugins, you will need the following installed on your computer
|
||||
|
||||
1. The source code of Zoraxy
|
||||
2. Go compiler
|
||||
3. VSCode (recommended, or any editor of your choice)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Start Zoraxy at least once
|
||||
|
||||
If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
|
||||
|
||||
```bash
|
||||
cd src/
|
||||
go mod tidy
|
||||
go build
|
||||
sudo ./zoraxy
|
||||
```
|
||||
|
||||
This would allow Zoraxy to generate all the required folder structure on startup.
|
||||
|
||||
After the startup process completes, you would see a folder named "plugins" in the working directory of Zoraxy.
|
||||
|
||||
---
|
||||
|
||||
## Steps 2: Prepare the development environment for Zoraxy Plugin
|
||||
|
||||
Next, you will need to think of a name for your plugin. Lets name our new plugin "Lapwing".
|
||||
|
||||
**Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.**
|
||||
|
||||
Follow the steps below to create the folder structure
|
||||
|
||||
### 2.1 Create Plugin Folder
|
||||
|
||||
Create a folder with your plugin name in the `plugins` folder. After creating the folder, you would have something like `plugins/Lapwing/`.
|
||||
|
||||
### 2.2 Locate and copy Zoraxy Plugin library
|
||||
|
||||
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/Lapwing/mod/zoraxy_plugin`.
|
||||
|
||||
### 2.3 Prepare Go Project structure
|
||||
|
||||
Create the `main.go` file for your plugin. In the example above, it would be located at `plugins/Lapwing/main.go`.
|
||||
|
||||
Use `go mod init yourdomain.com/foo/plugin_name` to initiate your plugin. By default the `go.mod` file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be `go mod init github.com/your_user_name/Lapwing`.
|
||||
|
||||
---
|
||||
|
||||
## Steps 3: Open plugin folder in IDE
|
||||
|
||||
Now open your preferred IDE or text editor and use your plugin folder as the project folder
|
||||
|
||||
Now, you are ready to start developing Zoraxy plugin!
|
26
docs/plugins/docs/1. Introduction/3. Installing Plugin.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Installing Plugin
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
### Install via Plugin Store
|
||||
|
||||
(Work in progress)
|
||||
|
||||
### Manual Install
|
||||
|
||||
The plugin shall be placed inside the `plugins/{{plugin_name}}/` directory where the binary executable name must be matching with the plugin name.
|
||||
|
||||
If you are on Linux, also make sure Zoraxy have the execution permission of the plugin. You can use the following command to enable execution of the plugin binary on Linux with the current user (Assume Zoraxy is run by the current user)
|
||||
|
||||
```bash
|
||||
cd ./plugins/{{plugin_name}}/
|
||||
chmod +x ./{{plugin_name}}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Sometime plugins might come with additional assets other than the binary file. If that is the case, extract all of the plugins content into the folder with the plugin's name.
|
||||
|
||||
After the folder structure is ready, restart Zoraxy. If you are using systemd for Zoraxy, use `sudo systemctl restart zoraxy` to restart Zoraxy via systemd service.
|
40
docs/plugins/docs/1. Introduction/4. Enable Plugins.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Enable Plugins
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
To enable and assign a plugin to a certain HTTP Proxy Rule, you will need to do the following steps
|
||||
|
||||
## 1. Create a tag for your HTTP Proxy Rules
|
||||
|
||||
Let say you want to enable debugger on some of your HTTP Proxy Rules. You can do that by first creating a tag in the tag editor. In the example below, we will be using the tag "debug". After adding the tag to the HTTP Proxy rule, you will see something like this.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 2. Enable Plugin
|
||||
|
||||
Click on the "Enable" button on the plugin which you want to enable
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 3. Assign Plugin to HTTP Proxy Rule
|
||||
|
||||
Finally, select the tag that you just created in the dropdown menu
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
Afterward, you will see the plugin is attached to the target tag
|
||||
|
||||

|
||||
|
||||
It means the plugin is enabled on the HTTP proxy rule
|
||||
|
13
docs/plugins/docs/1. Introduction/5. Viewing Plugin Info.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Viewing Plugin Info
|
||||
|
||||
To view plugin information, you can click on the (i) icon in the plugin list.
|
||||
|
||||

|
||||
|
||||
Next, a side menu will pop up from the side. Here ,you can see the current Plugin information and runtime values including Working directories and runtime assigned port.
|
||||
|
||||
If you are a developer (which you probably is considering you are reading this doc), you can click on the "developer insight" dropdown to show the capture paths registered by this plugin for debug purposes.
|
||||
|
||||
|
||||
|
||||

|
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 6.9 KiB |
17
docs/plugins/docs/2. Architecture/1. Plugin Architecture.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Plugin Architecture
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
|
||||
|
||||
1. Introspect
|
||||
2. Configure
|
||||
3. Forwarding
|
||||
|
||||
The overall flow looks like this.
|
||||
|
||||

|
||||
|
||||
This design make sure that the Zoraxy plugins do not depends on platform dependent implementations that uses, for example, unix socket. This also avoided protocol that require complex conversion to and from HTTP request (data structure) like gRPC, while making sure the plugin can be cross compile into different CPU architecture or OS environment with little to no effect on its performance.
|
101
docs/plugins/docs/2. Architecture/2. Introspect.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Introspect
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
|
||||
|
||||
**This is a pre-defined structure where the plugin must provide to Zoraxy** when the plugin is being started with the `-introspect` flag.
|
||||
|
||||
The introspect structure is defined under the `zoraxy_plugin` library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
|
||||
|
||||
```go
|
||||
type IntroSpect struct {
|
||||
/* Plugin metadata */
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
/*
|
||||
|
||||
Endpoint Settings
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
The introspect provide Zoraxy the required information to start the plugin and how to interact with it. For more details on what those capture settings are for, see "Capture Mode" section.
|
||||
|
||||
---
|
||||
|
||||
## Introspect Manual Triggering
|
||||
|
||||
To manually test if the introspect return is correct, you can try using the `-introspect` flag on any Zoraxy plugin. You should be able to see an output like so.
|
||||
|
||||
```json
|
||||
$ ./debugger -introspect
|
||||
{
|
||||
"id": "org.aroz.zoraxy.debugger",
|
||||
"name": "Plugin Debugger",
|
||||
"author": "aroz.org",
|
||||
"author_contact": "https://aroz.org",
|
||||
"description": "A debugger for Zoraxy \u003c-\u003e plugin communication pipeline",
|
||||
"url": "https://zoraxy.aroz.org",
|
||||
"type": 0,
|
||||
"version_major": 1,
|
||||
"version_minor": 0,
|
||||
"version_patch": 0,
|
||||
"static_capture_paths": [
|
||||
{
|
||||
"capture_path": "/test_a"
|
||||
},
|
||||
{
|
||||
"capture_path": "/test_b"
|
||||
}
|
||||
],
|
||||
"static_capture_ingress": "/s_capture",
|
||||
"dynamic_capture_sniff": "/d_sniff",
|
||||
"dynamic_capture_ingress": "/d_capture",
|
||||
"ui_path": "/debug",
|
||||
"subscription_path": "",
|
||||
"subscriptions_events": null
|
||||
}
|
||||
```
|
||||
|
18
docs/plugins/docs/2. Architecture/3. Configure.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Configure
|
||||
|
||||
Configure or Configure Spec is the `exec` call where Zoraxy start the plugin. The configure spec JSON structure is defined in `zoraxy_plugin` library.
|
||||
|
||||
As the time of writing, the `ConfigureSpec` only contains information on some basic info.
|
||||
|
||||
```go
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The `ConfigureSpec` struct will be parsed to JSON and pass to your plugin via the `-configure=(json payload here)`.
|
||||
|
||||
In your plugin, you can use the `zoraxy_plugin` library to parse it or parse it manually (if you are developing a plugin with other languages).
|
58
docs/plugins/docs/2. Architecture/4. Capture Modes.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Capture Modes
|
||||
|
||||
As you can see in the Introspect section, there are two types of capture mode in Zoraxy plugin API.
|
||||
|
||||
|
||||
|
||||
- Static Capture Mode
|
||||
- Dynamic Capture Mode
|
||||
|
||||
|
||||
|
||||
**Notes: When this document mention the term "endpoint", it means a particular sub-path on the plugin side. For example `/capture` or `/sniff`. In actual implementation, this can be a `http.HandleFunc` or `http.Handle` depends on the plugin implementation.**
|
||||
|
||||
---
|
||||
|
||||
## Static Capture Mode
|
||||
|
||||
Static Capture Mode register a static path to Zoraxy, when the plugin is enabled on a certain HTTP proxy rule, all request that matches the static capture registered paths are forwarded to the plugin without asking first. The overall process is shown in the diagram below.
|
||||
|
||||

|
||||
|
||||
The main benefit of static capture mode is that the capture paths are stored in radix tree. That means it takes O(logn) time to resolve the path and forward the request. Hence, **this mode is generally faster** if your plugin always listens to a few certain paths for extended functionalities.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Capture Mode
|
||||
|
||||
Dynamic Capture Mode register two endpoints to Zoraxy.
|
||||
|
||||
1. DynamicCaptureSniff - The sniffing endpoint where Zoraxy will first ask if the plugin want to handle this request
|
||||
2. DynamicCaptureIngress - The handling endpoint, where if the plugin reply the sniffing with "YES", Zoraxy forward the incoming request to this plugin at this defined endpoint.
|
||||
|
||||
The whole process will takes a few request exchange between plugin and Zoraxy core. Since both of them are communicating via the loopback interface, speed should not be too big of a concern here.
|
||||
|
||||
The request handling flow is shown in the diagram below.
|
||||
|
||||

|
||||
|
||||
Once Zoraxy receive a request from a client that matches one of the HTTP Proxy Rule, Zoraxy will forward the request header to all the plugins that matches the following criteria
|
||||
|
||||
1. The plugin is assigned to a tag that is currently attached to the given HTTP Proxy that the request is coming through
|
||||
2. The plugin is enabled and running
|
||||
3. The plugin has registered its dynamic capture sniffing endpoint in Introspect
|
||||
|
||||
Then the plugin `/sniff` endpoint will receive some basic header information about the request, and response with `SniffResultAccpet` or `SniffResultSkip` to accept or reject handling such request. The response are defined in `zoraxy_plugin` as a public type where you can access with `zoraxy_plugin.SniffresultAccept` and `zoraxy_plugin.SniffResultSkip` respectively.
|
||||
|
||||
Note that this shall only be used if static capture mode cannot satisfy your needs in implementation the feature you want, as **dynamic capture is way slower than static capture mode**.
|
||||
|
||||
---
|
||||
|
||||
## Mixing Capture Modes
|
||||
|
||||
It is possible for you to mix both Static and Capture modes if that is what you want. A few thing you need to know about mixing both mode in single plugin
|
||||
|
||||
1. Static capture mode has higher priority than dynamic capture mode across all plugins. That means if you have a request that matches Plugin A's static capture path and Plugin B's dynamic capture, the request will be first handled by Plugin A
|
||||
2. The same plugin can register both static and dynamic capture modes. Similar to item (1), if the request has already captured by your static capture path, Zoraxy will not proceed and forward the request header to your dynamic sniffing endpoint.
|
||||
3. In case there is a collision in static capture paths between two plugins, the longest one will have priority. For example, if Plugin A registered `/foo` and Plugin B registered `/foo/bar`, when a request to `/foo/bar/teacat` enter Zoraxy, Plugin B is used for handling such request.
|
||||
|
23
docs/plugins/docs/2. Architecture/5. Plugin UI.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Plugin UI
|
||||
|
||||
Last Update: 25/05/2025
|
||||
|
||||
---
|
||||
|
||||
A plugin can optionally expose a Web UI interface for user configuration.
|
||||
|
||||
**A plugin must provide a UI, as it is part of the control mechanism of the plugin life cycle. (i.e. Zoraxy use the plugin UI HTTP server to communicate with the plugin for control signals)** As plugin installed via plugin store provides limited ways for a user to configure the plugin, the plugin web UI will be the best way for user to setup your plugin.
|
||||
|
||||
## Plugin Web UI Access
|
||||
|
||||
If a plugin provide a Web UI endpoint for Zoraxy during the introspect process, a new item will be shown in the Plugins section on Zoraxy side menu. Below is an example of the Web UI of UPnP Port Forwarder plugin.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Front-end Developer Notes
|
||||
|
||||
The Web UI is implemented as a reverse proxy and embed in an iframe. So you do not need to handle CORS issues with the web UI (as it will be proxy internally by Zoraxy as exposed as something like a virtual directory mounted website).
|
||||
|
||||
However, the plugin web UI is exposed via the path `/plugin.ui/{{plugin_uuid}}/`, for example, `/plugin.ui/org.aroz.zoraxy.plugins.upnp/`. **When developing the plugin web UI, do not use absolute path for any resources used in the HTML file**, unless you are trying to re-use Zoraxy components like css or image elements stored in Zoraxy embedded web file system (e.g. `/img/logo.svg`).
|
15
docs/plugins/docs/2. Architecture/6. Compile a Plugin.md
Normal 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
|
||||
```
|
||||
|
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 171 KiB |
After Width: | Height: | Size: 70 KiB |
329
docs/plugins/docs/3. Basic Examples/1. Hello World.md
Normal file
@ -0,0 +1,329 @@
|
||||
# Hello World!
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Name your plugin
|
||||
|
||||
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 satisfies the requirement. Dont worry, the plugin file name is not the same as the plugin display name in the introspect.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create the plugin folder
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
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".
|
||||
|
||||
```bash
|
||||
# Assume you are already inside the src/ folder
|
||||
mkdir helloworld
|
||||
cd ./helloworld
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Create a go project
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
touch main.go
|
||||
go mod init example.com/zoraxy/helloworld
|
||||
ls
|
||||
# After you are done, you should see the followings
|
||||
# go.mod main.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Copy the Zoraxy plugin lib from Zoraxy source code
|
||||
|
||||
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/helloworld/mod/zoraxy_plugin`
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Create a web resources folder
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
```html
|
||||
<!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>
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. Creating a handler for Introspect
|
||||
|
||||
To create a handler for introspect, you can first start your plugin with a few constants.
|
||||
|
||||
1. Plugin ID, this must be unique. You can use a domain you own like `com.example.helloworld`
|
||||
2. UI Path, for now we uses "/" as this plugin do not have any other endpoints, so we can use the whole root just for web UI
|
||||
3. Web root, for trimming off from the embedded web folder so when user can visit your `index.html` by accessing `/` instead of needing to navigate to `/www`
|
||||
|
||||
|
||||
|
||||
After you have defined these constant, we can use `plugin.ServeAndRecvSpec` function to handle the handshake between Zoraxy and your plugin.
|
||||
|
||||
```go
|
||||
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 "separated version" of `ServeAndRecvSpec` is defined as ` ServeIntroSpect(pluginSpect *IntroSpect) ` and `RecvConfigureSpec() (*ConfigureSpec, error)`. See `zoraxy_plugin.go` for more information.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Creating a web server from embedded web fs
|
||||
|
||||
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.
|
||||
|
||||
```go
|
||||
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.
|
||||
|
||||
```go
|
||||
// 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.
|
||||
|
||||
```go
|
||||
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.**
|
||||
|
||||
---
|
||||
|
||||
## 8. Register & Serve the Web UI
|
||||
|
||||
After you have created a embedded web router, you can register it to the UI PATH as follows.
|
||||
|
||||
```go
|
||||
// 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.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build and Test
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 10. Full Code
|
||||
|
||||
This is the full code of the helloworld plugin main.go file.
|
||||
|
||||
```go
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
493
docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
Normal file
@ -0,0 +1,493 @@
|
||||
# RESTful API Call in Web UI
|
||||
Last Update: 29/05/2025
|
||||
|
||||
---
|
||||
|
||||
When developing a UI for your plugin, sometime you might need to make RESTFUL API calls to your plugin backend for setting up something or getting latest information from your plugin. In this example, I will show you how to create a plugin with RESTful api call capabilities with the embedded web server and the custom `cjax` function.
|
||||
|
||||
**Notes: This example assumes you have basic understanding on how to use jQuery `ajax` request.**
|
||||
|
||||
Lets get started!
|
||||
|
||||
---
|
||||
|
||||
## 1. Create the plugin folder structures
|
||||
|
||||
This step is identical to the Hello World example, where you create a plugin folder with the required go project structure in the folder. Please refer to the Hello World example section 1 to 5 for details.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Introspect
|
||||
|
||||
This is quite similar to the Hello World example as well, but we are changing some of the IDs to match what we want to do in this plugin.
|
||||
|
||||
```go
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "com.example.restful-example",
|
||||
Name: "Restful Example",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple demo for making RESTful API calls in 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)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Create an embedded web server with handlers
|
||||
|
||||
In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a `http.HandleFunc` to the plugin so our front-end can request and communicate with the backend.
|
||||
|
||||
```go
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
fmt.Println("Restful-example Exited")
|
||||
}, nil)
|
||||
|
||||
//Register a simple API endpoint that will echo the request body
|
||||
// Since we are using the default http.ServeMux, we can register the handler directly with the last
|
||||
// parameter as nil
|
||||
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
//Some handler code here
|
||||
}, nil)
|
||||
```
|
||||
|
||||
The `embedWebRouter.HandleFunc` last parameter is the `http.Mux`, where if you have multiple web server listening interface, you can fill in different Mux based on your implementation. On of the examples is that, when you are developing a static web server plugin, where you need a dedicated HTTP listening endpoint that is not the one Zoraxy assigned to your plugin, you need to create two http.Mux and assign one of them for Zoraxy plugin UI purpose.
|
||||
|
||||
---
|
||||
|
||||
## 4. Modify the front-end HTML file to make request to backend
|
||||
|
||||
To make a RESTFUL API to your plugin, **you must use relative path in your request URL**.
|
||||
|
||||
Absolute path that start with `/` is only use for accessing Zoraxy resouces. For example, when you access `/img/logo.svg`, Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of `/plugins/your_plugin_name/{your_web_root}/img/logo.svg`.
|
||||
|
||||
### Making GET request
|
||||
|
||||
Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like `./api/foo/bar` instead. Here is an example on a front-end and back-end implementation of a simple "echo" API.
|
||||
|
||||
The API logic is simple: when you make a GET request to the API with `?name=foobar`, it returns `Hello foobar`. Here is the backend implementation in your plugin code.
|
||||
|
||||
```go
|
||||
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is a simple echo API that will return the request body as response
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}, nil)
|
||||
```
|
||||
|
||||
And here is the front-end code in your HTML file
|
||||
|
||||
```html
|
||||
<!-- The example below show how HTTP GET is used with Zoraxy plugin -->
|
||||
<h3>Echo Test (HTTP GET)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="nameInput">Enter your name:</label>
|
||||
<input type="text" id="nameInput" placeholder="Your name">
|
||||
</div>
|
||||
<button class="ui button primary" id="sendRequestButton">Send Request</button>
|
||||
<div class="ui message" id="responseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('sendRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('nameInput').value;
|
||||
if (name.trim() === "") {
|
||||
alert("Please enter a name.");
|
||||
return;
|
||||
}
|
||||
// Note the relative path is used here!
|
||||
// GET do not require CSRF token, so you can use $.ajax directly
|
||||
// or $.cjax (defined in /script/utils.js) to make GET request
|
||||
$.ajax({
|
||||
url: `./api/echo`,
|
||||
type: 'GET',
|
||||
data: { name: name },
|
||||
success: function(data) {
|
||||
console.log('Response:', data.message);
|
||||
$('#responseMessage').text(data.message).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#responseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Making POST request
|
||||
|
||||
Making POST request is also similar to GET request, except when making the request, you will need pass the CSRF-Token with the payload. This is required due to security reasons (See [#267](https://github.com/tobychui/zoraxy/issues/267) for more details).
|
||||
|
||||
Since the CSRF validation is done by Zoraxy, your plugin backend code can be implemented just like an ordinary handler. Here is an example POST handling function that receive a FORM POST and print it in an HTML response.
|
||||
|
||||
```go
|
||||
embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range r.PostForm {
|
||||
for _, value := range values {
|
||||
// Generate a simple HTML response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "%s: %s<br>", key, value)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
|
||||
```
|
||||
|
||||
For the front-end, you will need to use the `$.cjax` function implemented in Zoraxy `utils.js` file. You can include this file by adding these two lines to your HTML file.
|
||||
|
||||
```html
|
||||
<script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<!- More lines here -->
|
||||
<!-- The example below shows how form post can be used in plugin -->
|
||||
<h3>Form Post Test (HTTP POST)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="postNameInput">Name:</label>
|
||||
<input type="text" id="postNameInput" placeholder="Your name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="postAgeInput">Age:</label>
|
||||
<input type="number" id="postAgeInput" placeholder="Your age">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Gender:</label>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderMale" name="gender" value="Male">
|
||||
<label for="genderMale">Male</label>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderFemale" name="gender" value="Female">
|
||||
<label for="genderFemale">Female</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary" id="postRequestButton">Send</button>
|
||||
<div class="ui message" id="postResponseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
After that, you can call to the `$.cjax` function just like what you would usually do with the `$ajax` function.
|
||||
|
||||
```javascript
|
||||
// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
|
||||
// alternatively you can use $.ajax with CSRF token in headers
|
||||
// the header is named "X-CSRF-Token" and the value is taken from the head
|
||||
// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
|
||||
$.cjax({
|
||||
url: './api/post',
|
||||
type: 'POST',
|
||||
data: { name: name, age: age, gender: gender },
|
||||
success: function(data) {
|
||||
console.log('Response:', data);
|
||||
$('#postResponseMessage').html(data).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#postResponseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### POST Request with Vanilla JS
|
||||
|
||||
It is possible to make POST request with Vanilla JS. Note that you will need to populate the csrf-token field yourself to make the request pass through the plugin UI request router in Zoraxy. Here is a basic example on how it could be done.
|
||||
|
||||
```javascript
|
||||
fetch('./api/post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken // Include the CSRF token in the headers
|
||||
},
|
||||
body: JSON.stringify({{your_data_here}})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Full Code
|
||||
|
||||
Here is the full code of the RESTFUL example for reference.
|
||||
|
||||
Front-end (`plugins/restful-example/www/index.html`)
|
||||
|
||||
```html
|
||||
<!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>RESTful Example</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--theme_bg_primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="standardContainer">
|
||||
<div class="ui container">
|
||||
<h1>RESTFul API Example</h1>
|
||||
<!-- The example below show how HTTP GET is used with Zoraxy plugin -->
|
||||
<h3>Echo Test (HTTP GET)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="nameInput">Enter your name:</label>
|
||||
<input type="text" id="nameInput" placeholder="Your name">
|
||||
</div>
|
||||
<button class="ui button primary" id="sendRequestButton">Send Request</button>
|
||||
<div class="ui message" id="responseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('sendRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('nameInput').value;
|
||||
if (name.trim() === "") {
|
||||
alert("Please enter a name.");
|
||||
return;
|
||||
}
|
||||
// Note the relative path is used here!
|
||||
// GET do not require CSRF token, so you can use $.ajax directly
|
||||
// or $.cjax (defined in /script/utils.js) to make GET request
|
||||
$.ajax({
|
||||
url: `./api/echo`,
|
||||
type: 'GET',
|
||||
data: { name: name },
|
||||
success: function(data) {
|
||||
console.log('Response:', data.message);
|
||||
$('#responseMessage').text(data.message).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#responseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- The example below shows how form post can be used in plugin -->
|
||||
<h3>Form Post Test (HTTP POST)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="postNameInput">Name:</label>
|
||||
<input type="text" id="postNameInput" placeholder="Your name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="postAgeInput">Age:</label>
|
||||
<input type="number" id="postAgeInput" placeholder="Your age">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Gender:</label>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderMale" name="gender" value="Male">
|
||||
<label for="genderMale">Male</label>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderFemale" name="gender" value="Female">
|
||||
<label for="genderFemale">Female</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary" id="postRequestButton">Send</button>
|
||||
<div class="ui message" id="postResponseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('postRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('postNameInput').value;
|
||||
const age = document.getElementById('postAgeInput').value;
|
||||
const genderMale = document.getElementById('genderMale').checked;
|
||||
const genderFemale = document.getElementById('genderFemale').checked;
|
||||
|
||||
if (name.trim() === "" || age.trim() === "" || (!genderMale && !genderFemale)) {
|
||||
alert("Please fill out all fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gender = genderMale ? "Male" : "Female";
|
||||
|
||||
// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
|
||||
// alternatively you can use $.ajax with CSRF token in headers
|
||||
// the header is named "X-CSRF-Token" and the value is taken from the head
|
||||
// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
|
||||
$.cjax({
|
||||
url: './api/post',
|
||||
type: 'POST',
|
||||
data: { name: name, age: age, gender: gender },
|
||||
success: function(data) {
|
||||
console.log('Response:', data);
|
||||
$('#postResponseMessage').html(data).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#postResponseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
Backend (`plugins/restful-example/main.go`)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "com.example.restful-example"
|
||||
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.restful-example",
|
||||
Name: "Restful Example",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple demo for making RESTful API calls in 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("Restful-example Exited")
|
||||
}, nil)
|
||||
|
||||
//Register a simple API endpoint that will echo the request body
|
||||
// Since we are using the default http.ServeMux, we can register the handler directly with the last
|
||||
// parameter as nil
|
||||
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is a simple echo API that will return the request body as response
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}, nil)
|
||||
|
||||
// Here is another example of a POST API endpoint that will echo the form data
|
||||
// This will handle POST requests to /api/post and return the form data as response
|
||||
embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range r.PostForm {
|
||||
for _, value := range values {
|
||||
// Generate a simple HTML response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "%s: %s<br>", key, value)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
|
||||
// Serve the restful-example page in the www folder
|
||||
http.Handle(UI_PATH, embedWebRouter.Handler())
|
||||
fmt.Println("Restful-example 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
What you should expect to see if everything is correctly loaded and working in Zoray
|
||||
|
||||

|
263
docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
Normal file
@ -0,0 +1,263 @@
|
||||
# Static Capture Example
|
||||
Last Update: 29/05/2025
|
||||
|
||||
---
|
||||
|
||||
This example demonstrates how to use static capture in Zoraxy plugins. Static capture allows you to define specific paths that will be intercepted by your plugin, enabling custom handling of requests to those paths.
|
||||
|
||||
**Notes: This example assumes you have already read Hello World example.**
|
||||
|
||||
---
|
||||
|
||||
## 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 static capture paths and ingress for your plugin.
|
||||
|
||||
```go
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "org.aroz.zoraxy.static-capture-example",
|
||||
Name: "Static Capture Example",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://aroz.org",
|
||||
Description: "An example for showing how static capture works in Zoraxy.",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{ CapturePath: "/test_a" },
|
||||
{ CapturePath: "/test_b" },
|
||||
},
|
||||
StaticCaptureIngress: "/s_capture",
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
Note the `StaticCapturePaths`. These are the paths that you want to capture in your plugin. These paths will be registered to Zoraxy and when a user have request that matches these paths (including subpaths), the request will get forwarded to your plugin. In this example, we are intercepting the `/test_a` and `test_b` sub-path.
|
||||
|
||||
We also defined a new value named `StaticCaptureIngress`. This is to tell Zoraxy that "if you receive requests that matches the above Static capture paths, please forward the request to this endpoint". In this example, this plugin asked Zoraxy to forward th HTTP traffic to `/s_capture` if anything is matched.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Register Static Capture Handlers
|
||||
|
||||
Static capture handlers are used to process requests to the defined paths. Similar to ordinary http.HandleFunc, you can register `http.HandleFunc` as follows.
|
||||
|
||||
```go
|
||||
pathRouter := plugin.NewPathRouter()
|
||||
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
```
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implement Handlers
|
||||
|
||||
Here are examples of handlers for the captured paths:
|
||||
|
||||
### Handler for `/test_a`
|
||||
|
||||
```go
|
||||
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
```
|
||||
|
||||
### Handler for `/test_b`
|
||||
|
||||
```go
|
||||
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
When the user request any HTTP Proxy Rule with the matching path, these two handlers will response to the request and return the hardcoded string above. Again, this is just for demonstration purpose and you should implement your functions here.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Render Debug UI
|
||||
|
||||
The debug UI provides a simple interface for testing and inspecting requests.
|
||||
|
||||
```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")
|
||||
}
|
||||
```
|
||||
|
||||
This is technically not related to static capturing, but it is really helpful to have a UI to help with printing debug information. You can access the page rendered by this function in the Zoraxy plugin menu. This should be replaced with the embedded web fs used in the Hello world example after the development is completed.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 6. Full Code
|
||||
|
||||
Here is the complete code for the static capture example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/static-capture-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.static-capture-example"
|
||||
UI_PATH = "/ui"
|
||||
STATIC_CAPTURE_INGRESS = "/s_capture"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "Static Capture Example",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://aroz.org",
|
||||
Description: "An example for showing how static capture works in Zoraxy.",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{ CapturePath: "/test_a" },
|
||||
{ CapturePath: "/test_b" },
|
||||
},
|
||||
StaticCaptureIngress: STATIC_CAPTURE_INGRESS,
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pathRouter := plugin.NewPathRouter()
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
|
||||
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
|
||||
fmt.Println("Static path capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
}
|
||||
|
||||
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 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 assign it to a localhost loopback HTTP proxy rule.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
When the plugin is running, requests to `/test_a` and `/test_b` will be intercepted by their respective handlers. **Requests to other paths will not pass through your plugin and will be handled by the default upstream server set by the HTTP proxy Rule.**
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
Example terminal output for requesting `/test_a`:
|
||||
|
||||
```
|
||||
This request is captured by A handler!
|
||||
Request URI: /test_a
|
||||
```
|
||||
|
||||
Example output for requesting `/test_b`:
|
||||
```
|
||||
This request is captured by the B handler!
|
||||
Request URI: /test_b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Enjoy exploring static capture in Zoraxy!
|
||||
|
@ -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!
|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 47 KiB |
27
docs/plugins/docs/index.md
Normal file
@ -0,0 +1,27 @@
|
||||
<div class="ts-image is-rounded">
|
||||
<img src="../assets/banner.png" alt="Zoraxy Plugin Banner" style="width: 100%; height: auto; border-radius: 0.5rem;">
|
||||
</div>
|
||||
|
||||
# Index
|
||||
|
||||
Welcome to the Zoraxy Plugin Documentation!
|
||||
Click on a topic in the side menu to begin navigating through the available resources and guides for developing and managing plugins.
|
||||
|
||||
## FAQ
|
||||
### What skills do I need for developing a plugin?
|
||||
Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
|
||||
|
||||
### Will a plugin crash the whole Zoraxy?
|
||||
No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
|
||||
|
||||
### Can I sell my plugin?
|
||||
Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
|
||||
|
||||
### How can I add my plugin to the official plugin store?
|
||||
To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
|
||||
|
||||
## GNU Free Documentation License
|
||||
|
||||
This documentation is licensed under the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. You may copy, distribute, and modify this document under the terms of the license.
|
||||
|
||||
A copy of the license is available at [https://www.gnu.org/licenses/fdl-1.3.html](https://www.gnu.org/licenses/fdl-1.3.html).
|
257
docs/plugins/docs/zoraxy_plugin API.md
Normal file
@ -0,0 +1,257 @@
|
||||
# Zoraxy Plugin APIs
|
||||
This API documentation is auto-generated from the Zoraxy plugin source code.
|
||||
|
||||
|
||||
<pre><code class='language-go'>
|
||||
package zoraxy_plugin // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
|
||||
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect)
|
||||
ServeIntroSpect Function
|
||||
|
||||
This function will check if the plugin is initialized with -introspect flag,
|
||||
if so, it will print the intro spect and exit
|
||||
|
||||
Place this function at the beginning of your plugin main function
|
||||
|
||||
|
||||
TYPES
|
||||
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
|
||||
}
|
||||
ConfigureSpec Payload
|
||||
|
||||
Zoraxy will start your plugin with -configure flag, the plugin shell read
|
||||
this payload as JSON and configure itself by the supplied values like
|
||||
starting a web server at given port that listens to 127.0.0.1:port
|
||||
|
||||
func RecvConfigureSpec() (*ConfigureSpec, error)
|
||||
RecvExecuteConfigureSpec Function
|
||||
|
||||
This function will read the configure spec from Zoraxy and return the
|
||||
ConfigureSpec object
|
||||
|
||||
Place this function after ServeIntroSpect function in your plugin main
|
||||
function
|
||||
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error)
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec See
|
||||
the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
|
||||
type ControlStatusCode int
|
||||
|
||||
const (
|
||||
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
|
||||
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
|
||||
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
|
||||
)
|
||||
type 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"`
|
||||
|
||||
// Has unexported fields.
|
||||
}
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
|
||||
A custom request object to be used in the dynamic sniffing
|
||||
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error)
|
||||
DecodeForwardRequestPayload decodes JSON bytes into a
|
||||
DynamicSniffForwardRequest object
|
||||
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest
|
||||
GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an
|
||||
http.Request object
|
||||
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request
|
||||
GetRequest returns the original http.Request object, for debugging purposes
|
||||
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string
|
||||
GetRequestUUID returns the request UUID if this UUID is empty string,
|
||||
that might indicate the request is not coming from the dynamic router
|
||||
|
||||
type IntroSpect struct {
|
||||
// Plugin metadata
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
// Static Capture Settings
|
||||
//
|
||||
// Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
// This is faster than dynamic capture, but less flexible
|
||||
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
// Dynamic Capture Settings
|
||||
//
|
||||
// Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
// if the plugin sniff returns 280, the traffic will be captured
|
||||
// otherwise, the traffic will be forwarded to the next plugin
|
||||
// This is slower than static capture, but more flexible
|
||||
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
// UI Path for your plugin
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
// Subscriptions Settings
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
}
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag, the plugin shell
|
||||
return this payload as JSON and exit
|
||||
|
||||
type PathRouter struct {
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPathRouter() *PathRouter
|
||||
NewPathRouter creates a new PathRouter
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request)
|
||||
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request))
|
||||
RegisterDynamicCaptureHandle register the dynamic capture ingress path with
|
||||
a handler
|
||||
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler)
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and
|
||||
paths
|
||||
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler)
|
||||
RegisterPathHandler registers a handler for a path
|
||||
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux)
|
||||
StartStaticCapture starts the static capture ingress
|
||||
|
||||
func (p *PathRouter) RemovePathHandler(path string)
|
||||
RemovePathHandler removes a handler for a path
|
||||
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool)
|
||||
SetDebugPrintMode sets the debug print mode
|
||||
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler)
|
||||
SetDefaultHandler sets the default handler for the router This handler will
|
||||
be called if no path handler is found
|
||||
|
||||
type PluginType int
|
||||
|
||||
const (
|
||||
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
|
||||
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
|
||||
)
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter
|
||||
NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
The handlerPrefix is the prefix of the handler used to route this router
|
||||
The handlerPrefix should start with a slash (e.g. /ui) that matches the
|
||||
http.Handle path All prefix should not end with a slash
|
||||
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux)
|
||||
Attach the file system UI handler to the target http.ServeMux
|
||||
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler
|
||||
GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
|
||||
RegisterTerminateHandler registers the terminate handler for the
|
||||
PluginUiRouter The terminate handler will be called when the plugin is
|
||||
terminated from Zoraxy plugin manager if mux is nil, the handler will be
|
||||
registered to http.DefaultServeMux
|
||||
|
||||
type PluginUiRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter
|
||||
NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS The
|
||||
targetFsPrefix is the prefix of the embed.FS where the UI files are stored
|
||||
The targetFsPrefix should be relative to the root of the embed.FS The
|
||||
targetFsPrefix should start with a slash (e.g. /web) that corresponds to the
|
||||
root folder of the embed.FS The handlerPrefix is the prefix of the handler
|
||||
used to route this router The handlerPrefix should start with a slash (e.g.
|
||||
/ui) that matches the http.Handle path All prefix should not end with a
|
||||
slash
|
||||
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux)
|
||||
Attach the embed UI handler to the target http.ServeMux
|
||||
|
||||
func (p *PluginUiRouter) Handler() http.Handler
|
||||
GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
|
||||
RegisterTerminateHandler registers the terminate handler for the
|
||||
PluginUiRouter The terminate handler will be called when the plugin is
|
||||
terminated from Zoraxy plugin manager if mux is nil, the handler will be
|
||||
registered to http.DefaultServeMux
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
type StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
}
|
||||
|
||||
type SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
</code></pre>
|
6
docs/plugins/examples.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="ts-content">
|
||||
<div class="ts-container">
|
||||
<h2>Examples</h2>
|
||||
<p>Test</p>
|
||||
</div>
|
||||
</div>
|
BIN
docs/plugins/favicon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
23
docs/plugins/gen_zoraxy_plugin_doc.sh
Normal file
@ -0,0 +1,23 @@
|
||||
#/bin/bash
|
||||
|
||||
# Cd into zoraxy plugin directory
|
||||
cd ../../src/mod/plugins/zoraxy_plugin/
|
||||
|
||||
|
||||
# Add header to the documentation
|
||||
echo "# Zoraxy Plugin APIs" > docs.md
|
||||
echo "This API documentation is auto-generated from the Zoraxy plugin source code." >> docs.md
|
||||
echo "" >> docs.md
|
||||
echo "" >> docs.md
|
||||
echo "<pre><code class='language-go'>" >> docs.md
|
||||
go doc -all >> docs.md
|
||||
echo "</code></pre>" >> docs.md
|
||||
|
||||
# Replace // import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" with
|
||||
# // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
|
||||
sed -i 's|// import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"|// import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"|g' docs.md
|
||||
|
||||
# Move the generated docs to the plugins/html directory
|
||||
mv docs.md "../../../../docs/plugins/docs/zoraxy_plugin API.md"
|
||||
|
||||
echo "Done generating Zoraxy plugin documentation."
|
16
docs/plugins/go.mod
Normal file
@ -0,0 +1,16 @@
|
||||
module imuslab.com/zoraxy/docs
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
79
docs/plugins/go.sum
Normal file
@ -0,0 +1,79 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
343
docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
Normal file
@ -0,0 +1,343 @@
|
||||
<!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>
|
||||
What is Zoraxy 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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 is-active" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="what-is-zoraxy-plugin">
|
||||
What is Zoraxy Plugin?
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not
|
||||
<span class="ts-text is-heavy">
|
||||
as the plugin library and interface are open source under the LGPL license
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
There are two primary types of plugins:
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-unordered">
|
||||
<div class="item">
|
||||
<span class="ts-text is-heavy">
|
||||
Router plugins
|
||||
</span>
|
||||
: Involved with connections from HTTP proxy rules.
|
||||
<br>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="ts-text is-heavy">
|
||||
Utility plugins
|
||||
</span>
|
||||
: Provide user interfaces for various network features that operate independently of the Zoraxy core.
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="how-plugins-are-distributed-installed">
|
||||
How plugins are distributed & installed
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as
|
||||
<span class="ts-text is-code">
|
||||
linux_amd64_foobar
|
||||
</span>
|
||||
,
|
||||
<span class="ts-text is-code">
|
||||
windows_amd64_foobar.exe
|
||||
</span>
|
||||
, or
|
||||
<span class="ts-text is-code">
|
||||
linux_arm64_foobar
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To manually install a plugin for testing, place the binary file into the
|
||||
<span class="ts-text is-code">
|
||||
/plugins/{plugin_name}/
|
||||
</span>
|
||||
folder within your Zoraxy installation directory.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-quote">
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Warning:
|
||||
</span>
|
||||
The binary name inside the folder must match the plugin folder name. For example, the binary should be named
|
||||
<span class="ts-text is-code">
|
||||
foobar
|
||||
</span>
|
||||
(or
|
||||
<span class="ts-text is-code">
|
||||
foobar.exe
|
||||
</span>
|
||||
on Windows) if placed in the
|
||||
<span class="ts-text is-code">
|
||||
/plugins/foobar/
|
||||
</span>
|
||||
folder. Avoid using names like
|
||||
<span class="ts-text is-code">
|
||||
foobar_plugin.exe
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="plugin-vs-pull-request">
|
||||
Plugin vs Pull Request
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-unordered">
|
||||
<div class="item">
|
||||
Designed to handle features that are challenging to integrate directly into the Zoraxy core.
|
||||
<br>
|
||||
</div>
|
||||
<div class="item">
|
||||
Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
|
||||
<br>
|
||||
</div>
|
||||
<div class="item">
|
||||
Allows for frequent updates to specific code components without impacting the core’s stability or causing downtime.
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h3 id="when-should-you-add-a-core-pr-or-a-plugin">
|
||||
When should you add a core PR or a plugin?
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-unordered">
|
||||
<div class="item">
|
||||
<span class="ts-text is-heavy">
|
||||
Core PR
|
||||
</span>
|
||||
: If the feature is relevant to most users and enhances Zoraxy’s core functionality, consider submitting a core Pull Request (PR).
|
||||
<br>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="ts-text is-heavy">
|
||||
Plugin
|
||||
</span>
|
||||
: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
The decision depends on the feature’s general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
|
||||
</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>
|
347
docs/plugins/html/1. Introduction/2. Getting Started.html
Normal file
@ -0,0 +1,347 @@
|
||||
<!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>
|
||||
Getting Started | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="getting-started">
|
||||
Getting Started
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To start developing plugins, you will need the following installed on your computer
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
The source code of Zoraxy
|
||||
</div>
|
||||
<div class="item">
|
||||
Go compiler
|
||||
</div>
|
||||
<div class="item">
|
||||
VSCode (recommended, or any editor of your choice)
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="step-1-start-zoraxy-at-least-once">
|
||||
Step 1: Start Zoraxy at least once
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-bash">cd src/
|
||||
go mod tidy
|
||||
go build
|
||||
sudo ./zoraxy
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This would allow Zoraxy to generate all the required folder structure on startup.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
After the startup process completes, you would see a folder named “plugins” in the working directory of Zoraxy.
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="steps-2-prepare-the-development-environment-for-zoraxy-plugin">
|
||||
Steps 2: Prepare the development environment for Zoraxy Plugin
|
||||
</h2>
|
||||
<p>
|
||||
Next, you will need to think of a name for your plugin. Lets name our new plugin “Lapwing”.
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Follow the steps below to create the folder structure
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="2-1-create-plugin-folder">
|
||||
2.1 Create Plugin Folder
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Create a folder with your plugin name in the
|
||||
<span class="ts-text is-code">
|
||||
plugins
|
||||
</span>
|
||||
folder. After creating the folder, you would have something like
|
||||
<span class="ts-text is-code">
|
||||
plugins/Lapwing/
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="2-2-locate-and-copy-zoraxy-plugin-library">
|
||||
2.2 Locate and copy Zoraxy Plugin library
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
Go module under
|
||||
<span class="ts-text is-code">
|
||||
src/mod/plugins/zoraxy_plugin
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
Copy the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
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
|
||||
<span class="ts-text is-code">
|
||||
mod
|
||||
</span>
|
||||
, then your copied library path should be
|
||||
<span class="ts-text is-code">
|
||||
plugins/Lapwing/mod/zoraxy_plugin
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<h3 id="2-3-prepare-go-project-structure">
|
||||
2.3 Prepare Go Project structure
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Create the
|
||||
<span class="ts-text is-code">
|
||||
main.go
|
||||
</span>
|
||||
file for your plugin. In the example above, it would be located at
|
||||
<span class="ts-text is-code">
|
||||
plugins/Lapwing/main.go
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Use
|
||||
<span class="ts-text is-code">
|
||||
go mod init yourdomain.com/foo/plugin_name
|
||||
</span>
|
||||
to initiate your plugin. By default the
|
||||
<span class="ts-text is-code">
|
||||
go.mod
|
||||
</span>
|
||||
file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be
|
||||
<span class="ts-text is-code">
|
||||
go mod init github.com/your_user_name/Lapwing
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="steps-3-open-plugin-folder-in-ide">
|
||||
Steps 3: Open plugin folder in IDE
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Now open your preferred IDE or text editor and use your plugin folder as the project folder
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Now, you are ready to start developing Zoraxy plugin!
|
||||
</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>
|
236
docs/plugins/html/1. Introduction/3. Installing Plugin.html
Normal file
@ -0,0 +1,236 @@
|
||||
<!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>
|
||||
Installing 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="installing-plugin">
|
||||
Installing Plugin
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h3 id="install-via-plugin-store">
|
||||
Install via Plugin Store
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
(Work in progress)
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="manual-install">
|
||||
Manual Install
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The plugin shall be placed inside the
|
||||
<span class="ts-text is-code">
|
||||
plugins/{{plugin_name}}/
|
||||
</span>
|
||||
directory where the binary executable name must be matching with the plugin name.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
If you are on Linux, also make sure Zoraxy have the execution permission of the plugin. You can use the following command to enable execution of the plugin binary on Linux with the current user (Assume Zoraxy is run by the current user)
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-bash">cd ./plugins/{{plugin_name}}/
|
||||
chmod +x ./{{plugin_name}}
|
||||
</code></pre>
|
||||
<p>
|
||||
Sometime plugins might come with additional assets other than the binary file. If that is the case, extract all of the plugins content into the folder with the plugin’s name.
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After the folder structure is ready, restart Zoraxy. If you are using systemd for Zoraxy, use
|
||||
<span class="ts-text is-code">
|
||||
sudo systemctl restart zoraxy
|
||||
</span>
|
||||
to restart Zoraxy via systemd service.
|
||||
</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>
|
253
docs/plugins/html/1. Introduction/4. Enable Plugins.html
Normal file
@ -0,0 +1,253 @@
|
||||
<!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>
|
||||
Enable Plugins | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="enable-plugins">
|
||||
Enable Plugins
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To enable and assign a plugin to a certain HTTP Proxy Rule, you will need to do the following steps
|
||||
</p>
|
||||
</p>
|
||||
<h2 id="1-create-a-tag-for-your-http-proxy-rules">
|
||||
1. Create a tag for your HTTP Proxy Rules
|
||||
</h2>
|
||||
<p>
|
||||
Let say you want to enable debugger on some of your HTTP Proxy Rules. You can do that by first creating a tag in the tag editor. In the example below, we will be using the tag “debug”. After adding the tag to the HTTP Proxy rule, you will see something like this.
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Enable Plugins/image-20250527193748456.png" alt="image-20250527193748456" />
|
||||
</div>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="2-enable-plugin">
|
||||
2. Enable Plugin
|
||||
</h2>
|
||||
<p>
|
||||
Click on the “Enable” button on the plugin which you want to enable
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Enable Plugins/image-20250527193601017.png" alt="image-20250527193601017" />
|
||||
</div>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="3-assign-plugin-to-http-proxy-rule">
|
||||
3. Assign Plugin to HTTP Proxy Rule
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Finally, select the tag that you just created in the dropdown menu
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Enable Plugins/image-20250527194052408.png" alt="image-20250527194052408" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Afterward, you will see the plugin is attached to the target tag
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Enable Plugins/image-20250527195703464.png" alt="image-20250527195703464" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
It means the plugin is enabled on the HTTP proxy rule
|
||||
</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>
|
213
docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
Normal file
@ -0,0 +1,213 @@
|
||||
<!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>
|
||||
Viewing Plugin Info | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="viewing-plugin-info">
|
||||
Viewing Plugin Info
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To view plugin information, you can click on the (i) icon in the plugin list.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/5. Viewing Plugin Info/image-20250530171732607.png" alt="image-20250530171732607" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Next, a side menu will pop up from the side. Here ,you can see the current Plugin information and runtime values including Working directories and runtime assigned port.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
If you are a developer (which you probably is considering you are reading this doc), you can click on the “developer insight” dropdown to show the capture paths registered by this plugin for debug purposes.
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/5. Viewing Plugin Info/image-20250530171724441.png" alt="image-20250530171724441" />
|
||||
</div>
|
||||
</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>
|
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 6.9 KiB |
227
docs/plugins/html/2. Architecture/1. Plugin Architecture.html
Normal file
@ -0,0 +1,227 @@
|
||||
<!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>
|
||||
Plugin Architecture | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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 is-active" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="plugin-architecture">
|
||||
Plugin Architecture
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
Introspect
|
||||
</div>
|
||||
<div class="item">
|
||||
Configure
|
||||
</div>
|
||||
<div class="item">
|
||||
Forwarding
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The overall flow looks like this.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/1. Plugin Architecture/plugin_workflow.png" alt="" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This design make sure that the Zoraxy plugins do not depends on platform dependent implementations that uses, for example, unix socket. This also avoided protocol that require complex conversion to and from HTTP request (data structure) like gRPC, while making sure the plugin can be cross compile into different CPU architecture or OS environment with little to no effect on its performance.
|
||||
</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>
|
313
docs/plugins/html/2. Architecture/2. Introspect.html
Normal file
@ -0,0 +1,313 @@
|
||||
<!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>
|
||||
Introspect | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="introspect">
|
||||
Introspect
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
This is a pre-defined structure where the plugin must provide to Zoraxy
|
||||
</span>
|
||||
when the plugin is being started with the
|
||||
<span class="ts-text is-code">
|
||||
-introspect
|
||||
</span>
|
||||
flag.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The introspect structure is defined under the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">type IntroSpect struct {
|
||||
/* Plugin metadata */
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
/*
|
||||
|
||||
Endpoint Settings
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
The introspect provide Zoraxy the required information to start the plugin and how to interact with it. For more details on what those capture settings are for, see “Capture Mode” section.
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="introspect-manual-triggering">
|
||||
Introspect Manual Triggering
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To manually test if the introspect return is correct, you can try using the
|
||||
<span class="ts-text is-code">
|
||||
-introspect
|
||||
</span>
|
||||
flag on any Zoraxy plugin. You should be able to see an output like so.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-json">$ ./debugger -introspect
|
||||
{
|
||||
"id": "org.aroz.zoraxy.debugger",
|
||||
"name": "Plugin Debugger",
|
||||
"author": "aroz.org",
|
||||
"author_contact": "https://aroz.org",
|
||||
"description": "A debugger for Zoraxy \u003c-\u003e plugin communication pipeline",
|
||||
"url": "https://zoraxy.aroz.org",
|
||||
"type": 0,
|
||||
"version_major": 1,
|
||||
"version_minor": 0,
|
||||
"version_patch": 0,
|
||||
"static_capture_paths": [
|
||||
{
|
||||
"capture_path": "/test_a"
|
||||
},
|
||||
{
|
||||
"capture_path": "/test_b"
|
||||
}
|
||||
],
|
||||
"static_capture_ingress": "/s_capture",
|
||||
"dynamic_capture_sniff": "/d_sniff",
|
||||
"dynamic_capture_ingress": "/d_capture",
|
||||
"ui_path": "/debug",
|
||||
"subscription_path": "",
|
||||
"subscriptions_events": null
|
||||
}
|
||||
</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>
|
241
docs/plugins/html/2. Architecture/3. Configure.html
Normal file
@ -0,0 +1,241 @@
|
||||
<!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>
|
||||
Configure | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="configure">
|
||||
Configure
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Configure or Configure Spec is the
|
||||
<span class="ts-text is-code">
|
||||
exec
|
||||
</span>
|
||||
call where Zoraxy start the plugin. The configure spec JSON structure is defined in
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
library.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
As the time of writing, the
|
||||
<span class="ts-text is-code">
|
||||
ConfigureSpec
|
||||
</span>
|
||||
only contains information on some basic info.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The
|
||||
<span class="ts-text is-code">
|
||||
ConfigureSpec
|
||||
</span>
|
||||
struct will be parsed to JSON and pass to your plugin via the
|
||||
<span class="ts-text is-code">
|
||||
-configure=(json payload here)
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
In your plugin, you can use the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
library to parse it or parse it manually (if you are developing a plugin with other languages).
|
||||
</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>
|
365
docs/plugins/html/2. Architecture/4. Capture Modes.html
Normal file
@ -0,0 +1,365 @@
|
||||
<!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>
|
||||
Capture Modes | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="capture-modes">
|
||||
Capture Modes
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
As you can see in the Introspect section, there are two types of capture mode in Zoraxy plugin API.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-unordered">
|
||||
<div class="item">
|
||||
Static Capture Mode
|
||||
</div>
|
||||
<div class="item">
|
||||
Dynamic Capture Mode
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<span class="ts-text is-heavy">
|
||||
Notes: When this document mention the term “endpoint”, it means a particular sub-path on the plugin side. For example
|
||||
<span class="ts-text is-code">
|
||||
/capture
|
||||
</span>
|
||||
or
|
||||
<span class="ts-text is-code">
|
||||
/sniff
|
||||
</span>
|
||||
. In actual implementation, this can be a
|
||||
<span class="ts-text is-code">
|
||||
http.HandleFunc
|
||||
</span>
|
||||
or
|
||||
<span class="ts-text is-code">
|
||||
http.Handle
|
||||
</span>
|
||||
depends on the plugin implementation.
|
||||
</span>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="static-capture-mode">
|
||||
Static Capture Mode
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Static Capture Mode register a static path to Zoraxy, when the plugin is enabled on a certain HTTP proxy rule, all request that matches the static capture registered paths are forwarded to the plugin without asking first. The overall process is shown in the diagram below.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/4. Capture Modes/static_capture.png" alt="static_capture" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The main benefit of static capture mode is that the capture paths are stored in radix tree. That means it takes O(logn) time to resolve the path and forward the request. Hence,
|
||||
<span class="ts-text is-heavy">
|
||||
this mode is generally faster
|
||||
</span>
|
||||
if your plugin always listens to a few certain paths for extended functionalities.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="dynamic-capture-mode">
|
||||
Dynamic Capture Mode
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Dynamic Capture Mode register two endpoints to Zoraxy.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
DynamicCaptureSniff - The sniffing endpoint where Zoraxy will first ask if the plugin want to handle this request
|
||||
</div>
|
||||
<div class="item">
|
||||
DynamicCaptureIngress - The handling endpoint, where if the plugin reply the sniffing with “YES”, Zoraxy forward the incoming request to this plugin at this defined endpoint.
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The whole process will takes a few request exchange between plugin and Zoraxy core. Since both of them are communicating via the loopback interface, speed should not be too big of a concern here.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The request handling flow is shown in the diagram below.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/4. Capture Modes/dynamic_capture.png" alt="dynamic_capture" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Once Zoraxy receive a request from a client that matches one of the HTTP Proxy Rule, Zoraxy will forward the request header to all the plugins that matches the following criteria
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
The plugin is assigned to a tag that is currently attached to the given HTTP Proxy that the request is coming through
|
||||
</div>
|
||||
<div class="item">
|
||||
The plugin is enabled and running
|
||||
</div>
|
||||
<div class="item">
|
||||
The plugin has registered its dynamic capture sniffing endpoint in Introspect
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Then the plugin
|
||||
<span class="ts-text is-code">
|
||||
/sniff
|
||||
</span>
|
||||
endpoint will receive some basic header information about the request, and response with
|
||||
<span class="ts-text is-code">
|
||||
SniffResultAccpet
|
||||
</span>
|
||||
or
|
||||
<span class="ts-text is-code">
|
||||
SniffResultSkip
|
||||
</span>
|
||||
to accept or reject handling such request. The response are defined in
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
as a public type where you can access with
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin.SniffresultAccept
|
||||
</span>
|
||||
and
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin.SniffResultSkip
|
||||
</span>
|
||||
respectively.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Note that this shall only be used if static capture mode cannot satisfy your needs in implementation the feature you want, as
|
||||
<span class="ts-text is-heavy">
|
||||
dynamic capture is way slower than static capture mode
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="mixing-capture-modes">
|
||||
Mixing Capture Modes
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
It is possible for you to mix both Static and Capture modes if that is what you want. A few thing you need to know about mixing both mode in single plugin
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
Static capture mode has higher priority than dynamic capture mode across all plugins. That means if you have a request that matches Plugin A’s static capture path and Plugin B’s dynamic capture, the request will be first handled by Plugin A
|
||||
</div>
|
||||
<div class="item">
|
||||
The same plugin can register both static and dynamic capture modes. Similar to item (1), if the request has already captured by your static capture path, Zoraxy will not proceed and forward the request header to your dynamic sniffing endpoint.
|
||||
</div>
|
||||
<div class="item">
|
||||
In case there is a collision in static capture paths between two plugins, the longest one will have priority. For example, if Plugin A registered
|
||||
<span class="ts-text is-code">
|
||||
/foo
|
||||
</span>
|
||||
and Plugin B registered
|
||||
<span class="ts-text is-code">
|
||||
/foo/bar
|
||||
</span>
|
||||
, when a request to
|
||||
<span class="ts-text is-code">
|
||||
/foo/bar/teacat
|
||||
</span>
|
||||
enter Zoraxy, Plugin B is used for handling such request.
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
251
docs/plugins/html/2. Architecture/5. Plugin UI.html
Normal file
@ -0,0 +1,251 @@
|
||||
<!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>
|
||||
Plugin UI | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="plugin-ui">
|
||||
Plugin UI
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
A plugin can optionally expose a Web UI interface for user configuration.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
A plugin must provide a UI, as it is part of the control mechanism of the plugin life cycle. (i.e. Zoraxy use the plugin UI HTTP server to communicate with the plugin for control signals)
|
||||
</span>
|
||||
As plugin installed via plugin store provides limited ways for a user to configure the plugin, the plugin web UI will be the best way for user to setup your plugin.
|
||||
</p>
|
||||
</p>
|
||||
<h2 id="plugin-web-ui-access">
|
||||
Plugin Web UI Access
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
If a plugin provide a Web UI endpoint for Zoraxy during the introspect process, a new item will be shown in the Plugins section on Zoraxy side menu. Below is an example of the Web UI of UPnP Port Forwarder plugin.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/5. Plugin UI/image-20250527201750613.png" alt="image-20250527201750613" />
|
||||
</div>
|
||||
</p>
|
||||
<h2 id="front-end-developer-notes">
|
||||
Front-end Developer Notes
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The Web UI is implemented as a reverse proxy and embed in an iframe. So you do not need to handle CORS issues with the web UI (as it will be proxy internally by Zoraxy as exposed as something like a virtual directory mounted website).
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
However, the plugin web UI is exposed via the path
|
||||
<span class="ts-text is-code">
|
||||
/plugin.ui/{{plugin_uuid}}/
|
||||
</span>
|
||||
, for example,
|
||||
<span class="ts-text is-code">
|
||||
/plugin.ui/org.aroz.zoraxy.plugins.upnp/
|
||||
</span>
|
||||
.
|
||||
<span class="ts-text is-heavy">
|
||||
When developing the plugin web UI, do not use absolute path for any resources used in the HTML file
|
||||
</span>
|
||||
, unless you are trying to re-use Zoraxy components like css or image elements stored in Zoraxy embedded web file system (e.g.
|
||||
<span class="ts-text is-code">
|
||||
/img/logo.svg
|
||||
</span>
|
||||
).
|
||||
</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>
|
204
docs/plugins/html/2. Architecture/6. Compile a Plugin.html
Normal file
@ -0,0 +1,204 @@
|
||||
<!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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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>
|
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 171 KiB |
After Width: | Height: | Size: 70 KiB |
668
docs/plugins/html/3. Basic Examples/1. Hello World.html
Normal file
@ -0,0 +1,668 @@
|
||||
<!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>
|
||||
Hello World | 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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 is-active" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="hello-world">
|
||||
Hello World!
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 25/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="1-name-your-plugin">
|
||||
1. Name your plugin
|
||||
</h2>
|
||||
<p>
|
||||
First things first, give your plugin a name. In this example, we are using the name “helloworld”.
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Plugin name cannot contain space or special characters
|
||||
</span>
|
||||
, so you must use a file name that satisfies the requirement. Dont worry, the plugin file name is not the same as the plugin display name in the introspect.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="2-create-the-plugin-folder">
|
||||
2. Create the plugin folder
|
||||
</h2>
|
||||
<p>
|
||||
If your zoraxy root folder do not contains a folder named “plugins”, it might implies that your Zoraxy is freshly clone from Github.
|
||||
<span class="ts-text is-heavy">
|
||||
You will need to build and run it once to start working on your plugin
|
||||
</span>
|
||||
, so if you have a newly cloned source code of Zoraxy, do the followings.
|
||||
</p>
|
||||
<pre><code class="language-bash">git clone https://github.com/tobychui/zoraxy
|
||||
cd src
|
||||
go mod tidy
|
||||
go build
|
||||
sudo ./zoraxy
|
||||
</code></pre>
|
||||
<p>
|
||||
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”.
|
||||
</p>
|
||||
<pre><code class="language-bash"># Assume you are already inside the src/ folder
|
||||
mkdir helloworld
|
||||
cd ./helloworld
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="3-create-a-go-project">
|
||||
3. Create a go project
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Similar to any Go project, you can start by creating a
|
||||
<span class="ts-text is-code">
|
||||
main.go
|
||||
</span>
|
||||
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
|
||||
<span class="ts-text is-code">
|
||||
go mod init
|
||||
</span>
|
||||
command.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-bash">touch main.go
|
||||
go mod init example.com/zoraxy/helloworld
|
||||
ls
|
||||
# After you are done, you should see the followings
|
||||
# go.mod main.go
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="4-copy-the-zoraxy-plugin-lib-from-zoraxy-source-code">
|
||||
4. Copy the Zoraxy plugin lib from Zoraxy source code
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
Go module under
|
||||
<span class="ts-text is-code">
|
||||
src/mod/plugins/zoraxy_plugin
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Copy the
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin
|
||||
</span>
|
||||
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
|
||||
<span class="ts-text is-code">
|
||||
mod
|
||||
</span>
|
||||
, then your copied library path should be
|
||||
<span class="ts-text is-code">
|
||||
plugins/helloworld/mod/zoraxy_plugin
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-bash">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
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="5-create-a-web-resources-folder">
|
||||
5. Create a web resources folder
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Lets create a www folder and put all our web resources, we need to create an
|
||||
<span class="ts-text is-code">
|
||||
index.html
|
||||
</span>
|
||||
file as our plugin web ui homepage. This can be done by creating a HTML file in the www folder.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-bash"># Assuming you are currently in the src/plugins/helloworld/ folder
|
||||
mkdir www
|
||||
cd www
|
||||
touch index.html
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
And here is an example
|
||||
<span class="ts-text is-code">
|
||||
index.html
|
||||
</span>
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><!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>
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="6-creating-a-handler-for-introspect">
|
||||
6. Creating a handler for Introspect
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To create a handler for introspect, you can first start your plugin with a few constants.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-list is-ordered">
|
||||
<div class="item">
|
||||
Plugin ID, this must be unique. You can use a domain you own like
|
||||
<span class="ts-text is-code">
|
||||
com.example.helloworld
|
||||
</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
UI Path, for now we uses “/” as this plugin do not have any other endpoints, so we can use the whole root just for web UI
|
||||
</div>
|
||||
<div class="item">
|
||||
Web root, for trimming off from the embedded web folder so when user can visit your
|
||||
<span class="ts-text is-code">
|
||||
index.html
|
||||
</span>
|
||||
by accessing
|
||||
<span class="ts-text is-code">
|
||||
/
|
||||
</span>
|
||||
instead of needing to navigate to
|
||||
<span class="ts-text is-code">
|
||||
/www
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After you have defined these constant, we can use
|
||||
<span class="ts-text is-code">
|
||||
plugin.ServeAndRecvSpec
|
||||
</span>
|
||||
function to handle the handshake between Zoraxy and your plugin.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">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)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
<span class="ts-text is-heavy">
|
||||
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 “separated version” of
|
||||
<span class="ts-text is-code">
|
||||
ServeAndRecvSpec
|
||||
</span>
|
||||
is defined as
|
||||
<span class="ts-text is-code">
|
||||
ServeIntroSpect(pluginSpect *IntroSpect)
|
||||
</span>
|
||||
and
|
||||
<span class="ts-text is-code">
|
||||
RecvConfigureSpec() (*ConfigureSpec, error)
|
||||
</span>
|
||||
. See
|
||||
<span class="ts-text is-code">
|
||||
zoraxy_plugin.go
|
||||
</span>
|
||||
for more information.
|
||||
</span>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="7-creating-a-web-server-from-embedded-web-fs">
|
||||
7. Creating a web server from embedded web fs
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After that, we need to create a web server to serve our plugin UI to Zoraxy via HTTP. This can be done via the
|
||||
<span class="ts-text is-code">
|
||||
http.FileServer
|
||||
</span>
|
||||
but for simplicity and ease of upgrade, the Zoraxy plugin library provided an easy to use embedded web FS server API for plugin developers.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To use the Zoraxy plugin embedded web server, you first need to embed your web fs into Zoraxy as such.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
//go:embed www/*
|
||||
var content embed.FS
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Then call to the
|
||||
<span class="ts-text is-code">
|
||||
NewPluginEmbedUIRouter
|
||||
</span>
|
||||
to create a new UI router from the embedded Fs.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">// 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)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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
|
||||
<span class="ts-text is-code">
|
||||
embedWebRouter
|
||||
</span>
|
||||
object has a function named
|
||||
<span class="ts-text is-code">
|
||||
RegisterTerminateHandler
|
||||
</span>
|
||||
where you can easily use this function to register actions that needed to be done before shutdown.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">embedWebRouter.RegisterTerminateHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Hello World Plugin Exited")
|
||||
}, nil)
|
||||
</code></pre>
|
||||
<p>
|
||||
<span class="ts-text is-heavy">
|
||||
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.
|
||||
</span>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="8-register-serve-the-web-ui">
|
||||
8. Register & Serve the Web UI
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After you have created a embedded web router, you can register it to the UI PATH as follows.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">// 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)
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="9-build-and-test">
|
||||
9. Build and Test
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After saving the
|
||||
<span class="ts-text is-code">
|
||||
main.go
|
||||
</span>
|
||||
file, you can now build your plugin with
|
||||
<span class="ts-text is-code">
|
||||
go build
|
||||
</span>
|
||||
. It should generate the plugin in your platform architecture and OS. If you are on Linux, it will be
|
||||
<span class="ts-text is-code">
|
||||
helloworld
|
||||
</span>
|
||||
and if you are on Windows, it will be
|
||||
<span class="ts-text is-code">
|
||||
helloworld.exe
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/1. Hello World/image-20250530134148399.png" alt="image-20250530134148399" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Tips
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
You can also enable the Developer Option - Plugin Auto Reload function if you are too lazy to restart Zoraxy everytime the plugin binary changed.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/1. Hello World/image-20250527210849767.png" alt="image-20250527210849767" />
|
||||
</div>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="10-full-code">
|
||||
10. Full Code
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This is the full code of the helloworld plugin main.go file.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</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>
|
804
docs/plugins/html/3. Basic Examples/2. RESTful Example.html
Normal file
@ -0,0 +1,804 @@
|
||||
<!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>
|
||||
RESTful 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="restful-api-call-in-web-ui">
|
||||
RESTful API Call in Web UI
|
||||
</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">
|
||||
When developing a UI for your plugin, sometime you might need to make RESTFUL API calls to your plugin backend for setting up something or getting latest information from your plugin. In this example, I will show you how to create a plugin with RESTful api call capabilities with the embedded web server and the custom
|
||||
<span class="ts-text is-code">
|
||||
cjax
|
||||
</span>
|
||||
function.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Notes: This example assumes you have basic understanding on how to use jQuery
|
||||
<span class="ts-text is-code">
|
||||
ajax
|
||||
</span>
|
||||
request.
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Lets get started!
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="1-create-the-plugin-folder-structures">
|
||||
1. Create the plugin folder structures
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This step is identical to the Hello World example, where you create a plugin folder with the required go project structure in the folder. Please refer to the Hello World example section 1 to 5 for details.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="2-create-introspect">
|
||||
2. Create Introspect
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This is quite similar to the Hello World example as well, but we are changing some of the IDs to match what we want to do in this plugin.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "com.example.restful-example",
|
||||
Name: "Restful Example",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple demo for making RESTful API calls in 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)
|
||||
}
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="3-create-an-embedded-web-server-with-handlers">
|
||||
3. Create an embedded web server with handlers
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a
|
||||
<span class="ts-text is-code">
|
||||
http.HandleFunc
|
||||
</span>
|
||||
to the plugin so our front-end can request and communicate with the backend.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
fmt.Println("Restful-example Exited")
|
||||
}, nil)
|
||||
|
||||
//Register a simple API endpoint that will echo the request body
|
||||
// Since we are using the default http.ServeMux, we can register the handler directly with the last
|
||||
// parameter as nil
|
||||
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
//Some handler code here
|
||||
}, nil)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The
|
||||
<span class="ts-text is-code">
|
||||
embedWebRouter.HandleFunc
|
||||
</span>
|
||||
last parameter is the
|
||||
<span class="ts-text is-code">
|
||||
http.Mux
|
||||
</span>
|
||||
, where if you have multiple web server listening interface, you can fill in different Mux based on your implementation. On of the examples is that, when you are developing a static web server plugin, where you need a dedicated HTTP listening endpoint that is not the one Zoraxy assigned to your plugin, you need to create two http.Mux and assign one of them for Zoraxy plugin UI purpose.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="4-modify-the-front-end-html-file-to-make-request-to-backend">
|
||||
4. Modify the front-end HTML file to make request to backend
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To make a RESTFUL API to your plugin,
|
||||
<span class="ts-text is-heavy">
|
||||
you must use relative path in your request URL
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Absolute path that start with
|
||||
<span class="ts-text is-code">
|
||||
/
|
||||
</span>
|
||||
is only use for accessing Zoraxy resouces. For example, when you access
|
||||
<span class="ts-text is-code">
|
||||
/img/logo.svg
|
||||
</span>
|
||||
, Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of
|
||||
<span class="ts-text is-code">
|
||||
/plugins/your_plugin_name/{your_web_root}/img/logo.svg
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="making-get-request">
|
||||
Making GET request
|
||||
</h3>
|
||||
<p>
|
||||
Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like
|
||||
<span class="ts-text is-code">
|
||||
./api/foo/bar
|
||||
</span>
|
||||
instead. Here is an example on a front-end and back-end implementation of a simple “echo” API.
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The API logic is simple: when you make a GET request to the API with
|
||||
<span class="ts-text is-code">
|
||||
?name=foobar
|
||||
</span>
|
||||
, it returns
|
||||
<span class="ts-text is-code">
|
||||
Hello foobar
|
||||
</span>
|
||||
. Here is the backend implementation in your plugin code.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is a simple echo API that will return the request body as response
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}, nil)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
And here is the front-end code in your HTML file
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><!-- The example below show how HTTP GET is used with Zoraxy plugin -->
|
||||
<h3>Echo Test (HTTP GET)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="nameInput">Enter your name:</label>
|
||||
<input type="text" id="nameInput" placeholder="Your name">
|
||||
</div>
|
||||
<button class="ui button primary" id="sendRequestButton">Send Request</button>
|
||||
<div class="ui message" id="responseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('sendRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('nameInput').value;
|
||||
if (name.trim() === "") {
|
||||
alert("Please enter a name.");
|
||||
return;
|
||||
}
|
||||
// Note the relative path is used here!
|
||||
// GET do not require CSRF token, so you can use $.ajax directly
|
||||
// or $.cjax (defined in /script/utils.js) to make GET request
|
||||
$.ajax({
|
||||
url: `./api/echo`,
|
||||
type: 'GET',
|
||||
data: { name: name },
|
||||
success: function(data) {
|
||||
console.log('Response:', data.message);
|
||||
$('#responseMessage').text(data.message).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#responseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</code></pre>
|
||||
<h3 id="making-post-request">
|
||||
Making POST request
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Making POST request is also similar to GET request, except when making the request, you will need pass the CSRF-Token with the payload. This is required due to security reasons (See
|
||||
<a href="https://github.com/tobychui/zoraxy/issues/267" target="_blank">
|
||||
#267
|
||||
</a>
|
||||
for more details).
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Since the CSRF validation is done by Zoraxy, your plugin backend code can be implemented just like an ordinary handler. Here is an example POST handling function that receive a FORM POST and print it in an HTML response.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range r.PostForm {
|
||||
for _, value := range values {
|
||||
// Generate a simple HTML response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "%s: %s<br>", key, value)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
For the front-end, you will need to use the
|
||||
<span class="ts-text is-code">
|
||||
$.cjax
|
||||
</span>
|
||||
function implemented in Zoraxy
|
||||
<span class="ts-text is-code">
|
||||
utils.js
|
||||
</span>
|
||||
file. You can include this file by adding these two lines to your HTML file.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<!- More lines here -->
|
||||
<!-- The example below shows how form post can be used in plugin -->
|
||||
<h3>Form Post Test (HTTP POST)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="postNameInput">Name:</label>
|
||||
<input type="text" id="postNameInput" placeholder="Your name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="postAgeInput">Age:</label>
|
||||
<input type="number" id="postAgeInput" placeholder="Your age">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Gender:</label>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderMale" name="gender" value="Male">
|
||||
<label for="genderMale">Male</label>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderFemale" name="gender" value="Female">
|
||||
<label for="genderFemale">Female</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary" id="postRequestButton">Send</button>
|
||||
<div class="ui message" id="postResponseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After that, you can call to the
|
||||
<span class="ts-text is-code">
|
||||
$.cjax
|
||||
</span>
|
||||
function just like what you would usually do with the
|
||||
<span class="ts-text is-code">
|
||||
$ajax
|
||||
</span>
|
||||
function.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-javascript">// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
|
||||
// alternatively you can use $.ajax with CSRF token in headers
|
||||
// the header is named "X-CSRF-Token" and the value is taken from the head
|
||||
// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
|
||||
$.cjax({
|
||||
url: './api/post',
|
||||
type: 'POST',
|
||||
data: { name: name, age: age, gender: gender },
|
||||
success: function(data) {
|
||||
console.log('Response:', data);
|
||||
$('#postResponseMessage').html(data).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#postResponseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
</code></pre>
|
||||
<h3 id="post-request-with-vanilla-js">
|
||||
POST Request with Vanilla JS
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
It is possible to make POST request with Vanilla JS. Note that you will need to populate the csrf-token field yourself to make the request pass through the plugin UI request router in Zoraxy. Here is a basic example on how it could be done.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-javascript">fetch('./api/post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken // Include the CSRF token in the headers
|
||||
},
|
||||
body: JSON.stringify({{your_data_here}})
|
||||
})
|
||||
</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 full code of the RESTFUL example for reference.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Front-end (
|
||||
<span class="ts-text is-code">
|
||||
plugins/restful-example/www/index.html
|
||||
</span>
|
||||
)
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><!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>RESTful Example</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--theme_bg_primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="standardContainer">
|
||||
<div class="ui container">
|
||||
<h1>RESTFul API Example</h1>
|
||||
<!-- The example below show how HTTP GET is used with Zoraxy plugin -->
|
||||
<h3>Echo Test (HTTP GET)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="nameInput">Enter your name:</label>
|
||||
<input type="text" id="nameInput" placeholder="Your name">
|
||||
</div>
|
||||
<button class="ui button primary" id="sendRequestButton">Send Request</button>
|
||||
<div class="ui message" id="responseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('sendRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('nameInput').value;
|
||||
if (name.trim() === "") {
|
||||
alert("Please enter a name.");
|
||||
return;
|
||||
}
|
||||
// Note the relative path is used here!
|
||||
// GET do not require CSRF token, so you can use $.ajax directly
|
||||
// or $.cjax (defined in /script/utils.js) to make GET request
|
||||
$.ajax({
|
||||
url: `./api/echo`,
|
||||
type: 'GET',
|
||||
data: { name: name },
|
||||
success: function(data) {
|
||||
console.log('Response:', data.message);
|
||||
$('#responseMessage').text(data.message).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#responseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- The example below shows how form post can be used in plugin -->
|
||||
<h3>Form Post Test (HTTP POST)</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label for="postNameInput">Name:</label>
|
||||
<input type="text" id="postNameInput" placeholder="Your name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="postAgeInput">Age:</label>
|
||||
<input type="number" id="postAgeInput" placeholder="Your age">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Gender:</label>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderMale" name="gender" value="Male">
|
||||
<label for="genderMale">Male</label>
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="genderFemale" name="gender" value="Female">
|
||||
<label for="genderFemale">Female</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary" id="postRequestButton">Send</button>
|
||||
<div class="ui message" id="postResponseMessage" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('postRequestButton').addEventListener('click', function() {
|
||||
const name = document.getElementById('postNameInput').value;
|
||||
const age = document.getElementById('postAgeInput').value;
|
||||
const genderMale = document.getElementById('genderMale').checked;
|
||||
const genderFemale = document.getElementById('genderFemale').checked;
|
||||
|
||||
if (name.trim() === "" || age.trim() === "" || (!genderMale && !genderFemale)) {
|
||||
alert("Please fill out all fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gender = genderMale ? "Male" : "Female";
|
||||
|
||||
// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
|
||||
// alternatively you can use $.ajax with CSRF token in headers
|
||||
// the header is named "X-CSRF-Token" and the value is taken from the head
|
||||
// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
|
||||
$.cjax({
|
||||
url: './api/post',
|
||||
type: 'POST',
|
||||
data: { name: name, age: age, gender: gender },
|
||||
success: function(data) {
|
||||
console.log('Response:', data);
|
||||
$('#postResponseMessage').html(data).show();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
$('#postResponseMessage').text('An error occurred while processing your request.').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Backend (
|
||||
<span class="ts-text is-code">
|
||||
plugins/restful-example/main.go
|
||||
</span>
|
||||
)
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "com.example.restful-example"
|
||||
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.restful-example",
|
||||
Name: "Restful Example",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple demo for making RESTful API calls in 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("Restful-example Exited")
|
||||
}, nil)
|
||||
|
||||
//Register a simple API endpoint that will echo the request body
|
||||
// Since we are using the default http.ServeMux, we can register the handler directly with the last
|
||||
// parameter as nil
|
||||
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is a simple echo API that will return the request body as response
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}, nil)
|
||||
|
||||
// Here is another example of a POST API endpoint that will echo the form data
|
||||
// This will handle POST requests to /api/post and return the form data as response
|
||||
embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range r.PostForm {
|
||||
for _, value := range values {
|
||||
// Generate a simple HTML response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "%s: %s<br>", key, value)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
|
||||
// Serve the restful-example page in the www folder
|
||||
http.Handle(UI_PATH, embedWebRouter.Handler())
|
||||
fmt.Println("Restful-example 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
What you should expect to see if everything is correctly loaded and working in Zoray
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/2. RESTful Example/image-20250530153148506.png" alt="image-20250530153148506" />
|
||||
</div>
|
||||
</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>
|
@ -0,0 +1,553 @@
|
||||
<!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>
|
||||
Static 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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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="static-capture-example">
|
||||
Static 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 static capture in Zoraxy plugins. Static capture allows you to define specific paths that will be intercepted by your plugin, enabling custom handling of requests to those paths.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Notes: This example assumes you have already read Hello World example.
|
||||
</span>
|
||||
</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 static capture paths and ingress for your plugin.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "org.aroz.zoraxy.static-capture-example",
|
||||
Name: "Static Capture Example",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://aroz.org",
|
||||
Description: "An example for showing how static capture works in Zoraxy.",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{ CapturePath: "/test_a" },
|
||||
{ CapturePath: "/test_b" },
|
||||
},
|
||||
StaticCaptureIngress: "/s_capture",
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Note the
|
||||
<span class="ts-text is-code">
|
||||
StaticCapturePaths
|
||||
</span>
|
||||
. These are the paths that you want to capture in your plugin. These paths will be registered to Zoraxy and when a user have request that matches these paths (including subpaths), the request will get forwarded to your plugin. In this example, we are intercepting the
|
||||
<span class="ts-text is-code">
|
||||
/test_a
|
||||
</span>
|
||||
and
|
||||
<span class="ts-text is-code">
|
||||
test_b
|
||||
</span>
|
||||
sub-path.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
We also defined a new value named
|
||||
<span class="ts-text is-code">
|
||||
StaticCaptureIngress
|
||||
</span>
|
||||
. This is to tell Zoraxy that “if you receive requests that matches the above Static capture paths, please forward the request to this endpoint”. In this example, this plugin asked Zoraxy to forward th HTTP traffic to
|
||||
<span class="ts-text is-code">
|
||||
/s_capture
|
||||
</span>
|
||||
if anything is matched.
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="3-register-static-capture-handlers">
|
||||
3. Register Static Capture Handlers
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Static capture handlers are used to process requests to the defined paths. Similar to ordinary http.HandleFunc, you can register
|
||||
<span class="ts-text is-code">
|
||||
http.HandleFunc
|
||||
</span>
|
||||
as follows.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">pathRouter := plugin.NewPathRouter()
|
||||
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The
|
||||
<span class="ts-text is-code">
|
||||
SetDefaultHandler
|
||||
</span>
|
||||
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
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Here are examples of handlers for the captured paths:
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="handler-for-test-a">
|
||||
Handler for
|
||||
<span class="ts-text is-code">
|
||||
/test_a
|
||||
</span>
|
||||
</h3>
|
||||
<pre><code class="language-go">func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="handler-for-test-b">
|
||||
Handler for
|
||||
<span class="ts-text is-code">
|
||||
/test_b
|
||||
</span>
|
||||
</h3>
|
||||
<pre><code class="language-go">func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
When the user request any HTTP Proxy Rule with the matching path, these two handlers will response to the request and return the hardcoded string above. Again, this is just for demonstration purpose and you should implement your functions here.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="5-render-debug-ui">
|
||||
5. Render Debug UI
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The debug UI provides a simple interface for testing and inspecting requests.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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")
|
||||
}
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This is technically not related to static capturing, but it is really helpful to have a UI to help with printing debug information. You can access the page rendered by this function in the Zoraxy plugin menu. This should be replaced with the embedded web fs used in the Hello world example after the development is completed.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Static Capture Example/image-20250530164549527.png" alt="image-20250530164549527" />
|
||||
</div>
|
||||
</p>
|
||||
<h2 id="6-full-code">
|
||||
6. Full Code
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Here is the complete code for the static capture example:
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-go">package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/static-capture-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.static-capture-example"
|
||||
UI_PATH = "/ui"
|
||||
STATIC_CAPTURE_INGRESS = "/s_capture"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "Static Capture Example",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://aroz.org",
|
||||
Description: "An example for showing how static capture works in Zoraxy.",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{ CapturePath: "/test_a" },
|
||||
{ CapturePath: "/test_b" },
|
||||
},
|
||||
StaticCaptureIngress: STATIC_CAPTURE_INGRESS,
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pathRouter := plugin.NewPathRouter()
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
|
||||
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
|
||||
fmt.Println("Static path capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
}
|
||||
|
||||
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="7-expected-output">
|
||||
7. 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 “debug” tag and assign it to a localhost loopback HTTP proxy rule.
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Static Capture Example/image-20250530164903842.png" alt="image-20250530164903842" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Static Capture Example/image-20250530164916476.png" alt="image-20250530164916476" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
When the plugin is running, requests to
|
||||
<span class="ts-text is-code">
|
||||
/test_a
|
||||
</span>
|
||||
and
|
||||
<span class="ts-text is-code">
|
||||
/test_b
|
||||
</span>
|
||||
will be intercepted by their respective handlers.
|
||||
<span class="ts-text is-heavy">
|
||||
Requests to other paths will not pass through your plugin and will be handled by the default upstream server set by the HTTP proxy Rule.
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/3. Static Capture Example/image-20250530165014188.png" alt="image-20250530165014188" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Example terminal output for requesting
|
||||
<span class="ts-text is-code">
|
||||
/test_a
|
||||
</span>
|
||||
:
|
||||
</p>
|
||||
</p>
|
||||
<pre><span class="ts-text is-code">This request is captured by A handler!
|
||||
Request URI: /test_a
|
||||
</span></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Example output for requesting
|
||||
<span class="ts-text is-code">
|
||||
/test_b
|
||||
</span>
|
||||
:
|
||||
</p>
|
||||
</p>
|
||||
<pre><span class="ts-text is-code">This request is captured by the B handler!
|
||||
Request URI: /test_b
|
||||
</span></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Enjoy exploring static capture in Zoraxy!
|
||||
</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>
|
@ -0,0 +1,677 @@
|
||||
<!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="/plugins/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="/plugins/html/assets/logo.png" dark_src="/plugins/html/assets/logo_white.png" src="/plugins/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="https://github.com/tobychui/zoraxy/tree/main/example/plugins" target="_blank" class="item">
|
||||
Examples
|
||||
<span class="ts-icon is-arrow-up-right-from-square-icon"></span>
|
||||
</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="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/plugins/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="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</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(&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)
|
||||
}
|
||||
</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("/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)))
|
||||
}
|
||||
}
|
||||
})
|
||||
</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 “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.
|
||||
</p>
|
||||
<pre><code class="language-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"`
|
||||
}
|
||||
</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, "**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")
|
||||
}
|
||||
</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 (
|
||||
"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")
|
||||
}
|
||||
|
||||
</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 “debug” 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: "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
|
||||
</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>
|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 47 KiB |
BIN
docs/plugins/html/assets/banner.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
docs/plugins/html/assets/banner.psd
Normal file
BIN
docs/plugins/html/assets/logo.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/plugins/html/assets/logo_white.png
Normal file
After Width: | Height: | Size: 4.1 KiB |