mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-10-04 09:59:41 +02:00
Compare commits
165 Commits
cd822ed904
...
v3.2.7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2c3f36d9a3 | ||
![]() |
030ef2e01c | ||
![]() |
85cad1e2b6 | ||
![]() |
94afb6e3a5 | ||
![]() |
84a4eaaf95 | ||
![]() |
f98e1b8218 | ||
![]() |
778df1af0f | ||
![]() |
2140e5b0b5 | ||
![]() |
e9c1d14e23 | ||
![]() |
5477822015 | ||
![]() |
b0922c466d | ||
![]() |
1faaae21d7 | ||
![]() |
53c73e1e77 | ||
![]() |
0805da9d13 | ||
![]() |
52f652fbaf | ||
![]() |
d5a980094b | ||
![]() |
3a2b38aac7 | ||
![]() |
2a6f4d52b2 | ||
![]() |
6efab48d33 | ||
![]() |
e2c0fe3abf | ||
![]() |
1c26d60c8f | ||
![]() |
c745d82cf3 | ||
![]() |
36a48b5fe0 | ||
![]() |
c8e42dcf59 | ||
![]() |
fa4700a114 | ||
![]() |
f6c48ef793 | ||
![]() |
da347cf1cb | ||
![]() |
a36357dc04 | ||
![]() |
c88fb0329b | ||
![]() |
0debd0b907 | ||
![]() |
218c5aff40 | ||
![]() |
c57fa39554 | ||
![]() |
2f98ecd0c6 | ||
![]() |
73e4994ddc | ||
![]() |
fd70b7d2dc | ||
![]() |
dbf4648646 | ||
![]() |
797ad92623 | ||
![]() |
afdcc71358 | ||
![]() |
46f0ae6896 | ||
![]() |
1c84a8f9cf | ||
![]() |
00013f3562 | ||
![]() |
b743e0ea28 | ||
![]() |
7e6d60063e | ||
![]() |
dbd795a158 | ||
![]() |
df55157221 | ||
![]() |
af0641c067 | ||
![]() |
66ff18c631 | ||
![]() |
14bef4ef98 | ||
![]() |
22d2a0c6ca | ||
![]() |
c3afdefe45 | ||
![]() |
d9fd38260f | ||
![]() |
bf5ffa100c | ||
![]() |
a175c258c9 | ||
![]() |
7c3a1a9cfc | ||
![]() |
471e94c893 | ||
![]() |
19fd6057e0 | ||
![]() |
3ad8e5acb3 | ||
![]() |
dda922cb64 | ||
![]() |
d4d0adb297 | ||
![]() |
e4950bbbe6 | ||
![]() |
e1fd28f595 | ||
![]() |
f45d5f46b4 | ||
![]() |
cfd8f988fd | ||
![]() |
e4a12b27a6 | ||
![]() |
abcd550261 | ||
![]() |
e718ff1c72 | ||
![]() |
e477a40299 | ||
![]() |
d74ecb2444 | ||
![]() |
fe2db92392 | ||
![]() |
ac3f12718a | ||
![]() |
dc12ee1716 | ||
![]() |
d6c907b13f | ||
![]() |
9c99f6c734 | ||
![]() |
c2866f27f8 | ||
![]() |
2daf3cd2cb | ||
![]() |
51145edae7 | ||
![]() |
bd5d225a94 | ||
![]() |
0f621d0edd | ||
![]() |
9230f9374d | ||
![]() |
c982541a40 | ||
![]() |
6493a82e5f | ||
![]() |
39e05032c9 | ||
![]() |
077192e08e | ||
![]() |
4e32f31f0a | ||
![]() |
381184cd92 | ||
![]() |
223ae9e112 | ||
![]() |
aff1975c5a | ||
![]() |
ad2519d894 | ||
![]() |
40f915f7fb | ||
![]() |
e3e31d9f22 | ||
![]() |
be5f631b9f | ||
![]() |
f9e51bfd27 | ||
![]() |
39b5da36d9 | ||
![]() |
5c6950ca56 | ||
![]() |
d187c32a8a | ||
![]() |
ed8f9b7337 | ||
![]() |
46cfc02493 | ||
![]() |
2d43890fcf | ||
![]() |
5a38c1d407 | ||
![]() |
dd93f9a2c4 | ||
![]() |
70b1ccfa6e | ||
![]() |
100c1e9c04 | ||
![]() |
a33600d3e2 | ||
![]() |
c4c10d2130 | ||
![]() |
4d3d1b25cb | ||
![]() |
118b5e5114 | ||
![]() |
ad53b894c0 | ||
![]() |
a0a394885c | ||
![]() |
51334a3a75 | ||
![]() |
6f5fadc085 | ||
![]() |
45506c8772 | ||
![]() |
c091b9d1ca | ||
![]() |
691cb603ce | ||
![]() |
e53724d6e5 | ||
![]() |
e225407b03 | ||
![]() |
273cae2a98 | ||
![]() |
6b3b89f7bf | ||
![]() |
2d611a559a | ||
![]() |
6c5eba01c2 | ||
![]() |
f641797d10 | ||
![]() |
f92ff068f3 | ||
![]() |
b59ac47c8c | ||
![]() |
8030f3d62a | ||
![]() |
f8f623e3e4 | ||
![]() |
061839756c | ||
![]() |
1dcaa0c257 | ||
![]() |
ffd3909964 | ||
![]() |
3ddccdffce | ||
![]() |
929d4cc82a | ||
![]() |
4f1cd8a571 | ||
![]() |
f6b3656bb1 | ||
![]() |
74a816216e | ||
![]() |
4a093cf096 | ||
![]() |
68f9fccf3a | ||
![]() |
f276040ad0 | ||
![]() |
2f40593daf | ||
![]() |
0b6dbd49bb | ||
![]() |
eb07917c14 | ||
![]() |
217bc48001 | ||
![]() |
38cfab4a09 | ||
![]() |
217e5e90ff | ||
![]() |
4a37a989a0 | ||
![]() |
eb540b774d | ||
![]() |
26d03f9ad4 | ||
![]() |
31ba4f20ae | ||
![]() |
650d61ba24 | ||
![]() |
6d0c0be8c2 | ||
![]() |
366a44a992 | ||
![]() |
7164b74d4a | ||
![]() |
b01a21f318 | ||
![]() |
809e1fa815 | ||
![]() |
c7b5e0994e | ||
![]() |
1f8684481a | ||
![]() |
0e74ff69c3 | ||
![]() |
f0fa71c5b4 | ||
![]() |
8cb47e19fa | ||
![]() |
475650de0d | ||
![]() |
b19867865c | ||
![]() |
df636c9f76 | ||
![]() |
e6b2cf09d7 | ||
![]() |
bf0df928c7 | ||
![]() |
eec6cec0db | ||
![]() |
0215171646 | ||
![]() |
e2882b6436 | ||
![]() |
61b873451f |
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -2,7 +2,7 @@ name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [ released, prereleased ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
@@ -33,7 +33,8 @@ jobs:
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||
|
||||
- name: Build and push Docker image
|
||||
- name: Build and push Docker image (Release)
|
||||
if: "!github.event.release.prerelease"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker
|
||||
@@ -45,3 +46,15 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push Docker image (Prerelease)
|
||||
if: "github.event.release.prerelease"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
21
.gitignore
vendored
21
.gitignore
vendored
@@ -29,8 +29,6 @@ src/Zoraxy_*_*
|
||||
src/certs/*
|
||||
src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.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
|
||||
@@ -41,14 +39,27 @@ src/sys.uuid
|
||||
src/zoraxy
|
||||
src/log/
|
||||
|
||||
|
||||
# dev-tags
|
||||
/Dockerfile
|
||||
/Entrypoint.sh
|
||||
|
||||
# docker testing stuff
|
||||
docker/test/
|
||||
docker/container-builder.sh
|
||||
docker/docker-compose.yaml
|
||||
|
||||
# plugins
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
example/plugins/ztnc/ztnc.db.lock
|
||||
docs/plugins/docs.exe
|
||||
*.exe
|
||||
.idea
|
||||
conf
|
||||
log
|
||||
tmp
|
||||
sys.*
|
||||
www/html/index.html
|
||||
*.exe
|
||||
/src/dist
|
||||
|
||||
/src/plugins
|
||||
.DS_Store
|
||||
|
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,3 +1,53 @@
|
||||
# v3.2.5 20 Jul 2025
|
||||
|
||||
|
||||
+ Added new API endpoint /api/proxy/setTlsConfig (for HTTP Proxy Editor TLS tab)
|
||||
+ Refactored TLS certificate management APIs with new handlers
|
||||
+ Removed redundant functions from src/cert.go and delegated to tlsCertManager
|
||||
+ Code optimization in tlscert module
|
||||
+ Introduced a new constant CONF_FOLDER and updated configuration storage paths (phasing out hard coded paths)
|
||||
+ Updated functions to set default TLS options when missing, default to SNI
|
||||
+ Added Proxy Protocol v1 support in stream proxy [jemmy1794](https://github.com/jemmy1794)
|
||||
+ Fixed Proxy UI bug [jemmy1794](https://github.com/jemmy1794)
|
||||
+ Fixed assign static server to localhost or all interfaces [#688](https://github.com/tobychui/zoraxy/issues/688)
|
||||
+ fixed empty SSO parameters by [7brend7](https://github.com/7brend7)
|
||||
+ sort list of loaded certificates by expire date by [7brend7](https://github.com/7brend7)
|
||||
+ Docker hardening by [PassiveLemon](https://github.com/PassiveLemon)
|
||||
+ Fixed sort by destination [#713](https://github.com/tobychui/zoraxy/issues/713)
|
||||
|
||||
# v3.2.4 28 Jun 2025
|
||||
|
||||
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
|
||||
|
||||
|
||||
+ Added Authentik support by [JokerQyou](https://github.com/tobychui/zoraxy/commits?author=JokerQyou)
|
||||
+ Added pluginsystem and moved GAN and Zerotier to plugins
|
||||
+ Add loopback detection [#573](https://github.com/tobychui/zoraxy/issues/573)
|
||||
+ Fixed Dark theme not working with Advanced Option accordion [#591](https://github.com/tobychui/zoraxy/issues/591)
|
||||
+ Update logger to include UserAgent by [Raithmir](https://github.com/Raithmir)
|
||||
+ Fixed memory usage in UI [#600](https://github.com/tobychui/zoraxy/issues/600)
|
||||
+ Added docker-compose.yml by [SamuelPalubaCZ](https://github.com/tobychui/zoraxy/commits?author=SamuelPalubaCZ)
|
||||
+ Added more statistics for proxy hosts [#201](https://github.com/tobychui/zoraxy/issues/201) and [#608](https://github.com/tobychui/zoraxy/issues/608)
|
||||
+ Fixed origin field in logs [#618](https://github.com/tobychui/zoraxy/issues/618)
|
||||
+ Added FreeBSD support by Andreas Burri
|
||||
+ Fixed HTTP proxy redirect [#626](https://github.com/tobychui/zoraxy/issues/626)
|
||||
+ Fixed proxy handling #629](https://github.com/tobychui/zoraxy/issues/629)
|
||||
+ Move Scope ID handling into CIDR check by [Nirostar](https://github.com/tobychui/zoraxy/commits?author=Nirostar)
|
||||
+ Prevent the browser from filling the saved Zoraxy login account by [WHFo](https://github.com/tobychui/zoraxy/commits?author=WHFo)
|
||||
+ Added port number and http proto to http proxy list link
|
||||
+ Fixed headers for authelia by [james-d-elliott](https://github.com/tobychui/zoraxy/commits?author=james-d-elliott)
|
||||
+ Refactored docker container list and UI improvements by [eyerrock](https://github.com/tobychui/zoraxy/commits?author=eyerrock)
|
||||
+ Refactored Dockerfile by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||
+ Added new HTTP proxy UI
|
||||
+ Added inbound host name edit function
|
||||
+ Added static web server option to disable listen to all interface
|
||||
+ Merged SSO implementations (Oauth2) [#649](https://github.com/tobychui/zoraxy/pull/649)
|
||||
+ Merged forward-auth optimization [#692(https://github.com/tobychui/zoraxy/pull/692)
|
||||
+ Optimized SSO UI
|
||||
+ Refactored docker image workflows by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||
+ Added disable chunked transfer encoding checkbox (for upstreams that uses legacy HTTP implementations)
|
||||
+ Bug fixes [#694](https://github.com/tobychui/zoraxy/issues/694), [#659](https://github.com/tobychui/zoraxy/issues/659) by [jemmy1794](https://github.com/tobychui/zoraxy/commits?author=jemmy1794), [#695](https://github.com/tobychui/zoraxy/issues/695)
|
||||
|
||||
# v3.1.9 1 Mar 2025
|
||||
|
||||
+ Fixed netstat underflow bug
|
||||
|
17
CODEOWNERS
Normal file
17
CODEOWNERS
Normal file
@@ -0,0 +1,17 @@
|
||||
# tobycui is the default owner for all files in this repository
|
||||
* @tobychui
|
||||
|
||||
# PassiveLemon is the docker maintainer
|
||||
/docker @PassiveLemon
|
||||
|
||||
# james-d-elliott is the community maintainer for forward-auth related functions
|
||||
# /src/mod/auth/sso/forward @james-d-elliott
|
||||
|
||||
# jemmy1794 maintains the stream proxy module
|
||||
/src/mod/streamproxy @jemmy1794
|
||||
|
||||
# AnthonyMichaelTDM maintains the plugin and event systems
|
||||
/src/mod/plugins @AnthonyMichaelTDM
|
||||
/example/plugins @AnthonyMichaelTDM
|
||||
/src/**/plugin_*.go @AnthonyMichaelTDM
|
||||
/src/mod/eventsystem @AnthonyMichaelTDM
|
46
README.md
46
README.md
@@ -13,22 +13,24 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
- Basic Auth
|
||||
- Alias Hostnames
|
||||
- Custom Headers
|
||||
- Load Balancing
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (and SAN certs)
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- Stream Proxy (TCP & UDP)
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Plugin System
|
||||
- Utilities
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- Wake-On-Lan
|
||||
- Debug Forward Proxy
|
||||
- IP Scanner
|
||||
- Port Scanner
|
||||
- Others
|
||||
- Basic single-admin management mode
|
||||
- External permission management system for easy system integration
|
||||
@@ -107,6 +109,8 @@ Usage of zoraxy:
|
||||
If web server is enabled by default (default true)
|
||||
-default_inbound_port int
|
||||
Default web server listening port (default 443)
|
||||
-dev
|
||||
Use external web folder for UI development
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-earlyrenew int
|
||||
@@ -164,19 +168,7 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
|
||||
|
||||
## Global Area Network Controller
|
||||
|
||||
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
||||
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
|
||||
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
```
|
||||
|
||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||
|
||||
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
Moved to official plugin repo, see [ztnc](https://github.com/aroz-online/zoraxy-official-plugins/tree/main/src/ztnc) plugin
|
||||
|
||||
## Web SSH
|
||||
|
||||
@@ -199,10 +191,30 @@ Loopback web SSH connections, by default, are disabled. This means that if you a
|
||||
|
||||
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
|
||||
|
||||
- Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
- Forward Auth [@james-d-elliott](https://github.com/james-d-elliott)
|
||||
|
||||
- (Legacy) Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
|
||||
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
|
||||
|
||||
- ACME
|
||||
|
||||
- ACME integration (Looking for maintainer)
|
||||
|
||||
- DNS Challenge by [@zen8841](https://github.com/zen8841)
|
||||
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)
|
||||
|
||||
- Change Log [@Morethanevil](https://github.com/Morethanevil)
|
||||
|
||||
### Looking for Maintainer
|
||||
|
||||
- ACME DNS Challenge Module
|
||||
- Logging (including analysis & attack prevention) Module
|
||||
|
||||
Thank you so much for your contributions!
|
||||
|
||||
## Sponsor This Project
|
||||
@@ -210,7 +222,7 @@ Thank you so much for your contributions!
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
- [PassiveLemon (Docker compatibility maintainer)](https://github.com/PassiveLemon)
|
||||
|
||||
## License
|
||||
|
||||
|
2
docker/.gitignore
vendored
Normal file
2
docker/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
example/
|
||||
src/
|
@@ -34,34 +34,18 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
|
||||
## 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
|
||||
FROM docker.io/alpine:latest
|
||||
|
||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
||||
RUN apk add --update --no-cache python3 sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||
rm -rf /var/cache/apk/* /tmp/*
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/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 --chmod=700 ./entrypoint.py /opt/zoraxy/
|
||||
|
||||
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/ &&\
|
||||
RUN mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
echo "tun" | tee -a /etc/modules
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
@@ -73,7 +57,10 @@ ENV CFGUPGRADE="true"
|
||||
ENV DB="auto"
|
||||
ENV DOCKER="true"
|
||||
ENV EARLYRENEW="30"
|
||||
ENV ENABLELOG="true"
|
||||
ENV ENABLELOGCOMPRESS="true"
|
||||
ENV FASTGEOIP="false"
|
||||
ENV LOGROTATE="0"
|
||||
ENV MDNS="true"
|
||||
ENV MDNSNAME="''"
|
||||
ENV NOAUTH="false"
|
||||
@@ -89,7 +76,7 @@ VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||
|
||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||
ENTRYPOINT [ "python3", "-u", "/opt/zoraxy/entrypoint.py" ]
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
||||
|
||||
|
@@ -87,7 +87,10 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|
||||
| `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. |
|
||||
| `ENABLELOG` | `true` (Boolean) | Enable system wide logging, set to false for writing log to STDOUT only. |
|
||||
| `ENABLELOGCOMPRESS` | `true` (Boolean) | Enable log compression for rotated log files. |
|
||||
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
|
||||
| `LOGROTATE` | `0` (Integer) | Enable log rotation and set the maximum log file size in KB (e.g. 25 for 25KB), set to 0 for disable. |
|
||||
| `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. |
|
||||
@@ -119,18 +122,14 @@ Or for Docker Compose:
|
||||
|
||||
### 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.
|
||||
Zoraxy includes a (experimental) store to download and use official plugins right from inside Zoraxy, no preparation required.
|
||||
For those looking to use custom plugins, build your plugins and place them inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location).
|
||||
|
||||
### Building
|
||||
|
||||
To build the Docker image:
|
||||
- Check out the repository/branch.
|
||||
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
|
||||
- Copy the Zoraxy `src/` 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.
|
||||
|
@@ -1,19 +0,0 @@
|
||||
#!/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
|
||||
|
134
docker/entrypoint.py
Normal file
134
docker/entrypoint.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
zoraxy_proc = None
|
||||
zerotier_proc = None
|
||||
|
||||
def getenv(key, default=None):
|
||||
return os.environ.get(key, default)
|
||||
|
||||
def run(command):
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {command} - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def popen(command):
|
||||
proc = subprocess.Popen(command)
|
||||
time.sleep(1)
|
||||
if proc.poll() is not None:
|
||||
print(f"{command} exited early with code {proc.returncode}")
|
||||
raise RuntimeError(f"Failed to start {command}")
|
||||
return proc
|
||||
|
||||
def cleanup(_signum, _frame):
|
||||
print("Shutdown signal received. Cleaning up...")
|
||||
|
||||
global zoraxy_proc, zerotier_proc
|
||||
|
||||
if zoraxy_proc and zoraxy_proc.poll() is None:
|
||||
print("Terminating Zoraxy...")
|
||||
zoraxy_proc.terminate()
|
||||
|
||||
if zerotier_proc and zerotier_proc.poll() is None:
|
||||
print("Terminating ZeroTier-One...")
|
||||
zerotier_proc.terminate()
|
||||
|
||||
if zoraxy_proc:
|
||||
try:
|
||||
zoraxy_proc.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
zoraxy_proc.kill()
|
||||
zoraxy_proc.wait()
|
||||
|
||||
if zerotier_proc:
|
||||
try:
|
||||
zerotier_proc.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
zerotier_proc.kill()
|
||||
zerotier_proc.wait()
|
||||
|
||||
try:
|
||||
os.unlink("/var/lib/zerotier-one")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Failed to unlink ZeroTier socket: {e}")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
def start_zerotier():
|
||||
print("Starting ZeroTier...")
|
||||
|
||||
global zerotier_proc
|
||||
|
||||
config_dir = "/opt/zoraxy/config/zerotier/"
|
||||
zt_path = "/var/lib/zerotier-one"
|
||||
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
os.symlink(config_dir, zt_path, target_is_directory=True)
|
||||
except FileExistsError:
|
||||
print(f"Symlink {zt_path} already exists, skipping creation.")
|
||||
|
||||
zerotier_proc = popen(["zerotier-one"])
|
||||
|
||||
def start_zoraxy():
|
||||
print("Starting Zoraxy...")
|
||||
|
||||
global zoraxy_proc
|
||||
|
||||
zoraxy_args = [
|
||||
"zoraxy",
|
||||
f"-autorenew={ getenv('AUTORENEW', '86400') }",
|
||||
f"-cfgupgrade={ getenv('CFGUPGRADE', 'true') }",
|
||||
f"-db={ getenv('DB', 'auto') }",
|
||||
f"-docker={ getenv('DOCKER', 'true') }",
|
||||
f"-earlyrenew={ getenv('EARLYRENEW', '30') }",
|
||||
f"-enablelog={ getenv('ENABLELOG', 'true') }",
|
||||
f"-enablelogcompress={ getenv('ENABLELOGCOMPRESS', 'true') }",
|
||||
f"-fastgeoip={ getenv('FASTGEOIP', 'false') }",
|
||||
f"-logrotate={ getenv('LOGROTATE', '0') }",
|
||||
f"-mdns={ getenv('MDNS', 'true') }",
|
||||
f"-mdnsname={ getenv('MDNSNAME', "''") }",
|
||||
f"-noauth={ getenv('NOAUTH', 'false') }",
|
||||
f"-plugin={ getenv('PLUGIN', '/opt/zoraxy/plugin/') }",
|
||||
f"-port=:{ getenv('PORT', '8000') }",
|
||||
f"-sshlb={ getenv('SSHLB', 'false') }",
|
||||
f"-update_geoip={ getenv('UPDATE_GEOIP', 'false') }",
|
||||
f"-version={ getenv('VERSION', 'false') }",
|
||||
f"-webfm={ getenv('WEBFM', 'true') }",
|
||||
f"-webroot={ getenv('WEBROOT', './www') }",
|
||||
]
|
||||
|
||||
zoraxy_proc = popen(zoraxy_args)
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, cleanup)
|
||||
signal.signal(signal.SIGINT, cleanup)
|
||||
|
||||
print("Updating CA certificates...")
|
||||
run(["update-ca-certificates"])
|
||||
|
||||
print("Updating GeoIP data...")
|
||||
run(["zoraxy", "-update_geoip=true"])
|
||||
|
||||
os.chdir("/opt/zoraxy/config/")
|
||||
|
||||
if getenv("ZEROTIER", "false") == "true":
|
||||
start_zerotier()
|
||||
|
||||
start_zoraxy()
|
||||
|
||||
signal.pause()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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 &
|
||||
zerotierpid=$!
|
||||
echo "ZeroTier daemon started."
|
||||
fi
|
||||
|
||||
echo "Starting 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" \
|
||||
&
|
||||
|
||||
zoraxypid=$!
|
||||
wait "$zoraxypid"
|
||||
wait "$zerotierpid"
|
||||
|
@@ -457,7 +457,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<i class="divider"> </i>
|
||||
<a class="section externallink" href="" target="_blank">
|
||||
<a class="section externallink" href="https://zoraxy.aroz.org/plugins/html/" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="green code icon"></i>
|
||||
<div class="content" i18n>
|
||||
@@ -519,8 +519,8 @@
|
||||
</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 class="item"><a href="https://github.com/aroz-online/zoraxy-official-plugins" target="_blank">Offical Plugin List</a></div>
|
||||
<div class="item"><a href="https://zoraxy.aroz.org/plugins/html/" target="_blank">Plugin Development Guide</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
|
@@ -14,6 +14,12 @@ import (
|
||||
"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"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the Go program with the specified arguments
|
||||
./docs.exe -m=build -root="/plugins/html/"
|
||||
./docs.exe -m=build -root=plugins/html/
|
@@ -19,7 +19,7 @@ func optimizeCss(htmlContent []byte) ([]byte, error) {
|
||||
|
||||
doc.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||
//For each of the image element, replace the parent from p to div
|
||||
orginalParent, err := s.Parent().Html()
|
||||
originalParent, err := s.Parent().Html()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting parent HTML:", err)
|
||||
return
|
||||
@@ -33,10 +33,10 @@ func optimizeCss(htmlContent []byte) ([]byte, error) {
|
||||
encodedSrc := (&url.URL{Path: src}).String()
|
||||
|
||||
//Patch the bug in the parser that converts " />" to "/>"
|
||||
orginalParent = strings.ReplaceAll(orginalParent, "/>", " />")
|
||||
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, orginalParent, "<div class=\"ts-image is-rounded\" style=\"max-width: 800px\">"+orginalParent+"</div>", 1)
|
||||
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
|
||||
|
@@ -3,4 +3,10 @@ go build
|
||||
# Run the Go program with the specified arguments
|
||||
./docs.exe -m=build
|
||||
|
||||
./docs.exe
|
||||
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
|
||||
|
@@ -12,7 +12,7 @@ Let start with a really simple Hello World plugin. This only function of this pl
|
||||
|
||||
First things first, give your plugin a name. In this example, we are using the name "helloworld".
|
||||
|
||||
**Plugin name cannot contain space or special characters**, so you must use a file name that satisfy the requirement. No worry, the plugin file name is not the same as the plugin display name in the introspect.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -56,7 +56,7 @@ ls
|
||||
|
||||
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the `zoraxy_plugin` Go module under `src/mod/plugins/zoraxy_plugin`
|
||||
|
||||
Copy the `zoraxy_plugin` folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as `mod`, then your copied library path should be `plugins/helloword/mod/zoraxy_plugin`
|
||||
Copy the `zoraxy_plugin` folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as `mod`, then your copied library path should be `plugins/helloworld/mod/zoraxy_plugin`
|
||||
|
||||
```bash
|
||||
mkdir ./mod
|
||||
@@ -170,7 +170,7 @@ func main(){
|
||||
|
||||
|
||||
|
||||
**Notes: If some post processing is needed between Introspect and Configure, you can use two seperate function to handle the first start and the second starting of your plugin. The "seperated version" of `ServeAndRecvSpec` is defined as ` ServeIntroSpect(pluginSpect *IntroSpect) ` and `RecvConfigureSpec() (*ConfigureSpec, error)`. See `zoraxy_plugin.go` for more information.**
|
||||
**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,8 +7,6 @@ This example demonstrates how to use static capture in Zoraxy plugins. Static ca
|
||||
|
||||
**Notes: This example assumes you have already read Hello World example.**
|
||||
|
||||
Let's dive in!
|
||||
|
||||
---
|
||||
|
||||
## 1. Create the plugin folder structure
|
||||
|
@@ -318,7 +318,7 @@ If everything is correctly setup, you should see the following page when request
|
||||
|
||||
Example terminal output for requesting `/foobar/*`:
|
||||
|
||||
```
|
||||
```html
|
||||
[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
|
||||
|
257
docs/plugins/docs/zoraxy_plugin API.md
Normal file
257
docs/plugins/docs/zoraxy_plugin API.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Zoraxy Plugin APIs
|
||||
This API documentation is auto-generated from the Zoraxy plugin source code.
|
||||
|
||||
|
||||
<pre><code class='language-go'>
|
||||
package zoraxy_plugin // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
|
||||
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect)
|
||||
ServeIntroSpect Function
|
||||
|
||||
This function will check if the plugin is initialized with -introspect flag,
|
||||
if so, it will print the intro spect and exit
|
||||
|
||||
Place this function at the beginning of your plugin main function
|
||||
|
||||
|
||||
TYPES
|
||||
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
|
||||
}
|
||||
ConfigureSpec Payload
|
||||
|
||||
Zoraxy will start your plugin with -configure flag, the plugin shell read
|
||||
this payload as JSON and configure itself by the supplied values like
|
||||
starting a web server at given port that listens to 127.0.0.1:port
|
||||
|
||||
func RecvConfigureSpec() (*ConfigureSpec, error)
|
||||
RecvExecuteConfigureSpec Function
|
||||
|
||||
This function will read the configure spec from Zoraxy and return the
|
||||
ConfigureSpec object
|
||||
|
||||
Place this function after ServeIntroSpect function in your plugin main
|
||||
function
|
||||
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error)
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec See
|
||||
the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
|
||||
type ControlStatusCode int
|
||||
|
||||
const (
|
||||
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
|
||||
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
|
||||
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
|
||||
)
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
// Has unexported fields.
|
||||
}
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
|
||||
A custom request object to be used in the dynamic sniffing
|
||||
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error)
|
||||
DecodeForwardRequestPayload decodes JSON bytes into a
|
||||
DynamicSniffForwardRequest object
|
||||
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest
|
||||
GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an
|
||||
http.Request object
|
||||
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request
|
||||
GetRequest returns the original http.Request object, for debugging purposes
|
||||
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string
|
||||
GetRequestUUID returns the request UUID if this UUID is empty string,
|
||||
that might indicate the request is not coming from the dynamic router
|
||||
|
||||
type IntroSpect struct {
|
||||
// Plugin metadata
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
// Static Capture Settings
|
||||
//
|
||||
// Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
// This is faster than dynamic capture, but less flexible
|
||||
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
// Dynamic Capture Settings
|
||||
//
|
||||
// Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
// if the plugin sniff returns 280, the traffic will be captured
|
||||
// otherwise, the traffic will be forwarded to the next plugin
|
||||
// This is slower than static capture, but more flexible
|
||||
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
// UI Path for your plugin
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
// Subscriptions Settings
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
}
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag, the plugin shell
|
||||
return this payload as JSON and exit
|
||||
|
||||
type PathRouter struct {
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPathRouter() *PathRouter
|
||||
NewPathRouter creates a new PathRouter
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request)
|
||||
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request))
|
||||
RegisterDynamicCaptureHandle register the dynamic capture ingress path with
|
||||
a handler
|
||||
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler)
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and
|
||||
paths
|
||||
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler)
|
||||
RegisterPathHandler registers a handler for a path
|
||||
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux)
|
||||
StartStaticCapture starts the static capture ingress
|
||||
|
||||
func (p *PathRouter) RemovePathHandler(path string)
|
||||
RemovePathHandler removes a handler for a path
|
||||
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool)
|
||||
SetDebugPrintMode sets the debug print mode
|
||||
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler)
|
||||
SetDefaultHandler sets the default handler for the router This handler will
|
||||
be called if no path handler is found
|
||||
|
||||
type PluginType int
|
||||
|
||||
const (
|
||||
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
|
||||
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
|
||||
)
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter
|
||||
NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
The handlerPrefix is the prefix of the handler used to route this router
|
||||
The handlerPrefix should start with a slash (e.g. /ui) that matches the
|
||||
http.Handle path All prefix should not end with a slash
|
||||
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux)
|
||||
Attach the file system UI handler to the target http.ServeMux
|
||||
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler
|
||||
GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
|
||||
RegisterTerminateHandler registers the terminate handler for the
|
||||
PluginUiRouter The terminate handler will be called when the plugin is
|
||||
terminated from Zoraxy plugin manager if mux is nil, the handler will be
|
||||
registered to http.DefaultServeMux
|
||||
|
||||
type PluginUiRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
// Has unexported fields.
|
||||
}
|
||||
|
||||
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter
|
||||
NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS The
|
||||
targetFsPrefix is the prefix of the embed.FS where the UI files are stored
|
||||
The targetFsPrefix should be relative to the root of the embed.FS The
|
||||
targetFsPrefix should start with a slash (e.g. /web) that corresponds to the
|
||||
root folder of the embed.FS The handlerPrefix is the prefix of the handler
|
||||
used to route this router The handlerPrefix should start with a slash (e.g.
|
||||
/ui) that matches the http.Handle path All prefix should not end with a
|
||||
slash
|
||||
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux)
|
||||
Attach the embed UI handler to the target http.ServeMux
|
||||
|
||||
func (p *PluginUiRouter) Handler() http.Handler
|
||||
GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
|
||||
RegisterTerminateHandler registers the terminate handler for the
|
||||
PluginUiRouter The terminate handler will be called when the plugin is
|
||||
terminated from Zoraxy plugin manager if mux is nil, the handler will be
|
||||
registered to http.DefaultServeMux
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
type StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
}
|
||||
|
||||
type SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
</code></pre>
|
23
docs/plugins/gen_zoraxy_plugin_doc.sh
Normal file
23
docs/plugins/gen_zoraxy_plugin_doc.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#/bin/bash
|
||||
|
||||
# Cd into zoraxy plugin directory
|
||||
cd ../../src/mod/plugins/zoraxy_plugin/
|
||||
|
||||
|
||||
# Add header to the documentation
|
||||
echo "# Zoraxy Plugin APIs" > docs.md
|
||||
echo "This API documentation is auto-generated from the Zoraxy plugin source code." >> docs.md
|
||||
echo "" >> docs.md
|
||||
echo "" >> docs.md
|
||||
echo "<pre><code class='language-go'>" >> docs.md
|
||||
go doc -all >> docs.md
|
||||
echo "</code></pre>" >> docs.md
|
||||
|
||||
# Replace // import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" with
|
||||
# // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
|
||||
sed -i 's|// import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"|// import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"|g' docs.md
|
||||
|
||||
# Move the generated docs to the plugins/html directory
|
||||
mv docs.md "../../../../docs/plugins/docs/zoraxy_plugin API.md"
|
||||
|
||||
echo "Done generating Zoraxy plugin documentation."
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<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="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<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="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<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="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item is-active" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item is-active" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item is-active" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item is-active" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<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="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item is-active" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item is-active" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item is-active" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item is-active" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item is-active" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item is-active" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<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="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
@@ -178,7 +182,7 @@
|
||||
<span class="ts-text is-heavy">
|
||||
Plugin name cannot contain space or special characters
|
||||
</span>
|
||||
, so you must use a file name that satisfy the requirement. No worry, the plugin file name is not the same as the plugin display name in the introspect.
|
||||
, 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>
|
||||
@@ -256,7 +260,7 @@ ls
|
||||
</span>
|
||||
, then your copied library path should be
|
||||
<span class="ts-text is-code">
|
||||
plugins/helloword/mod/zoraxy_plugin
|
||||
plugins/helloworld/mod/zoraxy_plugin
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
@@ -403,7 +407,7 @@ func main(){
|
||||
</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 “seperated version” of
|
||||
Notes: If some post processing is needed between Introspect and Configure, you can use two seperate function to handle the first start and the second starting of your plugin. The “separated version” of
|
||||
<span class="ts-text is-code">
|
||||
ServeAndRecvSpec
|
||||
</span>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item is-active" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item is-active" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item is-active" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
<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>
|
||||
@@ -175,9 +179,6 @@
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
Let’s dive in!
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="1-create-the-plugin-folder-structure">
|
||||
1. Create the plugin folder structure
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item is-active" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<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="/html/index.html">
|
||||
<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>
|
||||
@@ -611,7 +615,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
|
||||
:
|
||||
</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/
|
||||
<pre><code class="language-html">[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
|
||||
@@ -637,7 +641,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
|
||||
[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>
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
|
@@ -52,7 +52,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
<script src="/plugins/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
@@ -64,13 +64,14 @@
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
<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="#!" class="item">
|
||||
<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>
|
||||
@@ -87,19 +88,19 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
<a class="item" href="/plugins/html/1. Introduction/5. Viewing Plugin Info.html">
|
||||
Viewing Plugin Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -108,22 +109,22 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/6. Compile a Plugin.html">
|
||||
<a class="item" href="/plugins/html/2. Architecture/6. Compile a Plugin.html">
|
||||
Compile a Plugin
|
||||
</a>
|
||||
</div>
|
||||
@@ -132,22 +133,25 @@
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/3. Static Capture Example.html">
|
||||
Static Capture Example
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
<a class="item" href="/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html">
|
||||
Dynamic Capture Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item is-active" href="/html/index.html">
|
||||
<a class="item is-active" href="/plugins/html/index.html">
|
||||
index
|
||||
</a>
|
||||
<a class="item" href="/plugins/html/zoraxy_plugin API.html">
|
||||
zoraxy_plugin API
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
448
docs/plugins/html/zoraxy_plugin API.html
Normal file
448
docs/plugins/html/zoraxy_plugin API.html
Normal file
@@ -0,0 +1,448 @@
|
||||
<!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>
|
||||
zoraxy_plugin API | 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" 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 is-active" 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="zoraxy-plugin-apis">
|
||||
Zoraxy Plugin APIs
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
This API documentation is auto-generated from the Zoraxy plugin source code.
|
||||
</p>
|
||||
</p>
|
||||
<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>
|
||||
</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>
|
@@ -103,6 +103,11 @@
|
||||
"filename": "index.md",
|
||||
"title": "index",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"filename": "zoraxy_plugin API.md",
|
||||
"title": "zoraxy_plugin API",
|
||||
"type": "file"
|
||||
}
|
||||
]
|
||||
}
|
@@ -10,12 +10,6 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
Filename string `json:"filename"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
/* Change this before deploying */
|
||||
var (
|
||||
mode = flag.String("m", "web", "Mode to run the application: 'web' or 'build'")
|
||||
@@ -28,6 +22,10 @@ func main() {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if (*root_url)[0] != '/' {
|
||||
*root_url = "/" + *root_url
|
||||
}
|
||||
|
||||
switch *mode {
|
||||
case "build":
|
||||
build()
|
||||
|
@@ -71,8 +71,8 @@
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="#!" class="item">
|
||||
Examples
|
||||
<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>
|
||||
|
10
example/plugins/.gitignore
vendored
Normal file
10
example/plugins/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
api-call-example/api-call-example
|
||||
debugger/debugger
|
||||
dynamic-capture-example/dynamic-capture-example
|
||||
event-subscriber-example/event-subscriber-example
|
||||
helloworld/helloworld
|
||||
plugin2plugin-comms-peer1/plugin2plugin-comms-peer1
|
||||
plugin2plugin-comms-peer2/plugin2plugin-comms-peer2
|
||||
restful-example/restful-example
|
||||
static-capture-example/static-capture-example
|
||||
upnp/upnp
|
10
example/plugins/README.md
Normal file
10
example/plugins/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Example Plugins
|
||||
|
||||
This directory contains example plugins that demonstrate how to create and use plugins with the main application. Each plugin is designed to showcase different features and capabilities of the plugin system.
|
||||
|
||||
## Some Note-Worthy Examples
|
||||
|
||||
- **api-call-example**: Demonstrates how plugins can make API calls to zoraxy
|
||||
- **event-subscriber-example**: Shows how to subscribe to and handle events from zoraxy within the application
|
||||
- **plugin2plugin-comms-peer1**: Illustrates communication between two plugins via the event system, where this plugin acts as the first peer
|
||||
- **plugin2plugin-comms-peer2**: Similar to the above, but this plugin acts as the second peer in the communication
|
3
example/plugins/api-call-example/go.mod
Normal file
3
example/plugins/api-call-example/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/api-call-example
|
||||
|
||||
go 1.24.5
|
54
example/plugins/api-call-example/main.go
Normal file
54
example/plugins/api-call-example/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.api_call_example"
|
||||
UI_PATH = "/ui"
|
||||
)
|
||||
|
||||
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: PLUGIN_ID,
|
||||
Name: "API Call Example Plugin",
|
||||
Author: "Anthony Rubick",
|
||||
AuthorContact: "",
|
||||
Description: "An example plugin for making API calls",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Endpoint: "/plugin/api/access/list",
|
||||
Reason: "Used to display all configured Access Rules",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error serving introspect: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
http.HandleFunc(UI_PATH+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
RenderUI(runtimeCfg, w, r)
|
||||
})
|
||||
|
||||
serverAddr := fmt.Sprintf("127.0.0.1:%d", runtimeCfg.Port)
|
||||
fmt.Printf("Starting API Call Example Plugin on %s\n", serverAddr)
|
||||
http.ListenAndServe(serverAddr, nil)
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
# Zoraxy Plugin
|
||||
|
||||
## Overview
|
||||
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Copy the Module:**
|
||||
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||
|
||||
2. **Include the Structure:**
|
||||
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||
|
||||
3. **Modify as Needed:**
|
||||
- Customize the copied module to implement the desired functionality for your plugin.
|
||||
|
||||
## Directory Structure
|
||||
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
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) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
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"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
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 SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
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 PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
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, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
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 RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
282
example/plugins/api-call-example/ui.go
Normal file
282
example/plugins/api-call-example/ui.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to the permitted endpoint
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Make sure to set the Authorization header
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey) // Use the API key from the runtime config
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
// Check if the response status is OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return string(respDump), fmt.Errorf("received non-OK response status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to the permitted endpoint with an invalid key
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use an invalid API key
|
||||
req.Header.Set("Authorization", "Bearer invalid-key")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func unaccessibleEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to an endpoint that is not permitted
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/api/acme/listExpiredDomains", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use the API key from the runtime config
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func unpermittedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to an endpoint that is plugin-accessible but is not permitted
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/proxy/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use the API key from the runtime config
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
|
||||
// make several types of API calls to demonstrate the plugin functionality
|
||||
accessList, err := allowedEndpoint(config)
|
||||
var RenderedAccessListHTML string
|
||||
if err != nil {
|
||||
if accessList != "" {
|
||||
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p><pre>%s</pre>", err, html.EscapeString(accessList))
|
||||
} else {
|
||||
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p>", err)
|
||||
}
|
||||
} else {
|
||||
// Render the access list as HTML
|
||||
RenderedAccessListHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(accessList))
|
||||
}
|
||||
|
||||
// Make an API call with an invalid key
|
||||
invalidKeyResponse, err := allowedEndpointInvalidKey(config)
|
||||
var RenderedInvalidKeyResponseHTML string
|
||||
if err != nil {
|
||||
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<p>Error with invalid key: %v</p>", err)
|
||||
} else {
|
||||
// Render the invalid key response as HTML
|
||||
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(invalidKeyResponse))
|
||||
}
|
||||
|
||||
// Make an API call to an endpoint that is not plugin-accessible
|
||||
unaccessibleResponse, err := unaccessibleEndpoint(config)
|
||||
var RenderedUnaccessibleResponseHTML string
|
||||
if err != nil {
|
||||
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<p>Error with unaccessible endpoint: %v</p>", err)
|
||||
} else {
|
||||
// Render the unaccessible response as HTML
|
||||
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unaccessibleResponse))
|
||||
}
|
||||
|
||||
// Make an API call to an endpoint that is plugin-accessible but is not permitted
|
||||
unpermittedResponse, err := unpermittedEndpoint(config)
|
||||
var RenderedUnpermittedResponseHTML string
|
||||
if err != nil {
|
||||
RenderedUnpermittedResponseHTML = fmt.Sprintf("<p>Error with unpermitted endpoint: %v</p>", err)
|
||||
} else {
|
||||
// Render the unpermitted response as HTML
|
||||
RenderedUnpermittedResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unpermittedResponse))
|
||||
}
|
||||
|
||||
// Render the UI for the plugin
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>API Call Example Plugin UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.response-block {
|
||||
background-color: var(--theme_bg_primary);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.response-block:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.response-block h3 {
|
||||
margin-top: 0;
|
||||
color: var(--text_color);
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.response-block.success {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
.response-block.error {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
.response-block.warning {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
.response-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.response-content pre {
|
||||
background-color: var(--theme_highlight);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
height: 200px;
|
||||
max-height: 80vh;
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</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 class="ui container">
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">Welcome to the API Call Example Plugin UI</h1>
|
||||
<p>Plugin is running on port: ` + strconv.Itoa(config.Port) + `</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<h2>API Call Examples</h2>
|
||||
|
||||
<div class="response-block success">
|
||||
<h3>✅ Allowed Endpoint (Valid API Key)</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/access/list</code> with a valid API key:</p>
|
||||
<div class="response-content">
|
||||
` + RenderedAccessListHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block warning">
|
||||
<h3>⚠️ Invalid API Key</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/access/list</code> with an invalid API key:</p>
|
||||
<div class="response-content">
|
||||
` + RenderedInvalidKeyResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block warning">
|
||||
<h3>⚠️ Unpermitted Endpoint</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/proxy/list</code> (not a permitted endpoint):</p>
|
||||
<div class="response-content">
|
||||
` + RenderedUnpermittedResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block error">
|
||||
<h3>❌ Disallowed Endpoint</h3>
|
||||
<p>Making a GET request to <code>/api/acme/listExpiredDomains</code> (not a plugin-accessible endpoint):</p>
|
||||
<div class="response-content">
|
||||
` + RenderedUnaccessibleResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
w.Write([]byte(html))
|
||||
}
|
@@ -4,7 +4,12 @@
|
||||
echo "Copying zoraxy_plugin to all mods"
|
||||
for dir in ./*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r ../mod/plugins/zoraxy_plugin "$dir/mod"
|
||||
# remove existing zoraxy_plugin module, if it exists
|
||||
if [ -d "${dir}/mod/zoraxy_plugin" ]; then
|
||||
rm -r $dir/mod/zoraxy_plugin
|
||||
fi
|
||||
# copy over updated module
|
||||
cp -r ../../src/mod/plugins/zoraxy_plugin "$dir/mod"
|
||||
fi
|
||||
done
|
||||
|
||||
|
@@ -86,7 +86,7 @@ func main() {
|
||||
if strings.HasPrefix(dsfr.RequestURI, "/test_") {
|
||||
reqUUID := dsfr.GetRequestUUID()
|
||||
fmt.Println("Accepting request with UUID: " + reqUUID)
|
||||
return plugin.SniffResultAccpet
|
||||
return plugin.SniffResultAccept
|
||||
}
|
||||
|
||||
return plugin.SniffResultSkip
|
||||
|
@@ -17,7 +17,7 @@ import (
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
|
@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
|
181
example/plugins/debugger/mod/zoraxy_plugin/events/events.go
Normal file
181
example/plugins/debugger/mod/zoraxy_plugin/events/events.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
@@ -97,7 +103,10 @@ type IntroSpect struct {
|
||||
|
||||
/* 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
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
@@ -156,12 +167,12 @@ func RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("No port specified after -configure flag")
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No -configure flag found")
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -77,8 +77,8 @@ func main() {
|
||||
fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
|
||||
fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
|
||||
|
||||
// We want to handle this request, reply with aSniffResultAccept
|
||||
return plugin.SniffResultAccpet
|
||||
// We want to handle this request, reply with a SniffResultAccept
|
||||
return plugin.SniffResultAccept
|
||||
}
|
||||
|
||||
// If the request URI does not match, we skip this request
|
||||
|
@@ -17,7 +17,7 @@ import (
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
|
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
@@ -97,7 +103,10 @@ type IntroSpect struct {
|
||||
|
||||
/* 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
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
@@ -156,12 +167,12 @@ func RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("No port specified after -configure flag")
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No -configure flag found")
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
|
3
example/plugins/event-subscriber-example/go.mod
Normal file
3
example/plugins/event-subscriber-example/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/event-subscriber-example
|
||||
|
||||
go 1.24.5
|
106
example/plugins/event-subscriber-example/main.go
Normal file
106
example/plugins/event-subscriber-example/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
plugin "aroz.org/zoraxy/event-subscriber-example/mod/zoraxy_plugin"
|
||||
"aroz.org/zoraxy/event-subscriber-example/mod/zoraxy_plugin/events"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.event_subscriber_example"
|
||||
UI_PATH = "/ui"
|
||||
EVENT_PATH = "/notifyme/"
|
||||
)
|
||||
|
||||
var (
|
||||
EventLog = make([]events.Event, 0) // A slice to store events
|
||||
EventLogMutex = &sync.Mutex{} // Mutex to protect access to the event log
|
||||
)
|
||||
|
||||
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: PLUGIN_ID,
|
||||
Name: "Event Subscriber Example Plugin",
|
||||
Author: "Anthony Rubick",
|
||||
AuthorContact: "",
|
||||
Description: "An example plugin for event subscriptions, will display all events in the UI",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath: "/notifyme",
|
||||
SubscriptionsEvents: map[string]string{
|
||||
// for this example, we will subscribe to all events that exist at time of writing
|
||||
string(events.EventBlacklistedIPBlocked): "This event is triggered when a blacklisted IP is blocked",
|
||||
string(events.EventBlacklistToggled): "This event is triggered when the blacklist is toggled for an access rule",
|
||||
string(events.EventAccessRuleCreated): "This event is triggered when a new access ruleset is created",
|
||||
string(events.EventCustom): "This event is a custom event that can be emitted by any plugin, we subscribe to it to demonstrate a \"monitor\" plugin that can see all custom events emitted by other plugins",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error serving introspect: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
http.HandleFunc(UI_PATH+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
RenderUI(runtimeCfg, w, r)
|
||||
})
|
||||
http.HandleFunc(EVENT_PATH, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
var event events.Event
|
||||
|
||||
// read the request body
|
||||
if r.Body == nil || r.ContentLength == 0 {
|
||||
http.Error(w, "Request body is empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
buffer := bytes.NewBuffer(make([]byte, 0, r.ContentLength))
|
||||
if _, err := buffer.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse the event from the request body
|
||||
if err := events.ParseEvent(buffer.Bytes(), &event); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to parse event: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Typically, at this point you would use a switch statement on the event.Name
|
||||
// to route the event to the appropriate handler.
|
||||
//
|
||||
// For this example, we will just store the event and return a success message.
|
||||
EventLogMutex.Lock()
|
||||
defer EventLogMutex.Unlock()
|
||||
if len(EventLog) >= 100 { // Limit the log size to 100 events
|
||||
EventLog = EventLog[1:] // Remove the oldest event
|
||||
}
|
||||
EventLog = append(EventLog, event) // Store the event in the log
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprintf(w, "Event received: %s", event.Name)
|
||||
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
serverAddr := fmt.Sprintf("127.0.0.1:%d", runtimeCfg.Port)
|
||||
fmt.Printf("Starting API Call Example Plugin on %s\n", serverAddr)
|
||||
http.ListenAndServe(serverAddr, nil)
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
# Zoraxy Plugin
|
||||
|
||||
## Overview
|
||||
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Copy the Module:**
|
||||
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||
|
||||
2. **Include the Structure:**
|
||||
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||
|
||||
3. **Modify as Needed:**
|
||||
- Customize the copied module to implement the desired functionality for your plugin.
|
||||
|
||||
## Directory Structure
|
||||
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
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) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
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"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
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 SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
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 PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
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, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
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 RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
99
example/plugins/event-subscriber-example/ui.go
Normal file
99
example/plugins/event-subscriber-example/ui.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
plugin "aroz.org/zoraxy/event-subscriber-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
|
||||
// Render the UI for the plugin
|
||||
var eventLogHTML string
|
||||
if len(EventLog) == 0 {
|
||||
eventLogHTML = "<p>No events received yet<br>Try toggling a blacklist or something like that</p>"
|
||||
} else {
|
||||
EventLogMutex.Lock()
|
||||
defer EventLogMutex.Unlock()
|
||||
for _, event := range EventLog {
|
||||
rawEventData, _ := json.Marshal(event)
|
||||
|
||||
eventLogHTML += "<div class='response-block'>"
|
||||
eventLogHTML += "<h3>" + string(event.Name) + " at " + time.Unix(event.Timestamp, 0).Local().Format(time.RFC3339) + "</h3>"
|
||||
eventLogHTML += "<div class='response-content'>"
|
||||
eventLogHTML += "<p class='ui meta'>Event Data:</p>"
|
||||
eventLogHTML += "<pre>" + string(rawEventData) + "</pre>"
|
||||
eventLogHTML += "</div></div>"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Event Log</title>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.response-block {
|
||||
background-color: var(--theme_bg_primary);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 5px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.response-block:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.response-block h3 {
|
||||
margin-top: 0;
|
||||
color: var(--text_color);
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.response-content pre {
|
||||
background-color: var(--theme_highlight);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
height: fit-content;
|
||||
max-height: 80vh;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</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 class="ui container">
|
||||
|
||||
<h1>Event Log</h1>
|
||||
<div id="event-log" class="ui basic segment">` + eventLogHTML + `</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
w.Write([]byte(html))
|
||||
}
|
@@ -17,7 +17,7 @@ import (
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
|
@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
|
181
example/plugins/helloworld/mod/zoraxy_plugin/events/events.go
Normal file
181
example/plugins/helloworld/mod/zoraxy_plugin/events/events.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
@@ -97,7 +103,10 @@ type IntroSpect struct {
|
||||
|
||||
/* 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
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
@@ -156,12 +167,12 @@ func RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("No port specified after -configure flag")
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No -configure flag found")
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
|
224
example/plugins/plugin2plugin-comms-peer1/api.go
Normal file
224
example/plugins/plugin2plugin-comms-peer1/api.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
plugin "aroz.org/zoraxy/plugins/plugin2plugin-comms-peer1/mod/zoraxy_plugin"
|
||||
"aroz.org/zoraxy/plugins/plugin2plugin-comms-peer1/mod/zoraxy_plugin/events"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
Sent bool `json:"sent"`
|
||||
}
|
||||
|
||||
var (
|
||||
// map of connected SSE clients
|
||||
messageHistory []Message = make([]Message, 0)
|
||||
messageHistoryMu = &sync.Mutex{}
|
||||
clients = make(map[chan *events.CustomEvent]struct{})
|
||||
clientsMu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func sendMessageToPeer(config *plugin.ConfigureSpec, message string) error {
|
||||
// build the request payload
|
||||
event := events.CustomEvent{
|
||||
SourcePlugin: PLUGIN_ID,
|
||||
Recipients: []string{PEER_ID},
|
||||
Payload: map[string]any{"message": message},
|
||||
}
|
||||
|
||||
// Make an API call to the peer plugin's endpoint
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/event/emit", config.ZoraxyPort)
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(event); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, apiURL, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure to set the Authorization header
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey) // Use the API key from the runtime config
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
response_body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("Response Body: %s\n", string(response_body))
|
||||
return fmt.Errorf("failed to call the zoraxy API: %s, %v", resp.Status, string(response_body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSendMessage(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message body
|
||||
var body struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, "Failed to parse JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
message := body.Message
|
||||
if message == "" {
|
||||
http.Error(w, "Message cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// send the message to the peer plugin
|
||||
err := sendMessageToPeer(config, message)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to send message to peer: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the sent message
|
||||
messageHistoryMu.Lock()
|
||||
messageHistory = append(messageHistory, Message{Message: message, Sent: true})
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Message sent to peer successfully"))
|
||||
}
|
||||
|
||||
func handleFetchMessageHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
messageHistoryMu.Lock()
|
||||
historyCopy := make([]Message, len(messageHistory))
|
||||
copy(historyCopy, messageHistory)
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
resp := struct {
|
||||
Messages []Message `json:"messages"`
|
||||
}{
|
||||
Messages: historyCopy,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func handleReceivedEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var event events.Event
|
||||
|
||||
// read the request body
|
||||
if r.Body == nil || r.ContentLength == 0 {
|
||||
http.Error(w, "Request body is empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
buffer := bytes.NewBuffer(make([]byte, 0, r.ContentLength))
|
||||
if _, err := buffer.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse the event from the request body
|
||||
if err := events.ParseEvent(buffer.Bytes(), &event); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to parse event: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Name {
|
||||
case events.EventCustom:
|
||||
// downcast event.Data to CustomEvent
|
||||
customData, ok := event.Data.(*events.CustomEvent)
|
||||
if !ok {
|
||||
http.Error(w, "Invalid event data for CustomEvent", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Log the received message
|
||||
messageHistoryMu.Lock()
|
||||
if msg, exists := customData.Payload["message"].(string); exists {
|
||||
messageHistory = append(messageHistory, Message{Message: msg, Sent: false})
|
||||
}
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
// Broadcast to all connected SSE clients
|
||||
broadcastMessage(customData)
|
||||
|
||||
// Respond to the sender
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Event received successfully"))
|
||||
// For demonstration, print the message to the console
|
||||
fmt.Printf("Received message from plugin %s: %v\n", customData.SourcePlugin, customData.Payload["message"])
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("Unhandled event type: %s", event.Name), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SSE handler
|
||||
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("SSE connection established")
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
eventChan := make(chan *events.CustomEvent)
|
||||
clientsMu.Lock()
|
||||
clients[eventChan] = struct{}{}
|
||||
clientsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
clientsMu.Lock()
|
||||
delete(clients, eventChan)
|
||||
clientsMu.Unlock()
|
||||
close(eventChan)
|
||||
}()
|
||||
|
||||
// Send events as they arrive
|
||||
for event := range eventChan {
|
||||
data, _ := json.Marshal(event)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all clients
|
||||
func broadcastMessage(message *events.CustomEvent) {
|
||||
clientsMu.Lock()
|
||||
defer clientsMu.Unlock()
|
||||
for ch := range clients {
|
||||
select {
|
||||
case ch <- message:
|
||||
default:
|
||||
// If the client is not listening, skip
|
||||
}
|
||||
}
|
||||
}
|
3
example/plugins/plugin2plugin-comms-peer1/go.mod
Normal file
3
example/plugins/plugin2plugin-comms-peer1/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/plugins/plugin2plugin-comms-peer1
|
||||
|
||||
go 1.24.5
|
94
example/plugins/plugin2plugin-comms-peer1/main.go
Normal file
94
example/plugins/plugin2plugin-comms-peer1/main.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "aroz.org/zoraxy/plugins/plugin2plugin-comms-peer1/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// Notes:
|
||||
// This plugin handles updating the UI with new messages received from the peer plugin via SSE, other option you
|
||||
// could use are WebSockets or polling the server at intervals
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.plugin2plugin_comms_peer1"
|
||||
PEER_ID = "org.aroz.zoraxy.plugin2plugin_comms_peer2"
|
||||
UI_PATH = "/ui"
|
||||
SUBSCRIPTION_PATH = "/notifyme"
|
||||
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: PLUGIN_ID,
|
||||
Name: "Plugin2Plugin Comms Peer 1",
|
||||
Author: "Anthony Rubick",
|
||||
AuthorContact: "",
|
||||
Description: "An example plugin for demonstrating plugin to plugin communications - Peer 1",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Endpoint: "/plugin/event/emit",
|
||||
Reason: "Used to send events to the peer plugin",
|
||||
},
|
||||
},
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath: SUBSCRIPTION_PATH,
|
||||
SubscriptionsEvents: map[string]string{
|
||||
"dummy": "A dummy event to satisfy the requirement of having at least one event",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error serving introspect: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
// for debugging, use the following line instead
|
||||
// embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, "."+ WEB_ROOT, UI_PATH)
|
||||
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Plugin Exited")
|
||||
}, nil)
|
||||
|
||||
// Serve the API
|
||||
RegisterAPIs(runtimeCfg)
|
||||
|
||||
// Serve the web page in the www folder
|
||||
http.Handle(UI_PATH+"/", embedWebRouter.Handler())
|
||||
http.HandleFunc(SUBSCRIPTION_PATH+"/", handleReceivedEvent)
|
||||
fmt.Println("Plugin2Plugin Comms Peer 1 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)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterAPIs(cfg *plugin.ConfigureSpec) {
|
||||
// Add API handlers here
|
||||
http.HandleFunc(UI_PATH+"/api/send_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleSendMessage(cfg, w, r)
|
||||
})
|
||||
http.HandleFunc(UI_PATH+"/api/events", handleSSE)
|
||||
http.HandleFunc(UI_PATH+"/api/message_history", handleFetchMessageHistory)
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
# Zoraxy Plugin
|
||||
|
||||
## Overview
|
||||
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Copy the Module:**
|
||||
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||
|
||||
2. **Include the Structure:**
|
||||
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||
|
||||
3. **Modify as Needed:**
|
||||
- Customize the copied module to implement the desired functionality for your plugin.
|
||||
|
||||
## Directory Structure
|
||||
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
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) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
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"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
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 SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
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 PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
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, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
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 RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
152
example/plugins/plugin2plugin-comms-peer1/www/index.html
Normal file
152
example/plugins/plugin2plugin-comms-peer1/www/index.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Plugin2Plugin Comms</title>
|
||||
<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>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.sent-message {
|
||||
background-color: var(--theme_bg_primary);
|
||||
border-left: 5px solid #155724;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
.received-message {
|
||||
background-color: var(--theme_bg_secondary);
|
||||
border-left: 5px solid #004085;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
</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 class="ui container">
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container"></div>
|
||||
<script>
|
||||
// Function to show toast message
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = $('<div class="ui message ' + type + '" style="opacity: 0;">' + message + '</div>');
|
||||
$('.toast-container').append(toast);
|
||||
toast.animate({opacity: 1}, 300);
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(function() {
|
||||
toast.animate({opacity: 0}, 300, function() {
|
||||
toast.remove();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">Welcome to the Plugin2Plugin Comms Peer 1 UI</h1>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Message Form -->
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">Send Message to Peer Plugin</h2>
|
||||
<div class="ui form" id="messageForm">
|
||||
<div class="field">
|
||||
<label for="messageInput">Message:</label>
|
||||
<input type="text" id="messageInput" name="message" placeholder="Enter your message" required>
|
||||
</div>
|
||||
<button class="ui primary button" id="sendMessageButton">Send Message</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Handle form submission
|
||||
$('#sendMessageButton').click(function(event) {
|
||||
event.preventDefault();
|
||||
const message = $('#messageInput').val();
|
||||
$.cjax({
|
||||
url: './api/send_message',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ message: message }),
|
||||
success: function(response) {
|
||||
showToast('Message sent!');
|
||||
// Log the sent message
|
||||
const sentMessage = $('<div class="item sent-message"><div class="content"><div class="header">Sent:</div><div class="description">' + message + '</div></div></div>');
|
||||
$('#messageLog').prepend(sentMessage);
|
||||
$('#messageInput').val(''); // Clear input field
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
showToast('Error sending message!', 'error');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Message Log -->
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">Messages</h2>
|
||||
<div id="messageLog" class="ui relaxed divided list" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Messages will be appended here -->
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Set up EventSource to listen for incoming messages
|
||||
const eventSource = new EventSource('./api/events');
|
||||
eventSource.onmessage = function(e) {
|
||||
const event = JSON.parse(e.data);
|
||||
if (event && event.payload && event.payload.message) {
|
||||
const receivedMessage = $('<div class="item received-message"><div class="content"><div class="header">Received:</div><div class="description">' + event.payload.message + '</div></div></div>');
|
||||
$('#messageLog').prepend(receivedMessage);
|
||||
}
|
||||
showToast('New message received!');
|
||||
};
|
||||
eventSource.onerror = function(err) {
|
||||
console.error('EventSource failed:', err);
|
||||
eventSource.close();
|
||||
};
|
||||
// Clean up EventSource on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
eventSource.close();
|
||||
});
|
||||
|
||||
// Fetch and display message history on page load
|
||||
$(document).ready(function() {
|
||||
$.cjax({
|
||||
url: './api/message_history',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response && response.messages) {
|
||||
response.messages.forEach(function(msg) {
|
||||
const messageClass = msg.sent ? 'sent-message' : 'received-message';
|
||||
const messageItem = $('<div class="item ' + messageClass + '"><div class="content"><div class="header">' + (msg.sent ? 'Sent:' : 'Received:') + '</div><div class="description">' + msg.message + '</div></div></div>');
|
||||
$('#messageLog').append(messageItem);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error fetching message history:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
224
example/plugins/plugin2plugin-comms-peer2/api.go
Normal file
224
example/plugins/plugin2plugin-comms-peer2/api.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
plugin "aroz.org/zoraxy/plugins/plugin2plugin-comms-peer2/mod/zoraxy_plugin"
|
||||
"aroz.org/zoraxy/plugins/plugin2plugin-comms-peer2/mod/zoraxy_plugin/events"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
Sent bool `json:"sent"`
|
||||
}
|
||||
|
||||
var (
|
||||
// map of connected SSE clients
|
||||
messageHistory []Message = make([]Message, 0)
|
||||
messageHistoryMu = &sync.Mutex{}
|
||||
clients = make(map[chan *events.CustomEvent]struct{})
|
||||
clientsMu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func sendMessageToPeer(config *plugin.ConfigureSpec, message string) error {
|
||||
// build the request payload
|
||||
event := events.CustomEvent{
|
||||
SourcePlugin: PLUGIN_ID,
|
||||
Recipients: []string{PEER_ID},
|
||||
Payload: map[string]any{"message": message},
|
||||
}
|
||||
|
||||
// Make an API call to the peer plugin's endpoint
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/event/emit", config.ZoraxyPort)
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(event); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, apiURL, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure to set the Authorization header
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey) // Use the API key from the runtime config
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
response_body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("Response Body: %s\n", string(response_body))
|
||||
return fmt.Errorf("failed to call the zoraxy API: %s, %v", resp.Status, string(response_body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSendMessage(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message body
|
||||
var body struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, "Failed to parse JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
message := body.Message
|
||||
if message == "" {
|
||||
http.Error(w, "Message cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// send the message to the peer plugin
|
||||
err := sendMessageToPeer(config, message)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to send message to peer: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the sent message
|
||||
messageHistoryMu.Lock()
|
||||
messageHistory = append(messageHistory, Message{Message: message, Sent: true})
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Message sent to peer successfully"))
|
||||
}
|
||||
|
||||
func handleFetchMessageHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
messageHistoryMu.Lock()
|
||||
historyCopy := make([]Message, len(messageHistory))
|
||||
copy(historyCopy, messageHistory)
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
resp := struct {
|
||||
Messages []Message `json:"messages"`
|
||||
}{
|
||||
Messages: historyCopy,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func handleReceivedEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var event events.Event
|
||||
|
||||
// read the request body
|
||||
if r.Body == nil || r.ContentLength == 0 {
|
||||
http.Error(w, "Request body is empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
buffer := bytes.NewBuffer(make([]byte, 0, r.ContentLength))
|
||||
if _, err := buffer.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse the event from the request body
|
||||
if err := events.ParseEvent(buffer.Bytes(), &event); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to parse event: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Name {
|
||||
case events.EventCustom:
|
||||
// downcast event.Data to CustomEvent
|
||||
customData, ok := event.Data.(*events.CustomEvent)
|
||||
if !ok {
|
||||
http.Error(w, "Invalid event data for CustomEvent", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Log the received message
|
||||
messageHistoryMu.Lock()
|
||||
if msg, exists := customData.Payload["message"].(string); exists {
|
||||
messageHistory = append(messageHistory, Message{Message: msg, Sent: false})
|
||||
}
|
||||
messageHistoryMu.Unlock()
|
||||
|
||||
// Broadcast to all connected SSE clients
|
||||
broadcastMessage(customData)
|
||||
|
||||
// Respond to the sender
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Event received successfully"))
|
||||
// For demonstration, print the message to the console
|
||||
fmt.Printf("Received message from plugin %s: %v\n", customData.SourcePlugin, customData.Payload["message"])
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("Unhandled event type: %s", event.Name), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SSE handler
|
||||
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("SSE connection established")
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
eventChan := make(chan *events.CustomEvent)
|
||||
clientsMu.Lock()
|
||||
clients[eventChan] = struct{}{}
|
||||
clientsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
clientsMu.Lock()
|
||||
delete(clients, eventChan)
|
||||
clientsMu.Unlock()
|
||||
close(eventChan)
|
||||
}()
|
||||
|
||||
// Send events as they arrive
|
||||
for event := range eventChan {
|
||||
data, _ := json.Marshal(event)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all clients
|
||||
func broadcastMessage(message *events.CustomEvent) {
|
||||
clientsMu.Lock()
|
||||
defer clientsMu.Unlock()
|
||||
for ch := range clients {
|
||||
select {
|
||||
case ch <- message:
|
||||
default:
|
||||
// If the client is not listening, skip
|
||||
}
|
||||
}
|
||||
}
|
3
example/plugins/plugin2plugin-comms-peer2/go.mod
Normal file
3
example/plugins/plugin2plugin-comms-peer2/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/plugins/plugin2plugin-comms-peer2
|
||||
|
||||
go 1.24.5
|
94
example/plugins/plugin2plugin-comms-peer2/main.go
Normal file
94
example/plugins/plugin2plugin-comms-peer2/main.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "aroz.org/zoraxy/plugins/plugin2plugin-comms-peer2/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// Notes:
|
||||
// This plugin handles updating the UI with new messages received from the peer plugin via SSE, other option you
|
||||
// could use are WebSockets or polling the server at intervals
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.plugin2plugin_comms_peer2"
|
||||
PEER_ID = "org.aroz.zoraxy.plugin2plugin_comms_peer1"
|
||||
UI_PATH = "/ui"
|
||||
SUBSCRIPTION_PATH = "/notifyme"
|
||||
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: PLUGIN_ID,
|
||||
Name: "Plugin2Plugin Comms Peer 2",
|
||||
Author: "Anthony Rubick",
|
||||
AuthorContact: "",
|
||||
Description: "An example plugin for demonstrating plugin to plugin communications - Peer 2",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Endpoint: "/plugin/event/emit",
|
||||
Reason: "Used to send events to the peer plugin",
|
||||
},
|
||||
},
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath: SUBSCRIPTION_PATH,
|
||||
SubscriptionsEvents: map[string]string{
|
||||
"dummy": "A dummy event to satisfy the requirement of having at least one event",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error serving introspect: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
// for debugging, use the following line instead
|
||||
// embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, "."+ WEB_ROOT, UI_PATH)
|
||||
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Plugin Exited")
|
||||
}, nil)
|
||||
|
||||
// Serve the API
|
||||
RegisterAPIs(runtimeCfg)
|
||||
|
||||
// Serve the web page in the www folder
|
||||
http.Handle(UI_PATH+"/", embedWebRouter.Handler())
|
||||
http.HandleFunc(SUBSCRIPTION_PATH+"/", handleReceivedEvent)
|
||||
fmt.Println("Plugin2Plugin Comms Peer 2 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)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterAPIs(cfg *plugin.ConfigureSpec) {
|
||||
// Add API handlers here
|
||||
http.HandleFunc(UI_PATH+"/api/send_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleSendMessage(cfg, w, r)
|
||||
})
|
||||
http.HandleFunc(UI_PATH+"/api/events", handleSSE)
|
||||
http.HandleFunc(UI_PATH+"/api/message_history", handleFetchMessageHistory)
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
# Zoraxy Plugin
|
||||
|
||||
## Overview
|
||||
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Copy the Module:**
|
||||
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||
|
||||
2. **Include the Structure:**
|
||||
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||
|
||||
3. **Modify as Needed:**
|
||||
- Customize the copied module to implement the desired functionality for your plugin.
|
||||
|
||||
## Directory Structure
|
||||
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
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) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
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"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// 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 NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler function for the given pattern
|
||||
// The pattern should start with the handler prefix, e.g. /ui/hello
|
||||
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
|
||||
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
|
||||
// If mux is nil, use the default ServeMux
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Make sure the pattern starts with the handler prefix
|
||||
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
|
||||
pattern = p.HandlerPrefix + pattern
|
||||
}
|
||||
|
||||
// Register the handler with the http.ServeMux
|
||||
mux.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventName represents the type of event
|
||||
type EventName string
|
||||
|
||||
// EventPayload interface for all event payloads
|
||||
type EventPayload interface {
|
||||
// GetName returns the event type
|
||||
GetName() EventName
|
||||
|
||||
// Returns the "source" of the event, that is, the component or plugin that emitted the event
|
||||
GetEventSource() string
|
||||
}
|
||||
|
||||
// Event represents a system event
|
||||
type Event struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
UUID string `json:"uuid"` // UUID for the event
|
||||
Data EventPayload `json:"data"`
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBlacklistedIPBlocked is emitted when a blacklisted IP is blocked
|
||||
EventBlacklistedIPBlocked EventName = "blacklistedIpBlocked"
|
||||
// EventBlacklistToggled is emitted when the blacklist is toggled for an access rule
|
||||
EventBlacklistToggled EventName = "blacklistToggled"
|
||||
// EventAccessRuleCreated is emitted when a new access ruleset is created
|
||||
EventAccessRuleCreated EventName = "accessRuleCreated"
|
||||
// A custom event emitted by a plugin, with the intention of being broadcast
|
||||
// to the designated recipient(s)
|
||||
EventCustom EventName = "customEvent"
|
||||
// A dummy event to satisfy the requirement of having at least one event
|
||||
EventDummy EventName = "dummy"
|
||||
|
||||
// Add more event types as needed
|
||||
)
|
||||
|
||||
var validEventNames = map[EventName]bool{
|
||||
EventBlacklistedIPBlocked: true,
|
||||
EventBlacklistToggled: true,
|
||||
EventAccessRuleCreated: true,
|
||||
EventCustom: true,
|
||||
EventDummy: true,
|
||||
// Add more event types as needed
|
||||
// NOTE: Keep up-to-date with event names specified above
|
||||
}
|
||||
|
||||
// Check if the event name is valid
|
||||
func (name EventName) IsValid() bool {
|
||||
return validEventNames[name]
|
||||
}
|
||||
|
||||
// BlacklistedIPBlockedEvent represents an event when a blacklisted IP is blocked
|
||||
type BlacklistedIPBlockedEvent struct {
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
RequestedURL string `json:"requested_url"`
|
||||
Hostname string `json:"hostname"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetName() EventName {
|
||||
return EventBlacklistedIPBlocked
|
||||
}
|
||||
|
||||
func (e *BlacklistedIPBlockedEvent) GetEventSource() string {
|
||||
return "proxy-access"
|
||||
}
|
||||
|
||||
// BlacklistToggledEvent represents an event when the blacklist is disabled for an access rule
|
||||
type BlacklistToggledEvent struct {
|
||||
RuleID string `json:"rule_id"`
|
||||
Enabled bool `json:"enabled"` // Whether the blacklist is enabled or disabled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetName() EventName {
|
||||
return EventBlacklistToggled
|
||||
}
|
||||
|
||||
func (e *BlacklistToggledEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
// AccessRuleCreatedEvent represents an event when a new access ruleset is created
|
||||
type AccessRuleCreatedEvent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
BlacklistEnabled bool `json:"blacklist_enabled"`
|
||||
WhitelistEnabled bool `json:"whitelist_enabled"`
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetName() EventName {
|
||||
return EventAccessRuleCreated
|
||||
}
|
||||
|
||||
func (e *AccessRuleCreatedEvent) GetEventSource() string {
|
||||
return "accesslist-api"
|
||||
}
|
||||
|
||||
type CustomEvent struct {
|
||||
SourcePlugin string `json:"source_plugin"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetName() EventName {
|
||||
return EventCustom
|
||||
}
|
||||
|
||||
func (e *CustomEvent) GetEventSource() string {
|
||||
return e.SourcePlugin
|
||||
}
|
||||
|
||||
// ParseEvent parses a JSON byte slice into an Event struct
|
||||
func ParseEvent(jsonData []byte, event *Event) error {
|
||||
// First, determine the event type, and parse shared fields, from the JSON data
|
||||
var temp struct {
|
||||
Name EventName `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the event name and timestamp
|
||||
event.Name = temp.Name
|
||||
event.Timestamp = temp.Timestamp
|
||||
event.UUID = temp.UUID
|
||||
|
||||
// Now, based on the event type, unmarshal the specific payload
|
||||
switch temp.Name {
|
||||
case EventBlacklistedIPBlocked:
|
||||
type tempData struct {
|
||||
Data BlacklistedIPBlockedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventBlacklistToggled:
|
||||
type tempData struct {
|
||||
Data BlacklistToggledEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventAccessRuleCreated:
|
||||
type tempData struct {
|
||||
Data AccessRuleCreatedEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
case EventCustom:
|
||||
type tempData struct {
|
||||
Data CustomEvent `json:"data"`
|
||||
}
|
||||
var payload tempData
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
event.Data = &payload.Data
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
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 SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
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 PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
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, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
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 RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
152
example/plugins/plugin2plugin-comms-peer2/www/index.html
Normal file
152
example/plugins/plugin2plugin-comms-peer2/www/index.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Plugin2Plugin Comms</title>
|
||||
<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>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.sent-message {
|
||||
background-color: var(--theme_bg_primary);
|
||||
border-left: 5px solid #155724;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
.received-message {
|
||||
background-color: var(--theme_bg_secondary);
|
||||
border-left: 5px solid #004085;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
</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 class="ui container">
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container"></div>
|
||||
<script>
|
||||
// Function to show toast message
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = $('<div class="ui message ' + type + '" style="opacity: 0;">' + message + '</div>');
|
||||
$('.toast-container').append(toast);
|
||||
toast.animate({opacity: 1}, 300);
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(function() {
|
||||
toast.animate({opacity: 0}, 300, function() {
|
||||
toast.remove();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">Welcome to the Plugin2Plugin Comms Peer 2 UI</h1>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Message Form -->
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">Send Message to Peer Plugin</h2>
|
||||
<div class="ui form" id="messageForm">
|
||||
<div class="field">
|
||||
<label for="messageInput">Message:</label>
|
||||
<input type="text" id="messageInput" name="message" placeholder="Enter your message" required>
|
||||
</div>
|
||||
<button class="ui primary button" id="sendMessageButton">Send Message</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Handle form submission
|
||||
$('#sendMessageButton').click(function(event) {
|
||||
event.preventDefault();
|
||||
const message = $('#messageInput').val();
|
||||
$.cjax({
|
||||
url: './api/send_message',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ message: message }),
|
||||
success: function(response) {
|
||||
showToast('Message sent!');
|
||||
// Log the sent message
|
||||
const sentMessage = $('<div class="item sent-message"><div class="content"><div class="header">Sent:</div><div class="description">' + message + '</div></div></div>');
|
||||
$('#messageLog').prepend(sentMessage);
|
||||
$('#messageInput').val(''); // Clear input field
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
showToast('Error sending message!', 'error');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Message Log -->
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">Messages</h2>
|
||||
<div id="messageLog" class="ui relaxed divided list" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Messages will be appended here -->
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Set up EventSource to listen for incoming messages
|
||||
const eventSource = new EventSource('./api/events');
|
||||
eventSource.onmessage = function(e) {
|
||||
const event = JSON.parse(e.data);
|
||||
if (event && event.payload && event.payload.message) {
|
||||
const receivedMessage = $('<div class="item received-message"><div class="content"><div class="header">Received:</div><div class="description">' + event.payload.message + '</div></div></div>');
|
||||
$('#messageLog').prepend(receivedMessage);
|
||||
}
|
||||
showToast('New message received!');
|
||||
};
|
||||
eventSource.onerror = function(err) {
|
||||
console.error('EventSource failed:', err);
|
||||
eventSource.close();
|
||||
};
|
||||
// Clean up EventSource on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
eventSource.close();
|
||||
});
|
||||
|
||||
// Fetch and display message history on page load
|
||||
$(document).ready(function() {
|
||||
$.cjax({
|
||||
url: './api/message_history',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response && response.messages) {
|
||||
response.messages.forEach(function(msg) {
|
||||
const messageClass = msg.sent ? 'sent-message' : 'received-message';
|
||||
const messageItem = $('<div class="item ' + messageClass + '"><div class="content"><div class="header">' + (msg.sent ? 'Sent:' : 'Received:') + '</div><div class="description">' + msg.message + '</div></div></div>');
|
||||
$('#messageLog').append(messageItem);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error fetching message history:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -17,7 +17,7 @@ import (
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user