mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-09-27 06:29:36 +02:00
Compare commits
101 Commits
70b1ccfa6e
...
v3.2.7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
85cad1e2b6 | ||
![]() |
94afb6e3a5 | ||
![]() |
84a4eaaf95 | ||
![]() |
f98e1b8218 | ||
![]() |
778df1af0f | ||
![]() |
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 | ||
![]() |
a33600d3e2 | ||
![]() |
a0a394885c | ||
![]() |
51334a3a75 | ||
![]() |
6f5fadc085 |
11
.gitignore
vendored
11
.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,11 +39,15 @@ 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
|
||||
@@ -58,3 +60,6 @@ sys.*
|
||||
www/html/index.html
|
||||
*.exe
|
||||
/src/dist
|
||||
|
||||
/src/plugins
|
||||
.DS_Store
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,3 +1,20 @@
|
||||
# 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.
|
||||
|
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
|
@@ -198,6 +198,12 @@ Some section of Zoraxy are contributed by our amazing community and if you have
|
||||
- (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)
|
||||
|
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"
|
||||
|
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 {
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -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/upnp/mod/zoraxy_plugin/events/events.go
Normal file
181
example/plugins/upnp/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")
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64 darwin/amd64
|
||||
temp = $(subst /, ,$@)
|
||||
os = $(word 1, $(temp))
|
||||
arch = $(word 2, $(temp))
|
||||
|
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/eventsystem"
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin/events"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@@ -97,6 +99,17 @@ func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// emit an event for the new access rule creation
|
||||
eventsystem.Publisher.Emit(
|
||||
&events.AccessRuleCreatedEvent{
|
||||
ID: ruleUUID,
|
||||
Name: ruleName,
|
||||
Desc: ruleDesc,
|
||||
BlacklistEnabled: false,
|
||||
WhitelistEnabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@@ -359,6 +372,11 @@ func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
eventsystem.Publisher.Emit(&events.BlacklistToggledEvent{
|
||||
RuleID: ruleID,
|
||||
Enabled: rule.BlacklistEnabled,
|
||||
})
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
@@ -566,11 +584,12 @@ func handleWhitelistAllowLoopback(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
switch enable {
|
||||
case "true":
|
||||
rule.ToggleAllowLoopback(true)
|
||||
} else if enable == "false" {
|
||||
case "false":
|
||||
rule.ToggleAllowLoopback(false)
|
||||
} else {
|
||||
default:
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
}
|
||||
|
@@ -382,7 +382,9 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
||||
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
||||
|
||||
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
|
||||
authRouter.HandleFunc("/api/log/errors", LogViewer.HandleLogErrorSummary)
|
||||
authRouter.HandleFunc("/api/log/rotate/debug.trigger", SystemWideLogger.HandleDebugTriggerLogRotation)
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
}
|
||||
|
@@ -108,9 +108,9 @@ func filterProxyConfigFilename(filename string) string {
|
||||
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//Get filename for saving
|
||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
||||
filename := filepath.Join(CONF_HTTP_PROXY, endpoint.RootOrMatchingDomain+".config")
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||
filename = "./conf/proxy/root.config"
|
||||
filename = filepath.Join(CONF_HTTP_PROXY, "root.config")
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
@@ -125,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(endpoint string) error {
|
||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
||||
filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config")
|
||||
if endpoint == "/" {
|
||||
filename = "./conf/proxy/root.config"
|
||||
filename = filepath.Join(CONF_HTTP_PROXY, "/root.config")
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
@@ -179,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Specify the folder path to be zipped
|
||||
if !utils.FileExists("./conf") {
|
||||
if !utils.FileExists(CONF_FOLDER) {
|
||||
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
||||
return
|
||||
}
|
||||
folderPath := "./conf"
|
||||
folderPath := CONF_FOLDER
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
@@ -284,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// Create the target directory to unzip the files
|
||||
targetDir := "./conf"
|
||||
targetDir := CONF_FOLDER
|
||||
if utils.FileExists(targetDir) {
|
||||
//Backup the old config to old
|
||||
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
||||
//os.Rename(*path_conf, backupPath)
|
||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||
os.Rename(CONF_FOLDER, CONF_FOLDER+".old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||
}
|
||||
|
||||
err = os.MkdirAll(targetDir, os.ModePerm)
|
||||
|
31
src/def.go
31
src/def.go
@@ -44,7 +44,7 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.2.4"
|
||||
SYSTEM_VERSION = "3.2.7"
|
||||
DEVELOPMENT_BUILD = false
|
||||
|
||||
/* System Constants */
|
||||
@@ -63,14 +63,19 @@ const (
|
||||
LOG_EXTENSION = ".log"
|
||||
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
||||
|
||||
/* Configuration Folder Storage Path Constants */
|
||||
CONF_HTTP_PROXY = "./conf/proxy"
|
||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||
CONF_CERT_STORE = "./conf/certs"
|
||||
CONF_REDIRECTION = "./conf/redirect"
|
||||
CONF_ACCESS_RULE = "./conf/access"
|
||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
|
||||
/*
|
||||
Configuration Folder Storage Path Constants
|
||||
Note: No tailing slash in the path
|
||||
*/
|
||||
CONF_FOLDER = "./conf"
|
||||
CONF_HTTP_PROXY = CONF_FOLDER + "/proxy"
|
||||
CONF_STREAM_PROXY = CONF_FOLDER + "/streamproxy"
|
||||
CONF_CERT_STORE = CONF_FOLDER + "/certs"
|
||||
CONF_REDIRECTION = CONF_FOLDER + "/redirect"
|
||||
CONF_ACCESS_RULE = CONF_FOLDER + "/access"
|
||||
CONF_PATH_RULE = CONF_FOLDER + "/rules/pathrules"
|
||||
CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json"
|
||||
CONF_GEODB_PATH = CONF_FOLDER + "/geodb"
|
||||
)
|
||||
|
||||
/* System Startup Flags */
|
||||
@@ -89,6 +94,11 @@ var (
|
||||
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||
|
||||
/* Logging Configuration Flags */
|
||||
enableLog = flag.Bool("enablelog", true, "Enable system wide logging, set to false for writing log to STDOUT only")
|
||||
enableLogCompression = flag.Bool("enablelogcompress", true, "Enable log compression for rotated log files")
|
||||
logRotate = flag.String("logrotate", "0", "Enable log rotation and set the maximum log file size in KB, also support K, M, G suffix (e.g. 200M), set to 0 to disable")
|
||||
|
||||
/* Default Configuration Flags */
|
||||
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
|
||||
defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default")
|
||||
@@ -144,6 +154,9 @@ var (
|
||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//Plugin auth related
|
||||
pluginApiKeyManager *auth.APIKeyManager //API key manager for plugin authentication
|
||||
|
||||
//Authentication Provider
|
||||
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
||||
oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
|
||||
|
185
src/go.mod
185
src/go.mod
@@ -1,14 +1,14 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.22.2
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-acme/lego/v4 v4.25.2
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
@@ -20,109 +20,121 @@ require (
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/text v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.3.9 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.6 // indirect
|
||||
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.235 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
||||
github.com/go-acme/alidns-20150109/v4 v4.5.10 // indirect
|
||||
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.3 // indirect
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.0.8 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.112.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-resty/resty/v2 v2.16.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/linode/linodego v1.44.0 // indirect
|
||||
github.com/linode/linodego v1.53.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/miekg/dns v1.1.67 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -131,13 +143,12 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.10.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||
github.com/nrdcg/desec v0.11.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
@@ -145,57 +156,51 @@ require (
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.8 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.3.2 // indirect
|
||||
github.com/sacloud/go-http v0.1.9 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.16.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
|
||||
github.com/vultr/govultr/v3 v3.21.1 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.14.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.13.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
google.golang.org/api v0.214.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
|
682
src/go.sum
682
src/go.sum
File diff suppressed because it is too large
Load Diff
19
src/main.go
19
src/main.go
@@ -69,7 +69,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
if *geoDbUpdate {
|
||||
geodb.DownloadGeoDBUpdate("./conf/geodb")
|
||||
geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -99,8 +99,9 @@ func main() {
|
||||
}
|
||||
nodeUUID = string(uuidBytes)
|
||||
|
||||
//Create a new webmin mux and csrf middleware layer
|
||||
//Create a new webmin mux, plugin mux and csrf middleware layer
|
||||
webminPanelMux = http.NewServeMux()
|
||||
pluginAPIMux := http.NewServeMux()
|
||||
csrfMiddleware = csrf.Protect(
|
||||
[]byte(nodeUUID),
|
||||
csrf.CookieName(CSRF_COOKIENAME),
|
||||
@@ -112,13 +113,19 @@ func main() {
|
||||
//Startup all modules, see start.go
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
//Initiate APIs
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs(webminPanelMux)
|
||||
initRestAPI(pluginAPIMux)
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
// Create a entry mux to accept all management interface requests
|
||||
entryMux := http.NewServeMux()
|
||||
entryMux.Handle("/plugin/", pluginAPIMux) //For plugins API access
|
||||
entryMux.Handle("/", csrfMiddleware(webminPanelMux)) //For webmin UI access, require csrf token
|
||||
|
||||
// Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
ReverseProxtInit()
|
||||
ReverseProxyInit()
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
@@ -133,7 +140,7 @@ func main() {
|
||||
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://" + *webUIPort)
|
||||
}
|
||||
|
||||
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
||||
err = http.ListenAndServe(*webUIPort, entryMux)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@@ -60,6 +60,7 @@ func NewAccessController(options *Options) (*Controller, error) {
|
||||
//Create one
|
||||
js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
|
||||
os.WriteFile(defaultRuleSettingFile, js, 0775)
|
||||
|
||||
}
|
||||
|
||||
//Generate a controller object
|
||||
@@ -191,6 +192,7 @@ func (c *Controller) AddNewAccessRule(newRule *AccessRule) error {
|
||||
|
||||
//Save rule to file
|
||||
newRule.SaveChanges()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
@@ -92,3 +93,42 @@ func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetBlacklistedIPComment returns the comment for a blacklisted IP address
|
||||
// Searches blacklist for a Country (if country-code provided), IP address, or CIDR that matches the IP address
|
||||
// returns error if not found
|
||||
func (s *AccessRule) GetBlacklistedIPComment(ipAddr string) (string, error) {
|
||||
if countryInfo, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr); err == nil {
|
||||
CCBlacklist := *s.BlackListContryCode
|
||||
countryCode := strings.ToLower(countryInfo.CountryIsoCode)
|
||||
|
||||
if comment, ok := CCBlacklist[countryCode]; ok {
|
||||
return comment, nil
|
||||
}
|
||||
}
|
||||
|
||||
IPBlacklist := *s.BlackListIP
|
||||
if comment, ok := IPBlacklist[ipAddr]; ok {
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
//Check for CIDR
|
||||
for ipOrCIDR, comment := range IPBlacklist {
|
||||
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
|
||||
if wildcardMatch {
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
|
||||
if cidrMatch {
|
||||
return comment, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("IP %s not found in blacklist", ipAddr)
|
||||
}
|
||||
|
||||
// GetParent returns the parent controller
|
||||
func (s *AccessRule) GetParent() *Controller {
|
||||
return s.parent
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
package acmedns
|
||||
|
||||
/*
|
||||
THIS MODULE IS GENERATED AUTOMATICALLY
|
||||
DO NOT EDIT THIS FILE
|
||||
@@ -10,15 +9,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/active24"
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/allinkl"
|
||||
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/auroradns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/autodns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/axelname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azion"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azure"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bindman"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bluecat"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bookmyname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/brandit"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bunny"
|
||||
"github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
||||
@@ -29,13 +33,13 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||
"github.com/go-acme/lego/v4/providers/dns/conohav3"
|
||||
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||
"github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
||||
"github.com/go-acme/lego/v4/providers/dns/directadmin"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnspod"
|
||||
@@ -44,10 +48,12 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dyn"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dyndnsfree"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dynu"
|
||||
"github.com/go-acme/lego/v4/providers/dns/easydns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/efficientip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/epik"
|
||||
"github.com/go-acme/lego/v4/providers/dns/f5xc"
|
||||
"github.com/go-acme/lego/v4/providers/dns/freemyip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandi"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
||||
@@ -80,7 +86,9 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/loopia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/luadns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
||||
"github.com/go-acme/lego/v4/providers/dns/manageengine"
|
||||
"github.com/go-acme/lego/v4/providers/dns/metaname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/metaregistrar"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mijnhost"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mittwald"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
||||
@@ -91,6 +99,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/netcup"
|
||||
"github.com/go-acme/lego/v4/providers/dns/netlify"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nicru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nodion"
|
||||
@@ -101,7 +110,9 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/plesk"
|
||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rackspace"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rainyun"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
||||
"github.com/go-acme/lego/v4/providers/dns/regfish"
|
||||
"github.com/go-acme/lego/v4/providers/dns/regru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
||||
@@ -115,7 +126,9 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/shellrent"
|
||||
"github.com/go-acme/lego/v4/providers/dns/simply"
|
||||
"github.com/go-acme/lego/v4/providers/dns/sonic"
|
||||
"github.com/go-acme/lego/v4/providers/dns/spaceship"
|
||||
"github.com/go-acme/lego/v4/providers/dns/stackpath"
|
||||
"github.com/go-acme/lego/v4/providers/dns/technitium"
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/transip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ultradns"
|
||||
@@ -130,20 +143,32 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/webnames"
|
||||
"github.com/go-acme/lego/v4/providers/dns/websupport"
|
||||
"github.com/go-acme/lego/v4/providers/dns/wedos"
|
||||
"github.com/go-acme/lego/v4/providers/dns/westcn"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex360"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandexcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zoneedit"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zoneee"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zonomi"
|
||||
|
||||
)
|
||||
|
||||
// name is the DNS provider name, e.g. cloudflare or gandi
|
||||
// JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
|
||||
func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64) (challenge.Provider, error) {
|
||||
//name is the DNS provider name, e.g. cloudflare or gandi
|
||||
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
|
||||
func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64)(challenge.Provider, error){
|
||||
pgDuration := time.Duration(propagationTimeout) * time.Second
|
||||
plInterval := time.Duration(pollingInterval) * time.Second
|
||||
switch name {
|
||||
|
||||
|
||||
case "active24":
|
||||
cfg := active24.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return active24.NewDNSProviderConfig(cfg)
|
||||
case "alidns":
|
||||
cfg := alidns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -189,6 +214,24 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return autodns.NewDNSProviderConfig(cfg)
|
||||
case "axelname":
|
||||
cfg := axelname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return axelname.NewDNSProviderConfig(cfg)
|
||||
case "azion":
|
||||
cfg := azion.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return azion.NewDNSProviderConfig(cfg)
|
||||
case "azure":
|
||||
cfg := azure.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -207,6 +250,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return azuredns.NewDNSProviderConfig(cfg)
|
||||
case "baiducloud":
|
||||
cfg := baiducloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return baiducloud.NewDNSProviderConfig(cfg)
|
||||
case "bindman":
|
||||
cfg := bindman.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -225,6 +277,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return bluecat.NewDNSProviderConfig(cfg)
|
||||
case "bookmyname":
|
||||
cfg := bookmyname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return bookmyname.NewDNSProviderConfig(cfg)
|
||||
case "brandit":
|
||||
cfg := brandit.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -315,6 +376,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return conoha.NewDNSProviderConfig(cfg)
|
||||
case "conohav3":
|
||||
cfg := conohav3.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return conohav3.NewDNSProviderConfig(cfg)
|
||||
case "constellix":
|
||||
cfg := constellix.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -369,15 +439,6 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return directadmin.NewDNSProviderConfig(cfg)
|
||||
case "dnshomede":
|
||||
cfg := dnshomede.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return dnshomede.NewDNSProviderConfig(cfg)
|
||||
case "dnsimple":
|
||||
cfg := dnsimple.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -450,6 +511,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return dyn.NewDNSProviderConfig(cfg)
|
||||
case "dyndnsfree":
|
||||
cfg := dyndnsfree.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return dyndnsfree.NewDNSProviderConfig(cfg)
|
||||
case "dynu":
|
||||
cfg := dynu.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -486,6 +556,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return epik.NewDNSProviderConfig(cfg)
|
||||
case "f5xc":
|
||||
cfg := f5xc.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return f5xc.NewDNSProviderConfig(cfg)
|
||||
case "freemyip":
|
||||
cfg := freemyip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -774,6 +853,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return mailinabox.NewDNSProviderConfig(cfg)
|
||||
case "manageengine":
|
||||
cfg := manageengine.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return manageengine.NewDNSProviderConfig(cfg)
|
||||
case "metaname":
|
||||
cfg := metaname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -783,6 +871,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return metaname.NewDNSProviderConfig(cfg)
|
||||
case "metaregistrar":
|
||||
cfg := metaregistrar.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return metaregistrar.NewDNSProviderConfig(cfg)
|
||||
case "mijnhost":
|
||||
cfg := mijnhost.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -873,6 +970,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return nicmanager.NewDNSProviderConfig(cfg)
|
||||
case "nicru":
|
||||
cfg := nicru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return nicru.NewDNSProviderConfig(cfg)
|
||||
case "nifcloud":
|
||||
cfg := nifcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -963,6 +1069,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return rackspace.NewDNSProviderConfig(cfg)
|
||||
case "rainyun":
|
||||
cfg := rainyun.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return rainyun.NewDNSProviderConfig(cfg)
|
||||
case "rcodezero":
|
||||
cfg := rcodezero.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -972,6 +1087,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return rcodezero.NewDNSProviderConfig(cfg)
|
||||
case "regfish":
|
||||
cfg := regfish.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return regfish.NewDNSProviderConfig(cfg)
|
||||
case "regru":
|
||||
cfg := regru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -1089,6 +1213,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return sonic.NewDNSProviderConfig(cfg)
|
||||
case "spaceship":
|
||||
cfg := spaceship.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return spaceship.NewDNSProviderConfig(cfg)
|
||||
case "stackpath":
|
||||
cfg := stackpath.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -1098,6 +1231,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return stackpath.NewDNSProviderConfig(cfg)
|
||||
case "technitium":
|
||||
cfg := technitium.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return technitium.NewDNSProviderConfig(cfg)
|
||||
case "tencentcloud":
|
||||
cfg := tencentcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -1224,6 +1366,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return wedos.NewDNSProviderConfig(cfg)
|
||||
case "westcn":
|
||||
cfg := westcn.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return westcn.NewDNSProviderConfig(cfg)
|
||||
case "yandex":
|
||||
cfg := yandex.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
@@ -1251,6 +1402,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return yandexcloud.NewDNSProviderConfig(cfg)
|
||||
case "zoneedit":
|
||||
cfg := zoneedit.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.PropagationTimeout = pgDuration
|
||||
cfg.PollingInterval = plInterval
|
||||
return zoneedit.NewDNSProviderConfig(cfg)
|
||||
case "zoneee":
|
||||
cfg := zoneee.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
|
@@ -1,4 +1,31 @@
|
||||
{
|
||||
"active24": {
|
||||
"Name": "active24",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIKey",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Secret",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alidns": {
|
||||
"Name": "alidns",
|
||||
"ConfigableFields": [
|
||||
@@ -144,6 +171,60 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"axelname": {
|
||||
"Name": "axelname",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Nickname",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Token",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"azion": {
|
||||
"Name": "azion",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "PersonalToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PageSize",
|
||||
"Datatype": "int"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"azure": {
|
||||
"Name": "azure",
|
||||
"ConfigableFields": [
|
||||
@@ -278,6 +359,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"baiducloud": {
|
||||
"Name": "baiducloud",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "AccessKeyID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "SecretAccessKey",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"bindman": {
|
||||
"Name": "bindman",
|
||||
"ConfigableFields": [
|
||||
@@ -348,6 +451,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bookmyname": {
|
||||
"Name": "bookmyname",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Username",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Password",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"brandit": {
|
||||
"Name": "brandit",
|
||||
"ConfigableFields": [
|
||||
@@ -423,10 +553,6 @@
|
||||
"civo": {
|
||||
"Name": "civo",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "ProjectID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Token",
|
||||
"Datatype": "string"
|
||||
@@ -440,7 +566,12 @@
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": []
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"clouddns": {
|
||||
"Name": "clouddns",
|
||||
@@ -492,6 +623,10 @@
|
||||
"Title": "ZoneToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "BaseURL",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
@@ -632,6 +767,41 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"conohav3": {
|
||||
"Name": "conohav3",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Region",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "TenantID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "UserID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Password",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constellix": {
|
||||
"Name": "constellix",
|
||||
"ConfigableFields": [
|
||||
@@ -806,29 +976,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"dnshomede": {
|
||||
"Name": "dnshomede",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "Credentials",
|
||||
"Datatype": "map[string]string"
|
||||
},
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dnsimple": {
|
||||
"Name": "dnsimple",
|
||||
"ConfigableFields": [
|
||||
@@ -1044,6 +1191,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"dyndnsfree": {
|
||||
"Name": "dyndnsfree",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Username",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Password",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dynu": {
|
||||
"Name": "dynu",
|
||||
"ConfigableFields": [
|
||||
@@ -1164,6 +1338,37 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"f5xc": {
|
||||
"Name": "f5xc",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "TenantName",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "GroupName",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"freemyip": {
|
||||
"Name": "freemyip",
|
||||
"ConfigableFields": [
|
||||
@@ -1608,6 +1813,10 @@
|
||||
"Title": "SSLVerify",
|
||||
"Datatype": "bool"
|
||||
},
|
||||
{
|
||||
"Title": "CACertificate",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
@@ -2020,6 +2229,28 @@
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"manageengine": {
|
||||
"Name": "manageengine",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "ClientID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "ClientSecret",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"metaname": {
|
||||
"Name": "metaname",
|
||||
"ConfigableFields": [
|
||||
@@ -2042,6 +2273,29 @@
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"metaregistrar": {
|
||||
"Name": "metaregistrar",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mijnhost": {
|
||||
"Name": "mijnhost",
|
||||
"ConfigableFields": [
|
||||
@@ -2327,6 +2581,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"nicru": {
|
||||
"Name": "nicru",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Username",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Password",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "ServiceID",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Secret",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"nifcloud": {
|
||||
"Name": "nifcloud",
|
||||
"ConfigableFields": [
|
||||
@@ -2633,6 +2917,29 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"rainyun": {
|
||||
"Name": "rainyun",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIKey",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rcodezero": {
|
||||
"Name": "rcodezero",
|
||||
"ConfigableFields": [
|
||||
@@ -2656,6 +2963,29 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"regfish": {
|
||||
"Name": "regfish",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIKey",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"regru": {
|
||||
"Name": "regru",
|
||||
"ConfigableFields": [
|
||||
@@ -2698,6 +3028,10 @@
|
||||
"Title": "Nameserver",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "TSIGFile",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "TSIGAlgorithm",
|
||||
"Datatype": "string"
|
||||
@@ -2789,6 +3123,10 @@
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "PrivateZone",
|
||||
"Datatype": "bool"
|
||||
},
|
||||
{
|
||||
"Title": "WaitForRecordSetsChanged",
|
||||
"Datatype": "bool"
|
||||
@@ -3045,6 +3383,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"spaceship": {
|
||||
"Name": "spaceship",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "APIKey",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "APISecret",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stackpath": {
|
||||
"Name": "stackpath",
|
||||
"ConfigableFields": [
|
||||
@@ -3071,6 +3436,33 @@
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"technitium": {
|
||||
"Name": "technitium",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "BaseURL",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "APIToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tencentcloud": {
|
||||
"Name": "tencentcloud",
|
||||
"ConfigableFields": [
|
||||
@@ -3285,7 +3677,12 @@
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": []
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "QuoteValue",
|
||||
"Datatype": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
"vkcloud": {
|
||||
"Name": "vkcloud",
|
||||
@@ -3452,6 +3849,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"westcn": {
|
||||
"Name": "westcn",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "Username",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "Password",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"yandex": {
|
||||
"Name": "yandex",
|
||||
"ConfigableFields": [
|
||||
@@ -3524,6 +3948,33 @@
|
||||
],
|
||||
"HiddenFields": []
|
||||
},
|
||||
"zoneedit": {
|
||||
"Name": "zoneedit",
|
||||
"ConfigableFields": [
|
||||
{
|
||||
"Title": "User",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "AuthToken",
|
||||
"Datatype": "string"
|
||||
},
|
||||
{
|
||||
"Title": "PropagationTimeout",
|
||||
"Datatype": "time.Duration"
|
||||
},
|
||||
{
|
||||
"Title": "PollingInterval",
|
||||
"Datatype": "time.Duration"
|
||||
}
|
||||
],
|
||||
"HiddenFields": [
|
||||
{
|
||||
"Title": "HTTPClient",
|
||||
"Datatype": "*http.Client"
|
||||
}
|
||||
]
|
||||
},
|
||||
"zoneee": {
|
||||
"Name": "zoneee",
|
||||
"ConfigableFields": [
|
||||
|
112
src/mod/auth/plugin_apikey_manager.go
Normal file
112
src/mod/auth/plugin_apikey_manager.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// PluginAPIKey represents an API key for a plugin
|
||||
type PluginAPIKey struct {
|
||||
PluginID string
|
||||
APIKey string
|
||||
PermittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint // List of permitted API endpoints
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// APIKeyManager manages API keys for plugins
|
||||
type APIKeyManager struct {
|
||||
keys map[string]*PluginAPIKey // key: API key, value: plugin info
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAPIKeyManager creates a new API key manager
|
||||
func NewAPIKeyManager() *APIKeyManager {
|
||||
return &APIKeyManager{
|
||||
keys: make(map[string]*PluginAPIKey),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAPIKey generates a new API key for a plugin
|
||||
func (m *APIKeyManager) GenerateAPIKey(pluginID string, permittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint) (*PluginAPIKey, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Generate a cryptographically secure random key
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
// Hash the random bytes to create the API key
|
||||
hash := sha256.Sum256(bytes)
|
||||
apiKey := hex.EncodeToString(hash[:])
|
||||
|
||||
// Create the plugin API key
|
||||
pluginAPIKey := &PluginAPIKey{
|
||||
PluginID: pluginID,
|
||||
APIKey: apiKey,
|
||||
PermittedEndpoints: permittedEndpoints,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Store the API key
|
||||
m.keys[apiKey] = pluginAPIKey
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKey validates an API key and returns the associated plugin information
|
||||
func (m *APIKeyManager) ValidateAPIKey(apiKey string) (*PluginAPIKey, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
pluginAPIKey, exists := m.keys[apiKey]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("invalid API key")
|
||||
}
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKeyForEndpoint validates an API key for a specific endpoint
|
||||
func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string, apiKey string) (*PluginAPIKey, error) {
|
||||
pluginAPIKey, err := m.ValidateAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the endpoint is permitted
|
||||
for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints {
|
||||
if permittedEndpoint.Endpoint == endpoint && permittedEndpoint.Method == method {
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("endpoint not permitted for this API key")
|
||||
}
|
||||
|
||||
// RevokeAPIKeysForPlugin revokes all API keys for a specific plugin
|
||||
func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
keysToRemove := []string{}
|
||||
for apiKey, pluginAPIKey := range m.keys {
|
||||
if pluginAPIKey.PluginID == pluginID {
|
||||
keysToRemove = append(keysToRemove, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
for _, apiKey := range keysToRemove {
|
||||
delete(m.keys, apiKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
94
src/mod/auth/plugin_middleware.go
Normal file
94
src/mod/auth/plugin_middleware.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Handles the API-Key based authentication for plugins
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_API_PREFIX = "/plugin"
|
||||
)
|
||||
|
||||
type PluginMiddlewareOptions struct {
|
||||
DeniedHandler http.HandlerFunc //Thing(s) to do when request is rejected
|
||||
ApiKeyManager *APIKeyManager
|
||||
TargetMux *http.ServeMux
|
||||
}
|
||||
|
||||
// PluginAuthMiddleware provides authentication middleware for plugin API requests
|
||||
type PluginAuthMiddleware struct {
|
||||
option PluginMiddlewareOptions
|
||||
endpoints map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
// NewPluginAuthMiddleware creates a new plugin authentication middleware
|
||||
func NewPluginAuthMiddleware(option PluginMiddlewareOptions) *PluginAuthMiddleware {
|
||||
return &PluginAuthMiddleware{
|
||||
option: option,
|
||||
endpoints: make(map[string]http.HandlerFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PluginAuthMiddleware) HandleAuthCheck(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
// Check for API key in the Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
// No authorization header
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a plugin API key (Bearer token)
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
// Not a Bearer token
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the API key
|
||||
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Validate the API key for this endpoint
|
||||
_, err := m.option.ApiKeyManager.ValidateAPIKeyForEndpoint(r.URL.Path, r.Method, apiKey)
|
||||
if err != nil {
|
||||
// Invalid API key or endpoint not permitted
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the original handler
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
// wraps an HTTP handler with plugin authentication middleware
|
||||
func (m *PluginAuthMiddleware) HandleFunc(endpoint string, handler http.HandlerFunc) error {
|
||||
// ensure the endpoint is prefixed with PLUGIN_API_PREFIX
|
||||
if !strings.HasPrefix(endpoint, PLUGIN_API_PREFIX) {
|
||||
endpoint = PLUGIN_API_PREFIX + endpoint
|
||||
}
|
||||
|
||||
// Check if the endpoint already registered
|
||||
if _, exist := m.endpoints[endpoint]; exist {
|
||||
fmt.Println("WARNING! Duplicated registering of plugin api endpoint: " + endpoint)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
m.endpoints[endpoint] = handler
|
||||
|
||||
wrappedHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
m.HandleAuthCheck(w, r, handler)
|
||||
}
|
||||
|
||||
// Ok. Register handler
|
||||
if m.option.TargetMux == nil {
|
||||
http.HandleFunc(endpoint, wrappedHandler)
|
||||
} else {
|
||||
m.option.TargetMux.HandleFunc(endpoint, wrappedHandler)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -13,14 +13,21 @@ const (
|
||||
DatabaseKeyRequestHeaders = "requestHeaders"
|
||||
DatabaseKeyRequestIncludedCookies = "requestIncludedCookies"
|
||||
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
|
||||
DatabaseKeyRequestIncludeBody = "requestIncludeBody"
|
||||
DatabaseKeyUseXOriginalHeaders = "useXOriginalHeaders"
|
||||
|
||||
HeaderXForwardedProto = "X-Forwarded-Proto"
|
||||
HeaderXForwardedHost = "X-Forwarded-Host"
|
||||
HeaderXForwardedFor = "X-Forwarded-For"
|
||||
HeaderXForwardedURI = "X-Forwarded-URI"
|
||||
HeaderXForwardedFor = "X-Forwarded-For"
|
||||
HeaderXForwardedMethod = "X-Forwarded-Method"
|
||||
|
||||
HeaderCookie = "Cookie"
|
||||
HeaderXOriginalURL = "X-Original-URL"
|
||||
HeaderXOriginalIP = "X-Original-IP"
|
||||
HeaderXOriginalMethod = "X-Original-Method"
|
||||
|
||||
HeaderCookie = "Cookie"
|
||||
HeaderLocation = "Location"
|
||||
|
||||
HeaderUpgrade = "Upgrade"
|
||||
HeaderConnection = "Connection"
|
||||
|
@@ -2,8 +2,10 @@ package forward
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
@@ -34,6 +36,13 @@ type AuthRouterOptions struct {
|
||||
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
|
||||
RequestExcludedCookies []string
|
||||
|
||||
// RequestIncludeBody enables copying the request body to the request to the authorization server.
|
||||
RequestIncludeBody bool
|
||||
|
||||
// UseXOriginalHeaders is a boolean that determines if the X-Original-* headers should be used instead of the
|
||||
// X-Forwarded-* headers.
|
||||
UseXOriginalHeaders bool
|
||||
|
||||
Logger *logger.Logger
|
||||
Database *database.Database
|
||||
}
|
||||
@@ -57,15 +66,8 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
||||
|
||||
// Helper function to clean empty strings from split results
|
||||
cleanSplit := func(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludeBody, &options.RequestIncludeBody)
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyUseXOriginalHeaders, &options.UseXOriginalHeaders)
|
||||
|
||||
options.ResponseHeaders = cleanSplit(responseHeaders)
|
||||
options.ResponseClientHeaders = cleanSplit(responseClientHeaders)
|
||||
@@ -73,7 +75,7 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
||||
options.RequestIncludedCookies = cleanSplit(requestIncludedCookies)
|
||||
options.RequestExcludedCookies = cleanSplit(requestExcludedCookies)
|
||||
|
||||
return &AuthRouter{
|
||||
r := &AuthRouter{
|
||||
client: &http.Client{
|
||||
CheckRedirect: func(r *http.Request, via []*http.Request) (err error) {
|
||||
return http.ErrUseLastResponse
|
||||
@@ -81,6 +83,10 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
||||
},
|
||||
options: options,
|
||||
}
|
||||
|
||||
r.logOptions()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// HandleAPIOptions is the internal handler for setting the options.
|
||||
@@ -90,6 +96,8 @@ func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
|
||||
ar.handleOptionsGET(w, r)
|
||||
case http.MethodPost:
|
||||
ar.handleOptionsPOST(w, r)
|
||||
case http.MethodDelete:
|
||||
ar.handleOptionsDelete(w, r)
|
||||
default:
|
||||
ar.handleOptionsMethodNotAllowed(w, r)
|
||||
}
|
||||
@@ -103,6 +111,8 @@ func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
|
||||
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
|
||||
DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies,
|
||||
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
|
||||
DatabaseKeyRequestIncludeBody: ar.options.RequestIncludeBody,
|
||||
DatabaseKeyUseXOriginalHeaders: ar.options.UseXOriginalHeaders,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
@@ -125,6 +135,8 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
||||
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
|
||||
requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies)
|
||||
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
|
||||
requestIncludeBody, _ := utils.PostPara(r, DatabaseKeyRequestIncludeBody)
|
||||
useXOriginalHeaders, _ := utils.PostPara(r, DatabaseKeyUseXOriginalHeaders)
|
||||
|
||||
// Write changes to runtime
|
||||
ar.options.Address = address
|
||||
@@ -133,6 +145,8 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
||||
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
|
||||
ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
|
||||
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
||||
ar.options.RequestIncludeBody, _ = strconv.ParseBool(requestIncludeBody)
|
||||
ar.options.UseXOriginalHeaders, _ = strconv.ParseBool(useXOriginalHeaders)
|
||||
|
||||
// Write changes to database
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address)
|
||||
@@ -141,6 +155,32 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies)
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludeBody, ar.options.RequestIncludeBody)
|
||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyUseXOriginalHeaders, ar.options.UseXOriginalHeaders)
|
||||
|
||||
ar.logOptions()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (ar *AuthRouter) handleOptionsDelete(w http.ResponseWriter, r *http.Request) {
|
||||
ar.options.Address = ""
|
||||
ar.options.ResponseHeaders = nil
|
||||
ar.options.ResponseClientHeaders = nil
|
||||
ar.options.RequestHeaders = nil
|
||||
ar.options.RequestIncludedCookies = nil
|
||||
ar.options.RequestExcludedCookies = nil
|
||||
ar.options.RequestIncludeBody = false
|
||||
ar.options.UseXOriginalHeaders = false
|
||||
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyAddress)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyResponseHeaders)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyResponseClientHeaders)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyRequestHeaders)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyRequestIncludedCookies)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyRequestExcludedCookies)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyRequestIncludeBody)
|
||||
ar.options.Database.Delete(DatabaseTable, DatabaseKeyUseXOriginalHeaders)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@@ -158,9 +198,6 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
// Make a request to Authz Server to verify the request
|
||||
// TODO: Add opt-in support for copying the request body to the forward auth request. Currently it's just an
|
||||
// empty body which is usually fine in most instances. It's likely best to see if anyone wants this feature
|
||||
// as I'm unaware of any specific forward auth implementation that needs it.
|
||||
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
|
||||
if err != nil {
|
||||
return ar.handle500Error(w, err, "Unable to create request")
|
||||
@@ -171,7 +208,17 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
|
||||
|
||||
// TODO: Add support for headers from upstream proxies. This will likely involve implementing some form of
|
||||
// proxy specific trust system within Zoraxy.
|
||||
rSetForwardedHeaders(r, req)
|
||||
if ar.options.UseXOriginalHeaders {
|
||||
rSetXOriginalHeaders(r, req)
|
||||
} else {
|
||||
rSetXForwardedHeaders(r, req)
|
||||
}
|
||||
|
||||
if ar.options.RequestIncludeBody {
|
||||
if err = rCopyBody(r, req); err != nil {
|
||||
return ar.handle500Error(w, err, "Unable to perform forwarded auth due to a request copy error")
|
||||
}
|
||||
}
|
||||
|
||||
// Make the Authz Request.
|
||||
respForwarded, err := ar.client.Do(req)
|
||||
@@ -202,15 +249,14 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
|
||||
// Copy the unsuccessful response.
|
||||
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
|
||||
|
||||
w.WriteHeader(respForwarded.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(respForwarded.Body)
|
||||
if err != nil {
|
||||
return ar.handle500Error(w, err, "Unable to read response to forward auth request")
|
||||
if ar.options.UseXOriginalHeaders && respForwarded.StatusCode == 401 && respForwarded.Header.Get(HeaderLocation) != "" {
|
||||
w.WriteHeader(http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(respForwarded.StatusCode)
|
||||
}
|
||||
|
||||
if _, err = w.Write(body); err != nil {
|
||||
return ar.handle500Error(w, err, "Unable to write response")
|
||||
if _, err = io.Copy(w, respForwarded.Body); err != nil {
|
||||
return ar.handle500Error(w, err, "Unable to copy response")
|
||||
}
|
||||
|
||||
return ErrUnauthorized
|
||||
@@ -224,3 +270,7 @@ func (ar *AuthRouter) handle500Error(w http.ResponseWriter, err error, message s
|
||||
|
||||
return ErrInternalServerError
|
||||
}
|
||||
|
||||
func (ar *AuthRouter) logOptions() {
|
||||
ar.options.Logger.PrintAndLog(LogTitle, fmt.Sprintf("Forward Authz Options -> Address: %s, Response Headers: %s, Response Client Headers: %s, Request Headers: %s, Request Included Cookies: %s, Request Excluded Cookies: %s, Request Include Body: %t, Use X-Original Headers: %t", ar.options.Address, strings.Join(ar.options.ResponseHeaders, ";"), strings.Join(ar.options.ResponseClientHeaders, ";"), strings.Join(ar.options.RequestHeaders, ";"), strings.Join(ar.options.RequestIncludedCookies, ";"), strings.Join(ar.options.RequestExcludedCookies, ";"), ar.options.RequestIncludeBody, ar.options.UseXOriginalHeaders), nil)
|
||||
}
|
||||
|
@@ -1,8 +1,11 @@
|
||||
package forward
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -121,17 +124,65 @@ func stringInSliceFold(needle string, haystack []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func rSetForwardedHeaders(r, req *http.Request) {
|
||||
if r.RemoteAddr != "" {
|
||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||
|
||||
if ip := net.ParseIP(before); ip != nil {
|
||||
req.Header.Set(HeaderXForwardedFor, ip.String())
|
||||
}
|
||||
func rSetIPHeader(r, req *http.Request, headers ...string) {
|
||||
if r.RemoteAddr == "" || len(headers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||
|
||||
ip := net.ParseIP(before)
|
||||
if ip == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
req.Header.Set(header, ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
func rSetXForwardedHeaders(r, req *http.Request) {
|
||||
rSetIPHeader(r, req, HeaderXForwardedFor)
|
||||
req.Header.Set(HeaderXForwardedMethod, r.Method)
|
||||
req.Header.Set(HeaderXForwardedProto, scheme(r))
|
||||
req.Header.Set(HeaderXForwardedHost, r.Host)
|
||||
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
|
||||
}
|
||||
|
||||
func rSetXOriginalHeaders(r, req *http.Request) {
|
||||
// The X-Forwarded-For header has larger support, so we include both.
|
||||
rSetIPHeader(r, req, HeaderXOriginalIP, HeaderXForwardedFor)
|
||||
|
||||
original := &url.URL{
|
||||
Scheme: scheme(r),
|
||||
Host: r.Host,
|
||||
Path: r.URL.Path,
|
||||
}
|
||||
|
||||
req.Header.Set(HeaderXOriginalMethod, r.Method)
|
||||
req.Header.Set(HeaderXOriginalURL, original.String())
|
||||
}
|
||||
|
||||
func rCopyBody(req, freq *http.Request) (err error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
freq.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanSplit(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(s, ",")
|
||||
}
|
||||
|
@@ -66,86 +66,117 @@ func NewOAuth2Router(options *OAuth2RouterOptions) *OAuth2Router {
|
||||
|
||||
// HandleSetOAuth2Settings is the internal handler for setting the OAuth URL and HTTPS
|
||||
func (ar *OAuth2Router) HandleSetOAuth2Settings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl,
|
||||
"oauth2ServerUrl": ar.options.OAuth2ServerURL,
|
||||
"oauth2TokenUrl": ar.options.OAuth2TokenURL,
|
||||
"oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl,
|
||||
"oauth2Scopes": ar.options.OAuth2Scopes,
|
||||
"oauth2ClientSecret": ar.options.OAuth2ClientSecret,
|
||||
"oauth2ClientId": ar.options.OAuth2ClientId,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the settings
|
||||
var oauth2ServerUrl, oauth2TokenURL, oauth2Scopes, oauth2UserInfoUrl string
|
||||
oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl")
|
||||
if err != nil {
|
||||
oauth2ServerUrl, err = utils.PostPara(r, "oauth2ServerUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ServerUrl not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2TokenURL, err = utils.PostPara(r, "oauth2TokenUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2TokenUrl not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Scopes, err = utils.PostPara(r, "oauth2Scopes")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2Scopes not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2UserInfoUrl, err = utils.PostPara(r, "oauth2UserInfoUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2UserInfoUrl not found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
oauth2ClientId, err := utils.PostPara(r, "oauth2ClientId")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ClientId not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2ClientSecret, err := utils.PostPara(r, "oauth2ClientSecret")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ClientSecret not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.OAuth2WellKnownUrl = oauth2WellKnownUrl
|
||||
ar.options.OAuth2ServerURL = oauth2ServerUrl
|
||||
ar.options.OAuth2TokenURL = oauth2TokenURL
|
||||
ar.options.OAuth2UserInfoUrl = oauth2UserInfoUrl
|
||||
ar.options.OAuth2ClientId = oauth2ClientId
|
||||
ar.options.OAuth2ClientSecret = oauth2ClientSecret
|
||||
ar.options.OAuth2Scopes = oauth2Scopes
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("oauth2", "oauth2WellKnownUrl", oauth2WellKnownUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2ServerUrl", oauth2ServerUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2TokenUrl", oauth2TokenURL)
|
||||
ar.options.Database.Write("oauth2", "oauth2UserInfoUrl", oauth2UserInfoUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2ClientId", oauth2ClientId)
|
||||
ar.options.Database.Write("oauth2", "oauth2ClientSecret", oauth2ClientSecret)
|
||||
ar.options.Database.Write("oauth2", "oauth2Scopes", oauth2Scopes)
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ar.handleSetOAuthSettingsGET(w, r)
|
||||
case http.MethodPost:
|
||||
ar.handleSetOAuthSettingsPOST(w, r)
|
||||
case http.MethodDelete:
|
||||
ar.handleSetOAuthSettingsDELETE(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (ar *OAuth2Router) handleSetOAuthSettingsGET(w http.ResponseWriter, r *http.Request) {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl,
|
||||
"oauth2ServerUrl": ar.options.OAuth2ServerURL,
|
||||
"oauth2TokenUrl": ar.options.OAuth2TokenURL,
|
||||
"oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl,
|
||||
"oauth2Scopes": ar.options.OAuth2Scopes,
|
||||
"oauth2ClientSecret": ar.options.OAuth2ClientSecret,
|
||||
"oauth2ClientId": ar.options.OAuth2ClientId,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (ar *OAuth2Router) handleSetOAuthSettingsPOST(w http.ResponseWriter, r *http.Request) {
|
||||
//Update the settings
|
||||
var oauth2ServerUrl, oauth2TokenURL, oauth2Scopes, oauth2UserInfoUrl string
|
||||
|
||||
oauth2ClientId, err := utils.PostPara(r, "oauth2ClientId")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ClientId not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2ClientSecret, err := utils.PostPara(r, "oauth2ClientSecret")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ClientSecret not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl")
|
||||
if err != nil {
|
||||
oauth2ServerUrl, err = utils.PostPara(r, "oauth2ServerUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2ServerUrl not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2TokenURL, err = utils.PostPara(r, "oauth2TokenUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2TokenUrl not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2UserInfoUrl, err = utils.PostPara(r, "oauth2UserInfoUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2UserInfoUrl not found")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Scopes, err = utils.PostPara(r, "oauth2Scopes")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "oauth2Scopes not found")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
oauth2Scopes, _ = utils.PostPara(r, "oauth2Scopes")
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.OAuth2WellKnownUrl = oauth2WellKnownUrl
|
||||
ar.options.OAuth2ServerURL = oauth2ServerUrl
|
||||
ar.options.OAuth2TokenURL = oauth2TokenURL
|
||||
ar.options.OAuth2UserInfoUrl = oauth2UserInfoUrl
|
||||
ar.options.OAuth2ClientId = oauth2ClientId
|
||||
ar.options.OAuth2ClientSecret = oauth2ClientSecret
|
||||
ar.options.OAuth2Scopes = oauth2Scopes
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("oauth2", "oauth2WellKnownUrl", oauth2WellKnownUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2ServerUrl", oauth2ServerUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2TokenUrl", oauth2TokenURL)
|
||||
ar.options.Database.Write("oauth2", "oauth2UserInfoUrl", oauth2UserInfoUrl)
|
||||
ar.options.Database.Write("oauth2", "oauth2ClientId", oauth2ClientId)
|
||||
ar.options.Database.Write("oauth2", "oauth2ClientSecret", oauth2ClientSecret)
|
||||
ar.options.Database.Write("oauth2", "oauth2Scopes", oauth2Scopes)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (ar *OAuth2Router) handleSetOAuthSettingsDELETE(w http.ResponseWriter, r *http.Request) {
|
||||
ar.options.OAuth2WellKnownUrl = ""
|
||||
ar.options.OAuth2ServerURL = ""
|
||||
ar.options.OAuth2TokenURL = ""
|
||||
ar.options.OAuth2UserInfoUrl = ""
|
||||
ar.options.OAuth2ClientId = ""
|
||||
ar.options.OAuth2ClientSecret = ""
|
||||
ar.options.OAuth2Scopes = ""
|
||||
|
||||
ar.options.Database.Delete("oauth2", "oauth2WellKnownUrl")
|
||||
ar.options.Database.Delete("oauth2", "oauth2ServerUrl")
|
||||
ar.options.Database.Delete("oauth2", "oauth2TokenUrl")
|
||||
ar.options.Database.Delete("oauth2", "oauth2UserInfoUrl")
|
||||
ar.options.Database.Delete("oauth2", "oauth2ClientId")
|
||||
ar.options.Database.Delete("oauth2", "oauth2ClientSecret")
|
||||
ar.options.Database.Delete("oauth2", "oauth2Scopes")
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (ar *OAuth2Router) fetchOAuth2Configuration(config *oauth2.Config) (*oauth2.Config, error) {
|
||||
|
@@ -6,7 +6,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/eventsystem"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin/events"
|
||||
)
|
||||
|
||||
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
|
||||
@@ -43,6 +45,23 @@ func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory strin
|
||||
w.Write(template)
|
||||
}
|
||||
|
||||
// Emit blacklisted IP blocked event
|
||||
// Get the comment for this IP
|
||||
comment, err := accessRule.GetBlacklistedIPComment(clientIpAddr)
|
||||
if err != nil {
|
||||
comment = "blacklisted"
|
||||
}
|
||||
eventsystem.Publisher.Emit(
|
||||
&events.BlacklistedIPBlockedEvent{
|
||||
IP: clientIpAddr,
|
||||
Comment: comment,
|
||||
RequestedURL: r.URL.String(),
|
||||
Hostname: r.Host,
|
||||
UserAgent: r.Header.Get("User-Agent"),
|
||||
Method: r.Method,
|
||||
},
|
||||
)
|
||||
|
||||
return true, "blacklist"
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -70,9 +71,36 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
|
||||
//Check if the current path matches the exception rules
|
||||
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
|
||||
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
|
||||
//This path is excluded from basic auth
|
||||
return nil
|
||||
exceptionType := exceptionRule.RuleType
|
||||
switch exceptionType {
|
||||
case AuthExceptionType_Paths:
|
||||
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
|
||||
//This path is excluded from basic auth
|
||||
return nil
|
||||
}
|
||||
case AuthExceptionType_CIDR:
|
||||
requesterIp := netutils.GetRequesterIP(r)
|
||||
if requesterIp != "" {
|
||||
if requesterIp == exceptionRule.CIDR {
|
||||
// This IP is excluded from basic auth
|
||||
return nil
|
||||
}
|
||||
|
||||
wildcardMatch := netutils.MatchIpWildcard(requesterIp, exceptionRule.CIDR)
|
||||
if wildcardMatch {
|
||||
// This IP is excluded from basic auth
|
||||
return nil
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(requesterIp, exceptionRule.CIDR)
|
||||
if cidrMatch {
|
||||
// This IP is excluded from basic auth
|
||||
return nil
|
||||
}
|
||||
}
|
||||
default:
|
||||
//Unknown exception type, skip this rule
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package dpcore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@@ -75,7 +76,8 @@ type ResponseRewriteRuleSet struct {
|
||||
DisableChunkedTransferEncoding bool //Disable chunked transfer encoding
|
||||
|
||||
/* System Information Payload */
|
||||
Version string //Version number of Zoraxy, use for X-Proxy-By
|
||||
DevelopmentMode bool //Inject dev mode information to requests
|
||||
Version string //Version number of Zoraxy, use for X-Proxy-By
|
||||
}
|
||||
|
||||
type requestCanceler interface {
|
||||
@@ -284,7 +286,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
// Add user defined headers (to upstream)
|
||||
injectUserDefinedHeaders(outreq.Header, rrr.UpstreamHeaders)
|
||||
|
||||
// Rewrite outbound UA, must be after user headers
|
||||
// Rewrite outbound UA top upstream, must be after user headers
|
||||
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
|
||||
|
||||
//Fix proxmox transfer encoding bug if detected Proxmox Cookie
|
||||
@@ -292,6 +294,29 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
outreq.TransferEncoding = []string{"identity"}
|
||||
}
|
||||
|
||||
//Fix for issue #821
|
||||
if outreq.URL != nil && strings.EqualFold(outreq.URL.Scheme, "https") {
|
||||
if tr, ok := transport.(*http.Transport); ok {
|
||||
serverName := outreq.Host
|
||||
if h, _, err := net.SplitHostPort(serverName); err == nil {
|
||||
serverName = h
|
||||
}
|
||||
|
||||
if tr.TLSClientConfig == nil || tr.TLSClientConfig.ServerName != serverName {
|
||||
trc := tr.Clone()
|
||||
var cfg *tls.Config
|
||||
if tr.TLSClientConfig != nil {
|
||||
cfg = tr.TLSClientConfig.Clone()
|
||||
} else {
|
||||
cfg = &tls.Config{}
|
||||
}
|
||||
cfg.ServerName = serverName
|
||||
trc.TLSClientConfig = cfg
|
||||
transport = trc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
@@ -323,7 +348,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
|
||||
//Add debug X-Proxy-By tracker
|
||||
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)
|
||||
if rrr.DevelopmentMode {
|
||||
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)
|
||||
}
|
||||
|
||||
//Custom Location header rewriter functions
|
||||
if res.Header.Get("Location") != "" {
|
||||
@@ -391,7 +418,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
|
||||
return res.StatusCode, nil
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (int, error) {
|
||||
hij, ok := rw.(http.Hijacker)
|
||||
if !ok {
|
||||
@@ -407,12 +433,23 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (in
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
proxyConn, err := net.Dial("tcp", req.URL.Host)
|
||||
// Extract SNI/hostname for TLS handshake
|
||||
host := req.URL.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":443"
|
||||
}
|
||||
serverName := req.URL.Hostname()
|
||||
|
||||
// Connect with SNI offload
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: serverName,
|
||||
}
|
||||
proxyConn, err := tls.Dial("tcp", host, tlsConfig)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
clientConn.Close()
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
|
@@ -81,6 +81,7 @@ func (router *Router) StartProxyService() error {
|
||||
if router.Option.ForceTLSLatest {
|
||||
minVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
GetCertificate: router.Option.TlsManager.GetCert,
|
||||
MinVersion: uint16(minVersion),
|
||||
@@ -171,6 +172,7 @@ func (router *Router) StartProxyService() error {
|
||||
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
|
||||
PathPrefix: "",
|
||||
Version: sep.parent.Option.HostVersion,
|
||||
DevelopmentMode: sep.parent.Option.DevelopmentMode,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -265,63 +267,77 @@ func (router *Router) StartProxyService() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopProxyService stops the proxy server and waits for all listeners to close
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
if router.server == nil && router.tlsListener == nil && router.tlsRedirectStop == nil {
|
||||
return errors.New("reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := router.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Stop main TLS/HTTP server
|
||||
if router.server != nil {
|
||||
wg.Add(1)
|
||||
go func(srv *http.Server) {
|
||||
defer wg.Done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Error shutting down main server", err)
|
||||
}
|
||||
}(router.server)
|
||||
}
|
||||
|
||||
//Stop TLS listener
|
||||
if router.tlsListener != nil {
|
||||
router.tlsListener.Close()
|
||||
}
|
||||
|
||||
//Stop rate limiter
|
||||
if router.rateLimterStop != nil {
|
||||
go func() {
|
||||
// As the rate timer loop has a 1 sec ticker
|
||||
// stop the rate limiter in go routine can prevent
|
||||
// front end from freezing for 1 sec
|
||||
router.rateLimterStop <- true
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
//Stop TLS redirection (from port 80)
|
||||
// Stop TLS redirect server
|
||||
if router.tlsRedirectStop != nil {
|
||||
router.tlsRedirectStop <- true
|
||||
wg.Add(1)
|
||||
go func(ch chan bool) {
|
||||
defer wg.Done()
|
||||
ch <- true
|
||||
}(router.tlsRedirectStop)
|
||||
}
|
||||
|
||||
//Discard the server object
|
||||
router.tlsListener = nil
|
||||
// Stop rate limiter ticker if exists
|
||||
if router.rateLimterStop != nil {
|
||||
wg.Add(1)
|
||||
go func(ch chan bool) {
|
||||
defer wg.Done()
|
||||
ch <- true
|
||||
}(router.rateLimterStop)
|
||||
}
|
||||
|
||||
// Wait for all shutdown goroutines to finish
|
||||
wg.Wait()
|
||||
|
||||
// Clear server references
|
||||
router.server = nil
|
||||
router.Running = false
|
||||
router.tlsListener = nil
|
||||
router.tlsRedirectStop = nil
|
||||
router.rateLimterStop = nil
|
||||
router.Running = false
|
||||
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Proxy service stopped successfully", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart the current router if it is running.
|
||||
// Restart safely restarts the proxy server
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Restarting proxy server...", nil)
|
||||
if err := router.StopProxyService(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure ports are released
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err := router.StartProxyService(); err != nil {
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Failed to restart proxy server", err)
|
||||
return err
|
||||
}
|
||||
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Proxy server restarted successfully", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user