Compare commits

...

199 Commits
v3.1.4 ... main

Author SHA1 Message Date
Toby Chui
475650de0d Updated readme
- Updated readme
- Updated social banner and title assets
2025-05-31 15:05:36 +08:00
Toby Chui
b19867865c Merge branch 'main' of https://github.com/tobychui/zoraxy 2025-05-31 14:42:08 +08:00
Toby Chui
df636c9f76 Added link to plugin doc 2025-05-31 14:41:33 +08:00
Toby Chui
e6b2cf09d7
Merge pull request #679 from tobychui/plugin_doc
- Added plugin documentation
- Added documentation generator (markdown to html)
- Added API doc generating script (using go doc command)
- Added example plugins for static and dynamic captures
- Removed ztnc from plugin example doc (moved to official plugin repo)
2025-05-31 14:39:31 +08:00
Toby Chui
bf0df928c7 Fixed typo and optimized structure 2025-05-31 14:32:41 +08:00
Toby Chui
eec6cec0db Added api doc
- Added zoraxy_plugin module api doc generating script
- Added link to example source folder
- Fixed a few minor typo
2025-05-31 14:23:11 +08:00
Toby Chui
0215171646 Updated doc for release
- Fixed bug in build.sh and doc generator
2025-05-31 13:34:41 +08:00
Toby Chui
cd822ed904 Added dynamic capture example
- Added dynamic capture plugin example
- Added dynamic capture plugin doc
- Removed ztnc from plugin example
2025-05-30 21:21:08 +08:00
Toby Chui
a1df5a1060 Added static capture example
- Added static capture example
- Added instruction on how to view plugin info
2025-05-30 17:28:14 +08:00
Toby Chui
54c130165a Fixed documentation code highlight bug
- Fixed documentation css bug in code highlight
- Added static
2025-05-30 17:00:55 +08:00
Toby Chui
ddb1a8773e Added restful-api plugin
- Added restful-api example plugin
- Added more plugin docs
- Added zoraxy_plugin HandleFunc API
2025-05-30 15:57:59 +08:00
Toby Chui
29daa4402d Added more doc
- Added auto reload for doc engine
- Added helloworld example
2025-05-27 22:00:16 +08:00
Toby Chui
a85bf82c3e Added more docs 2025-05-26 22:03:07 +08:00
Toby Chui
c56e317bfd Added doc generator
- Added plugin doc generator
- Added getting start plugin doc
2025-05-25 22:17:38 +08:00
Toby Chui
b4c2f6bf13 Added plugin doc introduction 2025-05-22 21:55:45 +08:00
Toby Chui
9d2b8f224c
Merge pull request #671 from tobychui/v3.2.2
- Merged in forward-auth implementation
- Added upgrader for v3.2.1 to v3.2.2
2025-05-20 20:01:30 +08:00
Toby Chui
877692695e Added #653
- Added dev mode plugin auto-reload
- Optimized struct in plugin manager options
2025-05-11 14:02:07 +08:00
Toby Chui
b9c609e413 Added upgrader for forward-auth
- Added v3.2.1 to v3.2.2 upgrader for new forward auth logic
- Optimized css in sso html page
2025-05-10 22:57:13 +08:00
Toby Chui
7426cc2bb1
Merge pull request #628 from PassiveLemon/docker-plugins
Refactor Dockerfile
2025-05-04 11:27:31 +08:00
PassiveLemon
bd71335f47 Merge branch 'docker-plugins' of https://github.com/PassiveLemon/zoraxy-dev into docker-plugins 2025-05-03 18:24:51 -04:00
PassiveLemon
8bcc8e7095 Refactor Dockerfile
Switch to Alpine + compat and download zoraxy_plugin from the plugins repository
2025-04-29 23:00:19 -04:00
Toby Chui
b1824a66a3
Merge pull request #643 from eyerrock/refactor-docker-container-list
refactor: docker container list
2025-04-30 07:21:51 +08:00
PassiveLemon
cc6501db12 Refactor Dockerfile
Switch to Alpine + compat and download zoraxy_plugin from the plugins repository
2025-04-27 14:51:41 -04:00
Toby Chui
70d95bd4e4 Fixed build in start.go
- Fixed build error in start.go
- Moved authelia and authentik implementation to deprecated folder for reference purpose
2025-04-27 19:24:15 +08:00
Toby Chui
b590e15ef2
Merge pull request #646 from james-d-elliott/feat-forward-auth
feat: forward auth
2025-04-27 19:14:33 +08:00
James Elliott
b25f8aab3e
refactor: apply suggestions 2025-04-27 20:27:46 +10:00
James Elliott
c0578a33b6
fix: copy paste error 2025-04-27 20:27:26 +10:00
James Elliott
55a525106a
fix: value not set 2025-04-27 20:27:26 +10:00
James Elliott
e3b68b9aad
fix: copy paste error 2025-04-27 20:27:26 +10:00
James Elliott
3f1c50c009
feat: forward auth
Add support for request headers and response client headers.
2025-04-27 20:27:26 +10:00
James Elliott
8f046a0b47
feat: forward auth
This adds basic support for forwarded authentication similar to caddy and traefik. This replaces Authelia SSO as it effectively covers exactly the same use cases.
2025-04-27 20:27:23 +10:00
Toby Chui
0e5550487e
Merge pull request #644 from tobychui/v3.2.1
- Merged in authentik forward auth support
- Merged IPv6 whitelist patch
- Added support for basic per host name statistic
- Added experimental plugin store
- Added `$remote_ip` in custom header that filters port number from `$remote_addr`
- Fixed origin is not populated in log bug
- Fixed redirection location rewrite bug
2025-04-27 14:54:27 +08:00
Toby Chui
9781735983 Moved dev settings to flag
- Moved internal DEVELOPMENT_MODE flag to start parameters
2025-04-27 13:55:54 +08:00
Tim Dreyer
a98d86a303 fix: unexposed containers with existing proxy rule 2025-04-26 16:23:17 +02:00
Tim Dreyer
73e6530862 fix: docker api client version negotiation 2025-04-25 15:49:04 +02:00
Tim Dreyer
0c753ae531 refactor: rephrased ui 2025-04-25 14:55:31 +02:00
Tim Dreyer
6353cc532a refactor: restructured js 2025-04-25 14:38:42 +02:00
Tim Dreyer
e049761f36 feat: show only running containers checkbox 2025-04-24 22:43:04 +02:00
Tim Dreyer
4dc7175588 refactor: docker container list 2025-04-24 22:32:58 +02:00
Toby Chui
ffc67ede12 Added working plugin store prototype
- Added plugin install and remove api
2025-04-24 21:19:16 +08:00
Toby Chui
6750c7fe3d Added wip plugin store
- Added plugin store snippet
- Added plugin list sync functions
- Work in progress install / uninstall plugin function
2025-04-22 07:15:30 +08:00
Toby Chui
36c2c9a00e
Merge pull request #641 from james-d-elliott/fix-authelia-headers
fix(authelia): original headers
2025-04-21 18:56:28 +08:00
James Elliott
4f026e8c07
fix(authelia): original headers
This fixes the original headers imparted to authelia.
2025-04-21 13:15:21 +10:00
Toby Chui
72b100aab0
Update README.md
Fixed contributor list format
2025-04-19 08:45:57 +08:00
Toby Chui
291f12e5ea
Update README.md
Added community maintained section contact list
2025-04-19 08:44:22 +08:00
Toby Chui
0c8dfd8aa0 Added http proxy list link port
- Added port number and http proto to http proxy list link #635
2025-04-12 11:32:22 +08:00
Toby Chui
76e2861fea
Merge pull request #633 from WHFo/v3.2.1
Update httprp.html
2025-04-11 20:09:36 +08:00
WHFo
b23b967165
Update httprp.html
Prevent the browser from filling the saved Zoraxy login account into the  input searchInput
2025-04-11 12:02:28 +08:00
Toby Chui
d682d52eb7
Merge pull request #631 from Nirostar/patch-1
Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID
2025-04-08 21:12:07 +08:00
Niklas Roth
23eeeee701
Move Scope ID handling into CIDR check 2025-04-08 15:06:20 +02:00
Niklas Roth
e961e52dea
Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID 2025-04-08 14:44:11 +02:00
Toby Chui
b863a9720f Fixed #629
- Added $remote_ip to remote port number from remote address
2025-04-07 20:02:04 +08:00
Toby Chui
ca7cd0476c Updated location rewrite logic
- Updated to a much more relax logic for handling domain with port redirection
2025-04-06 17:05:30 +08:00
Toby Chui
a3cccee162 Added "blocks common exploits" module
- Added blocks common exploits prototype
- Added bot detection function (not included in dpcore yet) #615
2025-04-06 16:50:59 +08:00
Toby Chui
b9b992a817 Fixed #626
- Added checks for port in hostname redirection in dpcore util
2025-04-06 16:49:44 +08:00
Toby Chui
19d5695f1a
Merge pull request #627 from andyburri/freebsd
Add FreeBSD amd64 support
2025-04-06 15:21:28 +08:00
Andreas Burri
bcfc777d15 Feat: Add FreeBSD amd64 support 2025-04-06 07:09:54 +00:00
PassiveLemon
caa64ada76 Update Dockerfile 2025-04-05 15:41:29 -04:00
Toby Chui
ac91a3fef1 Fixed #201 and #608
- Added domain / host name specific statistics
- Added upstream forward request count
2025-04-03 13:35:34 +08:00
Toby Chui
05f1743ecd Fixed #681
- Fixed origin is not populated in log bug
2025-04-02 20:11:43 +08:00
Toby Chui
d4c1225f75
Merge pull request #568 from JokerQyou/feature/authentik-forward-auth
[WIP] Add Authentik forward auth support
2025-03-31 20:05:52 +08:00
Toby Chui
f245a61d32
Merge pull request #616 from SamuelPalubaCZ/main
Create docker-compose.yml
2025-03-31 18:47:40 +08:00
Samuel Paluba
5c2b8e4c31
Create docker-compose.yml
So portainer can use latest compose automaticaly
2025-03-31 01:01:22 +02:00
Toby Chui
f6eef46d3f
Update README.md
Removed GAN related parameters
2025-03-30 19:18:10 +08:00
Toby Chui
3adc669db9 Update main.css
Optimized web css
2025-03-30 19:16:56 +08:00
Toby Chui
85201885f0 Update main.css 2025-03-30 12:35:39 +08:00
Toby Chui
44b65d1bfa Fixed homepage css bug on mobile view 2025-03-30 12:34:37 +08:00
PassiveLemon
6cb9e8e427 Fix Docker workflow 2025-03-29 11:34:54 -04:00
Toby Chui
d4b1cc8c57 Fixed css bug
- Fixed menu become transport on mobile view
- Fixed css font not correctly loaded on page initiate
2025-03-29 16:09:35 +08:00
Toby Chui
0e749e8a41 Fixed favicon path 2025-03-29 14:21:22 +08:00
Toby Chui
2c219eceef
Merge pull request #601 from tobychui/v3.2.0
- Removed GAN and moved to plugin
- Added upnp port forwarder plugin
- Added loopback detection to whitelist
- Optimized embed web resources router implementation
- Completed plugin prototype interface
- Updated dpcore concurrent implementation
- Merged traffic log update
- Updated homepage
- Fixed memory leak on netstat
- Fixed and updated minor dark theme color pallets
2025-03-29 13:03:40 +08:00
Toby Chui
92a27cbeb8
Merge pull request #614 from PassiveLemon/docker-320
Update Docker image to 3.2.0
2025-03-29 12:47:44 +08:00
PassiveLemon
b8a47dc620 Update Docker build workflow
Added a pull for the previous latest so the layers from those can be reused
2025-03-28 23:59:44 -04:00
PassiveLemon
c4266559be Update Docker image for 3.2.0 2025-03-28 23:58:56 -04:00
Toby Chui
136989f2ea Added plugin dir parameter
- Added plugin dir parameter
- Fixed critical architectural bug that effects plugin UI in production mode
- Updated implementation of embed FS routing
- Minor dark theme update
- Fixed ztnc UI bug for msgbox and confirm box
2025-03-28 21:24:18 +08:00
Toby Chui
3e031605fc Disable dev mode for release 2025-03-25 19:11:56 +08:00
Toby Chui
eb265e3e94 Added comments to warn future dev
- Added timestamps label cleaning
- Added comments so it doesn't get commented again in future (related to #600)
2025-03-25 07:55:13 +08:00
Toby Chui
8504ff16cb Fixed #600
- Added experimental fix to memory issue (not sure if this is related but it seems after uncommenting these the memory issue no longer show up on Firefox and Edge)
2025-03-25 07:40:39 +08:00
Toby Chui
b71437058f Fine tuned dark theme color 2025-03-23 12:47:23 +08:00
Toby Chui
4d16758e0a
Merge pull request #593 from Raithmir/main
Update logger to include UserAgent
2025-03-22 12:32:13 +08:00
Raithmir
f2b4c47805
Update trafficlog.go
Update logger to include browser UserAgent string in log lines. This will allow Crowdsec to filter for known bad useragents.
2025-03-21 16:59:22 +00:00
Raithmir
7dff4f83b4
Update trafficlog.go
Add UserAgent to log lines.
2025-03-21 16:48:47 +00:00
Toby Chui
eb24bc0391 Fixed #591
- Updated dark theme color pallets
- Added automatic background change on login page
2025-03-20 21:51:47 +08:00
Toby Chui
dac3e8c925 Added German localization to homepage 2025-03-19 21:27:41 +08:00
Toby Chui
3f1c1f1395 Added screenshot zoom feature 2025-03-19 21:09:42 +08:00
Toby Chui
cd15fdf3c1 Updated new project hompage (wip) 2025-03-18 21:43:00 +08:00
Toby Chui
0fdfda436b Added plugin info view
- Added plugin info view
- Removed zerotier related start parameters
- Added automatic tag filter reset on tab change in http proxy page
2025-03-16 11:41:46 +08:00
Toby Chui
f8270e46c2 Added UI for plugin system and upnp example
- Added wip UI for plugin tag system
- Added upnp port forwarder plugin
- Added error and fatal printout for plugins
- Optimized UI flow for plugin context window
- Added dev web server for plugin development purpose
2025-03-15 21:02:44 +08:00
Toby Chui
4a99afa2f0 Fixed deadlock in plugin manager close
- Moved all mutex away from defer
- Updated plugin manager close logic to prevent deadlock
2025-03-11 21:22:09 +08:00
Toby Chui
dfd5ef5578 Modernized dpcore code
- Rewritten dpcore transport object with deprecated API removed
- Optimized concurrent connection counts from 32 to 256
- Updated random port range for plugins
- Added debug output to plugin library
2025-03-10 22:00:33 +08:00
Toby Chui
3e57a90bb6 Fixed #573
- Added whitelist loopback quick toggle
- Fixed plugin exit stuck bug
2025-03-09 17:02:48 +08:00
Toby Chui
23d4df1ed7 Added dynamic capture capabilities to plugin API
- Added dynamic capture ingress and sniff endpoint
- Removed static capture example (API update)
2025-03-02 12:26:44 +08:00
Toby Chui
39d6d16c2a Updated plugin interface
- Updated plugin interface to support static path routing
- Added autosave for statistic data (workaround for #561)
2025-03-02 09:15:50 +08:00
Toby Chui
b7e3888513 Removed zerotier related info
- Removed zt info from utils.html
2025-03-02 09:14:29 +08:00
Toby Chui
fd41a1cb91 Fixed dpcore TLSClientConfig is nil bug 2025-03-02 09:12:29 +08:00
Toby Chui
75c351e7e2 Removed GAN
- Removed UI and module of GAN
- this feature is moved to plugin
2025-03-02 09:12:07 +08:00
Joker
6a8057c3a7
fix passing wrong URI to Authentik outpost 2025-03-02 00:04:08 +08:00
Joker
ebf6ad6600
Add Authentik forward auth support 2025-03-01 15:29:36 +08:00
Toby Chui
549e492ffd
Merge pull request #567 from Morethanevil/main
Update CHANGELOG.md
2025-03-01 15:02:06 +08:00
Marcel
6351f25c00
Update CHANGELOG.md 2025-03-01 06:23:44 +01:00
Toby Chui
560b0058cd
Merge pull request #566 from tobychui/v3.1.9
- Fixed netstat underflow bug
- Fixed origin picker cookie bug
- Added prototype plugin system
- Added plugin examples
- Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
- Added country code display for quickban list
2025-03-01 10:09:46 +08:00
Toby Chui
28a0a837ba Plugin lifecycle optimization
- Added term flow before plugin is killed
- Updated example implementations
- Added SIGINT to Zoraxy for shutdown sequence (Fixes #561 ?)
2025-03-01 10:00:33 +08:00
Toby Chui
14e1341c34 Removed legacy example plugin files 2025-02-28 22:07:08 +08:00
Toby Chui
5abc4ac606 Added plugin context view
- Added plugin context view
- Moved plugin type definition to separate file
- Added wip request forwarder
2025-02-28 22:05:14 +08:00
Toby Chui
214b69b0b8 Updated example plugins
- Updated example plugins
- Added debugger
- Removed some trash files
2025-02-28 22:03:08 +08:00
Toby Chui
3993ac954c Fixed #247
- Added country of origin row for quickban list
2025-02-28 22:01:11 +08:00
Toby Chui
53657e8716 Added embed server for plugin library
- Added embeded resources server for plugin library
- Added ztnc plugin for global area network
- Added wide mode for side wrapper
2025-02-28 15:46:57 +08:00
Toby Chui
bddff0cf2f Added working plugin manager prototype
- Added experimental plugin UI proxy
- Added plugin icon loader
- Added plugin table renderer
2025-02-27 22:27:13 +08:00
Toby Chui
dd4df0b4db Update originPicker.go
- Removed unused function
2025-02-26 21:20:35 +08:00
Toby Chui
85709dacf6 Fixed #550
- Updated to not set the session cookie and lets the fallback method to detect for change in upstreams
2025-02-26 21:19:41 +08:00
Toby Chui
ad13b33283 Added plugin prototype
- Added proof of concept plugin prototype
- Added wip plugin page
2025-02-25 21:14:03 +08:00
Toby Chui
20959cd6cc Fixed #554
- Removed passive load balancer and default to active lb only
2025-02-20 20:25:20 +08:00
Toby Chui
394cf50e1d #550
- Instead of clearing the Zoraxy cookie on the client side, set the Zoraxy session in the server side to an empty value instead
2025-02-19 21:38:27 +08:00
Toby Chui
1116b643b5 Added plugin interface definations
- Added wip plugin interface
- Merged in PR for lego update
- Minor code optimization
2025-02-19 21:25:50 +08:00
Toby Chui
2e9d70da83
Merge pull request #545 from tobychui/bugfix_acme_LE_http01
Fixed lets encrypt ACME fail bug
2025-02-18 18:50:50 +08:00
Toby Chui
6130459f7c
Update issue templates
Added more details for bug templates
2025-02-18 18:50:34 +08:00
Toby Chui
2d29065812
Merge pull request #546 from Morethanevil/main
Update CHANGELOG.md
2025-02-17 06:23:57 +08:00
Marcel
2be7f711ba
Update CHANGELOG.md 2025-02-16 19:42:23 +01:00
Toby Chui
de9d3bfb65 Fixed netstat underflow bug
- Fixed netstat sometime underflow to a large negative number bug
2025-02-16 21:10:56 +08:00
Toby Chui
3e4c66b34f Updated lego
- Updated lego to fix ACME issue on lets encrypt
2025-02-16 20:44:02 +08:00
Toby Chui
895ee1e53f
Merge pull request #544 from tobychui/v3.1.8
- Exposed timeout value from dpcore to UI
- Added active load balancing (if uptime monitor is enabled on that rule)
- Refactorized io stats and remove dependencies over wmic
- Removed SMTP input validation
- Fixed sticky session bug
- Fixed passive load balancer bug
- Fixed dockerfile bug
2025-02-16 17:13:37 +08:00
Toby Chui
caf4ab331b Exposed dpcore timeout options
- Exposed idle timeout and response timeout option
- Updated upstream edit UI to use the new API
- Updated geodb
2025-02-16 16:58:25 +08:00
Toby Chui
36c1f149e6 Fixed #497
- Removed SMTP input validations
- Updated version no.
- Added todo for removing SMTP all together in future revisions
2025-02-16 09:08:08 +08:00
PassiveLemon
b0dc4d6670 Fix #542 2025-02-15 16:26:10 -05:00
Toby Chui
5d8bec7f24 Fixed sticky session bug
- Fixed sticky session bug in new active fallback lb implementation
2025-02-14 22:53:29 +08:00
Toby Chui
32f60dfba6 Fixed #523
- Fixed passive fallback logic
- Added active fallback setting notify from uptime monitor
2025-02-14 22:04:51 +08:00
Toby Chui
0abe4c12cf Fixed #526
- Fixed typos
2025-02-12 20:58:22 +08:00
Toby Chui
7555611ba5 Fixed h2c enable crash bug
- Moved h2c roundtripper to a dedicated module
- Fixed h2c enable crash bug
2025-02-11 21:53:21 +08:00
Toby Chui
e624227dae
Merge pull request #520 from eyerrock/wmic-refactor
Remove WMIC dependency and unify network stats retrieval
2025-02-11 19:38:56 +08:00
Toby Chui
27695584ab
Update README.md
Added new start flags into README
2025-02-09 13:44:55 +08:00
Toby Chui
e47a7a8357
Merge pull request #525 from Morethanevil/main
Update CHANGELOG.md
2025-02-09 10:53:13 +08:00
Marcel
3246f8ea2c
Update CHANGELOG.md
:)
2025-02-08 23:52:37 +01:00
Tim Dreyer
ccbda6d7c2 refactored io stats 2025-02-08 16:11:47 +01:00
Toby Chui
a7285438af
Merge pull request #522 from tobychui/v3.1.7
- Merged and added new tagging system for HTTP Proxy rules
- Added inline editing for redirection rules
- Added uptime monitor status dot detail info (now clickable)
- Added close connection support to port 80 listener
- Optimized port collision check on startup
- Optimized dark theme color scheme (Free consultation by [3S Design studio](https://www.3sdesign.io/))
- Fixed capital letter rule unable to delete bug
2025-02-08 18:40:15 +08:00
Toby Chui
693dba07b7 Updated tag filtering
- Added automatic empty tag removal when creating new proxy rule
2025-02-08 17:07:26 +08:00
Toby Chui
9b64278200
Merge pull request #521 from PassiveLemon/docker-term-fix
Refactor: Launch services in background and trap Docker TERM signal
2025-02-08 16:09:38 +08:00
Toby Chui
d04eff2bda Updated geodb
- Updated geoip database
2025-02-08 16:08:33 +08:00
Toby Chui
3320b56b19 Update tagEditor.html
- Optimized UX for tag editor
- Finished integration of tag system
2025-02-08 15:19:36 +08:00
PassiveLemon
99728144b3 Refactor: Launch services in background and trap Docker TERM signal 2025-02-08 01:37:03 -05:00
Toby Chui
05511ed4ca Updated tag system design
- Added search-able tag dropdown
- Implemented realtime quick search
- Added better tag coloring
2025-02-07 22:08:56 +08:00
Toby Chui
70abfe6fcf Restore dockerfile
- The docker file change shd be included in another PR
2025-02-06 20:36:23 +08:00
Toby Chui
6ab91c377f
Merge pull request #509 from adoolaard/dev-tags
Add Tagging Feature for Reverse Proxy Hosts + Search & Filter
2025-02-06 20:35:32 +08:00
Toby Chui
1863af0d63 Minor css update
- Changed inline edit button for redirection rule to circular to match http proxy rule page
2025-02-05 20:33:38 +08:00
Toby Chui
2a9d87787d Fixed #510
- Added inline edit for redirection rule
2025-02-05 20:24:42 +08:00
adoolaard
f753becd66
The proxy hosts broke on import, because the tags were missing. This is now fixed. 2025-02-03 15:10:13 +01:00
Toby Chui
bb2d0d5b46 Fixed #507 2025-02-03 21:10:24 +08:00
Toby Chui
07dc63a82c Added H2C (experimental)
- Added experimental H2C transporter
- Exposed default listening port and web server listen state to start parameters #474
2025-02-03 20:36:34 +08:00
adoolaard
97a6cf016a
Point on the I 2025-01-31 00:17:10 +01:00
adoolaard
8df68f1f4e
Zoeken en filteren werkt ook! 2025-01-30 22:48:48 +01:00
adoolaard
e4ad505f2a
Tags editor works! 2025-01-30 22:42:06 +01:00
adoolaard
a402c4f326
Tags are working, just not yet editable 2025-01-30 22:22:42 +01:00
adoolaard
791fbfa1b4
Updated gitignore 2025-01-30 21:48:40 +01:00
adoolaard
c49f2fd1db
Changed dockerfile to better cache 2025-01-30 21:22:19 +01:00
Toby Chui
7d9f240d56 Updated Close Conn resp for TLS
- Use No Resp instead of 200 for close connection mode default site settings
2025-01-18 22:10:45 +08:00
Toby Chui
e20f816080 Fixed #467
- Added status dot info in uptime monitor
- simplified the no response record to no_resp in default site
2025-01-18 21:49:35 +08:00
Toby Chui
eeb438eb18 Fixed #474
- Added automatic port check and reminder for beginners
2025-01-18 15:19:55 +08:00
Toby Chui
bfd64a885e Removed confirm from access
- Removed troublesome confirm popup from black / whitelist
- Minor fix to checkbox css
2025-01-15 20:59:09 +08:00
Toby Chui
45f61b3053 Optimized dark theme mode
- Make dark theme mode less dark
2025-01-15 20:44:20 +08:00
Toby Chui
0d4c71d0f6 Fixed #450 2024-12-31 22:56:51 +08:00
Toby Chui
d1e5581eea
Merge pull request #449 from tobychui/v3.1.6
- Exposed log file, sys.uuid and static web server path to start flag
- Optimized connection close implementation
- Added toggle for uptime monitor
- Added optional copy HTTP custom headers to websocket connection
2024-12-31 21:49:41 +08:00
Toby Chui
be5797c8a5 Updated geodb and minor instructions 2024-12-31 21:47:19 +08:00
Toby Chui
ebd316a7f1 Exposed log and db filepath setting 2024-12-31 21:14:37 +08:00
Toby Chui
84aec4387a Added CF and Fastly IP in access list
Added CF and Fastly Client IP passthrough header for access control ip resolver
2024-12-31 20:30:36 +08:00
Toby Chui
30dfb9cb65 Added new UI feature
- Added toggle for uptime monitor
- Added toggle for enable custom header passthrough to websocket
2024-12-30 21:41:15 +08:00
Toby Chui
0b1768ab5b Added manual toggle for websocket header copy
- Added setting for toggling websocket header copy
- Added close connection in TLS mode
2024-12-30 21:07:29 +08:00
Toby Chui
ad4721820b Added websocket header test and benchmark tool 2024-12-30 21:01:45 +08:00
Toby Chui
1d4c275db3 Fixed nil pointer exception in new setups 2024-12-29 16:11:00 +08:00
Toby Chui
b3ad97743c Fixed #444
- Restored legacy behavior if proxmox cookie is detected in request
2024-12-29 15:09:24 +08:00
Toby Chui
1a6a87e79b
Merge pull request #443 from Morethanevil/main
Update CHANGELOG.md
2024-12-28 15:19:43 +08:00
Marcel
749fd4b7af
Update CHANGELOG.md 2024-12-28 05:25:00 +01:00
Toby Chui
85422c0a74
Merge pull request #439 from tobychui/v3.1.5
Fixed hostname case sensitive bug
Fixed ACME table too wide css bug
Fixed HSTS toggle button bug
Fixed slow GeoIP resolve mode concurrent r/w bug
Added close connection as default site option
Added experimental authelia support
Added custom header support to websocket
Added levelDB as database implementation (not currently used)
Added external GeoIP db loading support
Restructured a lot of modules
2024-12-27 22:12:55 +08:00
Toby Chui
73999c1ae9
Merge pull request #440 from PassiveLemon/docker-3.1.5
Add 2 new flags to Docker container and image build instructions
2024-12-27 21:26:18 +08:00
PassiveLemon
0ad84b3415 Add 2 new flags 2024-12-26 16:17:02 -05:00
Toby Chui
64b6769695 Added external geoip db option
- Added support for loading geoip db from external file
- Added -update_geoip flag for automatically update the geoip
2024-12-24 21:12:26 +08:00
Toby Chui
e72b2f9e09 Updated geoip database 2024-12-24 20:34:10 +08:00
Toby Chui
992dd231f2 Fixed #435 2024-12-22 13:25:16 +08:00
Toby Chui
49555c1191 Fixed #430
+ Added no response and I'm a Teapot (config file editing only) to default site options
2024-12-17 22:08:32 +08:00
PassiveLemon
2fca458bd0 Image building instructions and README touch-ups 2024-12-16 18:14:02 -05:00
Toby Chui
2423d0fb3a Added experimental authelia support
- Integrated #33 code snippet
- Added UI for setting Authelia server address
- Updated authentication provider implementation
2024-12-15 15:52:59 +08:00
Toby Chui
bb0f55018c System arch optimization
- Optimized types and definitions
- Moved shutdown seq to start.go file
- Moved authelia to auth/sso module
- Added different auth types support (wip)
- Updated proxy config structure
- Added v3.1.4 to v3.1.5 auto upgrade utilities
- Fixed #426
- Optimized status page UI
- Added options to disable uptime montior in config
2024-12-12 20:49:53 +08:00
Toby Chui
9e95d84627 Fixed #422
- Added scroll to acme domain table
2024-12-10 21:13:26 +08:00
Toby Chui
e73841786b
Merge pull request #421 from 7brend7/authelia-integration
Add authelia-verify support
2024-12-10 21:02:58 +08:00
Borys Anikiyenko
d5449c947a Add authelia-verify suppport 2024-12-09 15:19:07 +02:00
Toby Chui
8ff51044bb Fixed #414
- Added sticky menu
- Optimized terminate routine for nil check
- Added test case for statistic module
2024-12-08 12:54:50 +08:00
Toby Chui
cc08c704de Database update
- Removed read-only mode
- Added LevelDB for big data storage

TODO: Update backup utilities to support new db structure
2024-12-06 23:34:21 +08:00
Toby Chui
2f1a6b5ba4
Merge pull request #416 from tobychui/main
Sync update from main branch
2024-12-06 19:46:51 +08:00
Toby Chui
4d163fe80f
Merge pull request #406 from Sickjuicy/main
Domain Name Server Option
2024-12-06 19:29:29 +08:00
Toby Chui
24371ed22e Fixed #415
- Fixed UI issue on the HSTS toggle
- Added error message on save error for HSTS
2024-12-06 19:06:59 +08:00
sickjuicy
12358d3522 added downward compality and spaces are cut from the json 2024-12-01 16:01:23 +01:00
Toby Chui
c39af1ff8e Update def.go
Updated version number
2024-12-01 21:22:43 +08:00
Toby Chui
6bf944e13c Fixed #401
- Fixed high concurrency panic on slow geoIP resolve mode
- Added test case for concurrent geodb access
2024-12-01 21:21:53 +08:00
sickjuicy
b653b805b8 Update autorenew.go 2024-12-01 04:29:29 +01:00
sickjuicy
eb91865b70 Added to read json for the renew cert and fixed bug where on creation of a new cert the old NameServer ware used 2024-12-01 04:25:01 +01:00
sickjuicy
57e72a8a90 added some commands back 2024-11-30 04:38:29 +01:00
sickjuicy
4dbf110edc more Cleanup 2024-11-30 04:20:39 +01:00
sickjuicy
1eefa99b72 Cleanup 2024-11-30 03:35:30 +01:00
sickjuicy
e6b2d458f7 Added Custom Name Server Option 2024-11-26 23:30:24 +01:00
Toby Chui
4a4483e09d
Merge pull request #400 from Morethanevil/main
Update CHANGELOG.md
2024-11-24 20:05:02 +08:00
Marcel
4485d1f811
Update CHANGELOG.md
Updated changelog

Great Work as always, dark mode looks cool. If you want a suggestion about colors, I recommend [Catppuccin](https://github.com/catppuccin)
2024-11-24 11:52:03 +01:00
356 changed files with 83567 additions and 46787 deletions

View File

@ -28,12 +28,16 @@ If applicable, add screenshots to help explain your problem.
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Host Environment (please complete the following information):**
**Host Environment (please complete following information, DO NOT REMOVE ANY FIELD(S)):**
- Arch: [e.g. arm64]
- Device: [e.g. Bananapi R2 PRO]
- OS: [e.g. Armbian]
- Version [e.g. 23.02 Bullseye ]
- Docker Version (if you are running Zoraxy in docker): [e.g. 3.0.4]
- Are you using Docker? (yes / no)
- Docker Version (fill in "N/A" for native deployment): [e.g. 3.0.4]
**Supplementary links**
If your issue is related to a particular open source project, paste the link here.
**Additional context**
Add any other context about the problem here.

View File

@ -24,10 +24,14 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Pull last image for layer reuse
run: |
docker pull docker.io/zoraxydocker/zoraxy:latest
- name: Setup building file structure
run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
- name: Build and push Docker image
uses: docker/build-push-action@v6

15
.gitignore vendored
View File

@ -30,7 +30,7 @@ src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
docker/docker-compose.yaml
src/mod/acme/test/stackoverflow.pem
/tools/dns_challenge_update/code-gen/acmedns
/tools/dns_challenge_update/code-gen/lego
@ -39,4 +39,15 @@ src/tmp/localhost.pem
src/www/html/index.html
src/sys.uuid
src/zoraxy
src/log/
src/log/
# dev-tags
/Dockerfile
/Entrypoint.sh
# plugins
example/plugins/ztnc/ztnc.db
example/plugins/ztnc/authtoken.secret
example/plugins/ztnc/ztnc.db.lock
*.exe

View File

@ -1,3 +1,74 @@
# v3.1.9 1 Mar 2025
+ Fixed netstat underflow bug
+ Fixed origin picker cookie bug [#550](https://github.com/tobychui/zoraxy/issues/550)
+ Added prototype plugin system
+ Added plugin examples
+ Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
+ Added country code display for quickban list [#247](https://github.com/tobychui/zoraxy/issues/247)
+ Removed passive load balancer and default to active lb only [#554](https://github.com/tobychui/zoraxy/issues/554)
# v3.1.8 16 Feb 2025
+ Exposed timeout value from dpcore to UI
+ Added active load balancing (if uptime monitor is enabled on that rule)
+ Re-factorized io stats and remove dependencies over wmic by [eyerrock](https://github.com/eyerrock)
+ Removed SMTP input validation [#497](https://github.com/tobychui/zoraxy/issues/497)
+ Fixed sticky session bug
+ Fixed passive load balancer bug
+ Fixed dockerfile bug by [PassiveLemon](https://github.com/PassiveLemon)
# v3.1.7 08 Feb 2025
+ Merged and added new tagging system for HTTP Proxy rules [by @adoolaard](https://github.com/adoolaard)
+ Added inline editing for redirection rules [#510](https://github.com/tobychui/zoraxy/issues/510)
+ Added uptime monitor status dot detail info (now clickable) [#467](https://github.com/tobychui/zoraxy/issues/467)
+ Added close connection support to port 80 listener [#405](https://github.com/tobychui/zoraxy/issues/450)
+ Optimized port collision check on startup
+ Optimized dark theme color scheme (Free consultation by 3S Design studio)
+ Fixed capital letter rule unable to delete bug [#507](https://github.com/tobychui/zoraxy/issues/507)
+ Fixed docker statistic not save bug [by @PassiveLemon](https://github.com/PassiveLemon) [#505](https://github.com/tobychui/zoraxy/issues/505)
# v3.1.6 31 Dec 2024
+ Exposed log file, sys.uuid and static web server path to start flag (customizable conf and sys.db path is still wip)
+ Optimized connection close implementation
+ Added toggle for uptime monitor
+ Added optional copy HTTP custom headers to websocket connection [#444](https://github.com/tobychui/zoraxy/issues/444)
# v3.1.5 28 Dec 2024
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
+ Added levelDB as database implementation (not currently used)
+ Added external GeoIP db loading support
+ Restructured a lot of modules
# v3.1.4 24 Nov 2024
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)
+ Added an auto sniffer for self-signed certificates
+ Added robots.txt to the project
+ Introduced an EU wrapper in the front-end for automatic registration of 26 countries [#378](https://github.com/tobychui/zoraxy/issues/378)
+ Moved all hard-coded values to a dedicated def.go file
+ Fixed a panic issue occurring on unsupported platform exits
+ Integrated fixes for SSH proxy and Docker snippet updates [#330](https://github.com/tobychui/zoraxy/issues/330) [#348](https://github.com/tobychui/zoraxy/issues/348)
+ **Changed the default listening port to 443 and enable TLS by default**
+ Optimized GeoIP database slow-search mode CPU usage
# v3.1.3 12 Nov 2024
+ Fixed a critical security bug [CVE-2024-52010](https://github.com/advisories/GHSA-7hpf-g48v-hw3j)
# v3.1.2 03 Nov 2024
+ Added auto start port 80 listener on acme certificate generator

View File

@ -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
@ -101,32 +103,44 @@ Usage of zoraxy:
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-cfgupgrade
Enable auto config upgrade if breaking change is detected (default true)
-db string
Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto")
-default_inbound_enabled
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
Number of days to early renew a soon expiring certificate (days) (default 30)
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-log string
Log folder path (default "./log")
-mdns
Enable mDNS scanner and transponder (default true)
-mdnsname string
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
-noauth
Disable authentication for management interface
-plugin string
Plugin folder path (default "./plugins")
-port string
Management web interface listening port (default ":8000")
-sshlb
Allow loopback web ssh connection (DANGER)
-update_geoip
Download the latest GeoIP data and exit
-uuid string
sys.uuid file path (default "./sys.uuid")
-version
Show version of this server
-webfm
Enable web file manager for static web server root folder (default true)
-webroot string
Static web server root folder. Only allow chnage in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
ZeroTier controller API port (default 9993)
Static web server root folder. Only allow change in start paramters (default "./www")
```
### External Permission Management Mode
@ -137,7 +151,8 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*Note: For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
> [!WARNING]
> For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
## Screenshots
@ -153,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
@ -178,18 +181,38 @@ Web SSH currently only supports Linux based OSes. The following platforms are su
### Loopback Connection
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
Loopback web SSH connections, by default, are disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
```bash
./zoraxy -sshlb=true
```
## Community Maintained Sections
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.
- 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
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

View File

@ -1,3 +1,4 @@
## Build Zoraxy
FROM docker.io/golang:alpine AS build-zoraxy
RUN mkdir -p /opt/zoraxy/source/ &&\
@ -12,31 +13,56 @@ RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\
chmod 755 /usr/local/bin/zoraxy
FROM docker.io/ubuntu:latest AS build-zerotier
## 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/ubuntu:latest
RUN apt-get update -y &&\
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
## 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 --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
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 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/
@ -44,22 +70,25 @@ ENV ZEROTIER="false"
ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true"
ENV EARLYRENEW="30"
ENV FASTGEOIP="false"
ENV MDNS="true"
ENV MDNSNAME="''"
ENV NOAUTH="false"
ENV PLUGIN="/opt/zoraxy/plugin/"
ENV PORT="8000"
ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false"
ENV WEBFM="true"
ENV WEBROOT="./www"
ENV ZTAUTH=""
ENV ZTPORT="9993"
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

View File

@ -9,7 +9,7 @@
If you are attempting to access your service from outside your network, make sure to forward ports 80 and 443 to the Zoraxy host to allow web traffic. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. Read more about it from [whatismyip](https://www.whatismyip.com/port-forwarding/).
In the examples below, make sure to update `/path/to/zoraxy/config/` with your actual path. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location.
In the examples below, make sure to update `/path/to/zoraxy/config/`. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location or a named Docker volume.
Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Change the port in the URL if you changed the management port.
@ -19,14 +19,15 @@ 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 \
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
-v /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/localtime:/etc/localtime \
-e FASTGEOIP="true" \
-e ZEROTIER="true" \
zoraxydocker/zoraxy:latest
```
@ -44,11 +45,13 @@ services:
- 8000:8000
volumes:
- /path/to/zoraxy/config/:/opt/zoraxy/config/
- /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"
ZEROTIER: "true"
```
### Ports
@ -64,9 +67,15 @@ services:
| Volume | Details |
|:-|:-|
| `/opt/zoraxy/config/` | Zoraxy configuration. |
| `/opt/zoraxy/plugin/` | Zoraxy plugins. |
| `/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 hosts 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).
@ -75,21 +84,54 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|:-|:-|:-|
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
| `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. |
| `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. |
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
| `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. |
| `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). |
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
| `PLUGIN` | `/opt/zoraxy/plugin/` (String) | Set the path for Zoraxy plugins. Only change this if you know what you are doing. |
| `PORT` | `8000` (Integer) | Management web interface listening port |
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
| `VERSION` | `false` (Boolean) | Show version of this server. |
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
| `ZTAUTH` | `""` (String) | ZeroTier authtoken for the local node. |
| `ZTPORT` | `9993` (Integer) | ZeroTier controller API port. |
> [!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
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
> [!IMPORTANT]
> Plugins are currently experimental.
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
- Run the build command with `docker build -t zoraxy_build .`
- You can now use the image `zoraxy_build`
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.

19
docker/build_plugins.sh Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "Copying zoraxy_plugin to all mods..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
fi
done
echo "Running go mod tidy and go build for all directories..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cd "$dir" || exit 1
go mod tidy
go build
cd "$1" || exit 1
fi
done

18
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
services:
zoraxy:
image: zoraxydocker/zoraxy:latest
container_name: zoraxy
restart: unless-stopped
ports:
- 80:80
- 443:443
- 8000:8000
volumes:
- /path/to/zoraxy/config/:/opt/zoraxy/config/
- /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"

View File

@ -1,32 +1,55 @@
#!/usr/bin/env bash
update-ca-certificates
echo "CA certificates updated"
cleanup() {
echo "Stop signal received. Shutting down..."
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
unlink /var/lib/zerotier-one/zerotier/
exit 0
}
trap cleanup SIGTERM SIGINT TERM INT
update-ca-certificates && echo "CA certificates updated."
zoraxy -update_geoip=true && echo "GeoIP data updated ."
echo "Building plugins..."
cd /opt/zoraxy/plugin/ || exit 1
build_plugins "$PWD"
echo "Plugins built."
cd /opt/zoraxy/config/ || exit 1
if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d
echo "ZeroTier daemon started"
zerotier-one -d &
zerotierpid=$!
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
exec zoraxy \
zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
-mdns="$MDNS" \
-mdnsname="$MDNSNAME" \
-noauth="$NOAUTH" \
-plugin="$PLUGIN" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
-ztauth="$ZTAUTH" \
-ztport="$ZTPORT"
&
zoraxypid=$!
wait "$zoraxypid"
wait "$zerotierpid"

View File

@ -0,0 +1,451 @@
GNU Free Documentation License
Version 1.3, 3 November 2008
Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
0. PREAMBLE
The purpose of this License is to make a manual, textbook, or other
functional and useful document "free" in the sense of freedom: to
assure everyone the effective freedom to copy and redistribute it,
with or without modifying it, either commercially or noncommercially.
Secondarily, this License preserves for the author and publisher a way
to get credit for their work, while not being considered responsible
for modifications made by others.
This License is a kind of "copyleft", which means that derivative
works of the document must themselves be free in the same sense. It
complements the GNU General Public License, which is a copyleft
license designed for free software.
We have designed this License in order to use it for manuals for free
software, because free software needs free documentation: a free
program should come with manuals providing the same freedoms that the
software does. But this License is not limited to software manuals;
it can be used for any textual work, regardless of subject matter or
whether it is published as a printed book. We recommend this License
principally for works whose purpose is instruction or reference.
1. APPLICABILITY AND DEFINITIONS
This License applies to any manual or other work, in any medium, that
contains a notice placed by the copyright holder saying it can be
distributed under the terms of this License. Such a notice grants a
world-wide, royalty-free license, unlimited in duration, to use that
work under the conditions stated herein. The "Document", below,
refers to any such manual or work. Any member of the public is a
licensee, and is addressed as "you". You accept the license if you
copy, modify or distribute the work in a way requiring permission
under copyright law.
A "Modified Version" of the Document means any work containing the
Document or a portion of it, either copied verbatim, or with
modifications and/or translated into another language.
A "Secondary Section" is a named appendix or a front-matter section of
the Document that deals exclusively with the relationship of the
publishers or authors of the Document to the Document's overall
subject (or to related matters) and contains nothing that could fall
directly within that overall subject. (Thus, if the Document is in
part a textbook of mathematics, a Secondary Section may not explain
any mathematics.) The relationship could be a matter of historical
connection with the subject or with related matters, or of legal,
commercial, philosophical, ethical or political position regarding
them.
The "Invariant Sections" are certain Secondary Sections whose titles
are designated, as being those of Invariant Sections, in the notice
that says that the Document is released under this License. If a
section does not fit the above definition of Secondary then it is not
allowed to be designated as Invariant. The Document may contain zero
Invariant Sections. If the Document does not identify any Invariant
Sections then there are none.
The "Cover Texts" are certain short passages of text that are listed,
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
the Document is released under this License. A Front-Cover Text may
be at most 5 words, and a Back-Cover Text may be at most 25 words.
A "Transparent" copy of the Document means a machine-readable copy,
represented in a format whose specification is available to the
general public, that is suitable for revising the document
straightforwardly with generic text editors or (for images composed of
pixels) generic paint programs or (for drawings) some widely available
drawing editor, and that is suitable for input to text formatters or
for automatic translation to a variety of formats suitable for input
to text formatters. A copy made in an otherwise Transparent file
format whose markup, or absence of markup, has been arranged to thwart
or discourage subsequent modification by readers is not Transparent.
An image format is not Transparent if used for any substantial amount
of text. A copy that is not "Transparent" is called "Opaque".
Examples of suitable formats for Transparent copies include plain
ASCII without markup, Texinfo input format, LaTeX input format, SGML
or XML using a publicly available DTD, and standard-conforming simple
HTML, PostScript or PDF designed for human modification. Examples of
transparent image formats include PNG, XCF and JPG. Opaque formats
include proprietary formats that can be read and edited only by
proprietary word processors, SGML or XML for which the DTD and/or
processing tools are not generally available, and the
machine-generated HTML, PostScript or PDF produced by some word
processors for output purposes only.
The "Title Page" means, for a printed book, the title page itself,
plus such following pages as are needed to hold, legibly, the material
this License requires to appear in the title page. For works in
formats which do not have any title page as such, "Title Page" means
the text near the most prominent appearance of the work's title,
preceding the beginning of the body of the text.
The "publisher" means any person or entity that distributes copies of
the Document to the public.
A section "Entitled XYZ" means a named subunit of the Document whose
title either is precisely XYZ or contains XYZ in parentheses following
text that translates XYZ in another language. (Here XYZ stands for a
specific section name mentioned below, such as "Acknowledgements",
"Dedications", "Endorsements", or "History".) To "Preserve the Title"
of such a section when you modify the Document means that it remains a
section "Entitled XYZ" according to this definition.
The Document may include Warranty Disclaimers next to the notice which
states that this License applies to the Document. These Warranty
Disclaimers are considered to be included by reference in this
License, but only as regards disclaiming warranties: any other
implication that these Warranty Disclaimers may have is void and has
no effect on the meaning of this License.
2. VERBATIM COPYING
You may copy and distribute the Document in any medium, either
commercially or noncommercially, provided that this License, the
copyright notices, and the license notice saying this License applies
to the Document are reproduced in all copies, and that you add no
other conditions whatsoever to those of this License. You may not use
technical measures to obstruct or control the reading or further
copying of the copies you make or distribute. However, you may accept
compensation in exchange for copies. If you distribute a large enough
number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and
you may publicly display copies.
3. COPYING IN QUANTITY
If you publish printed copies (or copies in media that commonly have
printed covers) of the Document, numbering more than 100, and the
Document's license notice requires Cover Texts, you must enclose the
copies in covers that carry, clearly and legibly, all these Cover
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
the back cover. Both covers must also clearly and legibly identify
you as the publisher of these copies. The front cover must present
the full title with all words of the title equally prominent and
visible. You may add other material on the covers in addition.
Copying with changes limited to the covers, as long as they preserve
the title of the Document and satisfy these conditions, can be treated
as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit
legibly, you should put the first ones listed (as many as fit
reasonably) on the actual cover, and continue the rest onto adjacent
pages.
If you publish or distribute Opaque copies of the Document numbering
more than 100, you must either include a machine-readable Transparent
copy along with each Opaque copy, or state in or with each Opaque copy
a computer-network location from which the general network-using
public has access to download using public-standard network protocols
a complete Transparent copy of the Document, free of added material.
If you use the latter option, you must take reasonably prudent steps,
when you begin distribution of Opaque copies in quantity, to ensure
that this Transparent copy will remain thus accessible at the stated
location until at least one year after the last time you distribute an
Opaque copy (directly or through your agents or retailers) of that
edition to the public.
It is requested, but not required, that you contact the authors of the
Document well before redistributing any large number of copies, to
give them a chance to provide you with an updated version of the
Document.
4. MODIFICATIONS
You may copy and distribute a Modified Version of the Document under
the conditions of sections 2 and 3 above, provided that you release
the Modified Version under precisely this License, with the Modified
Version filling the role of the Document, thus licensing distribution
and modification of the Modified Version to whoever possesses a copy
of it. In addition, you must do these things in the Modified Version:
A. Use in the Title Page (and on the covers, if any) a title distinct
from that of the Document, and from those of previous versions
(which should, if there were any, be listed in the History section
of the Document). You may use the same title as a previous version
if the original publisher of that version gives permission.
B. List on the Title Page, as authors, one or more persons or entities
responsible for authorship of the modifications in the Modified
Version, together with at least five of the principal authors of the
Document (all of its principal authors, if it has fewer than five),
unless they release you from this requirement.
C. State on the Title page the name of the publisher of the
Modified Version, as the publisher.
D. Preserve all the copyright notices of the Document.
E. Add an appropriate copyright notice for your modifications
adjacent to the other copyright notices.
F. Include, immediately after the copyright notices, a license notice
giving the public permission to use the Modified Version under the
terms of this License, in the form shown in the Addendum below.
G. Preserve in that license notice the full lists of Invariant Sections
and required Cover Texts given in the Document's license notice.
H. Include an unaltered copy of this License.
I. Preserve the section Entitled "History", Preserve its Title, and add
to it an item stating at least the title, year, new authors, and
publisher of the Modified Version as given on the Title Page. If
there is no section Entitled "History" in the Document, create one
stating the title, year, authors, and publisher of the Document as
given on its Title Page, then add an item describing the Modified
Version as stated in the previous sentence.
J. Preserve the network location, if any, given in the Document for
public access to a Transparent copy of the Document, and likewise
the network locations given in the Document for previous versions
it was based on. These may be placed in the "History" section.
You may omit a network location for a work that was published at
least four years before the Document itself, or if the original
publisher of the version it refers to gives permission.
K. For any section Entitled "Acknowledgements" or "Dedications",
Preserve the Title of the section, and preserve in the section all
the substance and tone of each of the contributor acknowledgements
and/or dedications given therein.
L. Preserve all the Invariant Sections of the Document,
unaltered in their text and in their titles. Section numbers
or the equivalent are not considered part of the section titles.
M. Delete any section Entitled "Endorsements". Such a section
may not be included in the Modified Version.
N. Do not retitle any existing section to be Entitled "Endorsements"
or to conflict in title with any Invariant Section.
O. Preserve any Warranty Disclaimers.
If the Modified Version includes new front-matter sections or
appendices that qualify as Secondary Sections and contain no material
copied from the Document, you may at your option designate some or all
of these sections as invariant. To do this, add their titles to the
list of Invariant Sections in the Modified Version's license notice.
These titles must be distinct from any other section titles.
You may add a section Entitled "Endorsements", provided it contains
nothing but endorsements of your Modified Version by various
parties--for example, statements of peer review or that the text has
been approved by an organization as the authoritative definition of a
standard.
You may add a passage of up to five words as a Front-Cover Text, and a
passage of up to 25 words as a Back-Cover Text, to the end of the list
of Cover Texts in the Modified Version. Only one passage of
Front-Cover Text and one of Back-Cover Text may be added by (or
through arrangements made by) any one entity. If the Document already
includes a cover text for the same cover, previously added by you or
by arrangement made by the same entity you are acting on behalf of,
you may not add another; but you may replace the old one, on explicit
permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License
give permission to use their names for publicity for or to assert or
imply endorsement of any Modified Version.
5. COMBINING DOCUMENTS
You may combine the Document with other documents released under this
License, under the terms defined in section 4 above for modified
versions, provided that you include in the combination all of the
Invariant Sections of all of the original documents, unmodified, and
list them all as Invariant Sections of your combined work in its
license notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and
multiple identical Invariant Sections may be replaced with a single
copy. If there are multiple Invariant Sections with the same name but
different contents, make the title of each such section unique by
adding at the end of it, in parentheses, the name of the original
author or publisher of that section if known, or else a unique number.
Make the same adjustment to the section titles in the list of
Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections Entitled "History"
in the various original documents, forming one section Entitled
"History"; likewise combine any sections Entitled "Acknowledgements",
and any sections Entitled "Dedications". You must delete all sections
Entitled "Endorsements".
6. COLLECTIONS OF DOCUMENTS
You may make a collection consisting of the Document and other
documents released under this License, and replace the individual
copies of this License in the various documents with a single copy
that is included in the collection, provided that you follow the rules
of this License for verbatim copying of each of the documents in all
other respects.
You may extract a single document from such a collection, and
distribute it individually under this License, provided you insert a
copy of this License into the extracted document, and follow this
License in all other respects regarding verbatim copying of that
document.
7. AGGREGATION WITH INDEPENDENT WORKS
A compilation of the Document or its derivatives with other separate
and independent documents or works, in or on a volume of a storage or
distribution medium, is called an "aggregate" if the copyright
resulting from the compilation is not used to limit the legal rights
of the compilation's users beyond what the individual works permit.
When the Document is included in an aggregate, this License does not
apply to the other works in the aggregate which are not themselves
derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these
copies of the Document, then if the Document is less than one half of
the entire aggregate, the Document's Cover Texts may be placed on
covers that bracket the Document within the aggregate, or the
electronic equivalent of covers if the Document is in electronic form.
Otherwise they must appear on printed covers that bracket the whole
aggregate.
8. TRANSLATION
Translation is considered a kind of modification, so you may
distribute translations of the Document under the terms of section 4.
Replacing Invariant Sections with translations requires special
permission from their copyright holders, but you may include
translations of some or all Invariant Sections in addition to the
original versions of these Invariant Sections. You may include a
translation of this License, and all the license notices in the
Document, and any Warranty Disclaimers, provided that you also include
the original English version of this License and the original versions
of those notices and disclaimers. In case of a disagreement between
the translation and the original version of this License or a notice
or disclaimer, the original version will prevail.
If a section in the Document is Entitled "Acknowledgements",
"Dedications", or "History", the requirement (section 4) to Preserve
its Title (section 1) will typically require changing the actual
title.
9. TERMINATION
You may not copy, modify, sublicense, or distribute the Document
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense, or distribute it is void, and
will automatically terminate your rights under this License.
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, receipt of a copy of some or all of the same material does
not give you any rights to use it.
10. FUTURE REVISIONS OF THIS LICENSE
The Free Software Foundation may publish new, revised versions of the
GNU Free Documentation License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns. See
https://www.gnu.org/licenses/.
Each version of the License is given a distinguishing version number.
If the Document specifies that a particular numbered version of this
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that specified version or
of any later version that has been published (not as a draft) by the
Free Software Foundation. If the Document does not specify a version
number of this License, you may choose any version ever published (not
as a draft) by the Free Software Foundation. If the Document
specifies that a proxy can decide which future versions of this
License can be used, that proxy's public statement of acceptance of a
version permanently authorizes you to choose that version for the
Document.
11. RELICENSING
"Massive Multiauthor Collaboration Site" (or "MMC Site") means any
World Wide Web server that publishes copyrightable works and also
provides prominent facilities for anybody to edit those works. A
public wiki that anybody can edit is an example of such a server. A
"Massive Multiauthor Collaboration" (or "MMC") contained in the site
means any set of copyrightable works thus published on the MMC site.
"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
license published by Creative Commons Corporation, a not-for-profit
corporation with a principal place of business in San Francisco,
California, as well as future copyleft versions of that license
published by that same organization.
"Incorporate" means to publish or republish a Document, in whole or in
part, as part of another Document.
An MMC is "eligible for relicensing" if it is licensed under this
License, and if all works that were first published under this License
somewhere other than this MMC, and subsequently incorporated in whole or
in part into the MMC, (1) had no cover texts or invariant sections, and
(2) were thus incorporated prior to November 1, 2008.
The operator of an MMC Site may republish an MMC contained in the site
under CC-BY-SA on the same site at any time before August 1, 2009,
provided the MMC is eligible for relicensing.
ADDENDUM: How to use this License for your documents
To use this License in a document you have written, include a copy of
the License in the document and put the following copyright and
license notices just after the title page:
Copyright (c) YEAR YOUR NAME.
Permission is granted to copy, distribute and/or modify this document
under the terms of 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.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
replace the "with...Texts." line with this:
with the Invariant Sections being LIST THEIR TITLES, with the
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
If you have Invariant Sections without Cover Texts, or some other
combination of the three, merge those two alternatives to suit the
situation.
If your document contains nontrivial examples of program code, we
recommend releasing these examples in parallel under your choice of
free software license, such as the GNU General Public License,
to permit their use in free software.

1
docs/dom-i18n.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(a,b){"use strict";"function"==typeof define&&define.amd?define([],function(){return a.domI18n=b()}):"object"==typeof exports?module.exports=b():a.domI18n=b()}(this,function(){"use strict";return function(a){function b(a){return a||(a=window.navigator.languages?window.navigator.languages[0]:window.navigator.language||window.navigator.userLanguage),-1===q.indexOf(a)&&(r&&console.warn(a+" is not available on the list of languages provided"),a=a.indexOf("-")?a.split("-")[0]:a),-1===q.indexOf(a)&&(r&&console.error(a+" is not compatible with any language provided"),a=p),a}function c(a){v=b(a),l()}function d(){u={}}function e(a){var b=a.getAttribute("data-dom-i18n-id");return b&&u&&u[b]}function f(a,b){var c="i18n"+Date.now()+1e3*Math.random();a.setAttribute("data-dom-i18n-id",c),u[c]=b}function g(a){return u&&u[a.getAttribute("data-dom-i18n-id")]}function h(a,b){var c={},d=a.firstElementChild,e=!d&&a[b].split(o);return q.forEach(function(b,f){var g;d?(g=a.children[f],g&&g.cloneNode&&(c[b]=g.cloneNode(!0))):(g=e[f],g&&(c[b]=String(g)))}),c}function i(a){var b,c,d=a.getAttribute(t),i=null!==a.getAttribute(s),k=d?d:"textContent";!i&&e(a)?b=g(a):(b=h(a,k),i||f(a,b)),c=b[v],"string"==typeof c?a[k]=c:"object"==typeof c&&j(a,c)}function j(a,b){k(a),a.appendChild(b)}function k(a){for(;a.lastChild;)a.removeChild(a.lastChild)}function l(){for(var a="string"==typeof n||n instanceof String?m.querySelectorAll(n):n,b=0;b<a.length;++b)i(a[b])}a=a||{};var m=a.rootElement||window.document,n=a.selector||"[data-translatable]",o=a.separator||" // ",p=a.defaultLanguage||"en",q=a.languages||["en"],r=void 0!==a.enableLog?a.enableLog:!0,s="data-no-cache",t="data-translatable-attr",u={},v=b(a.currentLanguage);return l(n),{changeLanguage:c,clearCachedElements:d}}});

BIN
docs/img/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
docs/img/preview-mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
docs/img/preview-mobile.psd Normal file

Binary file not shown.

BIN
docs/img/preview-pc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -1,128 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
<meta content="Reverse Proxy, Open Source, Aroz, Go, OS, NAS, Cloud" name="keywords">
<meta content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox" name="description">
<meta name="author" content="tobychui">
<!-- HTML Meta Tags -->
<title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<title>Homelab Gateway | Zoraxy</title>
<meta name="description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://zoraxy.aroz.org/">
<meta property="og:type" content="website">
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta property="og:title" content="Hello Zoraxy">
<meta property="og:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="aroz.org">
<meta property="twitter:domain" content="os.aroz.org">
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta name="twitter:title" content="Hello Zoraxy">
<meta name="twitter:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- JavaScript Libs-->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="dom-i18n.min.js"></script>
<link href="main.css" rel="stylesheet">
<!-- Css stuffs-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css" integrity="sha512-3quBdRGJyLy79hzhDDcBzANW+mVqPctrGCfIPosHQtMKb3rKsCxfyslzwlz2wj1dT8A7UX+sEvDjaUv+WExQrA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Favicons -->
<link href="favicon.png" rel="icon">
<link href="img/apple-touch-icon.png" rel="apple-touch-icon">
<!-- Google 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=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400&display=swap" rel="stylesheet">
<!-- Main Stylesheet File -->
<link href="style.css" rel="stylesheet">
<script
src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif !important;
color: #404040;
}
</style>
<!-- AOS.js-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js" integrity="sha512-A7AYk1fGKX6S2SsHywmPkrnzTZHrgiVT7GcQkLGDe2ev0aWb8zejytzS8wjo7PGEXKqJOrjQ4oORtnimIRZBtw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" integrity="sha512-1cK78a1o+ht2JcaW6g8OXYwqpev9+6GqOkz9xmBN9iUUhIndKtxwILGWYOSibOKjLsEdjyjZvYDq/cZwNeak0w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div class="main section">
<div class="left-menu">
<div class="iconWrapper">
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
</div>
<a href="#home" class="menu-item active" align="center">
<img src="img/icons/home.svg">
</a>
<a href="#features" class="menu-item" align="center">
<img src="img/icons/awesome.svg">
</a>
<a href="#screenshots" class="menu-item" align="center">
<img src="img/icons/screenshots.svg">
</a>
<a href="#plugins" class="menu-item" align="center">
<img src="img/icons/plugin.svg">
</a>
<a href="#source" class="menu-item" align="center">
<img src="img/icons/code.svg">
</a>
</div>
<div class="right-content">
<!-- Hero Banner Section -->
<div class="headbanner"></div>
<div id="home" class="herotext">
<div class="ui basic segment">
<div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
<div id="backToTopBtn" class="ui big icon button" onclick="backToTop();">
<i class="ui arrow up icon"></i>
</div>
<button id="rwdmenubtn" class="ui black big icon button"><i class="ui bars icon"></i></button>
<div id="mainmenu" class="ui segment">
<div class="ui container">
<div class="ui small stackable secondary menu">
<div class="item">
<img class="ui tiny image" src="img/logo.png">
</div>
<a class="item" href="#mainmenu" i18n>
Home // 主頁 // Startseite
</a>
<a class="item" href="#about" i18n>
About Zoraxy // 關於 Zoraxy // Über Zoraxy
</a>
<a class="item" href="#features" i18n>
Screenshots // 系統截圖 // Bildschirmfotos
</a>
<a class="item" href="#techspec" i18n>
Videos // 介紹影片 // Videos
</a>
<a class="item" href="#download" i18n>
Download // 下載 // Herunterladen
</a>
<a class="item" href="#learnmore" i18n>
Learn More // 了解更多 // Mehr erfahren
</a>
<a class="right floated item">
<div class="ui small selection dropdown">
<input type="hidden" id="language">
<div class="default text" style="color: #6cacff;"><i class="language icon"></i> Default</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="en">English</div>
<div class="item" data-value="zh">中文(正體)</div>
<div class="item" data-value="de">Deutsch</div>
</div>
</div>
</a>
</div>
<br><br>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br>
<table class="ui very basic collapsing unstackable celled table">
<thead>
<tr><th colspan="2">Quick Access</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4 class="ui image header">
<i class="ui download icon"></i>
<div class="content">
Download
<div class="sub header">Prebuild Binary
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
<tr>
<td>
<h4 class="ui image header">
<i class="ui github icon"></i>
<div class="content">
Github
<div class="sub header">Source Code
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
</table>
</div>
<div id="wavesWrapper">
</div>
</div>
<div class="messageBanner">
<div class="ui text container">
<p i18n>This site is currently under development. Some information might not be ready.
// 本網站目前仍在開發中,部分資訊可能尚未準備好。
// Diese Seite ist in Entwicklung. Einige Informationen sind möglicherweise nicht verfügbar.
</p>
</div>
</div>
<div id="slideshowBanner">
<div class="title">
<h1 i18n>Zoraxy</h1>
<div class="ui divider" style="border-top: 1px solid rgba(255,255,255,0.5); "></div>
<p i18n>The ultimate homelab networking toolbox for self-hosted services
// 簡化自家伺服器部署之事,初學者居家網絡必備良器
// Das ultimative Homelab-Netzwerk-Toolbox für selbstgehostete Dienste
</p>
<a href="https://github.com/tobychui/zoraxy/releases" class="ui basic white button" target="_blank"><i style="color:white;" class="ui download icon"></i><span i18n>Download // 立即下載 // Herunterladen </span></a>
<a href="https://github.com/tobychui/zoraxy" class="ui basic white button" target="_blank"><i style="color: white;" class="ui code icon"></i><span i18n>Source Code // 查看原始碼 // Quellcode</span></a>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
@ -137,250 +130,470 @@
</g>
</svg>
</div>
</div>
<!-- Features -->
<div id="features" class="section">
<div class="ui container">
<div class="ui basic segment">
<h1 class="ui header">
<img class="ui small image" src="img/icons/awesome.svg">
<div class="content">
Features
<div class="sub header">Highlighting a few important features of Zoraxy</div>
</div>
</h1>
<br>
<div class="ui stackable grid featureList">
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/proxy.svg">
<div class="content">
Reverse Proxy
</div>
</h3>
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/redirect.svg">
<div class="content">
Redirection
</div>
</h3>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/blacklist.svg">
<div class="content">
Geo-IP & Blacklist
</div>
</h3>
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/gan.svg">
<div class="content">
Global Area Network
</div>
</h3>
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
</div>
<!-- Row 2-->
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/terminal.svg">
<div class="content">
Web SSH
</div>
</h3>
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/stats.svg">
<div class="content">
Real Time Statistics
</div>
</h3>
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/scan.svg">
<div class="content">
Scanner & Utilities
</div>
</h3>
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/code.svg">
<div class="content">
Open Source
</div>
</h3>
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
</div>
</div>
</div>
<br><br><br><br>
<!-- About ArozOS-->
<div id="about" class="ui text container">
<div class="ui stackable grid" data-aos="fade-up">
<div class="six wide column" align="right">
<img class="ui medium image" src="img/preview-pc.png">
</div>
</div>
</div>
<!-- Screenshots -->
<div id="screenshots" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/screenshots.svg">
<div class="content">
Screenshots
<div class="sub header">A quick overview of the UI designs</div>
</div>
</h1>
<div class="ui three column stackable grid">
<div class="column">
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div>
</div>
</div>
</div>
<!-- Plugin Developments -->
<div id="plugins" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/plugin.svg">
<div class="content">
Plugins
<div class="sub header">Add custom routing rules via simple scripts</div>
</div>
</h1>
<div style="width: 100%; text-align: center;">
<br>
<p>Documentation work in progress</p>
</div>
</div>
</div>
<!-- Source code -->
<div id="source" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/code.svg">
<div class="content">
Source Code
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
</div>
</h1>
<br>
<div class="ui two column stackable grid">
<div class="column">
<h3 class="ui header">
<i class="grey github icon"></i>
<div class="content" style="text-align: left;">
<a href="https://github.com/tobychui/zoraxy">
Github
<div class="sub header">https://github.com/tobychui/zoraxy</div>
</a>
<div class="ten wide column">
<div class="about-text-wrapper">
<p class="about-title"><b i18n>Reverse Proxy // 反向代理 // Reverse-Proxy</b></p>
<p><span i18n>Easy setups with dynamic updates // 讓你想不到般簡單易用、迅速設定、動態更新 // Einfache Einrichtung mit dynamischen Updates</span></p>
<p i18n>Access your reverse proxy and self-hosted services from any computer with a browser, anytime, anywhere.
// 透過瀏覽器,隨時隨地在任何裝置上存取您的反向代理及自家伺服器服務。
// Greifen Sie jederzeit und überall von jedem Gerät aus auf Ihren Reverse-Proxy und selbst gehostete
</p>
<div class="ui list">
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Simple setups with web UI
// 透過網頁介面簡單設定即可使用
// Einfache Einrichtung mit Web-UI
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Change settings on the fly without restarting
// 即時更改設定,無需重新啟動
// Einstellungen ohne Neustart ändern
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
One of the best reverse proxy manager for beginners
// 可能是最適合初學者的反向代理管理器之一
// Einer der besten Reverse-Proxy-Manager für Anfänger
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Easily install plugins and edit configurations
// 輕鬆安裝插件並編輯設定
// Plugins einfach installieren und Konfigurationen bearbeiten
</div>
</div>
</div>
</h3>
</div>
<div class="column">
<h3 class="ui header">
<i class="blue mail icon"></i>
<div class="content" style="text-align: left;">
<a href="mailto:toby@imuslab.com">
Email Contact
<div class="sub header">toby@imuslab.com</div>
</a>
</div>
</h3>
</div>
</div>
</div>
</div>
</div>
<br><br>
<div class="ui container">
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
<div class="ui stackable grid" data-aos="fade-up">
<div class="six wide column" align="right">
<img class="ui medium image" src="img/preview-mobile.png">
</div>
<div class="ten wide column">
<div class="about-text-wrapper">
<p class="about-title"><b i18n>Real-time Analytics // 即時流量分析 // Echtzeit-Analysen</b></p>
<p><span i18n>Dynamic statistic and access control // 動態流量數據、權限與路由設定 // Dynamische Statistik und Zugriffskontrolle</span></p>
<p i18n>Provide real time statistical overview, take advantage of the real time traffic and situations to make better decisions.
// 提供即時統計概覽,利用即時流量和情況做出更好的決策。
// Bietet eine Echtzeit-Übersicht über die Statistiken, um bessere Entscheidungen zu treffen.
</p>
<div class="ui list">
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Real time visitor statistic
// 即時訪客統計概覽
// Echtzeit-Besucherstatistik
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Instant network utilitization overview
// 即時網路使用率概覽
// Sofortige Netzwerkübersicht
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
No-reload access control and settings
// 即時生效存取控制和設定
// Zugriffskontrolle und Einstellungen ohne Neustart
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
One-click setting change with no downtime
// 一鍵設定更改,無需停機
// Einstellungsänderung mit einem Klick ohne Ausfallzeiten
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<br><br><br><br>
<!-- Features -->
<div class="ui divider"></div>
<div id="features" class="ui container">
<div class="centered title">
<h1 i18n>Screenshots
// 系統截圖
// Bildschirmfotos
</h1>
</div>
<div class="ui three column stackable grid">
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/1.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/2.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/3.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/4.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/5.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/6.png">
</div>
<!-- <div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/7.png">
</div> -->
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/8.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/9.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/10.png">
</div>
</div>
<br><br><br>
</div>
</div>
<br>
<!-- Spec -->
<div id="techspec" class="blackbanner">
<br><br>
<div class="centered title">
<h1 style="font-weight: 600;" i18n>
Review Videos
// 介紹影片
// Videos
</h1>
</div>
<div>
<div class="videoScrollBar">
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/5-lps8DC6_Y?si=rkfePn9kiYKCvYUZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/49xQYLpmedE?si=fgba2iK55s1760Xr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/I_F97he5F2A?si=qKEXwDcjkX1nPejq" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/FNU08-ufByM?si=I2hq9vsapeXB2Oqb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
</div>
</div>
<br><br><br>
</div>
<!-- Download -->
<div id="download" class="ui text container">
<br><br>
<div class="centered title">
<h1 i18n>
Download
// 下載
// Herunterladen
</h1>
</div>
<div class="downloadTabWrapper">
<div class="ui top attached fluid stackable tabular menu">
<a class="active item" data-tab="linux"><i class="grey linux icon"></i> Linux</a>
<a class="item" data-tab="windows"><i class="blue windows icon"></i> Windows</a>
<a class="item" data-tab="rpi"><i class="red raspberry pi icon"></i><span i18n>SBCs // ARM 開發板 // SBCs</span></a>
<a class="item" data-tab="build"><i class="code icon"></i> <span i18n>Build from source // 從原始碼建置 // Aus dem Quellcode erstellen</span> </a>
</div>
<div class="ui bottom attached active tab segment" data-tab="linux">
<p i18n>
Install with command line
// 使用 CLI 下載並執行發行版本
// Installieren Sie mit der Befehlszeile
</p>
<div class="ui black message">
<code>
wget https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64<br>
chmod +x ./zoraxy_linux_amd64<br>
sudo ./zoraxy_linux_amd64
</code>
</div>
<br>
<p i18n>
Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_amd64");'>
<i class="black linux icon"></i>
<span i18n>Download x64
// 下載 64位元 執行檔
// Herunterladen x64
</span>
</button>
<span style="font-size: 1.2em; font-weight: 600; margin-right: 0.4em">OR</span>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_386");'>
<i class="black linux icon"></i>
<span i18n>Download x32
// 下載 32位元 執行檔
// Herunterladen x32
</span>
</button>
</div>
<div class="ui bottom attached tab segment" data-tab="windows">
<p i18n>
Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_windows_amd64.exe");'>
<i class="blue windows icon"></i>
<span i18n>
Download Zoraxy for Windows
// 下載 Windows 版 Zoraxy
// Zoraxy für Windows herunterladen
</span>
</button>
<br><br>
</div>
<div class="ui bottom attached tab segment" data-tab="rpi">
<p i18n>Install with command line (armv6-7, arm64, x86)
// 使用 CLI 下載並執行 armv6-7, arm64, x86
// Installieren Sie mit der Befehlszeile (armv6-7, arm64, x86)
</p>
<div class="ui black message">
<code>
# Check your CPU architecture<br>
uname -m <br>
<br>
# For arm64 (aarch64) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64<br>
<br>
# For armv6 (armv6l) / armv7 (armv7l) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm<br>
<br>
# For RISC-V (riscv64) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_riscv64<br>
<br>
chmod +x ./zoraxy<br>
sudo ./zoraxy <br>
</code>
</div>
<br>
<p i18n>Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm");'><i class="black download icon"></i> <span i18n></span>arm (v6, v7)</button>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm64");'><i class="black download icon"></i> <span i18n></span>arm64</button>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_riscv64");'><i class="grey download icon"></i> <span i18n></span>riscv64</button>
</div>
<div class="ui bottom attached tab segment" data-tab="build">
<p i18n>Require Go (Golang) compiler. Details build from source instruction can be found on Zoraxy Github README file.
// 需要 Go (Go 語言)編譯器。建置詳情可以在 Zoraxy Github README 檔案中找到。
// Erfordert den Go (Golang) Compiler. Detaillierte Anweisungen zum Erstellen aus dem Quellcode finden Sie in der Zoraxy Github README-Datei.
</p>
<div class="ui black message">
<code>
git clone https://github.com/tobychui/zoraxy<br>
cd ./zoraxy/src/<br>
go mod tidy<br>
go build<br>
sudo ./zoraxy <br>
</code>
</div>
</div>
</div>
<p>
<span i18n>After Zoraxy is started, navigate to
// 當 Zoraxy 執行檔 / 服務啟動後,使用瀏覽器開啟
// Nachdem Zoraxy gestartet wurde, navigieren Sie zu
</span>
<a href="http://localhost:8000" target="_blank">http://localhost:8000</a>
<span i18n>to continue account and system setup.
// 以繼續帳戶和系統設定。
// um die Konto- und Systemeinrichtung fortzusetzen.
</span>
</p>
<br><br>
</div>
<!-- Learn More -->
<div class="ui divider"></div>
<div id="learnmore" class="ui text container">
<div class="centered title" style="margin-bottom: 0px;">
<h1 i18n>Learn More
// 了解更多
// Mehr erfahren
</h1>
<p i18n>If you like this project, please feel free to give us a ⭐ star ⭐.
// 如果您喜歡這個開源專案,歡迎來給我們一顆 ⭐星星⭐ 喔!!
// Wenn Ihnen dieses Projekt gefällt, geben Sie uns bitte einen ⭐ Stern ⭐.
</p>
</div>
<div class="ui basic segment linkicons">
<div class="ui big breadcrumb">
<a class="section externallink" href="https://github.com/tobychui/zoraxy" target="_blank">
<div class="ui icon header">
<i class="black github icon"></i>
<div class="content" i18n>
Github
// 源碼
// Quellcode
</div>
</div>
</a>
<i class="divider"> </i>
<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>
Plugin Devs
// 插件開發
// Plugin-Entwickler
</div>
</div>
</a>
<i class=" divider"> </i>
<a class="section externallink" href="mailto:toby@imuslab.com" target="_blank">
<div class="ui icon header">
<i class="yellow mail icon"></i>
<div class="content" i18n>
Email
// 電子郵件
// E-Mail
</div>
</div>
</a>
<i class=" divider"> </i>
<a class="section externallink" href="https://t.me/ArOZBeta" target="_blank">
<div class="ui icon header">
<i class="blue telegram icon"></i>
<div class="content">
Telegram
</div>
</div>
</a>
</div>
</div>
<br><br><br><br>
</div>
<!-- Footer -->
<div id="footer">
<div class="ui container">
<br><br>
<div class="ui stackable grid" style="height: 100%;">
<div class="six wide column" style="height: 100%;">
<a href="https://zoraxy.aroz.org"><img src="img/logo_white.png" class="ui small image"></a>
<p><span style="font-weight: 300;">The Zoraxy Project</span><br>
© Toby Chui</p>
<div class="bottom-attach">
<br><br>
<div class="ui breadcrumb" style="margin-top: 0.4em;">
<div class="section" i18n><a style="color: white;" href="https://zoraxy.aroz.org" target="_blank">zoraxy.aroz.org</a></div>
<div class="divider"> / </div>
<div class="section">2018 - <span class="year">now</span></div>
</div>
</div>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Developer Tools
// 開發者工具
// Entwicklerwerkzeuge
</div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
<div class="item"><a href="" target="_blank">Offical Plugin List</a></div>
<div class="item"><a href="" target="_blank">Plugin Development Guide</a></div>
</div>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Project Spin-offs
// 衍生開源計劃
// Projekt-Ableger
</div>
<div class="item"><a href="https://os.aroz.org" target="_blank">ArozOS</a></div>
</div>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Related Links
// 相關連接
// Verwandte Links
</div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki/Getting-Started" target="_blank" i18n>Getting Started</a></div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Zoraxy Release</a></div>
<div class="item"><a href="https://hub.docker.com/r/zoraxydocker/zoraxy" target="_blank">Zoraxy Docker</a></div>
<div class="item"><a href="https://imuslab.com" target="_blank">imuslab</a></div>
</div>
</div>
</div>
</div>
<br><br><br><br>
</div>
<script>
$(".year").text(new Date().getFullYear() );
AOS.init();
$(".year").text(new Date().getFullYear());
$(".menu-item").on("click", function(){
$(".menu-item.active").removeClass("active");
$(this).addClass("active");
});
// Function to open the modal with the clicked image using jQuery
function openModal(src) {
// Remove the old modal if it exists
$('#imageModal').remove();
$(".right-content").on("scroll", function() {
var scrollPos = $(".right-content").scrollTop();
if (scrollPos < 10){
//Reaching the top
$('.menu-item.active').removeClass("active");
$($('.menu-item')[0]).addClass('active');
return;
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
//Reaching the bottom
$('.menu-item.active').removeClass("active");
$($('.menu-item').get().reverse()[0]).addClass('active');
return
const modal = $('<div style="display:none;">', { id: 'imageModal' }).css({
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '1000'
});
const img = $('<img>', { src: src }).css({
maxWidth: '80%',
maxHeight: '80%',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)'
});
modal.append(img);
$("body").css("overflow", "hidden");
modal.on('click', function() {
modal.remove();
$("body").css("overflow", "auto");
});
$('body').append(modal);
}
$('.menu-item').each(function() {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.offset().top <= (window.innerHeight / 2)) {
$('.menu-item.active').removeClass("active");
currLink.addClass("active");
console.log(currLink.attr("href"));
}
});
});
// Add click event listener to all screenshot images using jQuery
$('.screenshot').on('click', function() {
openModal($(this).attr('src'));
});
</script>
<!-- Locales -->
<script src="main.js" defer></script>
</body>
</html>
</html>

386
docs/index_legacy.html Normal file
View File

@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
<meta name="author" content="tobychui">
<!-- HTML Meta Tags -->
<title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://zoraxy.aroz.org/">
<meta property="og:type" content="website">
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="aroz.org">
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Favicons -->
<link href="favicon.png" rel="icon">
<!-- Google 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=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<!-- Main Stylesheet File -->
<link href="style.css" rel="stylesheet">
<script
src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif !important;
color: #404040;
}
</style>
</head>
<body>
<div class="main section">
<div class="left-menu">
<div class="iconWrapper">
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
</div>
<a href="#home" class="menu-item active" align="center">
<img src="img/icons/home.svg">
</a>
<a href="#features" class="menu-item" align="center">
<img src="img/icons/awesome.svg">
</a>
<a href="#screenshots" class="menu-item" align="center">
<img src="img/icons/screenshots.svg">
</a>
<a href="#plugins" class="menu-item" align="center">
<img src="img/icons/plugin.svg">
</a>
<a href="#source" class="menu-item" align="center">
<img src="img/icons/code.svg">
</a>
</div>
<div class="right-content">
<!-- Hero Banner Section -->
<div class="headbanner"></div>
<div id="home" class="herotext">
<div class="ui basic segment">
<div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
</div>
<br><br>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br>
<table class="ui very basic collapsing unstackable celled table">
<thead>
<tr><th colspan="2">Quick Access</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4 class="ui image header">
<i class="ui download icon"></i>
<div class="content">
Download
<div class="sub header">Prebuild Binary
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
<tr>
<td>
<h4 class="ui image header">
<i class="ui github icon"></i>
<div class="content">
Github
<div class="sub header">Source Code
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
</table>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
</div>
<!-- Features -->
<div id="features" class="section">
<div class="ui container">
<div class="ui basic segment">
<h1 class="ui header">
<img class="ui small image" src="img/icons/awesome.svg">
<div class="content">
Features
<div class="sub header">Highlighting a few important features of Zoraxy</div>
</div>
</h1>
<br>
<div class="ui stackable grid featureList">
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/proxy.svg">
<div class="content">
Reverse Proxy
</div>
</h3>
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/redirect.svg">
<div class="content">
Redirection
</div>
</h3>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/blacklist.svg">
<div class="content">
Geo-IP & Blacklist
</div>
</h3>
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/gan.svg">
<div class="content">
Global Area Network
</div>
</h3>
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
</div>
<!-- Row 2-->
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/terminal.svg">
<div class="content">
Web SSH
</div>
</h3>
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/stats.svg">
<div class="content">
Real Time Statistics
</div>
</h3>
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/scan.svg">
<div class="content">
Scanner & Utilities
</div>
</h3>
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/code.svg">
<div class="content">
Open Source
</div>
</h3>
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
</div>
</div>
</div>
</div>
</div>
<!-- Screenshots -->
<div id="screenshots" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/screenshots.svg">
<div class="content">
Screenshots
<div class="sub header">A quick overview of the UI designs</div>
</div>
</h1>
<div class="ui three column stackable grid">
<div class="column">
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div>
</div>
</div>
</div>
<!-- Plugin Developments -->
<div id="plugins" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/plugin.svg">
<div class="content">
Plugins
<div class="sub header">Add custom routing rules via simple scripts</div>
</div>
</h1>
<div style="width: 100%; text-align: center;">
<br>
<p>Documentation work in progress</p>
</div>
</div>
</div>
<!-- Source code -->
<div id="source" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/code.svg">
<div class="content">
Source Code
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
</div>
</h1>
<br>
<div class="ui two column stackable grid">
<div class="column">
<h3 class="ui header">
<i class="grey github icon"></i>
<div class="content" style="text-align: left;">
<a href="https://github.com/tobychui/zoraxy">
Github
<div class="sub header">https://github.com/tobychui/zoraxy</div>
</a>
</div>
</h3>
</div>
<div class="column">
<h3 class="ui header">
<i class="blue mail icon"></i>
<div class="content" style="text-align: left;">
<a href="mailto:toby@imuslab.com">
Email Contact
<div class="sub header">toby@imuslab.com</div>
</a>
</div>
</h3>
</div>
</div>
</div>
</div>
<br><br>
<div class="ui container">
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
</div>
<br><br><br>
</div>
</div>
<br>
<script>
$(".year").text(new Date().getFullYear() );
$(".menu-item").on("click", function(){
$(".menu-item.active").removeClass("active");
$(this).addClass("active");
});
$(".right-content").on("scroll", function() {
var scrollPos = $(".right-content").scrollTop();
if (scrollPos < 10){
//Reaching the top
$('.menu-item.active').removeClass("active");
$($('.menu-item')[0]).addClass('active');
return;
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
//Reaching the bottom
$('.menu-item.active').removeClass("active");
$($('.menu-item').get().reverse()[0]).addClass('active');
return
}
$('.menu-item').each(function() {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.offset().top <= (window.innerHeight / 2)) {
$('.menu-item.active').removeClass("active");
currLink.addClass("active");
console.log(currLink.attr("href"));
}
});
});
</script>
</body>
</html>

462
docs/main.css Normal file
View File

@ -0,0 +1,462 @@
/* Global */
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif;
}
body.en *:not(i){
font-family: 'Source Sans Pro', 'Noto Sans TC',sans-serif !important;
}
body.zh *:not(i){
font-family: 'Noto Sans TC',sans-serif !important;
}
body.jp *:not(i){
font-family: "Noto Sans JP", sans-serif !important;
}
body.zh-cn *:not(i){
font-family: 'Noto Sans SC',sans-serif !important;
}
.centered.title{
padding: 2em;
margin-bottom: 2em;
text-align: center;
}
.centered.title h1{
font-weight: 300 !important;
}
.messageBanner{
width: 100%;
background: #6cacff;
text-align: center;
color: white;
padding: 10px;
}
.messageBanner .header{
font-weight: 500;
}
#backToTopBtn{
position: fixed;
bottom: 1em;
right: 1em;
display:none;
z-index: 999;
border: 1px solid white;
background: #6cacff;
}
#backToTopBtn:hover{
opacity: 0.8;
}
#backToTopBtn i{
color: white;
}
/* Main Menu */
#mainmenu{
padding-top: 0.4em;
padding-bottom: 0.4em;
border-radius: 0;
margin-bottom: 0;
margin-top: 0;
background: transparent !important;
}
#slideshowBanner .ui.basic.white.button{
color: white;
box-shadow: 0 0 0 1px rgb(231, 231, 231) inset;
border-radius: 0.4em;
background: none !important;
}
#slideshowBanner .ui.basic.white.button:hover{
background-color: rgba(255, 255, 255, 0.3) !important;
}
#slideshowBanner .ui.basic.white.button:active{
background: rgba(255, 255, 255, 0.5) !important;
}
#rwdmenubtn{
display:none;
position: absolute;
background: white;
border: 1px solid #6cacff;
color: #6cacff;
}
#mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled){
font-size: 1.1em;
font-weight: 500;
border-bottom: 1px solid transparent;
transition: border-bottom ease-in-out 0.1s;
color: white !important;
border-radius: 0;
}
#mainmenu #mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled):hover{
background-color: transparent;
border-bottom: 1px solid #82adfc;
color: #82adfc !important;
}
/* Image Sldiers */
#slideshowBanner{
background: rgb(108,172,255);
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
position: relative;
height: 80vh;
}
.slideshow {
width: 100%;
overflow: hidden;
border-radius: 0;
max-height: 500px;
}
.slideshow .slides {
display: flex;
transition: transform 1s ease-in-out;
opacity: 0.6;
filter: blur(2px);
pointer-events: none;
user-select: none;
}
.slideshow .slide {
min-width: 100%;
box-sizing: border-box;
}
.slideshow .slide img {
width: 100%;
display: block;
}
.slideshow .dots{
text-align: center;
position: absolute;
bottom: 15px;
width: 100%;
}
.slideshow .dot {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 5px;
background-color: #bebebe;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.6s ease;
}
.dot.active {
background-color: #ffffff;
}
#slideshowBanner .title{
display: inline-block;
width: 100%;
max-width: 500px;
text-align: left;
position: absolute;
top: 50%;
margin-left: 10%;
transform: translateX(0%) translateY(-50%);
color: white;
}
#slideshowBanner .title .scrolldownTips{
display: none;
}
#slideshowBanner .title h1{
font-size: 4em;
font-weight: 600;
margin-bottom: 0;
}
#slideshowBanner .title p{
font-size: 1.2em;
}
/* About Zoraxy */
.about-text-wrapper{
margin-top: 3em;
}
.about-text-wrapper p, .about-text-wrapper .list .item{
font-weight: 300;
}
.about-title{
font-size: 2.4em;
font-weight: 300;
margin-bottom: 0em;
}
.about-title b{
font-weight: 800;
}
.about-text-wrapper .ui.list .item{
margin-bottom: 0.6em;
}
.about-text-wrapper .ui.list .item .icon{
padding-top: 0.15em;
}
/* Screenshots */
#features{
margin-bottom: 3em;
}
#features .screenshot{
transition: opacity 0.1s ease-in-out;
cursor: pointer;
}
#features .screenshot:hover{
opacity: 0.5;
}
/* Videos */
#techspec .centered.title{
color: white;
}
#techspec p {
color: white;
}
#techspec .videoScrollBar{
overflow-x: scroll;
display: block;
white-space: nowrap;
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
padding-top: 2em;
padding-bottom: 3em;
}
.introvideo{
display: inline-block !important;
}
.blackbanner{
width: 100%;
background: rgb(108,172,255);
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
min-height: 300px;
}
/* Download */
.downloadButton {
margin-top: 0.4em !important;
}
.downloadTabWrapper{
width: 100%;
overflow-x: hidden;
}
#download .ui.black.message{
word-break: break-all;
}
/* Learn More */
#learnmore .linkicons{
text-align: center;
width: 100%;
}
#learnmore .linkicons .divider{
margin-left: 1em;
margin-right: 1em;
}
#learnmore .linkicons .externallink{
margin-bottom: 0.6em;
transition: opacity 0.1s ease-in-out;
}
#learnmore .linkicons .externallink i{
/* color: #1b1c1d; */
font-weight: 300;
font-size: 1.5em;
}
#learnmore .linkicons .externallink:hover{
opacity: 0.8;
}
#learnmore .linkicons .externallink .content{
color: #1b1c1d;
font-weight: 500;
font-size: 0.6em;
}
/* Footer */
#footer{
background: rgb(85,131,238);
background: linear-gradient(48deg, rgba(85,131,238,1) 21%, rgba(108,172,255,1) 73%);
color: rgb(255, 255, 255);
}
#footer a {
color: rgb(209, 224, 255);
}
#footer a:hover{
color: rgb(255, 255, 255);
}
#footer .bottom-attach .divider{
color: rgb(212, 212, 212);
}
#footer .ui.list .title{
margin-bottom: 0.6em;
}
/* RWD Rules */
@media (max-width:960px) {
/* Main menu */
#mainmenu{
display:none;
z-index: 99;
position: absolute;
top: 0;
left: 0;
width: 100%;
background: #fdfdfd !important;
}
#rwdmenubtn{
display: block;
position: absolute;
top: 0.4em;
right: 0.4em;
z-index: 100;
}
/* Slideshows */
.slideshow {
min-height: 100vh;
}
.slideshow .slide{
height: 100% !important;
min-width: none;
}
.slideshow .slide img{
height: 100%;
width: auto;
}
#slideshowBanner .title{
padding: 1em;
margin-left: 0;
}
#slideshowBanner .title .scrolldownTips{
margin-top: 2em;
display: block;
}
#slideshowBanner .title .scrolldownTips img{
left: 50%;
transform: translateX(-50%);
}
#techspec .videoScrollBar{
overflow-x: auto;
display: block;
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
padding-top: 2em;
padding-bottom: 3em;
}
.introvideo {
display: block !important;
width: 100%;
margin-bottom: 1em;
}
.introvideo iframe{
width: 100%;
}
#download .stackable.tabular.menu .active.item{
background-color: rgb(243, 243, 243);
border-width: 0;
border-radius: 0.4em !important;
}
}
/*
Waves CSS
*/
#wavesWrapper{
position: absolute;
bottom: 0;
width: 100%;
left: 0;
}
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}

84
docs/main.js Normal file
View File

@ -0,0 +1,84 @@
/*
Localization
To add more locales, add to the html file with // (translated text)
after each DOM elements with attr i18n
And then add the language ISO key to the list below.
*/
let languages = ['en', 'zh', 'de'];
//Bind language change dropdown events
$(".dropdown").dropdown();
$("#language").on("change",function(){
let newLang = $("#language").parent().dropdown("get value");
i18n.changeLanguage(newLang);
$("body").attr("class", newLang);
});
//Initialize the i18n dom library
var i18n = domI18n({
selector: '[i18n]',
separator: ' // ',
languages: languages,
defaultLanguage: 'en'
});
$(document).ready(function(){
let userLang = navigator.language || navigator.userLanguage;
console.log("User language: " + userLang);
userLang = userLang.split("-")[0];
if (!languages.includes(userLang)) {
userLang = 'en';
}
i18n.changeLanguage(userLang);
$("body").attr("class", userLang);
});
/* Main Menu */
$("#rwdmenubtn").on("click", function(){
$("#mainmenu").slideToggle("fast");
})
//Handle resize
$(window).on("resize", function(){
if (window.innerWidth > 960){
$("#mainmenu").show();
}else{
$("#mainmenu").hide();
}
})
/*
Download
*/
$('.menu .item').tab();
//Download webpack and binary at the same time
function handleDownload(releasename){
let binaryURL = "https://github.com/tobychui/zoraxy/releases/latest/download/" + releasename;
window.open(binaryURL);
}
/* RWD */
window.addEventListener('scroll', function() {
var scrollPosition = window.scrollY || window.pageYOffset;
var windowHeight = window.innerHeight;
var hiddenDiv = document.querySelector('#backToTopBtn');
if (scrollPosition > windowHeight / 2) {
hiddenDiv.style.display = 'block';
} else {
hiddenDiv.style.display = 'none';
}
});
function backToTop(){
$('html, body').animate({scrollTop : 0},800, function(){
window.location.hash = "";
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View 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
View 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
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Run the Go program with the specified arguments
./docs.exe -m=build -root=plugins/html/

View 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
View 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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

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

View 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!

View 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.

View 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.
![image-20250527193748456](img/3. Enable Plugins/image-20250527193748456.png)
---
## 2. Enable Plugin
Click on the "Enable" button on the plugin which you want to enable
![image-20250527193601017](img/3. Enable Plugins/image-20250527193601017.png)
---
## 3. Assign Plugin to HTTP Proxy Rule
Finally, select the tag that you just created in the dropdown menu
![image-20250527194052408](img/3. Enable Plugins/image-20250527194052408.png)
Afterward, you will see the plugin is attached to the target tag
![image-20250527195703464](img/3. Enable Plugins/image-20250527195703464.png)
It means the plugin is enabled on the HTTP proxy rule

View File

@ -0,0 +1,13 @@
# Viewing Plugin Info
To view plugin information, you can click on the (i) icon in the plugin list.
![image-20250530171732607](img/5. Viewing Plugin Info/image-20250530171732607.png)
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.
![image-20250530171724441](img/5. Viewing Plugin Info/image-20250530171724441.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View 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.
![](img/1. Plugin Architecture/plugin_workflow.png)
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.

View 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
}
```

View 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).

View 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.
![static_capture](img/4. Capture Modes/static_capture.png)
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.
![dynamic_capture](img/4. Capture Modes/dynamic_capture.png)
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.

View 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.
![image-20250527201750613](img/5. Plugin UI/image-20250527201750613.png)
## 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`).

View File

@ -0,0 +1,15 @@
# Compile a Plugin
A plugin is basically a go program with a HTTP Server / Listener. The steps required to build a plugin is identical as building a ordinary go program.
```bash
# Assuming you are currently inside the root folder of your plugin
go mod tidy
go build
# Validate if the plugin is correctly build using -introspect flag
./{{your_plugin_name}} -introspect
# You should see your plugin information printed to STDOUT as JSON string
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View 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 plugins 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.
![image-20250530134148399](img/1. Hello World/image-20250530134148399.png)
**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.
![image-20250527210849767](img/1. Hello World/image-20250527210849767.png)
---
## 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)
}
}
```

View 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
![image-20250530153148506](img/2. RESTful Example/image-20250530153148506.png)

View 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.
![image-20250530164549527](img/3. Static Capture Example/image-20250530164549527.png)
## 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.
![image-20250530164903842](img/3. Static Capture Example/image-20250530164903842.png)
![image-20250530164916476](img/3. Static Capture Example/image-20250530164916476.png)
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.**
![image-20250530165014188](img/3. Static Capture Example/image-20250530165014188.png)
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!

View File

@ -0,0 +1,352 @@
# Dynamic Capture Example
Last Update: 29/05/2025
---
This example demonstrates how to use dynamic capture in Zoraxy plugins. Dynamic capture allows you to intercept requests based on real-time conditions, so you can program your plugin in a way that it can decided if it want to handle the request or not.
**Notes: This example assumes you have already read Hello World and Stataic Capture Example.**
Lets dive in!
---
## 1. Create the plugin folder structure
Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
---
## 2. Define Introspect
The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin.
```go
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: "org.aroz.zoraxy.dynamic-capture-example",
Name: "Dynamic Capture Example",
Author: "aroz.org",
AuthorContact: "https://aroz.org",
Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
URL: "https://zoraxy.aroz.org",
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: "/d_sniff",
DynamicCaptureIngress: "/d_capture",
UIPath: UI_PATH,
})
if err != nil {
panic(err)
}
```
Note the `DynamicCaptureSniff` and `DynamicCaptureIngress`. These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests.
---
## 3. Register Dynamic Capture Handlers
Dynamic capture handlers are used to process requests that match specific conditions.
```go
pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
fmt.Println("Accepting request with UUID: " + dsfr.GetRequestUUID())
return plugin.SniffResultAccpet
}
fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the dynamic capture handler!\n\nRequest Info:\n"))
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
w.Write([]byte("Request Method: " + r.Method + "\n"))
w.Write([]byte("Request Headers:\n"))
headers := make([]string, 0, len(r.Header))
for key := range r.Header {
headers = append(headers, key)
}
sort.Strings(headers)
for _, key := range headers {
for _, value := range r.Header[key] {
w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
}
}
})
```
The `RegisterDynamicSniffHandler` evaluates incoming requests, while the `RegisterDynamicCaptureHandle` processes the intercepted requests.
### Sniffing Logic
If a module registered a dynamic capture path, Zoraxy will forward the request headers as `DynamicSniffForwardRequest` (`dsfr`) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and "think" if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
```go
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
}
```
You can also use the `GetRequest()` function to get the `*http.Request` object or `GetRequestUUID()` to get a `string` value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
**Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled.**
In the sniffing stage, you can choose to either return `ControlStatusCode_CAPTURED`, where Zoraxy will forward the request to your plugin `DynamicCaptureIngress` endpoint, or `ControlStatusCode_UNHANDLED`, where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server.
### Capture Handling
The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the ` http.Request` by writing to the `http.ResponseWriter`.
If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the `X-Zoraxy-Requestid` header in the HTTP request. This is the same UUID as the one you get from `dsfr.GetRequestUUID()` in the sniffing stage if they are the same request object on Zoraxy side.
The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
```html
Request URI: /foobar/test
Request Method: GET
Request Headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate, br, zstd
(more fileds)
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: https
X-Real-Ip: 127.0.0.1
X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
```
You can extract the `X-Zoraxy-Requestid` value from the request header and do your matching for implementing your function if needed.
---
## 4. Render Debug UI
This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here.
```go
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, "%s: %s\n", name, value)
}
}
w.Header().Set("Content-Type", "text/html")
}
```
---
## 5. Full Code
Here is the complete code for the dynamic capture example:
```go
package main
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
UI_PATH = "/debug"
STATIC_CAPTURE_INGRESS = "/s_capture"
)
func main() {
// Serve the plugin intro spect
// This will print the plugin intro spect and exit if the -introspect flag is provided
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: "org.aroz.zoraxy.dynamic-capture-example",
Name: "Dynamic Capture Example",
Author: "aroz.org",
AuthorContact: "https://aroz.org",
Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
URL: "https://zoraxy.aroz.org",
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: "/d_sniff",
DynamicCaptureIngress: "/d_capture",
UIPath: UI_PATH,
/*
SubscriptionPath: "/subept",
SubscriptionsEvents: []plugin.SubscriptionEvent{
*/
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Setup the path router
pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
/*
Dynamic Captures
*/
pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
//In this example, we want to capture all URI
//that start with /foobar and forward it to the dynamic capture handler
if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
reqUUID := dsfr.GetRequestUUID()
fmt.Println("Accepting request with UUID: " + reqUUID)
// Print all the values of the request
fmt.Println("Method:", dsfr.Method)
fmt.Println("Hostname:", dsfr.Hostname)
fmt.Println("URL:", dsfr.URL)
fmt.Println("Header:")
for key, values := range dsfr.Header {
for _, value := range values {
fmt.Printf(" %s: %s\n", key, value)
}
}
fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
fmt.Println("Host:", dsfr.Host)
fmt.Println("RequestURI:", dsfr.RequestURI)
fmt.Println("Proto:", dsfr.Proto)
fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
// We want to handle this request, reply with aSniffResultAccept
return plugin.SniffResultAccpet
}
// If the request URI does not match, we skip this request
fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
// This is the dynamic capture handler where it actually captures and handle the request
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the dynamic capture handler!"))
// Print all the request info to the response writer
w.Write([]byte("\n\nRequest Info:\n"))
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
w.Write([]byte("Request Method: " + r.Method + "\n"))
w.Write([]byte("Request Headers:\n"))
headers := make([]string, 0, len(r.Header))
for key := range r.Header {
headers = append(headers, key)
}
sort.Strings(headers)
for _, key := range headers {
for _, value := range r.Header[key] {
w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
}
}
})
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
}
// Render the debug UI
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, "%s: %s\n", name, value)
}
}
w.Header().Set("Content-Type", "text/html")
}
```
---
## 6. Expected Output
To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the "debug" tag and assigning it to a localhost loopback HTTP proxy rule.
When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
If everything is correctly setup, you should see the following page when requesting any URL with prefix `(your_HTTP_proxy_rule_hostname)/foobar`
![image-20250530205430254](img/4. Dynamic Capture Example/image-20250530205430254.png)
Example terminal output for requesting `/foobar/*`:
```
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: "Windows"
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
```
---
Now you know how to develop a plugin in Zoraxy that handles special routings!

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View 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).

View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View 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
View 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
View 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=

View 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 &amp; 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&rsquo;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&rsquo;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&rsquo;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>

View 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 &ldquo;plugins&rdquo; 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 &ldquo;Lapwing&rdquo;.
</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&rsquo;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>

View 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&rsquo;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>

View 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 &ldquo;debug&rdquo;. 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 &ldquo;Enable&rdquo; 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>

View 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 &ldquo;developer insight&rdquo; 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View 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>

View 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:&quot;id&quot;` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:&quot;name&quot;` //Name of your plugin
Author string `json:&quot;author&quot;` //Author name of your plugin
AuthorContact string `json:&quot;author_contact&quot;` //Author contact of your plugin, like email
Description string `json:&quot;description&quot;` //Description of your plugin
URL string `json:&quot;url&quot;` //URL of your plugin
Type PluginType `json:&quot;type&quot;` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:&quot;version_major&quot;` //Major version of your plugin
VersionMinor int `json:&quot;version_minor&quot;` //Minor version of your plugin
VersionPatch int `json:&quot;version_patch&quot;` //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:&quot;static_capture_paths&quot;` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:&quot;static_capture_ingress&quot;` //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:&quot;dynamic_capture_sniff&quot;` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:&quot;dynamic_capture_ingress&quot;` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:&quot;ui_path&quot;` //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:&quot;subscription_path&quot;` //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:&quot;subscriptions_events&quot;` //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 &ldquo;Capture Mode&rdquo; 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
{
&quot;id&quot;: &quot;org.aroz.zoraxy.debugger&quot;,
&quot;name&quot;: &quot;Plugin Debugger&quot;,
&quot;author&quot;: &quot;aroz.org&quot;,
&quot;author_contact&quot;: &quot;https://aroz.org&quot;,
&quot;description&quot;: &quot;A debugger for Zoraxy \u003c-\u003e plugin communication pipeline&quot;,
&quot;url&quot;: &quot;https://zoraxy.aroz.org&quot;,
&quot;type&quot;: 0,
&quot;version_major&quot;: 1,
&quot;version_minor&quot;: 0,
&quot;version_patch&quot;: 0,
&quot;static_capture_paths&quot;: [
{
&quot;capture_path&quot;: &quot;/test_a&quot;
},
{
&quot;capture_path&quot;: &quot;/test_b&quot;
}
],
&quot;static_capture_ingress&quot;: &quot;/s_capture&quot;,
&quot;dynamic_capture_sniff&quot;: &quot;/d_sniff&quot;,
&quot;dynamic_capture_ingress&quot;: &quot;/d_capture&quot;,
&quot;ui_path&quot;: &quot;/debug&quot;,
&quot;subscription_path&quot;: &quot;&quot;,
&quot;subscriptions_events&quot;: 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>

View 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:&quot;port&quot;` //Port to listen
RuntimeConst RuntimeConstantValue `json:&quot;runtime_const&quot;` //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>

View 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 &ldquo;endpoint&rdquo;, 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 &ldquo;YES&rdquo;, 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&rsquo;s static capture path and Plugin B&rsquo;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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View 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 &ldquo;Hello World&rdquo; 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 &ldquo;helloworld&rdquo;.
</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 &ldquo;plugins&rdquo;, 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 &ldquo;helloworld&rdquo;.
</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 plugins 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 &quot;mod/plugins/zoraxy_plugin&quot; ./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">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;!-- CSRF token, if your plugin need to make POST request to backend --&gt;
&lt;meta name=&quot;zoraxy.csrf.Token&quot; content=&quot;{{.csrfToken}}&quot;&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/script/semantic/semantic.min.css&quot;&gt;
&lt;script src=&quot;/script/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;/script/semantic/semantic.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;/script/utils.js&quot;&gt;&lt;/script&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/main.css&quot;&gt;
&lt;title&gt;Hello World&lt;/title&gt;
&lt;style&gt;
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background:none;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- Dark theme script must be included after body tag--&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/darktheme.css&quot;&gt;
&lt;script src=&quot;/script/darktheme.js&quot;&gt;&lt;/script&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;h1&gt;Hello World&lt;/h1&gt;
&lt;p&gt;Welcome to your first Zoraxy plugin&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</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 &ldquo;/&rdquo; 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 = &quot;com.example.helloworld&quot;
UI_PATH = &quot;/&quot;
WEB_ROOT = &quot;/www&quot;
)
func main(){
runtimeCfg, err := plugin.ServeAndRecvSpec(&amp;plugin.IntroSpect{
ID: &quot;com.example.helloworld&quot;,
Name: &quot;Hello World Plugin&quot;,
Author: &quot;foobar&quot;,
AuthorContact: &quot;admin@example.com&quot;,
Description: &quot;A simple hello world plugin&quot;,
URL: &quot;https://example.com&quot;,
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 &quot;/&quot;
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 &ldquo;separated version&rdquo; 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 (
_ &quot;embed&quot;
&quot;fmt&quot;
plugin &quot;example.com/zoraxy/helloworld/mod/zoraxy_plugin&quot;
)
//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, &amp;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(&quot;Hello World Plugin Exited&quot;)
}, 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 &ldquo;freezed&rdquo; 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 &amp; 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(&quot;Hello World started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe(&quot;127.0.0.1:&quot;+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 (
&quot;embed&quot;
_ &quot;embed&quot;
&quot;fmt&quot;
&quot;net/http&quot;
&quot;strconv&quot;
plugin &quot;example.com/zoraxy/helloworld/mod/zoraxy_plugin&quot;
)
const (
PLUGIN_ID = &quot;com.example.helloworld&quot;
UI_PATH = &quot;/&quot;
WEB_ROOT = &quot;/www&quot;
)
//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(&amp;plugin.IntroSpect{
ID: &quot;com.example.helloworld&quot;,
Name: &quot;Hello World Plugin&quot;,
Author: &quot;foobar&quot;,
AuthorContact: &quot;admin@example.com&quot;,
Description: &quot;A simple hello world plugin&quot;,
URL: &quot;https://example.com&quot;,
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 &quot;/&quot;
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, &amp;content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
fmt.Println(&quot;Hello World Plugin Exited&quot;)
}, nil)
// Serve the hello world page in the www folder
http.Handle(UI_PATH, embedWebRouter.Handler())
fmt.Println(&quot;Hello World started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe(&quot;127.0.0.1:&quot;+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>

View 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(&amp;plugin.IntroSpect{
ID: &quot;com.example.restful-example&quot;,
Name: &quot;Restful Example&quot;,
Author: &quot;foobar&quot;,
AuthorContact: &quot;admin@example.com&quot;,
Description: &quot;A simple demo for making RESTful API calls in plugin&quot;,
URL: &quot;https://example.com&quot;,
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 &quot;/&quot;
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, &amp;content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
fmt.Println(&quot;Restful-example Exited&quot;)
}, 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(&quot;/api/echo&quot;, 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 &ldquo;echo&rdquo; 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(&quot;/api/echo&quot;, 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(&quot;name&quot;)
if name == &quot;&quot; {
http.Error(w, &quot;Missing 'name' query parameter&quot;, http.StatusBadRequest)
return
}
w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
response := map[string]string{&quot;message&quot;: fmt.Sprintf(&quot;Hello %s&quot;, name)}
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, &quot;Failed to encode response&quot;, 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">&lt;!-- The example below show how HTTP GET is used with Zoraxy plugin --&gt;
&lt;h3&gt;Echo Test (HTTP GET)&lt;/h3&gt;
&lt;div class=&quot;ui form&quot;&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;nameInput&quot;&gt;Enter your name:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;nameInput&quot; placeholder=&quot;Your name&quot;&gt;
&lt;/div&gt;
&lt;button class=&quot;ui button primary&quot; id=&quot;sendRequestButton&quot;&gt;Send Request&lt;/button&gt;
&lt;div class=&quot;ui message&quot; id=&quot;responseMessage&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
document.getElementById('sendRequestButton').addEventListener('click', function() {
const name = document.getElementById('nameInput').value;
if (name.trim() === &quot;&quot;) {
alert(&quot;Please enter a name.&quot;);
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();
}
});
});
&lt;/script&gt;
</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(&quot;/api/post&quot;, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, &quot;Invalid request method&quot;, http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, &quot;Failed to parse form data&quot;, http.StatusBadRequest)
return
}
for key, values := range r.PostForm {
for _, value := range values {
// Generate a simple HTML response
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
fmt.Fprintf(w, &quot;%s: %s&lt;br&gt;&quot;, 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">&lt;script src=&quot;/script/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;/script/utils.js&quot;&gt;&lt;/script&gt;
&lt;!- More lines here --&gt;
&lt;!-- The example below shows how form post can be used in plugin --&gt;
&lt;h3&gt;Form Post Test (HTTP POST)&lt;/h3&gt;
&lt;div class=&quot;ui form&quot;&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;postNameInput&quot;&gt;Name:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;postNameInput&quot; placeholder=&quot;Your name&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;postAgeInput&quot;&gt;Age:&lt;/label&gt;
&lt;input type=&quot;number&quot; id=&quot;postAgeInput&quot; placeholder=&quot;Your age&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label&gt;Gender:&lt;/label&gt;
&lt;div class=&quot;ui checkbox&quot;&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;genderMale&quot; name=&quot;gender&quot; value=&quot;Male&quot;&gt;
&lt;label for=&quot;genderMale&quot;&gt;Male&lt;/label&gt;
&lt;/div&gt;
&lt;div class=&quot;ui checkbox&quot;&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;genderFemale&quot; name=&quot;gender&quot; value=&quot;Female&quot;&gt;
&lt;label for=&quot;genderFemale&quot;&gt;Female&lt;/label&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;button class=&quot;ui button primary&quot; id=&quot;postRequestButton&quot;&gt;Send&lt;/button&gt;
&lt;div class=&quot;ui message&quot; id=&quot;postResponseMessage&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
</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 &quot;X-CSRF-Token&quot; and the value is taken from the head
// meta tag content (i.e. &lt;meta name=&quot;zoraxy.csrf.Token&quot; content=&quot;{{.csrfToken}}&quot;&gt;)
$.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">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;!-- CSRF token, if your plugin need to make POST request to backend --&gt;
&lt;meta name=&quot;zoraxy.csrf.Token&quot; content=&quot;{{.csrfToken}}&quot;&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/script/semantic/semantic.min.css&quot;&gt;
&lt;script src=&quot;/script/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;/script/semantic/semantic.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;/script/utils.js&quot;&gt;&lt;/script&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/main.css&quot;&gt;
&lt;title&gt;RESTful Example&lt;/title&gt;
&lt;style&gt;
body {
background-color: var(--theme_bg_primary);
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- Dark theme script must be included after body tag--&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/darktheme.css&quot;&gt;
&lt;script src=&quot;/script/darktheme.js&quot;&gt;&lt;/script&gt;
&lt;br&gt;
&lt;div class=&quot;standardContainer&quot;&gt;
&lt;div class=&quot;ui container&quot;&gt;
&lt;h1&gt;RESTFul API Example&lt;/h1&gt;
&lt;!-- The example below show how HTTP GET is used with Zoraxy plugin --&gt;
&lt;h3&gt;Echo Test (HTTP GET)&lt;/h3&gt;
&lt;div class=&quot;ui form&quot;&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;nameInput&quot;&gt;Enter your name:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;nameInput&quot; placeholder=&quot;Your name&quot;&gt;
&lt;/div&gt;
&lt;button class=&quot;ui button primary&quot; id=&quot;sendRequestButton&quot;&gt;Send Request&lt;/button&gt;
&lt;div class=&quot;ui message&quot; id=&quot;responseMessage&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
document.getElementById('sendRequestButton').addEventListener('click', function() {
const name = document.getElementById('nameInput').value;
if (name.trim() === &quot;&quot;) {
alert(&quot;Please enter a name.&quot;);
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();
}
});
});
&lt;/script&gt;
&lt;!-- The example below shows how form post can be used in plugin --&gt;
&lt;h3&gt;Form Post Test (HTTP POST)&lt;/h3&gt;
&lt;div class=&quot;ui form&quot;&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;postNameInput&quot;&gt;Name:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;postNameInput&quot; placeholder=&quot;Your name&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label for=&quot;postAgeInput&quot;&gt;Age:&lt;/label&gt;
&lt;input type=&quot;number&quot; id=&quot;postAgeInput&quot; placeholder=&quot;Your age&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;field&quot;&gt;
&lt;label&gt;Gender:&lt;/label&gt;
&lt;div class=&quot;ui checkbox&quot;&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;genderMale&quot; name=&quot;gender&quot; value=&quot;Male&quot;&gt;
&lt;label for=&quot;genderMale&quot;&gt;Male&lt;/label&gt;
&lt;/div&gt;
&lt;div class=&quot;ui checkbox&quot;&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;genderFemale&quot; name=&quot;gender&quot; value=&quot;Female&quot;&gt;
&lt;label for=&quot;genderFemale&quot;&gt;Female&lt;/label&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;button class=&quot;ui button primary&quot; id=&quot;postRequestButton&quot;&gt;Send&lt;/button&gt;
&lt;div class=&quot;ui message&quot; id=&quot;postResponseMessage&quot; style=&quot;display: none;&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
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() === &quot;&quot; || age.trim() === &quot;&quot; || (!genderMale &amp;&amp; !genderFemale)) {
alert(&quot;Please fill out all fields.&quot;);
return;
}
const gender = genderMale ? &quot;Male&quot; : &quot;Female&quot;;
// .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 &quot;X-CSRF-Token&quot; and the value is taken from the head
// meta tag content (i.e. &lt;meta name=&quot;zoraxy.csrf.Token&quot; content=&quot;{{.csrfToken}}&quot;&gt;)
$.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();
}
});
});
&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</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 (
&quot;embed&quot;
_ &quot;embed&quot;
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;net/http&quot;
&quot;strconv&quot;
plugin &quot;example.com/zoraxy/restful-example/mod/zoraxy_plugin&quot;
)
const (
PLUGIN_ID = &quot;com.example.restful-example&quot;
UI_PATH = &quot;/&quot;
WEB_ROOT = &quot;/www&quot;
)
//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(&amp;plugin.IntroSpect{
ID: &quot;com.example.restful-example&quot;,
Name: &quot;Restful Example&quot;,
Author: &quot;foobar&quot;,
AuthorContact: &quot;admin@example.com&quot;,
Description: &quot;A simple demo for making RESTful API calls in plugin&quot;,
URL: &quot;https://example.com&quot;,
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 &quot;/&quot;
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, &amp;content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
fmt.Println(&quot;Restful-example Exited&quot;)
}, 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(&quot;/api/echo&quot;, 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(&quot;name&quot;)
if name == &quot;&quot; {
http.Error(w, &quot;Missing 'name' query parameter&quot;, http.StatusBadRequest)
return
}
w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
response := map[string]string{&quot;message&quot;: fmt.Sprintf(&quot;Hello %s&quot;, name)}
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, &quot;Failed to encode response&quot;, 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(&quot;/api/post&quot;, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, &quot;Invalid request method&quot;, http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, &quot;Failed to parse form data&quot;, http.StatusBadRequest)
return
}
for key, values := range r.PostForm {
for _, value := range values {
// Generate a simple HTML response
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
fmt.Fprintf(w, &quot;%s: %s&lt;br&gt;&quot;, key, value)
}
}
}, nil)
// Serve the restful-example page in the www folder
http.Handle(UI_PATH, embedWebRouter.Handler())
fmt.Println(&quot;Restful-example started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe(&quot;127.0.0.1:&quot;+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>

View File

@ -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(&amp;plugin.IntroSpect{
ID: &quot;org.aroz.zoraxy.static-capture-example&quot;,
Name: &quot;Static Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;An example for showing how static capture works in Zoraxy.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
StaticCapturePaths: []plugin.StaticCaptureRule{
{ CapturePath: &quot;/test_a&quot; },
{ CapturePath: &quot;/test_b&quot; },
},
StaticCaptureIngress: &quot;/s_capture&quot;,
UIPath: UI_PATH,
})
if err != nil {
panic(err)
}
</code></pre>
<p>
<p class="ts-text">
Note the
<span class="ts-text is-code">
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 &ldquo;if you receive requests that matches the above Static capture paths, please forward the request to this endpoint&rdquo;. 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(&quot;/test_a&quot;, http.HandlerFunc(HandleCaptureA))
pathRouter.RegisterPathHandler(&quot;/test_b&quot;, http.HandlerFunc(HandleCaptureB))
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by the default handler!&lt;br&gt;Request URI: &quot; + 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(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by A handler!&lt;br&gt;Request URI: &quot; + 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(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by the B handler!&lt;br&gt;Request URI: &quot; + 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, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<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 (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;sort&quot;
&quot;strconv&quot;
plugin &quot;example.com/zoraxy/static-capture-example/mod/zoraxy_plugin&quot;
)
const (
PLUGIN_ID = &quot;org.aroz.zoraxy.static-capture-example&quot;
UI_PATH = &quot;/ui&quot;
STATIC_CAPTURE_INGRESS = &quot;/s_capture&quot;
)
func main() {
runtimeCfg, err := plugin.ServeAndRecvSpec(&amp;plugin.IntroSpect{
ID: PLUGIN_ID,
Name: &quot;Static Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;An example for showing how static capture works in Zoraxy.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
StaticCapturePaths: []plugin.StaticCaptureRule{
{ CapturePath: &quot;/test_a&quot; },
{ CapturePath: &quot;/test_b&quot; },
},
StaticCaptureIngress: STATIC_CAPTURE_INGRESS,
UIPath: UI_PATH,
})
if err != nil {
panic(err)
}
pathRouter := plugin.NewPathRouter()
pathRouter.RegisterPathHandler(&quot;/test_a&quot;, http.HandlerFunc(HandleCaptureA))
pathRouter.RegisterPathHandler(&quot;/test_b&quot;, http.HandlerFunc(HandleCaptureB))
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by the default handler!&lt;br&gt;Request URI: &quot; + r.URL.String()))
}))
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
http.HandleFunc(UI_PATH+&quot;/&quot;, RenderDebugUI)
fmt.Println(&quot;Static path capture example started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
http.ListenAndServe(&quot;127.0.0.1:&quot;+strconv.Itoa(runtimeCfg.Port), nil)
}
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by A handler!&lt;br&gt;Request URI: &quot; + r.URL.String()))
}
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
w.Write([]byte(&quot;This request is captured by the B handler!&lt;br&gt;Request URI: &quot; + r.URL.String()))
}
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="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 &ldquo;debug&rdquo; 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>

View File

@ -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(&amp;plugin.IntroSpect{
ID: &quot;org.aroz.zoraxy.dynamic-capture-example&quot;,
Name: &quot;Dynamic Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: &quot;/d_sniff&quot;,
DynamicCaptureIngress: &quot;/d_capture&quot;,
UIPath: UI_PATH,
})
if err != nil {
panic(err)
}
</code></pre>
<p>
<p class="ts-text">
Note the
<span class="ts-text is-code">
DynamicCaptureSniff
</span>
and
<span class="ts-text is-code">
DynamicCaptureIngress
</span>
. These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="3-register-dynamic-capture-handlers">
3. Register Dynamic Capture Handlers
</h2>
<p>
<p class="ts-text">
Dynamic capture handlers are used to process requests that match specific conditions.
</p>
</p>
<pre><code class="language-go">pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
pathRouter.RegisterDynamicSniffHandler(&quot;/d_sniff&quot;, http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
if strings.HasPrefix(dsfr.RequestURI, &quot;/foobar&quot;) {
fmt.Println(&quot;Accepting request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultAccpet
}
fmt.Println(&quot;Skipping request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle(&quot;/d_capture&quot;, http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(&quot;Welcome to the dynamic capture handler!\n\nRequest Info:\n&quot;))
w.Write([]byte(&quot;Request URI: &quot; + r.RequestURI + &quot;\n&quot;))
w.Write([]byte(&quot;Request Method: &quot; + r.Method + &quot;\n&quot;))
w.Write([]byte(&quot;Request Headers:\n&quot;))
headers := make([]string, 0, len(r.Header))
for key := range r.Header {
headers = append(headers, key)
}
sort.Strings(headers)
for _, key := range headers {
for _, value := range r.Header[key] {
w.Write([]byte(fmt.Sprintf(&quot;%s: %s\n&quot;, key, value)))
}
}
})
</code></pre>
<p>
<p class="ts-text">
The
<span class="ts-text is-code">
RegisterDynamicSniffHandler
</span>
evaluates incoming requests, while the
<span class="ts-text is-code">
RegisterDynamicCaptureHandle
</span>
processes the intercepted requests.
</p>
</p>
<h3 id="sniffing-logic">
Sniffing Logic
</h3>
<p>
If a module registered a dynamic capture path, Zoraxy will forward the request headers as
<span class="ts-text is-code">
DynamicSniffForwardRequest
</span>
(
<span class="ts-text is-code">
dsfr
</span>
) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and &ldquo;think&rdquo; if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
</p>
<pre><code class="language-go">type DynamicSniffForwardRequest struct {
Method string `json:&quot;method&quot;`
Hostname string `json:&quot;hostname&quot;`
URL string `json:&quot;url&quot;`
Header map[string][]string `json:&quot;header&quot;`
RemoteAddr string `json:&quot;remote_addr&quot;`
Host string `json:&quot;host&quot;`
RequestURI string `json:&quot;request_uri&quot;`
Proto string `json:&quot;proto&quot;`
ProtoMajor int `json:&quot;proto_major&quot;`
ProtoMinor int `json:&quot;proto_minor&quot;`
}
</code></pre>
<p>
<p class="ts-text">
You can also use the
<span class="ts-text is-code">
GetRequest()
</span>
function to get the
<span class="ts-text is-code">
*http.Request
</span>
object or
<span class="ts-text is-code">
GetRequestUUID()
</span>
to get a
<span class="ts-text is-code">
string
</span>
value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
</p>
</p>
<p>
<p class="ts-text">
<span class="ts-text is-heavy">
Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled.
</span>
</p>
</p>
<p>
<p class="ts-text">
In the sniffing stage, you can choose to either return
<span class="ts-text is-code">
ControlStatusCode_CAPTURED
</span>
, where Zoraxy will forward the request to your plugin
<span class="ts-text is-code">
DynamicCaptureIngress
</span>
endpoint, or
<span class="ts-text is-code">
ControlStatusCode_UNHANDLED
</span>
, where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server.
</p>
</p>
<h3 id="capture-handling">
Capture Handling
</h3>
<p>
<p class="ts-text">
The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the
<span class="ts-text is-code">
http.Request
</span>
by writing to the
<span class="ts-text is-code">
http.ResponseWriter
</span>
.
</p>
</p>
<p>
<p class="ts-text">
If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the
<span class="ts-text is-code">
X-Zoraxy-Requestid
</span>
header in the HTTP request. This is the same UUID as the one you get from
<span class="ts-text is-code">
dsfr.GetRequestUUID()
</span>
in the sniffing stage if they are the same request object on Zoraxy side.
</p>
</p>
<p>
<p class="ts-text">
The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
</p>
</p>
<pre><code class="language-html">Request URI: /foobar/test
Request Method: GET
Request Headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate, br, zstd
(more fileds)
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: https
X-Real-Ip: 127.0.0.1
X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
</code></pre>
<p>
<p class="ts-text">
You can extract the
<span class="ts-text is-code">
X-Zoraxy-Requestid
</span>
value from the request header and do your matching for implementing your function if needed.
</p>
</p>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="4-render-debug-ui">
4. Render Debug UI
</h2>
<p>
<p class="ts-text">
This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here.
</p>
</p>
<pre><code class="language-go">func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="5-full-code">
5. Full Code
</h2>
<p>
<p class="ts-text">
Here is the complete code for the dynamic capture example:
</p>
</p>
<pre><code class="language-go">package main
import (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;sort&quot;
&quot;strconv&quot;
&quot;strings&quot;
plugin &quot;example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin&quot;
)
const (
PLUGIN_ID = &quot;org.aroz.zoraxy.dynamic-capture-example&quot;
UI_PATH = &quot;/debug&quot;
STATIC_CAPTURE_INGRESS = &quot;/s_capture&quot;
)
func main() {
// Serve the plugin intro spect
// This will print the plugin intro spect and exit if the -introspect flag is provided
runtimeCfg, err := plugin.ServeAndRecvSpec(&amp;plugin.IntroSpect{
ID: &quot;org.aroz.zoraxy.dynamic-capture-example&quot;,
Name: &quot;Dynamic Capture Example&quot;,
Author: &quot;aroz.org&quot;,
AuthorContact: &quot;https://aroz.org&quot;,
Description: &quot;This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.&quot;,
URL: &quot;https://zoraxy.aroz.org&quot;,
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
DynamicCaptureSniff: &quot;/d_sniff&quot;,
DynamicCaptureIngress: &quot;/d_capture&quot;,
UIPath: UI_PATH,
/*
SubscriptionPath: &quot;/subept&quot;,
SubscriptionsEvents: []plugin.SubscriptionEvent{
*/
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Setup the path router
pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
/*
Dynamic Captures
*/
pathRouter.RegisterDynamicSniffHandler(&quot;/d_sniff&quot;, http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
//In this example, we want to capture all URI
//that start with /foobar and forward it to the dynamic capture handler
if strings.HasPrefix(dsfr.RequestURI, &quot;/foobar&quot;) {
reqUUID := dsfr.GetRequestUUID()
fmt.Println(&quot;Accepting request with UUID: &quot; + reqUUID)
// Print all the values of the request
fmt.Println(&quot;Method:&quot;, dsfr.Method)
fmt.Println(&quot;Hostname:&quot;, dsfr.Hostname)
fmt.Println(&quot;URL:&quot;, dsfr.URL)
fmt.Println(&quot;Header:&quot;)
for key, values := range dsfr.Header {
for _, value := range values {
fmt.Printf(&quot; %s: %s\n&quot;, key, value)
}
}
fmt.Println(&quot;RemoteAddr:&quot;, dsfr.RemoteAddr)
fmt.Println(&quot;Host:&quot;, dsfr.Host)
fmt.Println(&quot;RequestURI:&quot;, dsfr.RequestURI)
fmt.Println(&quot;Proto:&quot;, dsfr.Proto)
fmt.Println(&quot;ProtoMajor:&quot;, dsfr.ProtoMajor)
fmt.Println(&quot;ProtoMinor:&quot;, dsfr.ProtoMinor)
// We want to handle this request, reply with aSniffResultAccept
return plugin.SniffResultAccpet
}
// If the request URI does not match, we skip this request
fmt.Println(&quot;Skipping request with UUID: &quot; + dsfr.GetRequestUUID())
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle(&quot;/d_capture&quot;, http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
// This is the dynamic capture handler where it actually captures and handle the request
w.WriteHeader(http.StatusOK)
w.Write([]byte(&quot;Welcome to the dynamic capture handler!&quot;))
// Print all the request info to the response writer
w.Write([]byte(&quot;\n\nRequest Info:\n&quot;))
w.Write([]byte(&quot;Request URI: &quot; + r.RequestURI + &quot;\n&quot;))
w.Write([]byte(&quot;Request Method: &quot; + r.Method + &quot;\n&quot;))
w.Write([]byte(&quot;Request Headers:\n&quot;))
headers := make([]string, 0, len(r.Header))
for key := range r.Header {
headers = append(headers, key)
}
sort.Strings(headers)
for _, key := range headers {
for _, value := range r.Header[key] {
w.Write([]byte(fmt.Sprintf(&quot;%s: %s\n&quot;, key, value)))
}
}
})
http.HandleFunc(UI_PATH+&quot;/&quot;, RenderDebugUI)
fmt.Println(&quot;Dynamic capture example started at http://127.0.0.1:&quot; + strconv.Itoa(runtimeCfg.Port))
http.ListenAndServe(&quot;127.0.0.1:&quot;+strconv.Itoa(runtimeCfg.Port), nil)
}
// Render the debug UI
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, &quot;**Plugin UI Debug Interface**\n\n[Recv Headers] \n&quot;)
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, name, value)
}
}
w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
}
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<h2 id="6-expected-output">
6. Expected Output
</h2>
<p>
To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the &ldquo;debug&rdquo; tag and assigning it to a localhost loopback HTTP proxy rule.
</p>
<p>
<p class="ts-text">
When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
</p>
</p>
<p>
<p class="ts-text">
If everything is correctly setup, you should see the following page when requesting any URL with prefix
<span class="ts-text is-code">
(your_HTTP_proxy_rule_hostname)/foobar
</span>
</p>
</p>
<p>
<div class="ts-image is-rounded" style="max-width: 800px">
<img src="img/4. Dynamic Capture Example/image-20250530205430254.png" alt="image-20250530205430254" />
</div>
</p>
<p>
<p class="ts-text">
Example terminal output for requesting
<span class="ts-text is-code">
/foobar/*
</span>
:
</p>
</p>
<pre><span class="ts-text is-code">[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: &quot;Chromium&quot;;v=&quot;136&quot;, &quot;Microsoft Edge&quot;;v=&quot;136&quot;, &quot;Not.A/Brand&quot;;v=&quot;99&quot;
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: &quot;Windows&quot;
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
</span></pre>
<div class="ts-divider has-top-spaced-large"></div>
<p>
<p class="ts-text">
Now you know how to develop a plugin in Zoraxy that handles special routings!
</p>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
<script>
hljs.highlightAll();
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More