mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-28 18:31:45 +02:00
Compare commits
358 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d2b8f224c | |||
877692695e | |||
b9c609e413 | |||
7426cc2bb1 | |||
bd71335f47 | |||
8bcc8e7095 | |||
b1824a66a3 | |||
cc6501db12 | |||
70d95bd4e4 | |||
b590e15ef2 | |||
b25f8aab3e | |||
c0578a33b6 | |||
55a525106a | |||
e3b68b9aad | |||
3f1c50c009 | |||
8f046a0b47 | |||
0e5550487e | |||
9781735983 | |||
a98d86a303 | |||
73e6530862 | |||
0c753ae531 | |||
6353cc532a | |||
e049761f36 | |||
4dc7175588 | |||
ffc67ede12 | |||
6750c7fe3d | |||
36c2c9a00e | |||
4f026e8c07 | |||
72b100aab0 | |||
291f12e5ea | |||
0c8dfd8aa0 | |||
76e2861fea | |||
b23b967165 | |||
d682d52eb7 | |||
23eeeee701 | |||
e961e52dea | |||
b863a9720f | |||
ca7cd0476c | |||
a3cccee162 | |||
b9b992a817 | |||
19d5695f1a | |||
bcfc777d15 | |||
caa64ada76 | |||
ac91a3fef1 | |||
05f1743ecd | |||
d4c1225f75 | |||
f245a61d32 | |||
5c2b8e4c31 | |||
f6eef46d3f | |||
3adc669db9 | |||
85201885f0 | |||
44b65d1bfa | |||
6cb9e8e427 | |||
d4b1cc8c57 | |||
0e749e8a41 | |||
2c219eceef | |||
92a27cbeb8 | |||
b8a47dc620 | |||
c4266559be | |||
136989f2ea | |||
3e031605fc | |||
eb265e3e94 | |||
8504ff16cb | |||
b71437058f | |||
4d16758e0a | |||
f2b4c47805 | |||
7dff4f83b4 | |||
eb24bc0391 | |||
dac3e8c925 | |||
3f1c1f1395 | |||
cd15fdf3c1 | |||
0fdfda436b | |||
f8270e46c2 | |||
4a99afa2f0 | |||
dfd5ef5578 | |||
3e57a90bb6 | |||
23d4df1ed7 | |||
39d6d16c2a | |||
b7e3888513 | |||
fd41a1cb91 | |||
75c351e7e2 | |||
6a8057c3a7 | |||
ebf6ad6600 | |||
549e492ffd | |||
6351f25c00 | |||
560b0058cd | |||
28a0a837ba | |||
14e1341c34 | |||
5abc4ac606 | |||
214b69b0b8 | |||
3993ac954c | |||
53657e8716 | |||
bddff0cf2f | |||
dd4df0b4db | |||
85709dacf6 | |||
ad13b33283 | |||
20959cd6cc | |||
394cf50e1d | |||
1116b643b5 | |||
2e9d70da83 | |||
6130459f7c | |||
2d29065812 | |||
2be7f711ba | |||
de9d3bfb65 | |||
3e4c66b34f | |||
895ee1e53f | |||
caf4ab331b | |||
36c1f149e6 | |||
b0dc4d6670 | |||
5d8bec7f24 | |||
32f60dfba6 | |||
0abe4c12cf | |||
7555611ba5 | |||
e624227dae | |||
27695584ab | |||
e47a7a8357 | |||
3246f8ea2c | |||
ccbda6d7c2 | |||
a7285438af | |||
693dba07b7 | |||
9b64278200 | |||
d04eff2bda | |||
3320b56b19 | |||
99728144b3 | |||
05511ed4ca | |||
70abfe6fcf | |||
6ab91c377f | |||
1863af0d63 | |||
2a9d87787d | |||
f753becd66 | |||
bb2d0d5b46 | |||
07dc63a82c | |||
97a6cf016a | |||
8df68f1f4e | |||
e4ad505f2a | |||
a402c4f326 | |||
791fbfa1b4 | |||
c49f2fd1db | |||
7d9f240d56 | |||
e20f816080 | |||
eeb438eb18 | |||
bfd64a885e | |||
45f61b3053 | |||
0d4c71d0f6 | |||
d1e5581eea | |||
be5797c8a5 | |||
ebd316a7f1 | |||
84aec4387a | |||
30dfb9cb65 | |||
0b1768ab5b | |||
ad4721820b | |||
1d4c275db3 | |||
b3ad97743c | |||
1a6a87e79b | |||
749fd4b7af | |||
85422c0a74 | |||
73999c1ae9 | |||
0ad84b3415 | |||
64b6769695 | |||
e72b2f9e09 | |||
992dd231f2 | |||
49555c1191 | |||
2fca458bd0 | |||
2423d0fb3a | |||
bb0f55018c | |||
9e95d84627 | |||
e73841786b | |||
d5449c947a | |||
8ff51044bb | |||
cc08c704de | |||
2f1a6b5ba4 | |||
4d163fe80f | |||
24371ed22e | |||
12358d3522 | |||
c39af1ff8e | |||
6bf944e13c | |||
b653b805b8 | |||
eb91865b70 | |||
57e72a8a90 | |||
4dbf110edc | |||
1eefa99b72 | |||
e6b2d458f7 | |||
4a4483e09d | |||
4485d1f811 | |||
0eb0696670 | |||
9fca2354c6 | |||
e56b045689 | |||
763ccb4d60 | |||
4d4492069d | |||
f3591aa171 | |||
2dcf578cbe | |||
23a5c6ceb0 | |||
015889851a | |||
093ed9c212 | |||
0af8c67346 | |||
c5170bcb94 | |||
cd48388c02 | |||
373845f8fd | |||
293a527ffc | |||
e4facbc7b6 | |||
1c79fa4e96 | |||
6515eb99e3 | |||
ec5c24b9b8 | |||
df88084375 | |||
74017baecf | |||
294d504ee6 | |||
477429900e | |||
2e9bc77a5d | |||
ed178d857a | |||
4cf5d29692 | |||
634e9c9855 | |||
e79a70b7ac | |||
779115d06b | |||
9cb315ea67 | |||
43ba00ec8d | |||
4577fb1f2f | |||
f877bf9eda | |||
363b9b6d94 | |||
c5ca68868b | |||
f927bb539a | |||
5f64b622b5 | |||
9a371f5bcb | |||
172c5afa60 | |||
f98e04a9fc | |||
99295cad86 | |||
95d0a98576 | |||
00bfa262cb | |||
528be69fe0 | |||
6923f0d200 | |||
7255b62e31 | |||
cf14d12c31 | |||
90cf26306a | |||
cab2f4e63a | |||
75d773887c | |||
a944c3ff36 | |||
465f332dfc | |||
dfda3fe94b | |||
5c56da1180 | |||
3392013a5c | |||
8b4c601d50 | |||
3a2eaf8766 | |||
a45092a449 | |||
d5315e5b8e | |||
31cc1a69a1 | |||
d348cbf48b | |||
f6339868ac | |||
af10f2a644 | |||
3b247c31da | |||
d74e8badb9 | |||
b40131d212 | |||
563a12c860 | |||
8b2c3b7e03 | |||
608cc0c523 | |||
b558bcbfcf | |||
9ea3fa2542 | |||
01f68c5ef5 | |||
a7f89086d4 | |||
a5ef6456c6 | |||
87659b43bd | |||
ddbecf7b68 | |||
1b3a9de378 | |||
6dd62f509d | |||
d5cc6a6859 | |||
1d965da7d0 | |||
3567c70bab | |||
0a734e0bd3 | |||
f4fa92635c | |||
7d5151bb00 | |||
54475e4b99 | |||
6ac16caf37 | |||
97502db607 | |||
0747cf4b0f | |||
94483acc92 | |||
7626857c02 | |||
0f772a715b | |||
fd1439f746 | |||
ca37bfbfa6 | |||
c1e16d55ab | |||
f595da92a1 | |||
8a8ec1cb0b | |||
e53c3cf3c4 | |||
d17de5c200 | |||
97ff48ee70 | |||
d64b1174af | |||
bec363abab | |||
0dddd1f9e3 | |||
6bfcb2e1f5 | |||
02ff288280 | |||
b1c5bc2963 | |||
d3dbbf9052 | |||
f4a5c905e7 | |||
245379e91f | |||
955a2232df | |||
7eb7ae7ced | |||
3aa0f2d914 | |||
39b0c8c674 | |||
bddeae8365 | |||
8e0e9531e7 | |||
6ff22865e0 | |||
0828fd1958 | |||
82f84470f7 | |||
cf9a05f130 | |||
301072db90 | |||
cfcd10d64f | |||
c85760c73a | |||
b7bb918aa3 | |||
962f3e0566 | |||
0bcf2b2ae3 | |||
6bfeb8cf3d | |||
33def66386 | |||
cb469f28d2 | |||
8239f4cb53 | |||
e410b92e34 | |||
aca6e44b35 | |||
2aa35cbe6d | |||
745a54605f | |||
e3b61868a1 | |||
764b1944be | |||
100cd727fc | |||
7e62fef879 | |||
1a4a55721f | |||
bb9deccff6 | |||
a18413dd03 | |||
2cd1b1de3c | |||
3a2db63d61 | |||
123d3bcf3f | |||
3ec1d9c888 | |||
5785261c7e | |||
89e60649e5 | |||
5423b82858 | |||
57135a867e | |||
547855f30f | |||
05b477e90a | |||
3519c7841c | |||
e7b4054248 | |||
973d0b3372 | |||
704980d4f8 | |||
03974163d4 | |||
dfb81513b1 | |||
b604c66a2f | |||
dd84864dd4 | |||
443cd961d2 | |||
10048150bb | |||
85f9b297c4 | |||
07e524a007 | |||
25c7e8ac1a | |||
49babbd60f | |||
fa11422748 | |||
bb1b161ae2 | |||
9545343151 | |||
61e4d45430 | |||
6026c4fd53 | |||
e3f8c99ed3 | |||
fc88dfe72e | |||
d43322f7a5 | |||
b1a14872c3 | |||
df9deb3fbb | |||
9369237229 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -28,12 +28,16 @@ If applicable, add screenshots to help explain your problem.
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Host Environment (please complete the following information):**
|
||||
**Host Environment (please complete following information, DO NOT REMOVE ANY FIELD(S)):**
|
||||
- Arch: [e.g. arm64]
|
||||
- Device: [e.g. Bananapi R2 PRO]
|
||||
- OS: [e.g. Armbian]
|
||||
- Version [e.g. 23.02 Bullseye ]
|
||||
- Docker Version (if you are running Zoraxy in docker): [e.g. 3.0.4]
|
||||
- Are you using Docker? (yes / no)
|
||||
- Docker Version (fill in "N/A" for native deployment): [e.g. 3.0.4]
|
||||
|
||||
**Supplementary links**
|
||||
If your issue is related to a particular open source project, paste the link here.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
47
.github/workflows/docker.yml
vendored
Normal file
47
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Pull last image for layer reuse
|
||||
run: |
|
||||
docker pull docker.io/zoraxydocker/zoraxy:latest
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
zoraxydocker/zoraxy:latest
|
||||
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
41
.github/workflows/main.yml
vendored
41
.github/workflows/main.yml
vendored
@ -1,41 +0,0 @@
|
||||
name: Image Publisher
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker & GHCR
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
|
||||
- name: Build the image
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/docker/
|
||||
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg VERSION=${{ github.event.release.tag_name }} \
|
||||
--provenance=false \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||
--tag zoraxydocker/zoraxy:latest \
|
||||
.
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -30,7 +30,23 @@ src/certs/*
|
||||
src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/ImagePublisher.sh
|
||||
docker/docker-compose.yaml
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
/tools/dns_challenge_update/code-gen/acmedns
|
||||
/tools/dns_challenge_update/code-gen/lego
|
||||
src/tmp/localhost.key
|
||||
src/tmp/localhost.pem
|
||||
src/www/html/index.html
|
||||
src/sys.uuid
|
||||
src/zoraxy
|
||||
src/log/
|
||||
|
||||
|
||||
# dev-tags
|
||||
/Dockerfile
|
||||
/Entrypoint.sh
|
||||
|
||||
# plugins
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
example/plugins/ztnc/ztnc.db.lock
|
||||
|
173
CHANGELOG.md
173
CHANGELOG.md
@ -1,3 +1,176 @@
|
||||
# v3.1.9 1 Mar 2025
|
||||
|
||||
+ Fixed netstat underflow bug
|
||||
+ Fixed origin picker cookie bug [#550](https://github.com/tobychui/zoraxy/issues/550)
|
||||
+ Added prototype plugin system
|
||||
+ Added plugin examples
|
||||
+ Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
|
||||
+ Added country code display for quickban list [#247](https://github.com/tobychui/zoraxy/issues/247)
|
||||
+ Removed passive load balancer and default to active lb only [#554](https://github.com/tobychui/zoraxy/issues/554)
|
||||
|
||||
|
||||
# v3.1.8 16 Feb 2025
|
||||
|
||||
+ Exposed timeout value from dpcore to UI
|
||||
+ Added active load balancing (if uptime monitor is enabled on that rule)
|
||||
+ Re-factorized io stats and remove dependencies over wmic by [eyerrock](https://github.com/eyerrock)
|
||||
+ Removed SMTP input validation [#497](https://github.com/tobychui/zoraxy/issues/497)
|
||||
+ Fixed sticky session bug
|
||||
+ Fixed passive load balancer bug
|
||||
+ Fixed dockerfile bug by [PassiveLemon](https://github.com/PassiveLemon)
|
||||
|
||||
# v3.1.7 08 Feb 2025
|
||||
|
||||
+ Merged and added new tagging system for HTTP Proxy rules [by @adoolaard](https://github.com/adoolaard)
|
||||
+ Added inline editing for redirection rules [#510](https://github.com/tobychui/zoraxy/issues/510)
|
||||
+ Added uptime monitor status dot detail info (now clickable) [#467](https://github.com/tobychui/zoraxy/issues/467)
|
||||
+ Added close connection support to port 80 listener [#405](https://github.com/tobychui/zoraxy/issues/450)
|
||||
+ Optimized port collision check on startup
|
||||
+ Optimized dark theme color scheme (Free consultation by 3S Design studio)
|
||||
+ Fixed capital letter rule unable to delete bug [#507](https://github.com/tobychui/zoraxy/issues/507)
|
||||
+ Fixed docker statistic not save bug [by @PassiveLemon](https://github.com/PassiveLemon) [#505](https://github.com/tobychui/zoraxy/issues/505)
|
||||
|
||||
|
||||
# v3.1.6 31 Dec 2024
|
||||
|
||||
|
||||
+ Exposed log file, sys.uuid and static web server path to start flag (customizable conf and sys.db path is still wip)
|
||||
+ Optimized connection close implementation
|
||||
+ Added toggle for uptime monitor
|
||||
+ Added optional copy HTTP custom headers to websocket connection [#444](https://github.com/tobychui/zoraxy/issues/444)
|
||||
|
||||
# v3.1.5 28 Dec 2024
|
||||
|
||||
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
|
||||
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
|
||||
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
|
||||
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
|
||||
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
|
||||
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
|
||||
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
|
||||
+ Added levelDB as database implementation (not currently used)
|
||||
+ Added external GeoIP db loading support
|
||||
+ Restructured a lot of modules
|
||||
|
||||
# v3.1.4 24 Nov 2024
|
||||
|
||||
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)
|
||||
+ Added an auto sniffer for self-signed certificates
|
||||
+ Added robots.txt to the project
|
||||
+ Introduced an EU wrapper in the front-end for automatic registration of 26 countries [#378](https://github.com/tobychui/zoraxy/issues/378)
|
||||
+ Moved all hard-coded values to a dedicated def.go file
|
||||
+ Fixed a panic issue occurring on unsupported platform exits
|
||||
+ Integrated fixes for SSH proxy and Docker snippet updates [#330](https://github.com/tobychui/zoraxy/issues/330) [#348](https://github.com/tobychui/zoraxy/issues/348)
|
||||
+ **Changed the default listening port to 443 and enable TLS by default**
|
||||
+ Optimized GeoIP database slow-search mode CPU usage
|
||||
|
||||
|
||||
# v3.1.3 12 Nov 2024
|
||||
|
||||
+ Fixed a critical security bug [CVE-2024-52010](https://github.com/advisories/GHSA-7hpf-g48v-hw3j)
|
||||
|
||||
# v3.1.2 03 Nov 2024
|
||||
|
||||
+ Added auto start port 80 listener on acme certificate generator
|
||||
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
|
||||
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
|
||||
+ Added support for X-Remote-User
|
||||
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
|
||||
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
|
||||
+ Removed sorting on cert list
|
||||
+ Fixed request certificate button bug
|
||||
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
|
||||
+ Fixed unable to remove new stream proxy bug
|
||||
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
|
||||
+ Added more code to SSO system (disabled in release)
|
||||
|
||||
|
||||
# v3.1.1. 09 Sep 2024
|
||||
|
||||
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
|
||||
+ Added tour for basic operations
|
||||
+ Updated acme log to system wide logger implementation
|
||||
+ Fixed path traversal in file manager [#274](https://github.com/tobychui/zoraxy/issues/274)
|
||||
+ Removed Proxmox debug code
|
||||
+ Fixed trie tree implementations
|
||||
|
||||
**Thanks to all contributors**
|
||||
|
||||
+ Fix existing containers list in docker popup [7brend7](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3A7brend7)
|
||||
+ Fix network I/O chart not rendering [JokerQyou](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3AJokerQyou)
|
||||
+ Fix typo remvoeClass to removeClass [Aahmadsyamim](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Aahmadsyamim)
|
||||
+ Updated weighted random upstream implementation [bouroo](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Abouroo)
|
||||
|
||||
# v3.1.0 31 Jul 2024
|
||||
|
||||
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
|
||||
+ Fixed csrf vulnerability [#267](https://github.com/tobychui/zoraxy/issues/267)
|
||||
+ Fixed promox issue
|
||||
+ Fixed status code bug in upstream log [#254](https://github.com/tobychui/zoraxy/issues/254)
|
||||
+ Added host overwrite and hop-by-hop header remover
|
||||
+ Added early renew days settings [#256](https://github.com/tobychui/zoraxy/issues/256)
|
||||
+ Updated make file to force no CGO in cicd process
|
||||
+ Fixed bug in updater
|
||||
+ Fixed wildcard certificate renew bug [#249](https://github.com/tobychui/zoraxy/issues/249)
|
||||
+ Added certificate download function [#227](https://github.com/tobychui/zoraxy/issues/227)
|
||||
|
||||
# v3.0.9 16 Jul 2024
|
||||
|
||||
+ Added certificate download [#227](https://github.com/tobychui/zoraxy/issues/227)
|
||||
+ Updated netcup timeout value [#231](https://github.com/tobychui/zoraxy/issues/231)
|
||||
+ Updated geoip db
|
||||
+ Removed debug print from log viewer
|
||||
+ Upgraded netstat log printing to new log formatter
|
||||
+ Improved update module implementation
|
||||
|
||||
# v3.0.8 15 Jul 2024
|
||||
|
||||
+ Added apache style logging mechanism (and build-in log viewer) [#218](https://github.com/tobychui/zoraxy/issues/218)
|
||||
+ Fixed keep alive flushing issues [#235](https://github.com/tobychui/zoraxy/issues/235)
|
||||
+ Added multi-upstream supports [#100](https://github.com/tobychui/zoraxy/issues/100)
|
||||
+ Added stick session load balancer
|
||||
+ Added weighted random load balancer
|
||||
+ Added domain cleaning logic to domain / IP input fields
|
||||
+ Added HSTS "include subdomain" auto injector
|
||||
+ Added work-in-progress SSO / Oauth Server UI
|
||||
+ Fixed uptime monitor not updating on proxy rule change bug
|
||||
+ Optimized UI for create new proxy rule
|
||||
+ Removed service expose proxy feature
|
||||
|
||||
# v3.0.7 20 Jun 2024
|
||||
|
||||
+ Fixed redirection enable bug [#199](https://github.com/tobychui/zoraxy/issues/199)
|
||||
+ Fixed header tool user agent rewrite sequence
|
||||
+ Optimized rate limit UI
|
||||
+ Added HSTS and Permission Policy Editor [#163](https://github.com/tobychui/zoraxy/issues/163)
|
||||
+ Docker UX optimization start parameter `-docker`
|
||||
+ Docker container selector implementation for conditional compilations for Windows
|
||||
|
||||
From contributors:
|
||||
|
||||
+ Add Rate Limits Limits to Zoraxy fixes [185](https://github.com/tobychui/zoraxy/issues/185) by [Kirari04](https://github.com/Kirari04)
|
||||
+ Add docker containers list to set rule by [7brend7](https://github.com/7brend7) [PR202](https://github.com/tobychui/zoraxy/pull/202)
|
||||
|
||||
# v3.0.6 10 Jun 2024
|
||||
|
||||
+ Added fastly_client_ip to X-Real-IP auto rewrite
|
||||
+ Added atomic accumulator to TCP proxy
|
||||
+ Added white logo for future dark theme
|
||||
+ Added multi selection for white / blacklist [#176](https://github.com/tobychui/zoraxy/issues/176)
|
||||
+ Moved custom header rewrite to dpcore
|
||||
+ Restructure dpcore header rewrite sequence
|
||||
+ Added advance custom header settings (zoraxy to upstream and zoraxy to downstream mode)
|
||||
+ Added header remove feature
|
||||
+ Removed password requirement for SMTP [#162](https://github.com/tobychui/zoraxy/issues/162) [#80](https://github.com/tobychui/zoraxy/issues/80)
|
||||
+ Restructured TCP proxy into Stream Proxy (Support both TCP and UDP) [#147](https://github.com/tobychui/zoraxy/issues/147)
|
||||
+ Added stream proxy auto start [#169](https://github.com/tobychui/zoraxy/issues/169)
|
||||
+ Optimized UX for reminding user to click Apply after port change
|
||||
+ Added version number to footer [#160](https://github.com/tobychui/zoraxy/issues/160)
|
||||
|
||||
From contributors:
|
||||
|
||||
+ Fixed missing / unnecessary error check [PR187](https://github.com/tobychui/zoraxy/pull/187) by [Kirari04](https://github.com/Kirari04)
|
||||
|
||||
# v3.0.5 May 26 2024
|
||||
|
||||
|
||||
|
67
README.md
67
README.md
@ -4,7 +4,6 @@
|
||||
|
||||
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
@ -21,35 +20,39 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- TCP Tunneling / Proxy
|
||||
- Stream Proxy (TCP & UDP)
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Utilities
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- Wake-On-Lan
|
||||
- Debug Forward Proxy
|
||||
- IP Scanner
|
||||
- Others
|
||||
- Basic single-admin management mode
|
||||
- External permission management system for easy system integration
|
||||
- SMTP config for password reset
|
||||
- Dark Theme Mode
|
||||
|
||||
## Downloads
|
||||
|
||||
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
|
||||
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
|
||||
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
|
||||
/ [Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
|
||||
/ [Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
|
||||
|
||||
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
For other systems or architectures, please see [Releases](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
|
||||
|
||||
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)!
|
||||
Thank you for the well written and easy to follow tutorial by Reddit user [itsvmn](https://www.reddit.com/user/itsvmn/)!
|
||||
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
|
||||
|
||||
## Build from Source
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
Requires Go 1.23 or higher
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
@ -62,7 +65,7 @@ sudo ./zoraxy -port=:8000
|
||||
|
||||
## Usage
|
||||
|
||||
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
|
||||
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructions below for your desired deployment platform.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
@ -90,36 +93,50 @@ The installation method is same as Linux. For other ARM SBCs, please refer to yo
|
||||
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||
|
||||
### Start Paramters
|
||||
### Start Parameters
|
||||
|
||||
```
|
||||
Usage of zoraxy:
|
||||
-autorenew int
|
||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||
-cfgupgrade
|
||||
Enable auto config upgrade if breaking change is detected (default true)
|
||||
-db string
|
||||
Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto")
|
||||
-default_inbound_enabled
|
||||
If web server is enabled by default (default true)
|
||||
-default_inbound_port int
|
||||
Default web server listening port (default 443)
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-earlyrenew int
|
||||
Number of days to early renew a soon expiring certificate (days) (default 30)
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-log
|
||||
Log terminal output to file (default true)
|
||||
-log string
|
||||
Log folder path (default "./log")
|
||||
-mdns
|
||||
Enable mDNS scanner and transponder (default true)
|
||||
-mdnsname string
|
||||
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-plugin string
|
||||
Plugin folder path (default "./plugins")
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-sshlb
|
||||
Allow loopback web ssh connection (DANGER)
|
||||
-update_geoip
|
||||
Download the latest GeoIP data and exit
|
||||
-uuid string
|
||||
sys.uuid file path (default "./sys.uuid")
|
||||
-version
|
||||
Show version of this server
|
||||
-webfm
|
||||
Enable web file manager for static web server root folder (default true)
|
||||
-webroot string
|
||||
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
||||
-ztauth string
|
||||
ZeroTier authtoken for the local node
|
||||
-ztport int
|
||||
ZeroTier controller API port (default 9993)
|
||||
Static web server root folder. Only allow change in start paramters (default "./www")
|
||||
```
|
||||
|
||||
### External Permission Management Mode
|
||||
@ -130,7 +147,8 @@ If you already have an upstream reverse proxy server in place with permission ma
|
||||
./zoraxy -noauth=true
|
||||
```
|
||||
|
||||
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||
> [!WARNING]
|
||||
> For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||
|
||||
## Screenshots
|
||||
|
||||
@ -150,7 +168,7 @@ This project also compatible with [ZeroTier](https://www.zerotier.com/). However
|
||||
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
|
||||
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
@ -171,12 +189,22 @@ Web SSH currently only supports Linux based OSes. The following platforms are su
|
||||
|
||||
### Loopback Connection
|
||||
|
||||
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||
Loopback web SSH connections, by default, are disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||
|
||||
```bash
|
||||
./zoraxy -sshlb=true
|
||||
```
|
||||
|
||||
## Community Maintained Sections
|
||||
|
||||
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
|
||||
|
||||
- Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
Thank you so much for your contributions!
|
||||
|
||||
## Sponsor This Project
|
||||
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
@ -188,4 +216,3 @@ If you like the project and want to support us, please consider a donation. You
|
||||
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **This software is intended to be free of charge. If you have acquired this software from a third-party seller, the authors of this repository bears no responsibility for any technical difficulties assistance or support.**
|
||||
|
||||
|
||||
|
@ -1,17 +1,9 @@
|
||||
FROM docker.io/golang:alpine
|
||||
# VERSION comes from the main.yml workflow --build-arg
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache bash netcat-openbsd sudo
|
||||
## Build Zoraxy
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
RUN chmod -R 770 /opt/zoraxy/
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
# If you build it yourself, you will need to add the src directory into the docker directory.
|
||||
COPY ./src/ /opt/zoraxy/source/
|
||||
|
||||
@ -19,17 +11,85 @@ WORKDIR /opt/zoraxy/source/
|
||||
|
||||
RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
chmod 755 /usr/local/bin/zoraxy
|
||||
|
||||
RUN chmod 755 /usr/local/bin/zoraxy &&\
|
||||
chmod +x /usr/local/bin/zoraxy
|
||||
|
||||
## Build ZeroTier
|
||||
FROM docker.io/rust:1.79-alpine AS build-zerotier
|
||||
|
||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
RUN apk add --update --no-cache curl make gcc g++ linux-headers openssl-dev nano
|
||||
|
||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||
cd ZeroTierOne-*/zeroidc &&\
|
||||
cargo update -p getrandom &&\
|
||||
cd .. &&\
|
||||
make -f make-linux.mk &&\
|
||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
|
||||
## Fetch plugin
|
||||
FROM docker.io/golang:alpine AS fetch-plugin
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
RUN apk add --update --no-cache git
|
||||
|
||||
WORKDIR /opt/zoraxy/
|
||||
|
||||
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
|
||||
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
|
||||
## Main
|
||||
FROM docker.io/golang:alpine
|
||||
|
||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
||||
|
||||
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
|
||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
echo "tun" | tee -a /etc/modules
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
ENV ARGS="-noauth=false"
|
||||
ENV ZEROTIER="false"
|
||||
|
||||
ENTRYPOINT "zoraxy" "-port=:8000" "${ARGS}"
|
||||
ENV AUTORENEW="86400"
|
||||
ENV CFGUPGRADE="true"
|
||||
ENV DB="auto"
|
||||
ENV DOCKER="true"
|
||||
ENV EARLYRENEW="30"
|
||||
ENV FASTGEOIP="false"
|
||||
ENV MDNS="true"
|
||||
ENV MDNSNAME="''"
|
||||
ENV NOAUTH="false"
|
||||
ENV PLUGIN="/opt/zoraxy/plugin/"
|
||||
ENV PORT="8000"
|
||||
ENV SSHLB="false"
|
||||
ENV UPDATE_GEOIP="false"
|
||||
ENV VERSION="false"
|
||||
ENV WEBFM="true"
|
||||
ENV WEBROOT="./www"
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||
|
||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
||||
|
||||
|
160
docker/README.md
160
docker/README.md
@ -1,65 +1,137 @@
|
||||
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
|
||||
# Zoraxy Docker
|
||||
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
|
||||
## Setup: </br>
|
||||
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
|
||||
## Usage
|
||||
|
||||
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br>
|
||||
If you are attempting to access your service from outside your network, make sure to forward ports 80 and 443 to the Zoraxy host to allow web traffic. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. Read more about it from [whatismyip](https://www.whatismyip.com/port-forwarding/).
|
||||
|
||||
In the examples below, make sure to update `/path/to/zoraxy/config/`. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location or a named Docker volume.
|
||||
|
||||
Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Change the port in the URL if you changed the management port.
|
||||
|
||||
### Docker Run
|
||||
|
||||
### Using Docker run </br>
|
||||
```
|
||||
docker run -d --name (container name) -p (ports) -v (path to storage directory):/opt/zoraxy/data/ -e ARGS='(your arguments)' zoraxydocker/zoraxy:latest
|
||||
docker run -d \
|
||||
--name zoraxy \
|
||||
--restart unless-stopped \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
||||
-v /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/ \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /etc/localtime:/etc/localtime \
|
||||
-e FASTGEOIP="true" \
|
||||
zoraxydocker/zoraxy:latest
|
||||
```
|
||||
|
||||
### Using Docker Compose </br>
|
||||
### Docker Compose
|
||||
|
||||
```yml
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: (container name)
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- (external):8000
|
||||
volumes:
|
||||
- (path to storage directory):/opt/zoraxy/config/
|
||||
environment:
|
||||
ARGS: '(your arguments)'
|
||||
```
|
||||
|
||||
| Operator | Need | Details |
|
||||
|:-|:-|:-|
|
||||
| `-d` | Yes | will run the container in the background. |
|
||||
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
|
||||
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
||||
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
|
||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
|
||||
|
||||
## Examples: </br>
|
||||
### Docker Run </br>
|
||||
```
|
||||
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -e ARGS='-noauth=false' zoraxydocker/zoraxy:latest
|
||||
```
|
||||
|
||||
### Docker Compose </br>
|
||||
```yml
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
zoraxy:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8005:8000/tcp
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/
|
||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
ARGS: '-noauth=false'
|
||||
FASTGEOIP: "true"
|
||||
```
|
||||
|
||||
### Ports
|
||||
|
||||
| Port | Details |
|
||||
|:-|:-|
|
||||
| `80` | HTTP traffic. |
|
||||
| `443` | HTTPS traffic. |
|
||||
| `8000` | Management interface. Can be changed with the `PORT` env. |
|
||||
|
||||
### Volumes
|
||||
|
||||
| Volume | Details |
|
||||
|:-|:-|
|
||||
| `/opt/zoraxy/config/` | Zoraxy configuration. |
|
||||
| `/opt/zoraxy/plugin/` | Zoraxy plugins. |
|
||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||
|
||||
### Extra Hosts
|
||||
| Host | Details |
|
||||
|:-|:-|
|
||||
| `host.docker.internal:host-gateway` | Resolves host.docker.internal to the host’s gateway IP on the Docker bridge network, allowing containers to access services running on the host machine. |
|
||||
|
||||
### Environment
|
||||
|
||||
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
|
||||
|
||||
| Variable | Default | Details |
|
||||
|:-|:-|:-|
|
||||
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
|
||||
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
|
||||
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
|
||||
| `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. |
|
||||
| `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. |
|
||||
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
|
||||
| `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. |
|
||||
| `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). |
|
||||
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
|
||||
| `PLUGIN` | `/opt/zoraxy/plugin/` (String) | Set the path for Zoraxy plugins. Only change this if you know what you are doing. |
|
||||
| `PORT` | `8000` (Integer) | Management web interface listening port |
|
||||
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
|
||||
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
|
||||
| `VERSION` | `false` (Boolean) | Show version of this server. |
|
||||
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
|
||||
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
|
||||
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
||||
|
||||
### ZeroTier
|
||||
|
||||
If you are running with ZeroTier, make sure to add the following flags to ensure ZeroTier functionality:
|
||||
|
||||
`--cap_add NET_ADMIN` and `--device /dev/net/tun:/dev/net/tun`
|
||||
|
||||
Or for Docker Compose:
|
||||
```
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
```
|
||||
|
||||
### Plugins
|
||||
|
||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
||||
|
||||
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Plugins are currently experimental.
|
||||
|
||||
### Building
|
||||
|
||||
To build the Docker image:
|
||||
- Check out the repository/branch.
|
||||
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
|
||||
- Run the build command with `docker build -t zoraxy_build .`
|
||||
- You can now use the image `zoraxy_build`
|
||||
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
||||
|
||||
|
19
docker/build_plugins.sh
Normal file
19
docker/build_plugins.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Running go mod tidy and go build for all directories..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cd "$dir" || exit 1
|
||||
go mod tidy
|
||||
go build
|
||||
cd "$1" || exit 1
|
||||
fi
|
||||
done
|
||||
|
18
docker/docker-compose.yml
Normal file
18
docker/docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
||||
services:
|
||||
zoraxy:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
FASTGEOIP: "true"
|
55
docker/entrypoint.sh
Normal file
55
docker/entrypoint.sh
Normal file
@ -0,0 +1,55 @@
|
||||
#!/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"
|
||||
|
@ -1 +1 @@
|
||||
zoraxy.arozos.com
|
||||
zoraxy.aroz.org
|
451
docs/GNU Free Documentation License.txt
Normal file
451
docs/GNU Free Documentation License.txt
Normal file
@ -0,0 +1,451 @@
|
||||
|
||||
GNU Free Documentation License
|
||||
Version 1.3, 3 November 2008
|
||||
|
||||
|
||||
Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
0. PREAMBLE
|
||||
|
||||
The purpose of this License is to make a manual, textbook, or other
|
||||
functional and useful document "free" in the sense of freedom: to
|
||||
assure everyone the effective freedom to copy and redistribute it,
|
||||
with or without modifying it, either commercially or noncommercially.
|
||||
Secondarily, this License preserves for the author and publisher a way
|
||||
to get credit for their work, while not being considered responsible
|
||||
for modifications made by others.
|
||||
|
||||
This License is a kind of "copyleft", which means that derivative
|
||||
works of the document must themselves be free in the same sense. It
|
||||
complements the GNU General Public License, which is a copyleft
|
||||
license designed for free software.
|
||||
|
||||
We have designed this License in order to use it for manuals for free
|
||||
software, because free software needs free documentation: a free
|
||||
program should come with manuals providing the same freedoms that the
|
||||
software does. But this License is not limited to software manuals;
|
||||
it can be used for any textual work, regardless of subject matter or
|
||||
whether it is published as a printed book. We recommend this License
|
||||
principally for works whose purpose is instruction or reference.
|
||||
|
||||
|
||||
1. APPLICABILITY AND DEFINITIONS
|
||||
|
||||
This License applies to any manual or other work, in any medium, that
|
||||
contains a notice placed by the copyright holder saying it can be
|
||||
distributed under the terms of this License. Such a notice grants a
|
||||
world-wide, royalty-free license, unlimited in duration, to use that
|
||||
work under the conditions stated herein. The "Document", below,
|
||||
refers to any such manual or work. Any member of the public is a
|
||||
licensee, and is addressed as "you". You accept the license if you
|
||||
copy, modify or distribute the work in a way requiring permission
|
||||
under copyright law.
|
||||
|
||||
A "Modified Version" of the Document means any work containing the
|
||||
Document or a portion of it, either copied verbatim, or with
|
||||
modifications and/or translated into another language.
|
||||
|
||||
A "Secondary Section" is a named appendix or a front-matter section of
|
||||
the Document that deals exclusively with the relationship of the
|
||||
publishers or authors of the Document to the Document's overall
|
||||
subject (or to related matters) and contains nothing that could fall
|
||||
directly within that overall subject. (Thus, if the Document is in
|
||||
part a textbook of mathematics, a Secondary Section may not explain
|
||||
any mathematics.) The relationship could be a matter of historical
|
||||
connection with the subject or with related matters, or of legal,
|
||||
commercial, philosophical, ethical or political position regarding
|
||||
them.
|
||||
|
||||
The "Invariant Sections" are certain Secondary Sections whose titles
|
||||
are designated, as being those of Invariant Sections, in the notice
|
||||
that says that the Document is released under this License. If a
|
||||
section does not fit the above definition of Secondary then it is not
|
||||
allowed to be designated as Invariant. The Document may contain zero
|
||||
Invariant Sections. If the Document does not identify any Invariant
|
||||
Sections then there are none.
|
||||
|
||||
The "Cover Texts" are certain short passages of text that are listed,
|
||||
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
|
||||
the Document is released under this License. A Front-Cover Text may
|
||||
be at most 5 words, and a Back-Cover Text may be at most 25 words.
|
||||
|
||||
A "Transparent" copy of the Document means a machine-readable copy,
|
||||
represented in a format whose specification is available to the
|
||||
general public, that is suitable for revising the document
|
||||
straightforwardly with generic text editors or (for images composed of
|
||||
pixels) generic paint programs or (for drawings) some widely available
|
||||
drawing editor, and that is suitable for input to text formatters or
|
||||
for automatic translation to a variety of formats suitable for input
|
||||
to text formatters. A copy made in an otherwise Transparent file
|
||||
format whose markup, or absence of markup, has been arranged to thwart
|
||||
or discourage subsequent modification by readers is not Transparent.
|
||||
An image format is not Transparent if used for any substantial amount
|
||||
of text. A copy that is not "Transparent" is called "Opaque".
|
||||
|
||||
Examples of suitable formats for Transparent copies include plain
|
||||
ASCII without markup, Texinfo input format, LaTeX input format, SGML
|
||||
or XML using a publicly available DTD, and standard-conforming simple
|
||||
HTML, PostScript or PDF designed for human modification. Examples of
|
||||
transparent image formats include PNG, XCF and JPG. Opaque formats
|
||||
include proprietary formats that can be read and edited only by
|
||||
proprietary word processors, SGML or XML for which the DTD and/or
|
||||
processing tools are not generally available, and the
|
||||
machine-generated HTML, PostScript or PDF produced by some word
|
||||
processors for output purposes only.
|
||||
|
||||
The "Title Page" means, for a printed book, the title page itself,
|
||||
plus such following pages as are needed to hold, legibly, the material
|
||||
this License requires to appear in the title page. For works in
|
||||
formats which do not have any title page as such, "Title Page" means
|
||||
the text near the most prominent appearance of the work's title,
|
||||
preceding the beginning of the body of the text.
|
||||
|
||||
The "publisher" means any person or entity that distributes copies of
|
||||
the Document to the public.
|
||||
|
||||
A section "Entitled XYZ" means a named subunit of the Document whose
|
||||
title either is precisely XYZ or contains XYZ in parentheses following
|
||||
text that translates XYZ in another language. (Here XYZ stands for a
|
||||
specific section name mentioned below, such as "Acknowledgements",
|
||||
"Dedications", "Endorsements", or "History".) To "Preserve the Title"
|
||||
of such a section when you modify the Document means that it remains a
|
||||
section "Entitled XYZ" according to this definition.
|
||||
|
||||
The Document may include Warranty Disclaimers next to the notice which
|
||||
states that this License applies to the Document. These Warranty
|
||||
Disclaimers are considered to be included by reference in this
|
||||
License, but only as regards disclaiming warranties: any other
|
||||
implication that these Warranty Disclaimers may have is void and has
|
||||
no effect on the meaning of this License.
|
||||
|
||||
2. VERBATIM COPYING
|
||||
|
||||
You may copy and distribute the Document in any medium, either
|
||||
commercially or noncommercially, provided that this License, the
|
||||
copyright notices, and the license notice saying this License applies
|
||||
to the Document are reproduced in all copies, and that you add no
|
||||
other conditions whatsoever to those of this License. You may not use
|
||||
technical measures to obstruct or control the reading or further
|
||||
copying of the copies you make or distribute. However, you may accept
|
||||
compensation in exchange for copies. If you distribute a large enough
|
||||
number of copies you must also follow the conditions in section 3.
|
||||
|
||||
You may also lend copies, under the same conditions stated above, and
|
||||
you may publicly display copies.
|
||||
|
||||
|
||||
3. COPYING IN QUANTITY
|
||||
|
||||
If you publish printed copies (or copies in media that commonly have
|
||||
printed covers) of the Document, numbering more than 100, and the
|
||||
Document's license notice requires Cover Texts, you must enclose the
|
||||
copies in covers that carry, clearly and legibly, all these Cover
|
||||
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
|
||||
the back cover. Both covers must also clearly and legibly identify
|
||||
you as the publisher of these copies. The front cover must present
|
||||
the full title with all words of the title equally prominent and
|
||||
visible. You may add other material on the covers in addition.
|
||||
Copying with changes limited to the covers, as long as they preserve
|
||||
the title of the Document and satisfy these conditions, can be treated
|
||||
as verbatim copying in other respects.
|
||||
|
||||
If the required texts for either cover are too voluminous to fit
|
||||
legibly, you should put the first ones listed (as many as fit
|
||||
reasonably) on the actual cover, and continue the rest onto adjacent
|
||||
pages.
|
||||
|
||||
If you publish or distribute Opaque copies of the Document numbering
|
||||
more than 100, you must either include a machine-readable Transparent
|
||||
copy along with each Opaque copy, or state in or with each Opaque copy
|
||||
a computer-network location from which the general network-using
|
||||
public has access to download using public-standard network protocols
|
||||
a complete Transparent copy of the Document, free of added material.
|
||||
If you use the latter option, you must take reasonably prudent steps,
|
||||
when you begin distribution of Opaque copies in quantity, to ensure
|
||||
that this Transparent copy will remain thus accessible at the stated
|
||||
location until at least one year after the last time you distribute an
|
||||
Opaque copy (directly or through your agents or retailers) of that
|
||||
edition to the public.
|
||||
|
||||
It is requested, but not required, that you contact the authors of the
|
||||
Document well before redistributing any large number of copies, to
|
||||
give them a chance to provide you with an updated version of the
|
||||
Document.
|
||||
|
||||
|
||||
4. MODIFICATIONS
|
||||
|
||||
You may copy and distribute a Modified Version of the Document under
|
||||
the conditions of sections 2 and 3 above, provided that you release
|
||||
the Modified Version under precisely this License, with the Modified
|
||||
Version filling the role of the Document, thus licensing distribution
|
||||
and modification of the Modified Version to whoever possesses a copy
|
||||
of it. In addition, you must do these things in the Modified Version:
|
||||
|
||||
A. Use in the Title Page (and on the covers, if any) a title distinct
|
||||
from that of the Document, and from those of previous versions
|
||||
(which should, if there were any, be listed in the History section
|
||||
of the Document). You may use the same title as a previous version
|
||||
if the original publisher of that version gives permission.
|
||||
B. List on the Title Page, as authors, one or more persons or entities
|
||||
responsible for authorship of the modifications in the Modified
|
||||
Version, together with at least five of the principal authors of the
|
||||
Document (all of its principal authors, if it has fewer than five),
|
||||
unless they release you from this requirement.
|
||||
C. State on the Title page the name of the publisher of the
|
||||
Modified Version, as the publisher.
|
||||
D. Preserve all the copyright notices of the Document.
|
||||
E. Add an appropriate copyright notice for your modifications
|
||||
adjacent to the other copyright notices.
|
||||
F. Include, immediately after the copyright notices, a license notice
|
||||
giving the public permission to use the Modified Version under the
|
||||
terms of this License, in the form shown in the Addendum below.
|
||||
G. Preserve in that license notice the full lists of Invariant Sections
|
||||
and required Cover Texts given in the Document's license notice.
|
||||
H. Include an unaltered copy of this License.
|
||||
I. Preserve the section Entitled "History", Preserve its Title, and add
|
||||
to it an item stating at least the title, year, new authors, and
|
||||
publisher of the Modified Version as given on the Title Page. If
|
||||
there is no section Entitled "History" in the Document, create one
|
||||
stating the title, year, authors, and publisher of the Document as
|
||||
given on its Title Page, then add an item describing the Modified
|
||||
Version as stated in the previous sentence.
|
||||
J. Preserve the network location, if any, given in the Document for
|
||||
public access to a Transparent copy of the Document, and likewise
|
||||
the network locations given in the Document for previous versions
|
||||
it was based on. These may be placed in the "History" section.
|
||||
You may omit a network location for a work that was published at
|
||||
least four years before the Document itself, or if the original
|
||||
publisher of the version it refers to gives permission.
|
||||
K. For any section Entitled "Acknowledgements" or "Dedications",
|
||||
Preserve the Title of the section, and preserve in the section all
|
||||
the substance and tone of each of the contributor acknowledgements
|
||||
and/or dedications given therein.
|
||||
L. Preserve all the Invariant Sections of the Document,
|
||||
unaltered in their text and in their titles. Section numbers
|
||||
or the equivalent are not considered part of the section titles.
|
||||
M. Delete any section Entitled "Endorsements". Such a section
|
||||
may not be included in the Modified Version.
|
||||
N. Do not retitle any existing section to be Entitled "Endorsements"
|
||||
or to conflict in title with any Invariant Section.
|
||||
O. Preserve any Warranty Disclaimers.
|
||||
|
||||
If the Modified Version includes new front-matter sections or
|
||||
appendices that qualify as Secondary Sections and contain no material
|
||||
copied from the Document, you may at your option designate some or all
|
||||
of these sections as invariant. To do this, add their titles to the
|
||||
list of Invariant Sections in the Modified Version's license notice.
|
||||
These titles must be distinct from any other section titles.
|
||||
|
||||
You may add a section Entitled "Endorsements", provided it contains
|
||||
nothing but endorsements of your Modified Version by various
|
||||
parties--for example, statements of peer review or that the text has
|
||||
been approved by an organization as the authoritative definition of a
|
||||
standard.
|
||||
|
||||
You may add a passage of up to five words as a Front-Cover Text, and a
|
||||
passage of up to 25 words as a Back-Cover Text, to the end of the list
|
||||
of Cover Texts in the Modified Version. Only one passage of
|
||||
Front-Cover Text and one of Back-Cover Text may be added by (or
|
||||
through arrangements made by) any one entity. If the Document already
|
||||
includes a cover text for the same cover, previously added by you or
|
||||
by arrangement made by the same entity you are acting on behalf of,
|
||||
you may not add another; but you may replace the old one, on explicit
|
||||
permission from the previous publisher that added the old one.
|
||||
|
||||
The author(s) and publisher(s) of the Document do not by this License
|
||||
give permission to use their names for publicity for or to assert or
|
||||
imply endorsement of any Modified Version.
|
||||
|
||||
|
||||
5. COMBINING DOCUMENTS
|
||||
|
||||
You may combine the Document with other documents released under this
|
||||
License, under the terms defined in section 4 above for modified
|
||||
versions, provided that you include in the combination all of the
|
||||
Invariant Sections of all of the original documents, unmodified, and
|
||||
list them all as Invariant Sections of your combined work in its
|
||||
license notice, and that you preserve all their Warranty Disclaimers.
|
||||
|
||||
The combined work need only contain one copy of this License, and
|
||||
multiple identical Invariant Sections may be replaced with a single
|
||||
copy. If there are multiple Invariant Sections with the same name but
|
||||
different contents, make the title of each such section unique by
|
||||
adding at the end of it, in parentheses, the name of the original
|
||||
author or publisher of that section if known, or else a unique number.
|
||||
Make the same adjustment to the section titles in the list of
|
||||
Invariant Sections in the license notice of the combined work.
|
||||
|
||||
In the combination, you must combine any sections Entitled "History"
|
||||
in the various original documents, forming one section Entitled
|
||||
"History"; likewise combine any sections Entitled "Acknowledgements",
|
||||
and any sections Entitled "Dedications". You must delete all sections
|
||||
Entitled "Endorsements".
|
||||
|
||||
|
||||
6. COLLECTIONS OF DOCUMENTS
|
||||
|
||||
You may make a collection consisting of the Document and other
|
||||
documents released under this License, and replace the individual
|
||||
copies of this License in the various documents with a single copy
|
||||
that is included in the collection, provided that you follow the rules
|
||||
of this License for verbatim copying of each of the documents in all
|
||||
other respects.
|
||||
|
||||
You may extract a single document from such a collection, and
|
||||
distribute it individually under this License, provided you insert a
|
||||
copy of this License into the extracted document, and follow this
|
||||
License in all other respects regarding verbatim copying of that
|
||||
document.
|
||||
|
||||
|
||||
7. AGGREGATION WITH INDEPENDENT WORKS
|
||||
|
||||
A compilation of the Document or its derivatives with other separate
|
||||
and independent documents or works, in or on a volume of a storage or
|
||||
distribution medium, is called an "aggregate" if the copyright
|
||||
resulting from the compilation is not used to limit the legal rights
|
||||
of the compilation's users beyond what the individual works permit.
|
||||
When the Document is included in an aggregate, this License does not
|
||||
apply to the other works in the aggregate which are not themselves
|
||||
derivative works of the Document.
|
||||
|
||||
If the Cover Text requirement of section 3 is applicable to these
|
||||
copies of the Document, then if the Document is less than one half of
|
||||
the entire aggregate, the Document's Cover Texts may be placed on
|
||||
covers that bracket the Document within the aggregate, or the
|
||||
electronic equivalent of covers if the Document is in electronic form.
|
||||
Otherwise they must appear on printed covers that bracket the whole
|
||||
aggregate.
|
||||
|
||||
|
||||
8. TRANSLATION
|
||||
|
||||
Translation is considered a kind of modification, so you may
|
||||
distribute translations of the Document under the terms of section 4.
|
||||
Replacing Invariant Sections with translations requires special
|
||||
permission from their copyright holders, but you may include
|
||||
translations of some or all Invariant Sections in addition to the
|
||||
original versions of these Invariant Sections. You may include a
|
||||
translation of this License, and all the license notices in the
|
||||
Document, and any Warranty Disclaimers, provided that you also include
|
||||
the original English version of this License and the original versions
|
||||
of those notices and disclaimers. In case of a disagreement between
|
||||
the translation and the original version of this License or a notice
|
||||
or disclaimer, the original version will prevail.
|
||||
|
||||
If a section in the Document is Entitled "Acknowledgements",
|
||||
"Dedications", or "History", the requirement (section 4) to Preserve
|
||||
its Title (section 1) will typically require changing the actual
|
||||
title.
|
||||
|
||||
|
||||
9. TERMINATION
|
||||
|
||||
You may not copy, modify, sublicense, or distribute the Document
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense, or distribute it is void, and
|
||||
will automatically terminate your rights under this License.
|
||||
|
||||
However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally,
|
||||
unless and until the copyright holder explicitly and finally
|
||||
terminates your license, and (b) permanently, if the copyright holder
|
||||
fails to notify you of the violation by some reasonable means prior to
|
||||
60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, receipt of a copy of some or all of the same material does
|
||||
not give you any rights to use it.
|
||||
|
||||
|
||||
10. FUTURE REVISIONS OF THIS LICENSE
|
||||
|
||||
The Free Software Foundation may publish new, revised versions of the
|
||||
GNU Free Documentation License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in
|
||||
detail to address new problems or concerns. See
|
||||
https://www.gnu.org/licenses/.
|
||||
|
||||
Each version of the License is given a distinguishing version number.
|
||||
If the Document specifies that a particular numbered version of this
|
||||
License "or any later version" applies to it, you have the option of
|
||||
following the terms and conditions either of that specified version or
|
||||
of any later version that has been published (not as a draft) by the
|
||||
Free Software Foundation. If the Document does not specify a version
|
||||
number of this License, you may choose any version ever published (not
|
||||
as a draft) by the Free Software Foundation. If the Document
|
||||
specifies that a proxy can decide which future versions of this
|
||||
License can be used, that proxy's public statement of acceptance of a
|
||||
version permanently authorizes you to choose that version for the
|
||||
Document.
|
||||
|
||||
11. RELICENSING
|
||||
|
||||
"Massive Multiauthor Collaboration Site" (or "MMC Site") means any
|
||||
World Wide Web server that publishes copyrightable works and also
|
||||
provides prominent facilities for anybody to edit those works. A
|
||||
public wiki that anybody can edit is an example of such a server. A
|
||||
"Massive Multiauthor Collaboration" (or "MMC") contained in the site
|
||||
means any set of copyrightable works thus published on the MMC site.
|
||||
|
||||
"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
|
||||
license published by Creative Commons Corporation, a not-for-profit
|
||||
corporation with a principal place of business in San Francisco,
|
||||
California, as well as future copyleft versions of that license
|
||||
published by that same organization.
|
||||
|
||||
"Incorporate" means to publish or republish a Document, in whole or in
|
||||
part, as part of another Document.
|
||||
|
||||
An MMC is "eligible for relicensing" if it is licensed under this
|
||||
License, and if all works that were first published under this License
|
||||
somewhere other than this MMC, and subsequently incorporated in whole or
|
||||
in part into the MMC, (1) had no cover texts or invariant sections, and
|
||||
(2) were thus incorporated prior to November 1, 2008.
|
||||
|
||||
The operator of an MMC Site may republish an MMC contained in the site
|
||||
under CC-BY-SA on the same site at any time before August 1, 2009,
|
||||
provided the MMC is eligible for relicensing.
|
||||
|
||||
|
||||
ADDENDUM: How to use this License for your documents
|
||||
|
||||
To use this License in a document you have written, include a copy of
|
||||
the License in the document and put the following copyright and
|
||||
license notices just after the title page:
|
||||
|
||||
Copyright (c) YEAR YOUR NAME.
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
|
||||
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
|
||||
replace the "with...Texts." line with this:
|
||||
|
||||
with the Invariant Sections being LIST THEIR TITLES, with the
|
||||
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
|
||||
|
||||
If you have Invariant Sections without Cover Texts, or some other
|
||||
combination of the three, merge those two alternatives to suit the
|
||||
situation.
|
||||
|
||||
If your document contains nontrivial examples of program code, we
|
||||
recommend releasing these examples in parallel under your choice of
|
||||
free software license, such as the GNU General Public License,
|
||||
to permit their use in free software.
|
1
docs/dom-i18n.min.js
vendored
Normal file
1
docs/dom-i18n.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(a,b){"use strict";"function"==typeof define&&define.amd?define([],function(){return a.domI18n=b()}):"object"==typeof exports?module.exports=b():a.domI18n=b()}(this,function(){"use strict";return function(a){function b(a){return a||(a=window.navigator.languages?window.navigator.languages[0]:window.navigator.language||window.navigator.userLanguage),-1===q.indexOf(a)&&(r&&console.warn(a+" is not available on the list of languages provided"),a=a.indexOf("-")?a.split("-")[0]:a),-1===q.indexOf(a)&&(r&&console.error(a+" is not compatible with any language provided"),a=p),a}function c(a){v=b(a),l()}function d(){u={}}function e(a){var b=a.getAttribute("data-dom-i18n-id");return b&&u&&u[b]}function f(a,b){var c="i18n"+Date.now()+1e3*Math.random();a.setAttribute("data-dom-i18n-id",c),u[c]=b}function g(a){return u&&u[a.getAttribute("data-dom-i18n-id")]}function h(a,b){var c={},d=a.firstElementChild,e=!d&&a[b].split(o);return q.forEach(function(b,f){var g;d?(g=a.children[f],g&&g.cloneNode&&(c[b]=g.cloneNode(!0))):(g=e[f],g&&(c[b]=String(g)))}),c}function i(a){var b,c,d=a.getAttribute(t),i=null!==a.getAttribute(s),k=d?d:"textContent";!i&&e(a)?b=g(a):(b=h(a,k),i||f(a,b)),c=b[v],"string"==typeof c?a[k]=c:"object"==typeof c&&j(a,c)}function j(a,b){k(a),a.appendChild(b)}function k(a){for(;a.lastChild;)a.removeChild(a.lastChild)}function l(){for(var a="string"==typeof n||n instanceof String?m.querySelectorAll(n):n,b=0;b<a.length;++b)i(a[b])}a=a||{};var m=a.rootElement||window.document,n=a.selector||"[data-translatable]",o=a.separator||" // ",p=a.defaultLanguage||"en",q=a.languages||["en"],r=void 0!==a.enableLog?a.enableLog:!0,s="data-no-cache",t="data-translatable-attr",u={},v=b(a.currentLanguage);return l(n),{changeLanguage:c,clearCachedElements:d}}});
|
BIN
docs/img/logo_white.png
Normal file
BIN
docs/img/logo_white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
docs/img/preview-mobile.png
Normal file
BIN
docs/img/preview-mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
BIN
docs/img/preview-mobile.psd
Normal file
BIN
docs/img/preview-mobile.psd
Normal file
Binary file not shown.
BIN
docs/img/preview-pc.png
Normal file
BIN
docs/img/preview-pc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
883
docs/index.html
883
docs/index.html
@ -1,128 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
|
||||
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
|
||||
<meta content="Reverse Proxy, Open Source, Aroz, Go, OS, NAS, Cloud" name="keywords">
|
||||
<meta content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox" name="description">
|
||||
<meta name="author" content="tobychui">
|
||||
|
||||
<!-- HTML Meta Tags -->
|
||||
<title>Reverse Proxy Server | Zoraxy</title>
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<title>Homelab Gateway | Zoraxy</title>
|
||||
<meta name="description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://zoraxy.arozos.com/">
|
||||
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
<meta property="og:title" content="Hello Zoraxy">
|
||||
<meta property="og:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="arozos.com">
|
||||
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
<meta property="twitter:domain" content="os.aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<meta name="twitter:title" content="Hello Zoraxy">
|
||||
<meta name="twitter:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- JavaScript Libs-->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="dom-i18n.min.js"></script>
|
||||
<link href="main.css" rel="stylesheet">
|
||||
|
||||
<!-- Css stuffs-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css" integrity="sha512-3quBdRGJyLy79hzhDDcBzANW+mVqPctrGCfIPosHQtMKb3rKsCxfyslzwlz2wj1dT8A7UX+sEvDjaUv+WExQrA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
<link href="img/apple-touch-icon.png" rel="apple-touch-icon">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Main Stylesheet File -->
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.7.0.min.js"
|
||||
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif !important;
|
||||
color: #404040;
|
||||
}
|
||||
</style>
|
||||
<!-- AOS.js-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js" integrity="sha512-A7AYk1fGKX6S2SsHywmPkrnzTZHrgiVT7GcQkLGDe2ev0aWb8zejytzS8wjo7PGEXKqJOrjQ4oORtnimIRZBtw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" integrity="sha512-1cK78a1o+ht2JcaW6g8OXYwqpev9+6GqOkz9xmBN9iUUhIndKtxwILGWYOSibOKjLsEdjyjZvYDq/cZwNeak0w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="main section">
|
||||
<div class="left-menu">
|
||||
<div class="iconWrapper">
|
||||
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
|
||||
</div>
|
||||
<a href="#home" class="menu-item active" align="center">
|
||||
<img src="img/icons/home.svg">
|
||||
</a>
|
||||
<a href="#features" class="menu-item" align="center">
|
||||
<img src="img/icons/awesome.svg">
|
||||
</a>
|
||||
<a href="#screenshots" class="menu-item" align="center">
|
||||
<img src="img/icons/screenshots.svg">
|
||||
</a>
|
||||
<a href="#plugins" class="menu-item" align="center">
|
||||
<img src="img/icons/plugin.svg">
|
||||
</a>
|
||||
<a href="#source" class="menu-item" align="center">
|
||||
<img src="img/icons/code.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
|
||||
<div id="backToTopBtn" class="ui big icon button" onclick="backToTop();">
|
||||
<i class="ui arrow up icon"></i>
|
||||
</div>
|
||||
<button id="rwdmenubtn" class="ui black big icon button"><i class="ui bars icon"></i></button>
|
||||
<div id="mainmenu" class="ui segment">
|
||||
<div class="ui container">
|
||||
<div class="ui small stackable secondary menu">
|
||||
<div class="item">
|
||||
<img class="ui tiny image" src="img/logo.png">
|
||||
</div>
|
||||
<a class="item" href="#mainmenu" i18n>
|
||||
Home // 主頁 // Startseite
|
||||
</a>
|
||||
<a class="item" href="#about" i18n>
|
||||
About Zoraxy // 關於 Zoraxy // Über Zoraxy
|
||||
</a>
|
||||
<a class="item" href="#features" i18n>
|
||||
Screenshots // 系統截圖 // Bildschirmfotos
|
||||
</a>
|
||||
<a class="item" href="#techspec" i18n>
|
||||
Videos // 介紹影片 // Videos
|
||||
</a>
|
||||
<a class="item" href="#download" i18n>
|
||||
Download // 下載 // Herunterladen
|
||||
</a>
|
||||
<a class="item" href="#learnmore" i18n>
|
||||
Learn More // 了解更多 // Mehr erfahren
|
||||
</a>
|
||||
<a class="right floated item">
|
||||
<div class="ui small selection dropdown">
|
||||
<input type="hidden" id="language">
|
||||
<div class="default text" style="color: #6cacff;"><i class="language icon"></i> Default</div>
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="en">English</div>
|
||||
<div class="item" data-value="zh">中文(正體)</div>
|
||||
<div class="item" data-value="de">Deutsch</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
<br><br>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
<tr><th colspan="2">Quick Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui download icon"></i>
|
||||
<div class="content">
|
||||
Download
|
||||
<div class="sub header">Prebuild Binary
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui github icon"></i>
|
||||
<div class="content">
|
||||
Github
|
||||
<div class="sub header">Source Code
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
</div>
|
||||
</div>
|
||||
<div class="messageBanner">
|
||||
<div class="ui text container">
|
||||
<p i18n>This site is currently under development. Some information might not be ready.
|
||||
// 本網站目前仍在開發中,部分資訊可能尚未準備好。
|
||||
// Diese Seite ist in Entwicklung. Einige Informationen sind möglicherweise nicht verfügbar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="slideshowBanner">
|
||||
<div class="title">
|
||||
<h1 i18n>Zoraxy</h1>
|
||||
<div class="ui divider" style="border-top: 1px solid rgba(255,255,255,0.5); "></div>
|
||||
<p i18n>The ultimate homelab networking toolbox for self-hosted services
|
||||
// 簡化自家伺服器部署之事,初學者居家網絡必備良器
|
||||
// Das ultimative Homelab-Netzwerk-Toolbox für selbstgehostete Dienste
|
||||
</p>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" class="ui basic white button" target="_blank"><i style="color:white;" class="ui download icon"></i><span i18n>Download // 立即下載 // Herunterladen </span></a>
|
||||
<a href="https://github.com/tobychui/zoraxy" class="ui basic white button" target="_blank"><i style="color: white;" class="ui code icon"></i><span i18n>Source Code // 查看原始碼 // Quellcode</span></a>
|
||||
|
||||
</div>
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
@ -137,250 +130,470 @@
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div id="features" class="section">
|
||||
<div class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/awesome.svg">
|
||||
<div class="content">
|
||||
Features
|
||||
<div class="sub header">Highlighting a few important features of Zoraxy</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui stackable grid featureList">
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/proxy.svg">
|
||||
<div class="content">
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/redirect.svg">
|
||||
<div class="content">
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/blacklist.svg">
|
||||
<div class="content">
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/gan.svg">
|
||||
<div class="content">
|
||||
Global Area Network
|
||||
</div>
|
||||
</h3>
|
||||
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Row 2-->
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/terminal.svg">
|
||||
<div class="content">
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/stats.svg">
|
||||
<div class="content">
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/scan.svg">
|
||||
<div class="content">
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
<!-- About ArozOS-->
|
||||
<div id="about" class="ui text container">
|
||||
<div class="ui stackable grid" data-aos="fade-up">
|
||||
<div class="six wide column" align="right">
|
||||
<img class="ui medium image" src="img/preview-pc.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screenshots -->
|
||||
<div id="screenshots" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/screenshots.svg">
|
||||
<div class="content">
|
||||
Screenshots
|
||||
<div class="sub header">A quick overview of the UI designs</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Developments -->
|
||||
<div id="plugins" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/plugin.svg">
|
||||
<div class="content">
|
||||
Plugins
|
||||
<div class="sub header">Add custom routing rules via simple scripts</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div style="width: 100%; text-align: center;">
|
||||
<br>
|
||||
<p>Documentation work in progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div id="source" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Source Code
|
||||
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="grey github icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="https://github.com/tobychui/zoraxy">
|
||||
Github
|
||||
<div class="sub header">https://github.com/tobychui/zoraxy</div>
|
||||
</a>
|
||||
<div class="ten wide column">
|
||||
<div class="about-text-wrapper">
|
||||
<p class="about-title"><b i18n>Reverse Proxy // 反向代理 // Reverse-Proxy</b></p>
|
||||
<p><span i18n>Easy setups with dynamic updates // 讓你想不到般簡單易用、迅速設定、動態更新 // Einfache Einrichtung mit dynamischen Updates</span></p>
|
||||
<p i18n>Access your reverse proxy and self-hosted services from any computer with a browser, anytime, anywhere.
|
||||
// 透過瀏覽器,隨時隨地在任何裝置上存取您的反向代理及自家伺服器服務。
|
||||
// Greifen Sie jederzeit und überall von jedem Gerät aus auf Ihren Reverse-Proxy und selbst gehostete
|
||||
</p>
|
||||
<div class="ui list">
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Simple setups with web UI
|
||||
// 透過網頁介面簡單設定即可使用
|
||||
// Einfache Einrichtung mit Web-UI
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Change settings on the fly without restarting
|
||||
// 即時更改設定,無需重新啟動
|
||||
// Einstellungen ohne Neustart ändern
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
One of the best reverse proxy manager for beginners
|
||||
// 可能是最適合初學者的反向代理管理器之一
|
||||
// Einer der besten Reverse-Proxy-Manager für Anfänger
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Easily install plugins and edit configurations
|
||||
// 輕鬆安裝插件並編輯設定
|
||||
// Plugins einfach installieren und Konfigurationen bearbeiten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="blue mail icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="mailto:toby@imuslab.com">
|
||||
Email Contact
|
||||
<div class="sub header">toby@imuslab.com</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
|
||||
<div class="ui stackable grid" data-aos="fade-up">
|
||||
<div class="six wide column" align="right">
|
||||
<img class="ui medium image" src="img/preview-mobile.png">
|
||||
</div>
|
||||
<div class="ten wide column">
|
||||
<div class="about-text-wrapper">
|
||||
<p class="about-title"><b i18n>Real-time Analytics // 即時流量分析 // Echtzeit-Analysen</b></p>
|
||||
<p><span i18n>Dynamic statistic and access control // 動態流量數據、權限與路由設定 // Dynamische Statistik und Zugriffskontrolle</span></p>
|
||||
<p i18n>Provide real time statistical overview, take advantage of the real time traffic and situations to make better decisions.
|
||||
// 提供即時統計概覽,利用即時流量和情況做出更好的決策。
|
||||
// Bietet eine Echtzeit-Übersicht über die Statistiken, um bessere Entscheidungen zu treffen.
|
||||
</p>
|
||||
<div class="ui list">
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Real time visitor statistic
|
||||
// 即時訪客統計概覽
|
||||
// Echtzeit-Besucherstatistik
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Instant network utilitization overview
|
||||
// 即時網路使用率概覽
|
||||
// Sofortige Netzwerkübersicht
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
No-reload access control and settings
|
||||
// 即時生效存取控制和設定
|
||||
// Zugriffskontrolle und Einstellungen ohne Neustart
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
One-click setting change with no downtime
|
||||
// 一鍵設定更改,無需停機
|
||||
// Einstellungsänderung mit einem Klick ohne Ausfallzeiten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
<!-- Features -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="features" class="ui container">
|
||||
<div class="centered title">
|
||||
<h1 i18n>Screenshots
|
||||
// 系統截圖
|
||||
// Bildschirmfotos
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/1.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/2.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/3.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/4.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/5.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/6.png">
|
||||
</div>
|
||||
<!-- <div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/7.png">
|
||||
</div> -->
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/8.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/9.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/10.png">
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<!-- Spec -->
|
||||
<div id="techspec" class="blackbanner">
|
||||
<br><br>
|
||||
<div class="centered title">
|
||||
<h1 style="font-weight: 600;" i18n>
|
||||
Review Videos
|
||||
// 介紹影片
|
||||
// Videos
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div class="videoScrollBar">
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/5-lps8DC6_Y?si=rkfePn9kiYKCvYUZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/49xQYLpmedE?si=fgba2iK55s1760Xr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/I_F97he5F2A?si=qKEXwDcjkX1nPejq" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/FNU08-ufByM?si=I2hq9vsapeXB2Oqb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
|
||||
<!-- Download -->
|
||||
<div id="download" class="ui text container">
|
||||
<br><br>
|
||||
<div class="centered title">
|
||||
<h1 i18n>
|
||||
Download
|
||||
// 下載
|
||||
// Herunterladen
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="downloadTabWrapper">
|
||||
<div class="ui top attached fluid stackable tabular menu">
|
||||
<a class="active item" data-tab="linux"><i class="grey linux icon"></i> Linux</a>
|
||||
<a class="item" data-tab="windows"><i class="blue windows icon"></i> Windows</a>
|
||||
<a class="item" data-tab="rpi"><i class="red raspberry pi icon"></i><span i18n>SBCs // ARM 開發板 // SBCs</span></a>
|
||||
<a class="item" data-tab="build"><i class="code icon"></i> <span i18n>Build from source // 從原始碼建置 // Aus dem Quellcode erstellen</span> </a>
|
||||
</div>
|
||||
<div class="ui bottom attached active tab segment" data-tab="linux">
|
||||
<p i18n>
|
||||
Install with command line
|
||||
// 使用 CLI 下載並執行發行版本
|
||||
// Installieren Sie mit der Befehlszeile
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
wget https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64<br>
|
||||
chmod +x ./zoraxy_linux_amd64<br>
|
||||
sudo ./zoraxy_linux_amd64
|
||||
</code>
|
||||
</div>
|
||||
<br>
|
||||
<p i18n>
|
||||
Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_amd64");'>
|
||||
<i class="black linux icon"></i>
|
||||
<span i18n>Download x64
|
||||
// 下載 64位元 執行檔
|
||||
// Herunterladen x64
|
||||
</span>
|
||||
</button>
|
||||
<span style="font-size: 1.2em; font-weight: 600; margin-right: 0.4em">OR</span>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_386");'>
|
||||
<i class="black linux icon"></i>
|
||||
<span i18n>Download x32
|
||||
// 下載 32位元 執行檔
|
||||
// Herunterladen x32
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="windows">
|
||||
<p i18n>
|
||||
Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_windows_amd64.exe");'>
|
||||
<i class="blue windows icon"></i>
|
||||
<span i18n>
|
||||
Download Zoraxy for Windows
|
||||
// 下載 Windows 版 Zoraxy
|
||||
// Zoraxy für Windows herunterladen
|
||||
</span>
|
||||
</button>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="rpi">
|
||||
<p i18n>Install with command line (armv6-7, arm64, x86)
|
||||
// 使用 CLI 下載並執行 (armv6-7, arm64, x86)
|
||||
// Installieren Sie mit der Befehlszeile (armv6-7, arm64, x86)
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
# Check your CPU architecture<br>
|
||||
uname -m <br>
|
||||
<br>
|
||||
# For arm64 (aarch64) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64<br>
|
||||
<br>
|
||||
# For armv6 (armv6l) / armv7 (armv7l) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm<br>
|
||||
<br>
|
||||
# For RISC-V (riscv64) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_riscv64<br>
|
||||
<br>
|
||||
|
||||
chmod +x ./zoraxy<br>
|
||||
sudo ./zoraxy <br>
|
||||
</code>
|
||||
</div>
|
||||
<br>
|
||||
<p i18n>Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm");'><i class="black download icon"></i> <span i18n></span>arm (v6, v7)</button>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm64");'><i class="black download icon"></i> <span i18n></span>arm64</button>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_riscv64");'><i class="grey download icon"></i> <span i18n></span>riscv64</button>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="build">
|
||||
<p i18n>Require Go (Golang) compiler. Details build from source instruction can be found on Zoraxy Github README file.
|
||||
// 需要 Go (Go 語言)編譯器。建置詳情可以在 Zoraxy Github README 檔案中找到。
|
||||
// Erfordert den Go (Golang) Compiler. Detaillierte Anweisungen zum Erstellen aus dem Quellcode finden Sie in der Zoraxy Github README-Datei.
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
git clone https://github.com/tobychui/zoraxy<br>
|
||||
cd ./zoraxy/src/<br>
|
||||
go mod tidy<br>
|
||||
go build<br>
|
||||
sudo ./zoraxy <br>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<span i18n>After Zoraxy is started, navigate to
|
||||
// 當 Zoraxy 執行檔 / 服務啟動後,使用瀏覽器開啟
|
||||
// Nachdem Zoraxy gestartet wurde, navigieren Sie zu
|
||||
</span>
|
||||
<a href="http://localhost:8000" target="_blank">http://localhost:8000</a>
|
||||
<span i18n>to continue account and system setup.
|
||||
// 以繼續帳戶和系統設定。
|
||||
// um die Konto- und Systemeinrichtung fortzusetzen.
|
||||
</span>
|
||||
</p>
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
<!-- Learn More -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="learnmore" class="ui text container">
|
||||
<div class="centered title" style="margin-bottom: 0px;">
|
||||
<h1 i18n>Learn More
|
||||
// 了解更多
|
||||
// Mehr erfahren
|
||||
</h1>
|
||||
<p i18n>If you like this project, please feel free to give us a ⭐ star ⭐.
|
||||
// 如果您喜歡這個開源專案,歡迎來給我們一顆 ⭐星星⭐ 喔!!
|
||||
// Wenn Ihnen dieses Projekt gefällt, geben Sie uns bitte einen ⭐ Stern ⭐.
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui basic segment linkicons">
|
||||
<div class="ui big breadcrumb">
|
||||
<a class="section externallink" href="https://github.com/tobychui/zoraxy" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="black github icon"></i>
|
||||
<div class="content" i18n>
|
||||
Github
|
||||
// 源碼
|
||||
// Quellcode
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class="divider"> </i>
|
||||
<a class="section externallink" href="" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="green code icon"></i>
|
||||
<div class="content" i18n>
|
||||
Plugin Devs
|
||||
// 插件開發
|
||||
// Plugin-Entwickler
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class=" divider"> </i>
|
||||
<a class="section externallink" href="mailto:toby@imuslab.com" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="yellow mail icon"></i>
|
||||
<div class="content" i18n>
|
||||
Email
|
||||
// 電子郵件
|
||||
// E-Mail
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class=" divider"> </i>
|
||||
<a class="section externallink" href="https://t.me/ArOZBeta" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="blue telegram icon"></i>
|
||||
<div class="content">
|
||||
Telegram
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div id="footer">
|
||||
<div class="ui container">
|
||||
<br><br>
|
||||
<div class="ui stackable grid" style="height: 100%;">
|
||||
<div class="six wide column" style="height: 100%;">
|
||||
<a href="https://zoraxy.aroz.org"><img src="img/logo_white.png" class="ui small image"></a>
|
||||
<p><span style="font-weight: 300;">The Zoraxy Project</span><br>
|
||||
© Toby Chui</p>
|
||||
|
||||
<div class="bottom-attach">
|
||||
<br><br>
|
||||
<div class="ui breadcrumb" style="margin-top: 0.4em;">
|
||||
<div class="section" i18n><a style="color: white;" href="https://zoraxy.aroz.org" target="_blank">zoraxy.aroz.org</a></div>
|
||||
<div class="divider"> / </div>
|
||||
<div class="section">2018 - <span class="year">now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Developer Tools
|
||||
// 開發者工具
|
||||
// Entwicklerwerkzeuge
|
||||
</div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
|
||||
<div class="item"><a href="" target="_blank">Offical Plugin List</a></div>
|
||||
<div class="item"><a href="" target="_blank">Plugin Development Guide</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Project Spin-offs
|
||||
// 衍生開源計劃
|
||||
// Projekt-Ableger
|
||||
</div>
|
||||
<div class="item"><a href="https://os.aroz.org" target="_blank">ArozOS</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Related Links
|
||||
// 相關連接
|
||||
// Verwandte Links
|
||||
</div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki/Getting-Started" target="_blank" i18n>Getting Started</a></div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Zoraxy Release</a></div>
|
||||
<div class="item"><a href="https://hub.docker.com/r/zoraxydocker/zoraxy" target="_blank">Zoraxy Docker</a></div>
|
||||
<div class="item"><a href="https://imuslab.com" target="_blank">imuslab</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(".year").text(new Date().getFullYear() );
|
||||
AOS.init();
|
||||
$(".year").text(new Date().getFullYear());
|
||||
|
||||
$(".menu-item").on("click", function(){
|
||||
$(".menu-item.active").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
});
|
||||
// Function to open the modal with the clicked image using jQuery
|
||||
function openModal(src) {
|
||||
// Remove the old modal if it exists
|
||||
$('#imageModal').remove();
|
||||
|
||||
$(".right-content").on("scroll", function() {
|
||||
var scrollPos = $(".right-content").scrollTop();
|
||||
if (scrollPos < 10){
|
||||
//Reaching the top
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item')[0]).addClass('active');
|
||||
return;
|
||||
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
|
||||
//Reaching the bottom
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item').get().reverse()[0]).addClass('active');
|
||||
return
|
||||
const modal = $('<div style="display:none;">', { id: 'imageModal' }).css({
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: '1000'
|
||||
});
|
||||
|
||||
const img = $('<img>', { src: src }).css({
|
||||
maxWidth: '80%',
|
||||
maxHeight: '80%',
|
||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)'
|
||||
});
|
||||
|
||||
modal.append(img);
|
||||
$("body").css("overflow", "hidden");
|
||||
|
||||
modal.on('click', function() {
|
||||
modal.remove();
|
||||
$("body").css("overflow", "auto");
|
||||
});
|
||||
|
||||
$('body').append(modal);
|
||||
}
|
||||
$('.menu-item').each(function() {
|
||||
var currLink = $(this);
|
||||
var refElement = $(currLink.attr("href"));
|
||||
if (refElement.offset().top <= (window.innerHeight / 2)) {
|
||||
$('.menu-item.active').removeClass("active");
|
||||
currLink.addClass("active");
|
||||
console.log(currLink.attr("href"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add click event listener to all screenshot images using jQuery
|
||||
$('.screenshot').on('click', function() {
|
||||
openModal($(this).attr('src'));
|
||||
});
|
||||
</script>
|
||||
<!-- Locales -->
|
||||
<script src="main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
386
docs/index_legacy.html
Normal file
386
docs/index_legacy.html
Normal file
@ -0,0 +1,386 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
|
||||
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
|
||||
<meta name="author" content="tobychui">
|
||||
|
||||
<!-- HTML Meta Tags -->
|
||||
<title>Reverse Proxy Server | Zoraxy</title>
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
|
||||
<!-- Main Stylesheet File -->
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.7.0.min.js"
|
||||
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif !important;
|
||||
color: #404040;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main section">
|
||||
<div class="left-menu">
|
||||
<div class="iconWrapper">
|
||||
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
|
||||
</div>
|
||||
<a href="#home" class="menu-item active" align="center">
|
||||
<img src="img/icons/home.svg">
|
||||
</a>
|
||||
<a href="#features" class="menu-item" align="center">
|
||||
<img src="img/icons/awesome.svg">
|
||||
</a>
|
||||
<a href="#screenshots" class="menu-item" align="center">
|
||||
<img src="img/icons/screenshots.svg">
|
||||
</a>
|
||||
<a href="#plugins" class="menu-item" align="center">
|
||||
<img src="img/icons/plugin.svg">
|
||||
</a>
|
||||
<a href="#source" class="menu-item" align="center">
|
||||
<img src="img/icons/code.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
<br><br>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
<tr><th colspan="2">Quick Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui download icon"></i>
|
||||
<div class="content">
|
||||
Download
|
||||
<div class="sub header">Prebuild Binary
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui github icon"></i>
|
||||
<div class="content">
|
||||
Github
|
||||
<div class="sub header">Source Code
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div id="features" class="section">
|
||||
<div class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/awesome.svg">
|
||||
<div class="content">
|
||||
Features
|
||||
<div class="sub header">Highlighting a few important features of Zoraxy</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui stackable grid featureList">
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/proxy.svg">
|
||||
<div class="content">
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/redirect.svg">
|
||||
<div class="content">
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/blacklist.svg">
|
||||
<div class="content">
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/gan.svg">
|
||||
<div class="content">
|
||||
Global Area Network
|
||||
</div>
|
||||
</h3>
|
||||
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Row 2-->
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/terminal.svg">
|
||||
<div class="content">
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/stats.svg">
|
||||
<div class="content">
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/scan.svg">
|
||||
<div class="content">
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screenshots -->
|
||||
<div id="screenshots" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/screenshots.svg">
|
||||
<div class="content">
|
||||
Screenshots
|
||||
<div class="sub header">A quick overview of the UI designs</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Developments -->
|
||||
<div id="plugins" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/plugin.svg">
|
||||
<div class="content">
|
||||
Plugins
|
||||
<div class="sub header">Add custom routing rules via simple scripts</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div style="width: 100%; text-align: center;">
|
||||
<br>
|
||||
<p>Documentation work in progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div id="source" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Source Code
|
||||
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="grey github icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="https://github.com/tobychui/zoraxy">
|
||||
Github
|
||||
<div class="sub header">https://github.com/tobychui/zoraxy</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="blue mail icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="mailto:toby@imuslab.com">
|
||||
Email Contact
|
||||
<div class="sub header">toby@imuslab.com</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<script>
|
||||
$(".year").text(new Date().getFullYear() );
|
||||
|
||||
$(".menu-item").on("click", function(){
|
||||
$(".menu-item.active").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
});
|
||||
|
||||
$(".right-content").on("scroll", function() {
|
||||
var scrollPos = $(".right-content").scrollTop();
|
||||
if (scrollPos < 10){
|
||||
//Reaching the top
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item')[0]).addClass('active');
|
||||
return;
|
||||
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
|
||||
//Reaching the bottom
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item').get().reverse()[0]).addClass('active');
|
||||
return
|
||||
}
|
||||
$('.menu-item').each(function() {
|
||||
var currLink = $(this);
|
||||
var refElement = $(currLink.attr("href"));
|
||||
if (refElement.offset().top <= (window.innerHeight / 2)) {
|
||||
$('.menu-item.active').removeClass("active");
|
||||
currLink.addClass("active");
|
||||
console.log(currLink.attr("href"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
462
docs/main.css
Normal file
462
docs/main.css
Normal file
@ -0,0 +1,462 @@
|
||||
/* Global */
|
||||
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
}
|
||||
|
||||
body.en *:not(i){
|
||||
font-family: 'Source Sans Pro', 'Noto Sans TC',sans-serif !important;
|
||||
}
|
||||
|
||||
body.zh *:not(i){
|
||||
font-family: 'Noto Sans TC',sans-serif !important;
|
||||
}
|
||||
|
||||
body.jp *:not(i){
|
||||
font-family: "Noto Sans JP", sans-serif !important;
|
||||
}
|
||||
|
||||
body.zh-cn *:not(i){
|
||||
font-family: 'Noto Sans SC',sans-serif !important;
|
||||
}
|
||||
|
||||
|
||||
.centered.title{
|
||||
padding: 2em;
|
||||
margin-bottom: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.centered.title h1{
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
|
||||
.messageBanner{
|
||||
width: 100%;
|
||||
background: #6cacff;
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
.messageBanner .header{
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#backToTopBtn{
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
display:none;
|
||||
z-index: 999;
|
||||
border: 1px solid white;
|
||||
background: #6cacff;
|
||||
}
|
||||
|
||||
#backToTopBtn:hover{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#backToTopBtn i{
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Menu */
|
||||
#mainmenu{
|
||||
padding-top: 0.4em;
|
||||
padding-bottom: 0.4em;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#slideshowBanner .ui.basic.white.button{
|
||||
color: white;
|
||||
box-shadow: 0 0 0 1px rgb(231, 231, 231) inset;
|
||||
border-radius: 0.4em;
|
||||
background: none !important;
|
||||
}
|
||||
#slideshowBanner .ui.basic.white.button:hover{
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
#slideshowBanner .ui.basic.white.button:active{
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
#rwdmenubtn{
|
||||
display:none;
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #6cacff;
|
||||
color: #6cacff;
|
||||
}
|
||||
|
||||
#mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled){
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom ease-in-out 0.1s;
|
||||
color: white !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#mainmenu #mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled):hover{
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid #82adfc;
|
||||
color: #82adfc !important;
|
||||
}
|
||||
|
||||
/* Image Sldiers */
|
||||
#slideshowBanner{
|
||||
background: rgb(108,172,255);
|
||||
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
|
||||
position: relative;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.slideshow {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.slideshow .slides {
|
||||
display: flex;
|
||||
transition: transform 1s ease-in-out;
|
||||
opacity: 0.6;
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.slideshow .slide {
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.slideshow .slide img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slideshow .dots{
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slideshow .dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0 5px;
|
||||
background-color: #bebebe;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#slideshowBanner .title{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-left: 10%;
|
||||
transform: translateX(0%) translateY(-50%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#slideshowBanner .title h1{
|
||||
font-size: 4em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#slideshowBanner .title p{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* About Zoraxy */
|
||||
.about-text-wrapper{
|
||||
margin-top: 3em;
|
||||
}
|
||||
.about-text-wrapper p, .about-text-wrapper .list .item{
|
||||
font-weight: 300;
|
||||
}
|
||||
.about-title{
|
||||
font-size: 2.4em;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
.about-title b{
|
||||
font-weight: 800;
|
||||
}
|
||||
.about-text-wrapper .ui.list .item{
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
.about-text-wrapper .ui.list .item .icon{
|
||||
padding-top: 0.15em;
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
#features{
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
#features .screenshot{
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#features .screenshot:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Videos */
|
||||
#techspec .centered.title{
|
||||
color: white;
|
||||
}
|
||||
|
||||
#techspec p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#techspec .videoScrollBar{
|
||||
overflow-x: scroll;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
|
||||
padding-top: 2em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
.introvideo{
|
||||
display: inline-block !important;
|
||||
|
||||
}
|
||||
|
||||
.blackbanner{
|
||||
width: 100%;
|
||||
background: rgb(108,172,255);
|
||||
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
|
||||
min-height: 300px;
|
||||
|
||||
}
|
||||
|
||||
/* Download */
|
||||
.downloadButton {
|
||||
margin-top: 0.4em !important;
|
||||
}
|
||||
|
||||
.downloadTabWrapper{
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#download .ui.black.message{
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Learn More */
|
||||
#learnmore .linkicons{
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .divider{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink{
|
||||
margin-bottom: 0.6em;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink i{
|
||||
/* color: #1b1c1d; */
|
||||
font-weight: 300;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink:hover{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
#learnmore .linkicons .externallink .content{
|
||||
color: #1b1c1d;
|
||||
font-weight: 500;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
#footer{
|
||||
background: rgb(85,131,238);
|
||||
background: linear-gradient(48deg, rgba(85,131,238,1) 21%, rgba(108,172,255,1) 73%);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: rgb(209, 224, 255);
|
||||
}
|
||||
|
||||
#footer a:hover{
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#footer .bottom-attach .divider{
|
||||
color: rgb(212, 212, 212);
|
||||
}
|
||||
|
||||
#footer .ui.list .title{
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
/* RWD Rules */
|
||||
@media (max-width:960px) {
|
||||
/* Main menu */
|
||||
#mainmenu{
|
||||
display:none;
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #fdfdfd !important;
|
||||
}
|
||||
|
||||
#rwdmenubtn{
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
right: 0.4em;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Slideshows */
|
||||
.slideshow {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.slideshow .slide{
|
||||
height: 100% !important;
|
||||
min-width: none;
|
||||
}
|
||||
|
||||
.slideshow .slide img{
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#slideshowBanner .title{
|
||||
padding: 1em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips{
|
||||
margin-top: 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips img{
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#techspec .videoScrollBar{
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
|
||||
padding-top: 2em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
.introvideo {
|
||||
display: block !important;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.introvideo iframe{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#download .stackable.tabular.menu .active.item{
|
||||
background-color: rgb(243, 243, 243);
|
||||
border-width: 0;
|
||||
border-radius: 0.4em !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Waves CSS
|
||||
*/
|
||||
|
||||
#wavesWrapper{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.waves {
|
||||
position:relative;
|
||||
width: 100%;
|
||||
height:15vh;
|
||||
margin-bottom:-7px; /*Fix for safari gap*/
|
||||
min-height:100px;
|
||||
max-height:150px;
|
||||
}
|
||||
|
||||
|
||||
.parallax > use {
|
||||
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||
}
|
||||
.parallax > use:nth-child(1) {
|
||||
animation-delay: -8s;
|
||||
animation-duration: 28s;
|
||||
}
|
||||
.parallax > use:nth-child(2) {
|
||||
animation-delay: -12s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
.parallax > use:nth-child(3) {
|
||||
animation-delay: -16s;
|
||||
animation-duration: 52s;
|
||||
}
|
||||
.parallax > use:nth-child(4) {
|
||||
animation-delay: -20s;
|
||||
animation-duration: 80s;
|
||||
}
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px,0,0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(85px,0,0);
|
||||
}
|
||||
}
|
||||
/*Shrinking for mobile*/
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height:40px;
|
||||
min-height:40px;
|
||||
}
|
||||
}
|
84
docs/main.js
Normal file
84
docs/main.js
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Localization
|
||||
|
||||
To add more locales, add to the html file with // (translated text)
|
||||
after each DOM elements with attr i18n
|
||||
|
||||
And then add the language ISO key to the list below.
|
||||
*/
|
||||
let languages = ['en', 'zh', 'de'];
|
||||
|
||||
|
||||
//Bind language change dropdown events
|
||||
$(".dropdown").dropdown();
|
||||
$("#language").on("change",function(){
|
||||
let newLang = $("#language").parent().dropdown("get value");
|
||||
i18n.changeLanguage(newLang);
|
||||
$("body").attr("class", newLang);
|
||||
});
|
||||
|
||||
//Initialize the i18n dom library
|
||||
var i18n = domI18n({
|
||||
selector: '[i18n]',
|
||||
separator: ' // ',
|
||||
languages: languages,
|
||||
defaultLanguage: 'en'
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
let userLang = navigator.language || navigator.userLanguage;
|
||||
console.log("User language: " + userLang);
|
||||
userLang = userLang.split("-")[0];
|
||||
if (!languages.includes(userLang)) {
|
||||
userLang = 'en';
|
||||
}
|
||||
i18n.changeLanguage(userLang);
|
||||
$("body").attr("class", userLang);
|
||||
});
|
||||
|
||||
|
||||
/* Main Menu */
|
||||
$("#rwdmenubtn").on("click", function(){
|
||||
$("#mainmenu").slideToggle("fast");
|
||||
})
|
||||
|
||||
//Handle resize
|
||||
$(window).on("resize", function(){
|
||||
if (window.innerWidth > 960){
|
||||
$("#mainmenu").show();
|
||||
}else{
|
||||
$("#mainmenu").hide();
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
Download
|
||||
*/
|
||||
|
||||
$('.menu .item').tab();
|
||||
|
||||
//Download webpack and binary at the same time
|
||||
function handleDownload(releasename){
|
||||
let binaryURL = "https://github.com/tobychui/zoraxy/releases/latest/download/" + releasename;
|
||||
window.open(binaryURL);
|
||||
}
|
||||
|
||||
/* RWD */
|
||||
window.addEventListener('scroll', function() {
|
||||
var scrollPosition = window.scrollY || window.pageYOffset;
|
||||
var windowHeight = window.innerHeight;
|
||||
var hiddenDiv = document.querySelector('#backToTopBtn');
|
||||
|
||||
if (scrollPosition > windowHeight / 2) {
|
||||
hiddenDiv.style.display = 'block';
|
||||
} else {
|
||||
hiddenDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function backToTop(){
|
||||
$('html, body').animate({scrollTop : 0},800, function(){
|
||||
window.location.hash = "";
|
||||
});
|
||||
}
|
@ -15,4 +15,12 @@ The templates folder contains the template for overriding the build in error or
|
||||
|
||||
To use the template, copy and paste the `wwww` folder to the same directory as zoraxy executable (aka the src/ file if you `go build` with the current folder tree).
|
||||
|
||||
|
||||
|
||||
### Other Templates
|
||||
|
||||
There are a few pre-built templates that works with Zoraxy where you can find in the `other-templates` folder. Copy the folder into `www` and rename the folder to `templates` to active them.
|
||||
|
||||
|
||||
|
||||
It is worth mentioning that the uwu icons for not-found and access-denied are created by @SAWARATSUKI
|
||||
|
185
example/other-templates/templates_cf/blacklist.html
Normal file
185
example/other-templates/templates_cf/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
154
example/other-templates/templates_cf/notfound.html
Normal file
154
example/other-templates/templates_cf/notfound.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<link rel="icon" type="image/png" href="img/small_icon.png"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
|
||||
<title>404 - Host Not Found</title>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
font-weight: 300;
|
||||
color: rgb(88, 88, 88)
|
||||
}
|
||||
|
||||
.diagram{
|
||||
background-color: #ebebeb;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.diagramHeader{
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
@media (max-width:512px) {
|
||||
.widescreenOnly{
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
|
||||
.four.wide.column:not(.widescreenOnly){
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.ui.grid{
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<h1 style="font-size: 4rem;">Error 404</h1>
|
||||
<p style="font-size: 2rem; margin-bottom: 0.4em;">Target Host Not Found</p>
|
||||
<small id="timestamp"></small>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<div class="ui text container">
|
||||
<div class="ui grid">
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
|
||||
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
|
||||
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
|
||||
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
|
||||
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
|
||||
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
|
||||
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
|
||||
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
|
||||
82.989,143.204 "/>
|
||||
</svg>
|
||||
<small>You</small>
|
||||
<h2 class="diagramHeader">Browser</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
|
||||
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
|
||||
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
|
||||
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
|
||||
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
|
||||
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
|
||||
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
|
||||
83.971,148.123 "/>
|
||||
</svg>
|
||||
|
||||
<small>Gateway Node</small>
|
||||
<h2 class="diagramHeader">Reverse Proxy</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column" align="center">
|
||||
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
|
||||
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
|
||||
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
|
||||
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
|
||||
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
|
||||
</svg>
|
||||
<small id="host"></small>
|
||||
<h2 class="diagramHeader">Host</h2>
|
||||
<p style="font-weight: 500; color: #bd2426;">Not Found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<h1>What happend?</h1>
|
||||
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<h1>What can I do?</h1>
|
||||
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
|
||||
<p>Please try again in a few minutes</p>
|
||||
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Check if the proxy rules that match this hostname exists</div>
|
||||
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui container" style="color: grey; font-size: 90%">
|
||||
<p>Powered by Zoraxy</p>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
$("#timestamp").text(new Date());
|
||||
$("#host").text(location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
185
example/other-templates/templates_cf/whitelist.html
Normal file
185
example/other-templates/templates_cf/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
52
example/other-templates/templates_uwu/blacklist.html
Normal file
52
example/other-templates/templates_uwu/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
42
example/other-templates/templates_uwu/notfound.html
Normal file
42
example/other-templates/templates_uwu/notfound.html
Normal file
File diff suppressed because one or more lines are too long
52
example/other-templates/templates_uwu/whitelist.html
Normal file
52
example/other-templates/templates_uwu/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
32
example/plugins/build_all.sh
Normal file
32
example/plugins/build_all.sh
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# This script builds all the plugins in the current directory
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods"
|
||||
for dir in ./*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r ../mod/plugins/zoraxy_plugin "$dir/mod"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# Iterate over all directories in the current directory
|
||||
echo "Running go mod tidy and go build for all directories"
|
||||
for dir in */; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Processing directory: $dir"
|
||||
cd "$dir"
|
||||
|
||||
# Execute go mod tidy
|
||||
echo "Running go mod tidy in $dir"
|
||||
go mod tidy
|
||||
|
||||
# Execute go build
|
||||
echo "Running go build in $dir"
|
||||
go build
|
||||
|
||||
# Return to the parent directory
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Build process completed for all directories."
|
3
example/plugins/debugger/go.mod
Normal file
3
example/plugins/debugger/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/debugger
|
||||
|
||||
go 1.23.6
|
140
example/plugins/debugger/main.go
Normal file
140
example/plugins/debugger/main.go
Normal file
@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.debugger"
|
||||
UI_PATH = "/debug"
|
||||
STATIC_CAPTURE_INGRESS = "/s_capture"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Serve the plugin intro spect
|
||||
// This will print the plugin intro spect and exit if the -introspect flag is provided
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "org.aroz.zoraxy.debugger",
|
||||
Name: "Plugin Debugger",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://aroz.org",
|
||||
Description: "A debugger for Zoraxy <-> plugin communication pipeline",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{
|
||||
CapturePath: "/test_a",
|
||||
},
|
||||
{
|
||||
CapturePath: "/test_b",
|
||||
},
|
||||
},
|
||||
StaticCaptureIngress: "/s_capture",
|
||||
|
||||
DynamicCaptureSniff: "/d_sniff",
|
||||
DynamicCaptureIngress: "/d_capture",
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/*
|
||||
SubscriptionPath: "/subept",
|
||||
SubscriptionsEvents: []plugin.SubscriptionEvent{
|
||||
*/
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Setup the path router
|
||||
pathRouter := plugin.NewPathRouter()
|
||||
pathRouter.SetDebugPrintMode(true)
|
||||
|
||||
/*
|
||||
Static Routers
|
||||
*/
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//In theory this should never be called
|
||||
//but just in case the request is not captured by the path handlers
|
||||
//this will be the fallback handler
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
|
||||
/*
|
||||
Dynamic Captures
|
||||
*/
|
||||
pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
|
||||
//fmt.Println("Dynamic Capture Sniffed Request:")
|
||||
//fmt.Println("Request URI: " + dsfr.RequestURI)
|
||||
|
||||
//In this example, we want to capture all URI
|
||||
//that start with /test_ and forward it to the dynamic capture handler
|
||||
if strings.HasPrefix(dsfr.RequestURI, "/test_") {
|
||||
reqUUID := dsfr.GetRequestUUID()
|
||||
fmt.Println("Accepting request with UUID: " + reqUUID)
|
||||
return plugin.SniffResultAccpet
|
||||
}
|
||||
|
||||
return plugin.SniffResultSkip
|
||||
})
|
||||
pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is the dynamic capture handler where it actually captures and handle the request
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Welcome to the dynamic capture handler!"))
|
||||
|
||||
// Print all the request info to the response writer
|
||||
w.Write([]byte("\n\nRequest Info:\n"))
|
||||
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
|
||||
w.Write([]byte("Request Method: " + r.Method + "\n"))
|
||||
w.Write([]byte("Request Headers:\n"))
|
||||
headers := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, key := range headers {
|
||||
for _, value := range r.Header[key] {
|
||||
w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
|
||||
fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
}
|
||||
|
||||
// Handle the captured request
|
||||
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
/*for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}*/
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
/*for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}*/
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
19
example/plugins/debugger/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/debugger/mod/zoraxy_plugin/README.txt
Normal file
@ -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
|
145
example/plugins/debugger/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/debugger/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -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())
|
||||
}
|
162
example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type 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 == SniffResultAccpet {
|
||||
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
|
||||
}
|
156
example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go
Normal file
156
example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
105
example/plugins/debugger/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/debugger/mod/zoraxy_plugin/static_router.go
Normal file
@ -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)
|
||||
|
||||
}
|
||||
}
|
176
example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
176
example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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, see Zoraxy documentation for more details
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
//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()
|
||||
}
|
26
example/plugins/debugger/ui_info.go
Normal file
26
example/plugins/debugger/ui_info.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Render the debug UI
|
||||
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
|
||||
|
||||
headerKeys := make([]string, 0, len(r.Header))
|
||||
for name := range r.Header {
|
||||
headerKeys = append(headerKeys, name)
|
||||
}
|
||||
sort.Strings(headerKeys)
|
||||
for _, name := range headerKeys {
|
||||
values := r.Header[name]
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(w, "%s: %s\n", name, value)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
}
|
3
example/plugins/helloworld/go.mod
Normal file
3
example/plugins/helloworld/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module example.com/zoraxy/helloworld
|
||||
|
||||
go 1.23.6
|
BIN
example/plugins/helloworld/icon.png
Normal file
BIN
example/plugins/helloworld/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
63
example/plugins/helloworld/main.go
Normal file
63
example/plugins/helloworld/main.go
Normal file
@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "com.example.helloworld"
|
||||
UI_PATH = "/"
|
||||
WEB_ROOT = "/www"
|
||||
)
|
||||
|
||||
//go:embed www/*
|
||||
var content embed.FS
|
||||
|
||||
func main() {
|
||||
// Serve the plugin intro spect
|
||||
// This will print the plugin intro spect and exit if the -introspect flag is provided
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: "com.example.helloworld",
|
||||
Name: "Hello World Plugin",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple hello world plugin",
|
||||
URL: "https://example.com",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
// As this is a utility plugin, we don't need to capture any traffic
|
||||
// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
||||
// The router will also help to handle the termination of the plugin when
|
||||
// a user wants to stop the plugin via Zoraxy Web UI
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Hello World Plugin Exited")
|
||||
}, nil)
|
||||
|
||||
// Serve the hello world page in the www folder
|
||||
http.Handle(UI_PATH, embedWebRouter.Handler())
|
||||
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
19
example/plugins/helloworld/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/helloworld/mod/zoraxy_plugin/README.txt
Normal file
@ -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
|
145
example/plugins/helloworld/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/helloworld/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -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())
|
||||
}
|
162
example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type 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 == SniffResultAccpet {
|
||||
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
|
||||
}
|
156
example/plugins/helloworld/mod/zoraxy_plugin/embed_webserver.go
Normal file
156
example/plugins/helloworld/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
105
example/plugins/helloworld/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/helloworld/mod/zoraxy_plugin/static_router.go
Normal file
@ -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)
|
||||
|
||||
}
|
||||
}
|
176
example/plugins/helloworld/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
176
example/plugins/helloworld/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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, see Zoraxy documentation for more details
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
//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()
|
||||
}
|
35
example/plugins/helloworld/www/index.html
Normal file
35
example/plugins/helloworld/www/index.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- CSRF token, if your plugin need to make POST request to backend -->
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
|
||||
<script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<script src="/script/semantic/semantic.min.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<title>Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background:none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<div style="text-align: center;">
|
||||
<h1>Hello World</h1>
|
||||
<p>Welcome to your first Zoraxy plugin</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
327
example/plugins/upnp/api.go
Normal file
327
example/plugins/upnp/api.go
Normal file
@ -0,0 +1,327 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
API Handlers
|
||||
*/
|
||||
|
||||
func handleUsableState(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
js, _ := json.Marshal(upnpRouterExists)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
//Try to probe the UPnP router again
|
||||
TryStartUPnPClient()
|
||||
if upnpRouterExists {
|
||||
SendOK(w)
|
||||
} else {
|
||||
SendErrorResponse(w, "UPnP router not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get or set the enable state of the plugin
|
||||
func handleEnableState(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
js, _ := json.Marshal(upnpRuntimeConfig.Enabled)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
enable, err := PostBool(r, "enable")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !enable {
|
||||
//Close all the port forwards if UPnP client is available
|
||||
if upnpClient != nil {
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ClosePort(record.PortNumber)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if upnpClient == nil {
|
||||
SendErrorResponse(w, "No UPnP router in network")
|
||||
return
|
||||
}
|
||||
|
||||
//Forward all the ports if UPnP client is available
|
||||
if upnpClient != nil {
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ForwardPort(record.PortNumber, record.RuleName)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upnpRuntimeConfig.Enabled = enable
|
||||
SaveRuntimeConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func handleForwardPortEdit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
oldPort, err := PostInt(r, "oldPort")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name, err := PostPara(r, "name")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
SendErrorResponse(w, "invalid port number")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the old port exists
|
||||
found := false
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == oldPort {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
SendErrorResponse(w, "editing forward rule not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Delete the old port forward
|
||||
if oldPort != port && upnpClient != nil {
|
||||
//Remove the port forward if UPnP client is available
|
||||
err = upnpClient.ClosePort(oldPort)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Remove from runtime config
|
||||
for i, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == oldPort {
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Create the new forward rule
|
||||
if upnpClient != nil {
|
||||
//Forward the port if UPnP client is available
|
||||
err = upnpClient.ForwardPort(port, name)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Add to runtime config
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
|
||||
RuleName: name,
|
||||
PortNumber: port,
|
||||
})
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a port forward
|
||||
func handleForwardPortRemove(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if upnpClient != nil {
|
||||
//Remove the port forward if UPnP client is available
|
||||
err = upnpClient.ClosePort(port)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Remove from runtime config
|
||||
for i, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == port {
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the port forward operations
|
||||
func handleForwardPort(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// List all the forwarded ports
|
||||
js, _ := json.Marshal(upnpRuntimeConfig.ForwardRules)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
//Add a new port forward
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name, err := PostPara(r, "name")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
SendErrorResponse(w, "invalid port number")
|
||||
return
|
||||
}
|
||||
|
||||
if upnpClient != nil {
|
||||
//Forward the port if UPnP client is available
|
||||
err = upnpClient.ForwardPort(port, name)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Add to runtime config
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
|
||||
RuleName: name,
|
||||
PortNumber: port,
|
||||
})
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Network Utilities
|
||||
*/
|
||||
|
||||
// Send JSON response, with an extra json header
|
||||
func SendJSONResponse(w http.ResponseWriter, json string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(json))
|
||||
}
|
||||
|
||||
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
|
||||
}
|
||||
|
||||
func SendOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
// Get GET parameter
|
||||
func GetPara(r *http.Request, key string) (string, error) {
|
||||
// Get first value from the URL query
|
||||
value := r.URL.Query().Get(key)
|
||||
if len(value) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Get GET paramter as boolean, accept 1 or true
|
||||
func GetBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := GetPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST parameter
|
||||
func PostPara(r *http.Request, key string) (string, error) {
|
||||
// Try to parse the form
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Get first value from the form
|
||||
x := r.Form.Get(key)
|
||||
if len(x) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// Get POST paramter as boolean, accept 1 or true
|
||||
func PostBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST paramter as int
|
||||
func PostInt(r *http.Request, key string) (int, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
rx, err := strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return rx, nil
|
||||
}
|
13
example/plugins/upnp/go.mod
Normal file
13
example/plugins/upnp/go.mod
Normal file
@ -0,0 +1,13 @@
|
||||
module plugins.zoraxy.aroz.org/zoraxy/upnp
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6
|
||||
|
||||
require (
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
17
example/plugins/upnp/go.sum
Normal file
17
example/plugins/upnp/go.sum
Normal file
@ -0,0 +1,17 @@
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 h1:WKij6HF8ECp9E7K0E44dew9NrRDGiNR5u4EFsXnJUx4=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
BIN
example/plugins/upnp/icon.png
Normal file
BIN
example/plugins/upnp/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
BIN
example/plugins/upnp/icon.psd
Normal file
BIN
example/plugins/upnp/icon.psd
Normal file
Binary file not shown.
194
example/plugins/upnp/main.go
Normal file
194
example/plugins/upnp/main.go
Normal file
@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"plugins.zoraxy.aroz.org/zoraxy/upnp/mod/upnpc"
|
||||
plugin "plugins.zoraxy.aroz.org/zoraxy/upnp/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.plugins.upnp"
|
||||
UI_PATH = "/ui"
|
||||
WEB_ROOT = "/www"
|
||||
CONFIG_FILE = "upnp.json"
|
||||
AUTO_RENEW_INTERVAL = 12 * 60 * 60 // 12 hours
|
||||
)
|
||||
|
||||
type PortForwardRecord struct {
|
||||
RuleName string
|
||||
PortNumber int
|
||||
}
|
||||
|
||||
type UPnPConfig struct {
|
||||
ForwardRules []*PortForwardRecord
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
//go:embed www/*
|
||||
var content embed.FS
|
||||
|
||||
// Runtime variables
|
||||
var (
|
||||
upnpRouterExists bool = false
|
||||
upnpRuntimeConfig *UPnPConfig = &UPnPConfig{
|
||||
ForwardRules: []*PortForwardRecord{},
|
||||
Enabled: false,
|
||||
}
|
||||
upnpClient *upnpc.UPnPClient = nil
|
||||
renewTickerStop chan bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
//Handle introspect
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "UPnP Forwarder",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://github.com/aroz-online",
|
||||
Description: "A UPnP Port Forwarder Plugin for Zoraxy",
|
||||
URL: "https://github.com/aroz-online",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
fmt.Println("This is a plugin for Zoraxy and should not be run standalone\n Visit zoraxy.aroz.org to download Zoraxy.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Read the configuration from file
|
||||
if _, err := os.Stat(CONFIG_FILE); os.IsNotExist(err) {
|
||||
err = os.WriteFile(CONFIG_FILE, []byte("{}"), 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
cfgBytes, err := os.ReadFile(CONFIG_FILE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Load the configuration
|
||||
err = json.Unmarshal(cfgBytes, &upnpRuntimeConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Start upnp client and auto-renew ticker
|
||||
go func() {
|
||||
TryStartUPnPClient()
|
||||
}()
|
||||
|
||||
//Serve the plugin UI
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
// For debugging, use the following line instead
|
||||
//embedWebRouter := plugin.NewPluginFileSystemUIRouter(PLUGIN_ID, "."+WEB_ROOT, UI_PATH)
|
||||
//embedWebRouter.EnableDebug = true
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
if renewTickerStop != nil {
|
||||
renewTickerStop <- true
|
||||
}
|
||||
// Do cleanup here if needed
|
||||
upnpClient.Close()
|
||||
}, nil)
|
||||
embedWebRouter.AttachHandlerToMux(nil)
|
||||
|
||||
//Serve the API
|
||||
RegisterAPIs()
|
||||
|
||||
//Start the IO server
|
||||
fmt.Println("UPnP Forwarder 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)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAPIs registers the APIs for the plugin
|
||||
func RegisterAPIs() {
|
||||
http.HandleFunc(UI_PATH+"/api/usable", handleUsableState)
|
||||
http.HandleFunc(UI_PATH+"/api/enable", handleEnableState)
|
||||
http.HandleFunc(UI_PATH+"/api/forward", handleForwardPort)
|
||||
http.HandleFunc(UI_PATH+"/api/edit", handleForwardPortEdit)
|
||||
http.HandleFunc(UI_PATH+"/api/remove", handleForwardPortRemove)
|
||||
}
|
||||
|
||||
// TryStartUPnPClient tries to start the UPnP client
|
||||
func TryStartUPnPClient() {
|
||||
if renewTickerStop != nil {
|
||||
renewTickerStop <- true
|
||||
}
|
||||
|
||||
// Create UPnP client
|
||||
upnpClient, err := upnpc.NewUPNPClient()
|
||||
if err != nil {
|
||||
upnpRouterExists = false
|
||||
upnpRuntimeConfig.Enabled = false
|
||||
fmt.Println("UPnP router not found")
|
||||
SaveRuntimeConfig()
|
||||
return
|
||||
}
|
||||
upnpRouterExists = true
|
||||
|
||||
//Check if the client is enabled by default
|
||||
if upnpRuntimeConfig.Enabled {
|
||||
// Forward all the ports
|
||||
for _, rule := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ForwardPort(rule.PortNumber, rule.RuleName)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to forward port", rule.PortNumber, ":", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the auto-renew ticker
|
||||
_, renewTickerStop = SetupAutoRenewTicker()
|
||||
}
|
||||
|
||||
// SetupAutoRenewTicker sets up a ticker for auto-renewing the port forwarding rules
|
||||
func SetupAutoRenewTicker() (*time.Ticker, chan bool) {
|
||||
ticker := time.NewTicker(AUTO_RENEW_INTERVAL * time.Second)
|
||||
closeChan := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-closeChan:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if upnpClient != nil {
|
||||
upnpClient.RenewForwardRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ticker, closeChan
|
||||
}
|
||||
|
||||
// SaveRuntimeConfig saves the runtime configuration to file
|
||||
func SaveRuntimeConfig() error {
|
||||
cfgBytes, err := json.Marshal(upnpRuntimeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(CONFIG_FILE, cfgBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
135
example/plugins/upnp/mod/upnpc/upnpc.go
Normal file
135
example/plugins/upnp/mod/upnpc/upnpc.go
Normal file
@ -0,0 +1,135 @@
|
||||
package upnpc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitlab.com/NebulousLabs/go-upnp"
|
||||
)
|
||||
|
||||
/*
|
||||
uPNP Module
|
||||
|
||||
This module handles uPNP Connections to the gateway router and create a port forward entry
|
||||
for the host system at the given port (set with -port paramter)
|
||||
*/
|
||||
|
||||
type UPnPClient struct {
|
||||
Connection *upnp.IGD //UPnP conenction object
|
||||
ExternalIP string //Storage of external IP address
|
||||
RequiredPorts []int //All the required ports will be recored
|
||||
PolicyNames sync.Map //Name for the required port nubmer
|
||||
}
|
||||
|
||||
// NewUPNPClient creates a new UPnPClient object
|
||||
func NewUPNPClient() (*UPnPClient, error) {
|
||||
//Create uPNP forwarding in the NAT router
|
||||
fmt.Println("Discovering UPnP router in Local Area Network...")
|
||||
d, err := upnp.Discover()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
// discover external IP
|
||||
ip, err := d.ExternalIP()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
//Create the final obejcts
|
||||
newUPnPObject := &UPnPClient{
|
||||
Connection: d,
|
||||
ExternalIP: ip,
|
||||
RequiredPorts: []int{},
|
||||
}
|
||||
|
||||
return newUPnPObject, nil
|
||||
}
|
||||
|
||||
// ForwardPort forwards a port to the host
|
||||
func (u *UPnPClient) ForwardPort(portNumber int, ruleName string) error {
|
||||
fmt.Println("UPnP forwarding new port: ", portNumber, "for "+ruleName+" service")
|
||||
|
||||
//Check if port already forwarded
|
||||
_, ok := u.PolicyNames.Load(portNumber)
|
||||
if ok {
|
||||
//Port already forward. Ignore this request
|
||||
return errors.New("port already forwarded")
|
||||
}
|
||||
|
||||
// forward a port
|
||||
err := u.Connection.Forward(uint16(portNumber), ruleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.RequiredPorts = append(u.RequiredPorts, portNumber)
|
||||
u.PolicyNames.Store(portNumber, ruleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosePort closes the port forwarding
|
||||
func (u *UPnPClient) ClosePort(portNumber int) error {
|
||||
//Check if port is opened
|
||||
portOpened := false
|
||||
newRequiredPort := []int{}
|
||||
for _, thisPort := range u.RequiredPorts {
|
||||
if thisPort != portNumber {
|
||||
newRequiredPort = append(newRequiredPort, thisPort)
|
||||
} else {
|
||||
portOpened = true
|
||||
}
|
||||
}
|
||||
|
||||
if portOpened {
|
||||
//Update the port list
|
||||
u.RequiredPorts = newRequiredPort
|
||||
|
||||
// Close the port
|
||||
fmt.Println("Closing UPnP Port Forward: ", portNumber)
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
|
||||
//Delete the name registry
|
||||
u.PolicyNames.Delete(portNumber)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew forward rules, prevent router lease time from flushing the Upnp config
|
||||
func (u *UPnPClient) RenewForwardRules() {
|
||||
if u.Connection == nil {
|
||||
//UPnP router gone
|
||||
return
|
||||
}
|
||||
portsToRenew := u.RequiredPorts
|
||||
for _, thisPort := range portsToRenew {
|
||||
ruleName, ok := u.PolicyNames.Load(thisPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
u.ClosePort(thisPort)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
u.ForwardPort(thisPort, ruleName.(string))
|
||||
}
|
||||
fmt.Println("UPnP Port Forward rule renew completed")
|
||||
}
|
||||
|
||||
func (u *UPnPClient) Close() error {
|
||||
//Shutdown the default UPnP Object
|
||||
if u != nil {
|
||||
for _, portNumber := range u.RequiredPorts {
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
19
example/plugins/upnp/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/upnp/mod/zoraxy_plugin/README.txt
Normal file
@ -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
|
145
example/plugins/upnp/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/upnp/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -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())
|
||||
}
|
162
example/plugins/upnp/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/upnp/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type 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 == SniffResultAccpet {
|
||||
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
|
||||
}
|
156
example/plugins/upnp/mod/zoraxy_plugin/embed_webserver.go
Normal file
156
example/plugins/upnp/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
105
example/plugins/upnp/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/upnp/mod/zoraxy_plugin/static_router.go
Normal file
@ -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)
|
||||
|
||||
}
|
||||
}
|
176
example/plugins/upnp/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
176
example/plugins/upnp/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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, see Zoraxy documentation for more details
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
//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()
|
||||
}
|
1
example/plugins/upnp/upnp.json
Normal file
1
example/plugins/upnp/upnp.json
Normal file
@ -0,0 +1 @@
|
||||
{"ForwardRules":[],"Enabled":false}
|
302
example/plugins/upnp/www/index.html
Normal file
302
example/plugins/upnp/www/index.html
Normal file
@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- CSRF token, if your plugin need to make POST request to backend -->
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<title>UPnP Port Forwarder | Zoraxy</title>
|
||||
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
|
||||
<script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<script src="/script/semantic/semantic.min.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background:none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<div id="upnpForwarder" class="standardContainer">
|
||||
<div id="upnpRouterNotFoundWarning" class="ui basic segment" style="display: none;">
|
||||
<h2>UPnP Port Forwarder</h2>
|
||||
<p>Port forward using UPnP protocol, only works with some of the supported gateway routers</p>
|
||||
</div>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui message">
|
||||
<div class="header"><i class="yellow exclamation triangle icon"></i> UPnP Gateway Not Found</div>
|
||||
<p>No UPnP router found in network. Please ensure your router supports UPnP and is enabled.</p>
|
||||
<button id="retryBtn" onclick="searchUpnpRouter();" class="ui basic small button"><i class="green refresh icon"></i> Search again</button>
|
||||
</div>
|
||||
<div class="ui basic segment">
|
||||
<h3 class="ui header">UPnP Port Forwarder</h3>
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" id="upnpToggle" onchange="toggleUpnpState();">
|
||||
<label>Enable UPnP Forwarding</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule Name</th>
|
||||
<th>Forwarded Port</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="forwardList">
|
||||
<tr>
|
||||
<td>Example Rule</td>
|
||||
<td>8080</td>
|
||||
<td>
|
||||
<button class="ui button">Edit</button>
|
||||
<button class="ui button">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
<div>
|
||||
<h4>Port Forward Rules</h4>
|
||||
<form class="ui form" id="addRuleForm">
|
||||
<div class="field">
|
||||
<label>Rule Name</label>
|
||||
<input type="text" name="ruleName" placeholder="Rule Name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="Port" min="1" max="65535">
|
||||
</div>
|
||||
<button onclick="handleAddForward(event);" class="ui small basic button"><i class="ui blue add icon"></i> Add Rule</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
function toggleUpnpState() {
|
||||
let isChecked = $("#upnpToggle").prop("checked");
|
||||
$.cjax({
|
||||
url: './api/toggle',
|
||||
method: "POST",
|
||||
data: {
|
||||
enable: isChecked
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
// Error
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
// Success
|
||||
parent.msgbox("UPnP Forwarding " + (isChecked ? "enabled" : "disabled"), true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initUPnPEnableState(){
|
||||
$.cjax({
|
||||
url: './api/enable',
|
||||
success: function(data) {
|
||||
if (data == true){
|
||||
//Upnp forwarding enabled
|
||||
$("#upnpToggle").prop("checked", true);
|
||||
}else{
|
||||
//Upnp forwarding disabled
|
||||
$("#upnpToggle").prop("checked", false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initUPnPEnableState();
|
||||
|
||||
function searchUpnpRouter(){
|
||||
$("#retryBtn").addClass("loading");
|
||||
parent.msgbox("Searching for UPnP router (will take a few minutes)...", true);
|
||||
$.cjax({
|
||||
url: './api/usable',
|
||||
method: "POST",
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Not found
|
||||
parent.msgbox("UPnP router not found", false);
|
||||
}else{
|
||||
//Found
|
||||
parent.msgbox("UPnP router discovered", true);
|
||||
}
|
||||
initUpnpUsableState();
|
||||
$("#retryBtn").removeClass("loading");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Check if UPnP is usable
|
||||
function initUpnpUsableState(){
|
||||
$.cjax({
|
||||
url: './api/usable',
|
||||
success: function(data) {
|
||||
if (data == true){
|
||||
//Upnp router found in network, enable the page
|
||||
$('#upnpRouterNotFoundWarning').hide();
|
||||
}else{
|
||||
//No upnp router found in network, disable the page
|
||||
$('#upnpForwarder').addClass('disabled');
|
||||
$('#upnpRouterNotFoundWarning').show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddForward(event){
|
||||
event.preventDefault();
|
||||
let ruleName = $("#addRuleForm input[name='ruleName']").val();
|
||||
let port = $("#addRuleForm input[name='port']").val();
|
||||
if (ruleName == "" || port == ""){
|
||||
parent.msgbox("Please fill in all fields", false);
|
||||
return;
|
||||
}
|
||||
$.cjax({
|
||||
url: './api/forward',
|
||||
method: "POST",
|
||||
data: {
|
||||
name: ruleName,
|
||||
port: port
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule added successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
|
||||
$("#addRuleForm input[name='ruleName']").val('');
|
||||
$("#addRuleForm input[name='port']").val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveForward(portNo){
|
||||
$.cjax({
|
||||
url: './api/remove',
|
||||
method: "POST",
|
||||
data: {
|
||||
port: portNo
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule removed successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editForwardRule(row){
|
||||
let ruleName = $(row).closest('tr').find('td:eq(0)').text();
|
||||
let portNumber = $(row).closest('tr').find('td:eq(1)').text();
|
||||
$(row).closest('tr').html(`
|
||||
<td>
|
||||
<div class="ui fluid input">
|
||||
<input type="text" value="${ruleName}" onkeypress="if(event.key === 'Enter') $(this).closest('tr').find('td:eq(1) input').focus();">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui fluid input">
|
||||
<input type="number" value="${portNumber}" class="ui input" min="1" max="65535" onkeypress="if(event.key === 'Enter') saveForwardRule(this, '${portNumber}');">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="saveForwardRule(this, '${portNumber}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
|
||||
<button onclick="cancelEditForwardRule(this, '${ruleName}', '${portNumber}');" class="ui basic small circular icon button"><i class="ui cancel icon"></i></button>
|
||||
</td>
|
||||
`);
|
||||
}
|
||||
|
||||
function cancelEditForwardRule(){
|
||||
initForwardList();
|
||||
}
|
||||
|
||||
function saveForwardRule(row, portNo){
|
||||
let ruleName = $(row).closest('tr').find('td:eq(0) input').val();
|
||||
let newPortNo = $(row).closest('tr').find('td:eq(1) input').val();
|
||||
if (ruleName == "" || newPortNo == ""){
|
||||
parent.msgbox("Please fill in all fields", false);
|
||||
return;
|
||||
}
|
||||
$.cjax({
|
||||
url: './api/edit',
|
||||
method: "POST",
|
||||
data: {
|
||||
name: ruleName,
|
||||
port: newPortNo,
|
||||
oldPort: portNo
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule updated successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Load forward list
|
||||
function initForwardList(){
|
||||
$("#forwardList").html('<tr><td colspan="3"> <i class="ui loading spinner icon"></i> Loading...</tr>');
|
||||
$.cjax({
|
||||
url: './api/forward',
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
$("#forwardList").empty();
|
||||
let rows = '';
|
||||
data.forEach(row => {
|
||||
$("#forwardList").append(`
|
||||
<tr>
|
||||
<td>${row.RuleName}</td>
|
||||
<td>${row.PortNumber}</td>
|
||||
<td>
|
||||
<button onclick="editForwardRule(this);" class="ui basic small circular icon button"><i class="ui edit icon"></i></button>
|
||||
<button onclick="handleRemoveForward(${row.PortNumber});" class="ui basic small red circular icon button"><i class="ui red trash icon"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
if (data == null || data.length == 0){
|
||||
//No rules
|
||||
$("#forwardList").append(`
|
||||
<tr>
|
||||
<td colspan="3"><i class="ui green check circle icon"></i> No running port forward rules</td>
|
||||
</tr>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initUpnpUsableState();
|
||||
initForwardList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
11
example/plugins/ztnc/README.md
Normal file
11
example/plugins/ztnc/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
## Global Area Network Plugin
|
||||
|
||||
This plugin implements a user interface for ZeroTier Network Controller in Zoraxy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
AGPL
|
11
example/plugins/ztnc/go.mod
Normal file
11
example/plugins/ztnc/go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module aroz.org/zoraxy/ztnc
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/sys v0.30.0
|
||||
)
|
||||
|
||||
require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
30
example/plugins/ztnc/go.sum
Normal file
30
example/plugins/ztnc/go.sum
Normal file
@ -0,0 +1,30 @@
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
BIN
example/plugins/ztnc/icon.png
Normal file
BIN
example/plugins/ztnc/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
BIN
example/plugins/ztnc/icon.psd
Normal file
BIN
example/plugins/ztnc/icon.psd
Normal file
Binary file not shown.
83
example/plugins/ztnc/main.go
Normal file
83
example/plugins/ztnc/main.go
Normal file
@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"embed"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database"
|
||||
"aroz.org/zoraxy/ztnc/mod/ganserv"
|
||||
plugin "aroz.org/zoraxy/ztnc/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.ztnc"
|
||||
UI_RELPATH = "/ui"
|
||||
EMBED_FS_ROOT = "/web"
|
||||
DB_FILE_PATH = "ztnc.db"
|
||||
AUTH_TOKEN_PATH = "./authtoken.secret"
|
||||
)
|
||||
|
||||
//go:embed web/*
|
||||
var content embed.FS
|
||||
|
||||
var (
|
||||
sysdb *database.Database
|
||||
ganManager *ganserv.NetworkManager
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Serve the plugin intro spect
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "ztnc",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "zoraxy.aroz.org",
|
||||
Description: "UI for ZeroTier Network Controller",
|
||||
URL: "https://zoraxy.aroz.org",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
// As this is a utility plugin, we don't need to capture any traffic
|
||||
// but only serve the UI, so we set the UI (relative to the plugin path) to "/ui/" to match the HTTP Handler
|
||||
UIPath: UI_RELPATH,
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
||||
uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
|
||||
uiRouter.EnableDebug = true
|
||||
|
||||
// Register the shutdown handler
|
||||
uiRouter.RegisterTerminateHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
if sysdb != nil {
|
||||
sysdb.Close()
|
||||
}
|
||||
fmt.Println("ztnc Exited")
|
||||
}, nil)
|
||||
|
||||
// This will serve the index.html file embedded in the binary
|
||||
targetHandler := uiRouter.Handler()
|
||||
http.Handle(UI_RELPATH+"/", targetHandler)
|
||||
|
||||
// Start the GAN Network Controller
|
||||
err = startGanNetworkController()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Initiate the API endpoints
|
||||
initApiEndpoints()
|
||||
|
||||
// Start the HTTP server, only listen to loopback interface
|
||||
fmt.Println("Plugin UI server started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port) + UI_RELPATH)
|
||||
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
}
|
146
example/plugins/ztnc/mod/database/database.go
Normal file
146
example/plugins/ztnc/mod/database/database.go
Normal file
@ -0,0 +1,146 @@
|
||||
package database
|
||||
|
||||
/*
|
||||
ArOZ Online Database Access Module
|
||||
author: tobychui
|
||||
|
||||
This is an improved Object oriented base solution to the original
|
||||
aroz online database script.
|
||||
*/
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
|
||||
BackendType dbinc.BackendType
|
||||
Backend dbinc.Backend
|
||||
}
|
||||
|
||||
func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
|
||||
if runtime.GOARCH == "riscv64" {
|
||||
log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
|
||||
}
|
||||
return newDatabase(dbfile, backendType)
|
||||
}
|
||||
|
||||
// Get the recommended backend type for the current system
|
||||
func GetRecommendedBackendType() dbinc.BackendType {
|
||||
//Check if the system is running on RISCV hardware
|
||||
if runtime.GOARCH == "riscv64" {
|
||||
//RISCV hardware, currently only support FS emulated database
|
||||
return dbinc.BackendFSOnly
|
||||
} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
|
||||
//Powerful hardware
|
||||
return dbinc.BackendBoltDB
|
||||
//return dbinc.BackendLevelDB
|
||||
}
|
||||
|
||||
//Default to BoltDB, the safest option
|
||||
return dbinc.BackendBoltDB
|
||||
}
|
||||
|
||||
/*
|
||||
Create / Drop a table
|
||||
Usage:
|
||||
err := sysdb.NewTable("MyTable")
|
||||
err := sysdb.DropTable("MyTable")
|
||||
*/
|
||||
|
||||
// Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
return d.newTable(tableName)
|
||||
}
|
||||
|
||||
// Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
return d.tableExists(tableName)
|
||||
}
|
||||
|
||||
// Drop the given table
|
||||
func (d *Database) DropTable(tableName string) error {
|
||||
return d.dropTable(tableName)
|
||||
}
|
||||
|
||||
/*
|
||||
Write to database with given tablename and key. Example Usage:
|
||||
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
|
||||
thisDemo := demo{
|
||||
content: "Hello World",
|
||||
}
|
||||
|
||||
err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
*/
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
return d.write(tableName, key, value)
|
||||
}
|
||||
|
||||
/*
|
||||
Read from database and assign the content to a given datatype. Example Usage:
|
||||
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := new(demo)
|
||||
err := sysdb.Read("MyTable", "username/message",&thisDemo);
|
||||
*/
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
|
||||
return d.read(tableName, key, assignee)
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a key exists in the database table given tablename and key
|
||||
|
||||
if sysdb.KeyExists("MyTable", "username/message"){
|
||||
log.Println("Key exists")
|
||||
}
|
||||
*/
|
||||
func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
return d.keyExists(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
*/
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
return d.delete(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
//List table example usage
|
||||
//Assume the value is stored as a struct named "groupstruct"
|
||||
|
||||
entries, err := sysdb.ListTable("test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, keypairs := range entries{
|
||||
log.Println(string(keypairs[0]))
|
||||
group := new(groupstruct)
|
||||
json.Unmarshal(keypairs[1], &group)
|
||||
log.Println(group);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
return d.listTable(tableName)
|
||||
}
|
||||
|
||||
/*
|
||||
Close the database connection
|
||||
*/
|
||||
func (d *Database) Close() {
|
||||
d.close()
|
||||
}
|
70
example/plugins/ztnc/mod/database/database_core.go
Normal file
70
example/plugins/ztnc/mod/database/database_core.go
Normal file
@ -0,0 +1,70 @@
|
||||
//go:build !mipsle && !riscv64
|
||||
// +build !mipsle,!riscv64
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbbolt"
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
|
||||
)
|
||||
|
||||
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
|
||||
if backendType == dbinc.BackendFSOnly {
|
||||
return nil, errors.New("Unsupported backend type for this platform")
|
||||
}
|
||||
|
||||
if backendType == dbinc.BackendLevelDB {
|
||||
db, err := dbleveldb.NewDB(dbfile)
|
||||
return &Database{
|
||||
Db: nil,
|
||||
BackendType: backendType,
|
||||
Backend: db,
|
||||
}, err
|
||||
}
|
||||
|
||||
db, err := dbbolt.NewBoltDatabase(dbfile)
|
||||
return &Database{
|
||||
Db: nil,
|
||||
BackendType: backendType,
|
||||
Backend: db,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
return d.Backend.NewTable(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
return d.Backend.TableExists(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
return d.Backend.DropTable(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
return d.Backend.Write(tableName, key, value)
|
||||
}
|
||||
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
return d.Backend.Read(tableName, key, assignee)
|
||||
}
|
||||
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
return d.Backend.KeyExists(tableName, key)
|
||||
}
|
||||
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
return d.Backend.Delete(tableName, key)
|
||||
}
|
||||
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
return d.Backend.ListTable(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) close() {
|
||||
d.Backend.Close()
|
||||
}
|
196
example/plugins/ztnc/mod/database/database_openwrt.go
Normal file
196
example/plugins/ztnc/mod/database/database_openwrt.go
Normal file
@ -0,0 +1,196 @@
|
||||
//go:build mipsle || riscv64
|
||||
// +build mipsle riscv64
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
|
||||
)
|
||||
|
||||
/*
|
||||
OpenWRT or RISCV backend
|
||||
|
||||
For OpenWRT or RISCV platform, we will use the filesystem as the database backend
|
||||
as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
|
||||
in conditional compilation will create a build error on these platforms
|
||||
*/
|
||||
|
||||
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
|
||||
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
|
||||
dbRootPath = "fsdb/" + dbRootPath
|
||||
err := os.MkdirAll(dbRootPath, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
|
||||
return &Database{
|
||||
Db: dbRootPath,
|
||||
BackendType: dbinc.BackendFSOnly,
|
||||
Backend: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) dump(filename string) ([]string, error) {
|
||||
//Get all file objects from root
|
||||
rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*"))
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
//Filter out the folders
|
||||
rootFolders := []string{}
|
||||
for _, file := range rootfiles {
|
||||
if !isDirectory(file) {
|
||||
rootFolders = append(rootFolders, filepath.Base(file))
|
||||
}
|
||||
}
|
||||
|
||||
return rootFolders, nil
|
||||
}
|
||||
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if !fileExists(tablePath) {
|
||||
return os.MkdirAll(tablePath, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isDirectory(tablePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if d.tableExists(tableName) {
|
||||
return os.RemoveAll(tablePath)
|
||||
} else {
|
||||
return errors.New("table not exists")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
js, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755)
|
||||
}
|
||||
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
content, err := os.ReadFile(entryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &assignee)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
return fileExists(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
|
||||
return os.Remove(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
if !d.tableExists(tableName) {
|
||||
return [][][]byte{}, errors.New("table not exists")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry"))
|
||||
if err != nil {
|
||||
return [][][]byte{}, err
|
||||
}
|
||||
|
||||
var results [][][]byte = [][][]byte{}
|
||||
for _, entry := range entries {
|
||||
if !isDirectory(entry) {
|
||||
//Read it
|
||||
key := filepath.Base(entry)
|
||||
key = strings.TrimSuffix(key, filepath.Ext(key))
|
||||
key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/")
|
||||
|
||||
bkey := []byte(key)
|
||||
bval := []byte("")
|
||||
c, err := os.ReadFile(entry)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
bval = c
|
||||
results = append(results, [][]byte{bkey, bval})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) close() {
|
||||
//Nothing to close as it is file system
|
||||
}
|
||||
|
||||
func isDirectory(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
|
||||
func fileExists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
141
example/plugins/ztnc/mod/database/dbbolt/dbbolt.go
Normal file
141
example/plugins/ztnc/mod/database/dbbolt/dbbolt.go
Normal file
@ -0,0 +1,141 @@
|
||||
package dbbolt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db interface{} //This is the bolt database object
|
||||
}
|
||||
|
||||
func NewBoltDatabase(dbfile string) (*Database, error) {
|
||||
db, err := bolt.Open(dbfile, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Database{
|
||||
Db: db,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
if b == nil {
|
||||
return errors.New("table not exists")
|
||||
}
|
||||
return nil
|
||||
}) == nil
|
||||
}
|
||||
|
||||
// Drop the given table
|
||||
func (d *Database) DropTable(tableName string) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to table
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
jsonString, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
err = b.Put([]byte(key), jsonString)
|
||||
return err
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
json.Unmarshal(v, &assignee)
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
resultIsNil := false
|
||||
if !d.TableExists(tableName) {
|
||||
//Table not exists. Do not proceed accessing key
|
||||
//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
|
||||
return false
|
||||
}
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
if v == nil {
|
||||
resultIsNil = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
} else {
|
||||
if resultIsNil {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
tx.Bucket([]byte(tableName)).Delete([]byte(key))
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
var results [][][]byte
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
c := b.Cursor()
|
||||
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
results = append(results, [][]byte{k, v})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (d *Database) Close() {
|
||||
d.Db.(*bolt.DB).Close()
|
||||
}
|
67
example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go
Normal file
67
example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package dbbolt_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbbolt"
|
||||
)
|
||||
|
||||
func TestNewBoltDatabase(t *testing.T) {
|
||||
dbfile := "test.db"
|
||||
defer os.Remove(dbfile)
|
||||
|
||||
db, err := dbbolt.NewBoltDatabase(dbfile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new Bolt database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if db.Db == nil {
|
||||
t.Fatalf("Expected non-nil database object")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTable(t *testing.T) {
|
||||
dbfile := "test.db"
|
||||
defer os.Remove(dbfile)
|
||||
|
||||
db, err := dbbolt.NewBoltDatabase(dbfile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new Bolt database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.NewTable("testTable")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new table: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableExists(t *testing.T) {
|
||||
dbfile := "test.db"
|
||||
defer os.Remove(dbfile)
|
||||
|
||||
db, err := dbbolt.NewBoltDatabase(dbfile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new Bolt database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tableName := "testTable"
|
||||
err = db.NewTable(tableName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new table: %v", err)
|
||||
}
|
||||
|
||||
exists := db.TableExists(tableName)
|
||||
if !exists {
|
||||
t.Fatalf("Expected table %s to exist", tableName)
|
||||
}
|
||||
|
||||
nonExistentTable := "nonExistentTable"
|
||||
exists = db.TableExists(nonExistentTable)
|
||||
if exists {
|
||||
t.Fatalf("Expected table %s to not exist", nonExistentTable)
|
||||
}
|
||||
}
|
39
example/plugins/ztnc/mod/database/dbinc/dbinc.go
Normal file
39
example/plugins/ztnc/mod/database/dbinc/dbinc.go
Normal file
@ -0,0 +1,39 @@
|
||||
package dbinc
|
||||
|
||||
/*
|
||||
dbinc is the interface for all database backend
|
||||
*/
|
||||
type BackendType int
|
||||
|
||||
const (
|
||||
BackendBoltDB BackendType = iota //Default backend
|
||||
BackendFSOnly //OpenWRT or RISCV backend
|
||||
BackendLevelDB //LevelDB backend
|
||||
|
||||
BackEndAuto = BackendBoltDB
|
||||
)
|
||||
|
||||
type Backend interface {
|
||||
NewTable(tableName string) error
|
||||
TableExists(tableName string) bool
|
||||
DropTable(tableName string) error
|
||||
Write(tableName string, key string, value interface{}) error
|
||||
Read(tableName string, key string, assignee interface{}) error
|
||||
KeyExists(tableName string, key string) bool
|
||||
Delete(tableName string, key string) error
|
||||
ListTable(tableName string) ([][][]byte, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
func (b BackendType) String() string {
|
||||
switch b {
|
||||
case BackendBoltDB:
|
||||
return "BoltDB"
|
||||
case BackendFSOnly:
|
||||
return "File System Emulated Key-Value Store"
|
||||
case BackendLevelDB:
|
||||
return "LevelDB"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
152
example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go
Normal file
152
example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go
Normal file
@ -0,0 +1,152 @@
|
||||
package dbleveldb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// Ensure the DB struct implements the Backend interface
|
||||
var _ dbinc.Backend = (*DB)(nil)
|
||||
|
||||
type DB struct {
|
||||
db *leveldb.DB
|
||||
Table sync.Map //For emulating table creation
|
||||
batch leveldb.Batch //Batch write
|
||||
writeFlushTicker *time.Ticker //Ticker for flushing data into disk
|
||||
writeFlushStop chan bool //Stop channel for write flush ticker
|
||||
}
|
||||
|
||||
func NewDB(path string) (*DB, error) {
|
||||
//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
|
||||
if filepath.Ext(path) != "" {
|
||||
path = strings.ReplaceAll(path, ".", "_")
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thisDB := &DB{
|
||||
db: db,
|
||||
Table: sync.Map{},
|
||||
batch: leveldb.Batch{},
|
||||
}
|
||||
|
||||
//Create a ticker to flush data into disk every 1 seconds
|
||||
writeFlushTicker := time.NewTicker(1 * time.Second)
|
||||
writeFlushStop := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-writeFlushTicker.C:
|
||||
if thisDB.batch.Len() == 0 {
|
||||
//No flushing needed
|
||||
continue
|
||||
}
|
||||
err = db.Write(&thisDB.batch, nil)
|
||||
if err != nil {
|
||||
log.Println("[LevelDB] Failed to flush data into disk: ", err)
|
||||
}
|
||||
thisDB.batch.Reset()
|
||||
case <-writeFlushStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
thisDB.writeFlushTicker = writeFlushTicker
|
||||
thisDB.writeFlushStop = writeFlushStop
|
||||
|
||||
return thisDB, nil
|
||||
}
|
||||
|
||||
func (d *DB) NewTable(tableName string) error {
|
||||
//Create a table entry in the sync.Map
|
||||
d.Table.Store(tableName, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) TableExists(tableName string) bool {
|
||||
_, ok := d.Table.Load(tableName)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *DB) DropTable(tableName string) error {
|
||||
d.Table.Delete(tableName)
|
||||
iter := d.db.NewIterator(nil, nil)
|
||||
defer iter.Release()
|
||||
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
if filepath.Dir(string(key)) == tableName {
|
||||
err := d.db.Delete(key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) Write(tableName string, key string, value interface{}) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) Read(tableName string, key string, assignee interface{}) error {
|
||||
data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, assignee)
|
||||
}
|
||||
|
||||
func (d *DB) KeyExists(tableName string, key string) bool {
|
||||
_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *DB) Delete(tableName string, key string) error {
|
||||
return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
|
||||
}
|
||||
|
||||
func (d *DB) ListTable(tableName string) ([][][]byte, error) {
|
||||
iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
|
||||
defer iter.Release()
|
||||
|
||||
var result [][][]byte
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
//The key contains the table name as prefix. Trim it before returning
|
||||
value := iter.Value()
|
||||
result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
|
||||
}
|
||||
|
||||
err := iter.Error()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() {
|
||||
//Write the remaining data in batch back into disk
|
||||
d.writeFlushStop <- true
|
||||
d.writeFlushTicker.Stop()
|
||||
d.db.Write(&d.batch, nil)
|
||||
d.db.Close()
|
||||
}
|
141
example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go
Normal file
141
example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
package dbleveldb_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
|
||||
)
|
||||
|
||||
func TestNewDB(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
}
|
||||
|
||||
func TestNewTable(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.NewTable("testTable")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new table: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableExists(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
db.NewTable("testTable")
|
||||
if !db.TableExists("testTable") {
|
||||
t.Fatalf("Table should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropTable(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
db.NewTable("testTable")
|
||||
err = db.DropTable("testTable")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to drop table: %v", err)
|
||||
}
|
||||
|
||||
if db.TableExists("testTable") {
|
||||
t.Fatalf("Table should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAndRead(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
db.NewTable("testTable")
|
||||
err = db.Write("testTable", "testKey", "testValue")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to table: %v", err)
|
||||
}
|
||||
|
||||
var value string
|
||||
err = db.Read("testTable", "testKey", &value)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read from table: %v", err)
|
||||
}
|
||||
|
||||
if value != "testValue" {
|
||||
t.Fatalf("Expected 'testValue', got '%v'", value)
|
||||
}
|
||||
}
|
||||
func TestListTable(t *testing.T) {
|
||||
path := "/tmp/testdb"
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
db, err := dbleveldb.NewDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new DB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
db.NewTable("testTable")
|
||||
err = db.Write("testTable", "testKey1", "testValue1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to table: %v", err)
|
||||
}
|
||||
err = db.Write("testTable", "testKey2", "testValue2")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to table: %v", err)
|
||||
}
|
||||
|
||||
result, err := db.ListTable("testTable")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list table: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("Expected 2 entries, got %v", len(result))
|
||||
}
|
||||
|
||||
expected := map[string]string{
|
||||
"testTable/testKey1": "\"testValue1\"",
|
||||
"testTable/testKey2": "\"testValue2\"",
|
||||
}
|
||||
|
||||
for _, entry := range result {
|
||||
key := string(entry[0])
|
||||
value := string(entry[1])
|
||||
if expected[key] != value {
|
||||
t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
)
|
||||
|
||||
func readAuthTokenAsAdmin() (string, error) {
|
@ -5,15 +5,13 @@ package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
"golang.org/x/sys/windows"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Use admin permission to read auth token on Windows
|
||||
@ -46,15 +44,6 @@ func readAuthTokenAsAdmin() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
|
||||
retry := 0
|
||||
time.Sleep(3 * time.Second)
|
||||
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
|
||||
time.Sleep(3 * time.Second)
|
||||
log.Println("Waiting for ZeroTier authtoken extraction...")
|
||||
retry++
|
||||
}
|
||||
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err != nil {
|
||||
return "", err
|
@ -1,9 +1,10 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"aroz.org/zoraxy/ztnc/mod/database"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
||||
//Get controller info
|
||||
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
||||
if err != nil {
|
||||
log.Println("ZeroTier connection failed: ", err.Error())
|
||||
return &NetworkManager{
|
||||
authToken: option.AuthToken,
|
||||
apiPort: option.ApiPort,
|
@ -7,7 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
)
|
||||
|
||||
func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {
|
@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"aroz.org/zoraxy/ztnc/mod/ganserv"
|
||||
)
|
||||
|
||||
func TestGetRandomFreeIP(t *testing.T) {
|
@ -28,11 +28,17 @@ type NodeInfo struct {
|
||||
Clock int64 `json:"clock"`
|
||||
Config struct {
|
||||
Settings struct {
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
||||
PrimaryPort int `json:"primaryPort"`
|
||||
SoftwareUpdate string `json:"softwareUpdate"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
|
||||
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
|
||||
HomeDir string `json:"homeDir,omitempty"`
|
||||
ListeningOn []string `json:"listeningOn,omitempty"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
|
||||
PrimaryPort int `json:"primaryPort,omitempty"`
|
||||
SecondaryPort int `json:"secondaryPort,omitempty"`
|
||||
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
|
||||
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
|
||||
TertiaryPort int `json:"tertiaryPort,omitempty"`
|
||||
} `json:"settings"`
|
||||
} `json:"config"`
|
||||
Online bool `json:"online"`
|
||||
@ -46,7 +52,6 @@ type NodeInfo struct {
|
||||
VersionMinor int `json:"versionMinor"`
|
||||
VersionRev int `json:"versionRev"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
}
|
105
example/plugins/ztnc/mod/utils/conv.go
Normal file
105
example/plugins/ztnc/mod/utils/conv.go
Normal file
@ -0,0 +1,105 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func StringToInt64(number string) (int64, error) {
|
||||
i, err := strconv.ParseInt(number, 10, 64)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func Int64ToString(number int64) string {
|
||||
convedNumber := strconv.FormatInt(number, 10)
|
||||
return convedNumber
|
||||
}
|
||||
|
||||
func ReplaceSpecialCharacters(filename string) string {
|
||||
replacements := map[string]string{
|
||||
"#": "%pound%",
|
||||
"&": "%amp%",
|
||||
"{": "%left_cur%",
|
||||
"}": "%right_cur%",
|
||||
"\\": "%backslash%",
|
||||
"<": "%left_ang%",
|
||||
">": "%right_ang%",
|
||||
"*": "%aster%",
|
||||
"?": "%quest%",
|
||||
" ": "%space%",
|
||||
"$": "%dollar%",
|
||||
"!": "%exclan%",
|
||||
"'": "%sin_q%",
|
||||
"\"": "%dou_q%",
|
||||
":": "%colon%",
|
||||
"@": "%at%",
|
||||
"+": "%plus%",
|
||||
"`": "%backtick%",
|
||||
"|": "%pipe%",
|
||||
"=": "%equal%",
|
||||
".": "_",
|
||||
"/": "-",
|
||||
}
|
||||
|
||||
for char, replacement := range replacements {
|
||||
filename = strings.ReplaceAll(filename, char, replacement)
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
/* Zip File Handler */
|
||||
// zipFiles compresses multiple files into a single zip archive file
|
||||
func ZipFiles(filename string, files ...string) error {
|
||||
newZipFile, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, file := range files {
|
||||
if err := addFileToZip(zipWriter, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addFileToZip adds an individual file to a zip archive
|
||||
func addFileToZip(zipWriter *zip.Writer, filename string) error {
|
||||
fileToZip, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileToZip.Close()
|
||||
|
||||
info, err := fileToZip.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = filepath.Base(filename)
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, fileToZip)
|
||||
return err
|
||||
}
|
19
example/plugins/ztnc/mod/utils/template.go
Normal file
19
example/plugins/ztnc/mod/utils/template.go
Normal file
@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
/*
|
||||
Web Template Generator
|
||||
|
||||
This is the main system core module that perform function similar to what PHP did.
|
||||
To replace part of the content of any file, use {{paramter}} to replace it.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
func SendHTMLResponse(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(msg))
|
||||
}
|
202
example/plugins/ztnc/mod/utils/utils.go
Normal file
202
example/plugins/ztnc/mod/utils/utils.go
Normal file
@ -0,0 +1,202 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Common
|
||||
|
||||
Some commonly used functions in ArozOS
|
||||
|
||||
*/
|
||||
|
||||
// Response related
|
||||
func SendTextResponse(w http.ResponseWriter, msg string) {
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
// Send JSON response, with an extra json header
|
||||
func SendJSONResponse(w http.ResponseWriter, json string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(json))
|
||||
}
|
||||
|
||||
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
|
||||
}
|
||||
|
||||
func SendOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
// Get GET parameter
|
||||
func GetPara(r *http.Request, key string) (string, error) {
|
||||
// Get first value from the URL query
|
||||
value := r.URL.Query().Get(key)
|
||||
if len(value) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Get GET paramter as boolean, accept 1 or true
|
||||
func GetBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := GetPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST parameter
|
||||
func PostPara(r *http.Request, key string) (string, error) {
|
||||
// Try to parse the form
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Get first value from the form
|
||||
x := r.Form.Get(key)
|
||||
if len(x) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// Get POST paramter as boolean, accept 1 or true
|
||||
func PostBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST paramter as int
|
||||
func PostInt(r *http.Request, key string) (int, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
rx, err := strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return rx, nil
|
||||
}
|
||||
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
// File exists
|
||||
return true
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
// File does not exist
|
||||
return false
|
||||
}
|
||||
// Some other error
|
||||
return false
|
||||
}
|
||||
|
||||
func IsDir(path string) bool {
|
||||
if !FileExists(path) {
|
||||
return false
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
return true
|
||||
case mode.IsRegular():
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TimeToString(targetTime time.Time) string {
|
||||
return targetTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// Check if given string in a given slice
|
||||
func StringInArray(arr []string, str string) bool {
|
||||
for _, a := range arr {
|
||||
if a == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func StringInArrayIgnoreCase(arr []string, str string) bool {
|
||||
smallArray := []string{}
|
||||
for _, item := range arr {
|
||||
smallArray = append(smallArray, strings.ToLower(item))
|
||||
}
|
||||
|
||||
return StringInArray(smallArray, strings.ToLower(str))
|
||||
}
|
||||
|
||||
// Validate if the listening address is correct
|
||||
func ValidateListeningAddress(address string) bool {
|
||||
// Check if the address starts with a colon, indicating it's just a port
|
||||
if strings.HasPrefix(address, ":") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Split the address into host and port parts
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Try to parse it as just a port
|
||||
if _, err := strconv.Atoi(address); err == nil {
|
||||
return false // It's just a port number
|
||||
}
|
||||
return false // It's an invalid address
|
||||
}
|
||||
|
||||
// Check if the port part is a valid number
|
||||
if _, err := strconv.Atoi(port); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the host part is a valid IP address or empty (indicating any IP)
|
||||
if host != "" {
|
||||
if net.ParseIP(host) == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
19
example/plugins/ztnc/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/ztnc/mod/zoraxy_plugin/README.txt
Normal file
@ -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
|
145
example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -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())
|
||||
}
|
162
example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type 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 == SniffResultAccpet {
|
||||
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
|
||||
}
|
156
example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
Normal file
156
example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
105
example/plugins/ztnc/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/ztnc/mod/zoraxy_plugin/static_router.go
Normal file
@ -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)
|
||||
|
||||
}
|
||||
}
|
176
example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
176
example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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, see Zoraxy documentation for more details
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
//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()
|
||||
}
|
69
example/plugins/ztnc/start.go
Normal file
69
example/plugins/ztnc/start.go
Normal file
@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database"
|
||||
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
|
||||
"aroz.org/zoraxy/ztnc/mod/ganserv"
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
)
|
||||
|
||||
func startGanNetworkController() error {
|
||||
fmt.Println("Starting ZeroTier Network Controller")
|
||||
//Create a new database
|
||||
var err error
|
||||
sysdb, err = database.NewDatabase(DB_FILE_PATH, dbinc.BackendBoltDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Initiate the GAN server manager
|
||||
usingZtAuthToken := ""
|
||||
ztAPIPort := 9993
|
||||
|
||||
if utils.FileExists(AUTH_TOKEN_PATH) {
|
||||
authToken, err := os.ReadFile(AUTH_TOKEN_PATH)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading auth config file:", err)
|
||||
return err
|
||||
}
|
||||
usingZtAuthToken = string(authToken)
|
||||
fmt.Println("Loaded ZeroTier Auth Token from file")
|
||||
}
|
||||
|
||||
if usingZtAuthToken == "" {
|
||||
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting ZeroTier Auth Token:", err)
|
||||
}
|
||||
}
|
||||
|
||||
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
|
||||
AuthToken: usingZtAuthToken,
|
||||
ApiPort: ztAPIPort,
|
||||
Database: sysdb,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initApiEndpoints() {
|
||||
//UI_RELPATH must be the same as the one in the plugin intro spect
|
||||
// as Zoraxy plugin UI proxy will only forward the UI path to your plugin
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/info", ganManager.HandleGetNodeID)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/add", ganManager.HandleAddNetwork)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/remove", ganManager.HandleRemoveNetwork)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/list", ganManager.HandleListNetwork)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/members/list", ganManager.HandleMemberList)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
||||
http.HandleFunc(UI_RELPATH+"/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
<!-- This is being loaded in index.html as ajax -->
|
||||
<div class="standardContainer">
|
||||
<button onclick="exitToGanList();" class="ui large circular black icon button"><i class="angle left icon"></i></button>
|
||||
<div style="max-width: 300px; margin-top: 1em;">
|
||||
@ -16,12 +17,12 @@
|
||||
<div class="field">
|
||||
<label>Network Name</label>
|
||||
<input type="text" id="gaNetNameInput" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Network Description</label>
|
||||
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
|
||||
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
|
||||
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
|
||||
<label>Network Description</label>
|
||||
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
|
||||
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
|
||||
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
@ -214,8 +215,8 @@
|
||||
//Get CIDR from selected range group
|
||||
var cidr = $(".iprange.active").attr("cidr");
|
||||
|
||||
$.ajax({
|
||||
url: "/api/gan/network/setRange",
|
||||
$.cjax({
|
||||
url: "./api/gan/network/setRange",
|
||||
metohd: "POST",
|
||||
data:{
|
||||
netid: currentGANetID,
|
||||
@ -225,9 +226,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000)
|
||||
parent.msgbox(data.error, false, 5000)
|
||||
}else{
|
||||
msgbox("Network Range Updated")
|
||||
parent.msgbox("Network Range Updated")
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -240,8 +241,8 @@
|
||||
if (object != undefined){
|
||||
$(object).addClass("loading");
|
||||
}
|
||||
$.ajax({
|
||||
url: "/api/gan/network/name",
|
||||
$.cjax({
|
||||
url: "./api/gan/network/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
@ -252,7 +253,7 @@
|
||||
initNetNameAndDesc();
|
||||
if (object != undefined){
|
||||
$(object).removeClass("loading");
|
||||
msgbox("Network Metadata Updated");
|
||||
parent.msgbox("Network Metadata Updated");
|
||||
}
|
||||
$("#gannetDetailEdit").slideUp("fast");
|
||||
}
|
||||
@ -261,9 +262,9 @@
|
||||
|
||||
function initNetNameAndDesc(){
|
||||
//Get the details of the net
|
||||
$.get("/api/gan/network/name?netid=" + currentGANetID, function(data){
|
||||
$.get("./api/gan/network/name?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
$("#gaNetNameInput").val(data[0]);
|
||||
$(".ganetName").html(data[0]);
|
||||
@ -274,10 +275,10 @@
|
||||
}
|
||||
|
||||
function initNetDetails(){
|
||||
//Get the details of the net
|
||||
$.get("/api/gan/network/list?netid=" + currentGANetID, function(data){
|
||||
//Get the details of the net
|
||||
$.get("./api/gan/network/list?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
currentGaNetDetails = data;
|
||||
highlightCurrentGANetCIDR();
|
||||
@ -287,8 +288,8 @@
|
||||
|
||||
//Handle delete IP from memeber
|
||||
function deleteIpFromMemeber(memberid, ip){
|
||||
$.ajax({
|
||||
url: "/api/gan/members/ip",
|
||||
$.cjax({
|
||||
url: "./api/gan/members/ip",
|
||||
metohd: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
@ -298,9 +299,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP removed from member " + memberid)
|
||||
parent.msgbox("IP removed from member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
@ -330,12 +331,12 @@
|
||||
}
|
||||
|
||||
if (!isValidIPv4Address(newip)){
|
||||
msgbox(newip + " is not a valid IPv4 address", false, 5000)
|
||||
parent.msgbox(newip + " is not a valid IPv4 address", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/gan/members/ip",
|
||||
$.cjax({
|
||||
url: "./api/gan/members/ip",
|
||||
metohd: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
@ -345,9 +346,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP added to member " + memberid)
|
||||
parent.msgbox("IP added to member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
@ -357,7 +358,7 @@
|
||||
//Member table populate
|
||||
function renderMemeberTable(forceUpdate = false) {
|
||||
$.ajax({
|
||||
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||
url: './api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
let tableBody = $('#networkMemeberTable');
|
||||
@ -461,8 +462,8 @@
|
||||
$(".memberName").each(function(){
|
||||
let addr = $(this).attr("addr");
|
||||
let targetDOM = $(this);
|
||||
$.ajax({
|
||||
url: "/api/gan/members/name",
|
||||
$.cjax({
|
||||
url: "./api/gan/members/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
@ -481,14 +482,14 @@
|
||||
|
||||
function renameMember(targetMemberAddr){
|
||||
if (targetMemberAddr == ""){
|
||||
msgbox("Member address cannot be empty", false, 5000)
|
||||
parent.msgbox("Member address cannot be empty", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
|
||||
if (newname != null && newname.trim() != "") {
|
||||
$.ajax({
|
||||
url: "/api/gan/members/name",
|
||||
$.cjax({
|
||||
url: "./api/gan/members/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
@ -497,13 +498,13 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Member Name Updated");
|
||||
parent.msgbox("Member Name Updated");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -553,8 +554,8 @@
|
||||
function handleMemberAuth(object){
|
||||
let targetMemberAddr = $(object).attr("addr");
|
||||
let isAuthed = object.checked;
|
||||
$.ajax({
|
||||
url: "/api/gan/members/authorize",
|
||||
$.cjax({
|
||||
url: "./api/gan/members/authorize",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
@ -563,12 +564,12 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
if (isAuthed){
|
||||
msgbox("Member Authorized");
|
||||
parent.msgbox("Member Authorized");
|
||||
}else{
|
||||
msgbox("Member Deauthorized");
|
||||
parent.msgbox("Member Deauthorized");
|
||||
}
|
||||
|
||||
}
|
||||
@ -579,25 +580,26 @@
|
||||
}
|
||||
|
||||
function handleMemberDelete(addr){
|
||||
if (confirm("Confirm delete member " + addr + " ?")){
|
||||
$.ajax({
|
||||
url: "/api/gan/members/delete",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
memid: addr,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Member Deleted");
|
||||
parent.confirmBox("Confirm delete member " + addr + " ?", function(choice){
|
||||
if (choice){
|
||||
$.cjax({
|
||||
url: "./api/gan/members/delete",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
memid: addr,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
parent.msgbox("Member Deleted");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Add and remove this controller node to network as member
|
||||
@ -605,8 +607,8 @@
|
||||
$(".addControllerToNetworkBtn").addClass("disabled");
|
||||
$(".addControllerToNetworkBtn").addClass("loading");
|
||||
|
||||
$.ajax({
|
||||
url: "/api/gan/network/join",
|
||||
$.cjax({
|
||||
url: "./api/gan/network/join",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
@ -615,13 +617,18 @@
|
||||
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller joint " + currentGANetID);
|
||||
parent.msgbox("Controller joint " + currentGANetID);
|
||||
}
|
||||
setTimeout(function(){
|
||||
renderMemeberTable(true);
|
||||
}, 3000)
|
||||
},
|
||||
error: function(){
|
||||
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -630,17 +637,17 @@
|
||||
$(".removeControllerFromNetworkBtn").addClass("disabled");
|
||||
$(".removeControllerFromNetworkBtn").addClass("loading");
|
||||
|
||||
$.ajax({
|
||||
url: "/api/gan/network/leave",
|
||||
$.cjax({
|
||||
url: "./api/gan/network/leave",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller left " + currentGANetID);
|
||||
parent.msgbox("Controller left " + currentGANetID);
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
$(".removeControllerFromNetworkBtn").removeClass("disabled");
|
||||
@ -654,7 +661,7 @@
|
||||
currentGANetID = ganetId;
|
||||
$(".ganetID").text(ganetId);
|
||||
initNetNameAndDesc(ganetId);
|
||||
generateIPRangeTable(netRanges);
|
||||
generateIPRangeTable(netRanges);
|
||||
initNetDetails();
|
||||
renderMemeberTable(true);
|
||||
|
||||
@ -669,19 +676,77 @@
|
||||
|
||||
}
|
||||
|
||||
//Switch from other tabs back to this, exit to GAN list
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
exitToGanList();
|
||||
}
|
||||
|
||||
//Exit point
|
||||
function exitToGanList(){
|
||||
$("#gan").load("./components/gan.html", function(){
|
||||
if (tabSwitchEventBind["gan"]){
|
||||
tabSwitchEventBind["gan"]();
|
||||
}
|
||||
});
|
||||
location.href = "./index.html"
|
||||
}
|
||||
|
||||
//Debug functions
|
||||
if (typeof(msgbox) == "undefined"){
|
||||
msgbox = function(msg, error=false, timeout=3000){
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function ip2long (argIP) {
|
||||
// discuss at: https://locutus.io/php/ip2long/
|
||||
// original by: Waldo Malqui Silva (https://waldo.malqui.info)
|
||||
// improved by: Victor
|
||||
// revised by: fearphage (https://my.opera.com/fearphage/)
|
||||
// revised by: Theriault (https://github.com/Theriault)
|
||||
// estarget: es2015
|
||||
// example 1: ip2long('192.0.34.166')
|
||||
// returns 1: 3221234342
|
||||
// example 2: ip2long('0.0xABCDEF')
|
||||
// returns 2: 11259375
|
||||
// example 3: ip2long('255.255.255.256')
|
||||
// returns 3: false
|
||||
let i = 0
|
||||
// PHP allows decimal, octal, and hexadecimal IP components.
|
||||
// PHP allows between 1 (e.g. 127) to 4 (e.g 127.0.0.1) components.
|
||||
const pattern = new RegExp([
|
||||
'^([1-9]\\d*|0[0-7]*|0x[\\da-f]+)',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?$'
|
||||
].join(''), 'i')
|
||||
argIP = argIP.match(pattern) // Verify argIP format.
|
||||
if (!argIP) {
|
||||
// Invalid format.
|
||||
return false
|
||||
}
|
||||
// Reuse argIP variable for component counter.
|
||||
argIP[0] = 0
|
||||
for (i = 1; i < 5; i += 1) {
|
||||
argIP[0] += !!((argIP[i] || '').length)
|
||||
argIP[i] = parseInt(argIP[i]) || 0
|
||||
}
|
||||
// Continue to use argIP for overflow values.
|
||||
// PHP does not allow any component to overflow.
|
||||
argIP.push(256, 256, 256, 256)
|
||||
// Recalculate overflow of last component supplied to make up for missing components.
|
||||
argIP[4 + argIP[0]] *= Math.pow(256, 4 - argIP[0])
|
||||
if (argIP[1] >= argIP[5] ||
|
||||
argIP[2] >= argIP[6] ||
|
||||
argIP[3] >= argIP[7] ||
|
||||
argIP[4] >= argIP[8]) {
|
||||
return false
|
||||
}
|
||||
return argIP[1] * (argIP[0] === 1 || 16777216) +
|
||||
argIP[2] * (argIP[0] <= 2 || 65536) +
|
||||
argIP[3] * (argIP[0] <= 3 || 256) +
|
||||
argIP[4] * 1
|
||||
}
|
||||
|
||||
function long2ip (ip) {
|
||||
// discuss at: https://locutus.io/php/long2ip/
|
||||
// original by: Waldo Malqui Silva (https://fayr.us/waldo/)
|
||||
// example 1: long2ip( 3221234342 )
|
||||
// returns 1: '192.0.34.166'
|
||||
if (!isFinite(ip)) {
|
||||
return false
|
||||
}
|
||||
return [ip >>> 24 & 0xFF, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.')
|
||||
}
|
||||
|
||||
</script>
|
||||
</script>
|
267
example/plugins/ztnc/web/index.html
Normal file
267
example/plugins/ztnc/web/index.html
Normal file
@ -0,0 +1,267 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<title>Global Area Network | Zoraxy</title>
|
||||
<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/tablesort.js"></script>
|
||||
<script src="/script/countryCode.js"></script>
|
||||
<script src="/script/chart.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body{
|
||||
background:none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<div id="ganetWindow" class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Global Area Network</h2>
|
||||
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
|
||||
</div>
|
||||
<div class="gansnetworks">
|
||||
<div class="ganstats ui basic segment">
|
||||
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
|
||||
<h1 class="ui header" style="text-align: right;">
|
||||
<span class="ganControllerID"></span>
|
||||
<div class="sub header">Network Controller ID</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ui list">
|
||||
<div class="item">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
<div class="header" style="font-size: 1.2em;" id="ganetCount">0</div>
|
||||
<div class="description">Networks</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="desktop icon"></i>
|
||||
<div class="content">
|
||||
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
|
||||
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ganlist">
|
||||
<button class="ui basic orange button" onclick="addGANet();">Create New Network</button>
|
||||
<div class="ui divider"></div>
|
||||
<!--
|
||||
<div class="ui icon input" style="margin-bottom: 1em;">
|
||||
<input type="text" placeholder="Search a Network">
|
||||
<i class="circular search link icon"></i>
|
||||
</div>-->
|
||||
<div style="width: 100%; overflow-x: auto;">
|
||||
<table class="ui celled basic unstackable striped table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Subnet (Assign Range)</th>
|
||||
<th>Nodes</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="GANetList">
|
||||
<tr>
|
||||
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/*
|
||||
Network Management Functions
|
||||
*/
|
||||
function handleAddNetwork(){
|
||||
let networkName = $("#networkName").val().trim();
|
||||
if (networkName == ""){
|
||||
parent.msgbox("Network name cannot be empty", false, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
//Add network with default settings
|
||||
addGANet(networkName, "192.168.196.0/24");
|
||||
$("#networkName").val("");
|
||||
}
|
||||
|
||||
function initGANetID(){
|
||||
$.get("./api/gan/network/info", function(data){
|
||||
if (data.error !== undefined){
|
||||
parent.msgbox(data.error, false, 5000)
|
||||
}else{
|
||||
if (data != ""){
|
||||
$(".ganControllerID").text(data);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function addGANet() {
|
||||
$.cjax({
|
||||
url: "./api/gan/network/add",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
parent.msgbox(response.error, false, 5000);
|
||||
}else{
|
||||
parent.msgbox("Network added successfully");
|
||||
}
|
||||
console.log("Network added successfully:", response);
|
||||
listGANet();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log("Error adding network:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function listGANet(){
|
||||
$("#connectedNodes").attr("count", "0");
|
||||
|
||||
$.get("./api/gan/network/list", function(data){
|
||||
$("#GANetList").empty();
|
||||
if (data.error != undefined){
|
||||
console.log(data.error);
|
||||
parent.msgbox("Unable to load auth token for GANet", false, 5000);
|
||||
//token error or no zerotier found
|
||||
$(".gansnetworks").addClass("disabled");
|
||||
$("#GANetList").append(`<tr>
|
||||
<td colspan="6"><i class="red times circle icon"></i> Auth token access error or not found</td>
|
||||
</tr>`);
|
||||
$(".ganControllerID").text('Access Denied');
|
||||
}else{
|
||||
var nodeCount = 0;
|
||||
data.forEach(function(gan){
|
||||
$("#GANetList").append(`<tr class="ganetEntry" addr="${gan.nwid}">
|
||||
<td><a href="#" onclick="event.preventDefault(); openGANetDetails('${gan.nwid}');">${gan.nwid}</a></td>
|
||||
<td>${gan.name}</td>
|
||||
<td class="gandesc" addr="${gan.nwid}"></td>
|
||||
<td class="ganetSubnet"></td>
|
||||
<td class="ganetNodes"></td>
|
||||
<td>
|
||||
<button onclick="openGANetDetails('${gan.nwid}');" class="ui tiny basic icon button" title="Edit Network"><i class="edit icon"></i></button>
|
||||
<button onclick="removeGANet('${gan.nwid}');" class="ui tiny basic icon button" title="Remove Network"><i class="red remove icon"></i></button>
|
||||
</td>
|
||||
</tr>`);
|
||||
|
||||
nodeCount += 0;
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
$("#GANetList").append(`<tr>
|
||||
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
|
||||
</tr>`);
|
||||
}
|
||||
|
||||
$("#ganodeCount").text(nodeCount);
|
||||
$("#ganetCount").text(data.length);
|
||||
|
||||
//Load description
|
||||
$(".gandesc").each(function(){
|
||||
let addr = $(this).attr("addr");
|
||||
let domEle = $(this);
|
||||
$.get("./api/gan/network/name?netid=" + addr, function(data){
|
||||
$(domEle).text(data[1]);
|
||||
});
|
||||
});
|
||||
|
||||
$(".ganetEntry").each(function(){
|
||||
let addr = $(this).attr("addr");
|
||||
let subnetEle = $(this).find(".ganetSubnet");
|
||||
let nodeEle = $(this).find(".ganetNodes");
|
||||
|
||||
$.get("./api/gan/network/list?netid=" + addr, function(data){
|
||||
if (data.routes != undefined && data.routes.length > 0){
|
||||
|
||||
if (data.ipAssignmentPools != undefined && data.ipAssignmentPools.length > 0){
|
||||
$(subnetEle).html(`${data.routes[0].target} <br> (${data.ipAssignmentPools[0].ipRangeStart} - ${data.ipAssignmentPools[0].ipRangeEnd})`);
|
||||
}else{
|
||||
$(subnetEle).html(`${data.routes[0].target}<br>(Unassigned Range)`);
|
||||
}
|
||||
}else{
|
||||
$(subnetEle).text("Unassigned");
|
||||
}
|
||||
//console.log(data);
|
||||
});
|
||||
|
||||
$.get("./api/gan/members/list?netid=" + addr, function(data){
|
||||
$(nodeEle).text(data.length);
|
||||
let currentNodesCount = parseInt($("#connectedNodes").attr("count"));
|
||||
currentNodesCount += data.length;
|
||||
$("#connectedNodes").attr("count", currentNodesCount);
|
||||
$("#ganodeCount").text($("#connectedNodes").attr("count"));
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Remove the given GANet
|
||||
function removeGANet(netid){
|
||||
//Reusing Zoraxy confirm box
|
||||
parent.confirmBox("Confirm remove " + netid + "?", function(choice){
|
||||
if (choice == true){
|
||||
$.cjax({
|
||||
url: "./api/gan/network/remove",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id: netid,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
parent.msgbox("Net " + netid + " removed");
|
||||
}
|
||||
listGANet();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function openGANetDetails(netid){
|
||||
$("#ganetWindow").load("./details.html", function(){
|
||||
setTimeout(function(){
|
||||
initGanetDetails(netid);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
listGANet();
|
||||
initGANetID();
|
||||
});
|
||||
|
||||
if (typeof(msgbox) == "undefined"){
|
||||
msgbox = function(msg, error=false, timeout=3000){
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -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
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
|
||||
temp = $(subst /, ,$@)
|
||||
os = $(word 1, $(temp))
|
||||
arch = $(word 2, $(temp))
|
||||
@ -19,7 +19,7 @@ clean:
|
||||
|
||||
$(PLATFORMS):
|
||||
@echo "Building $(os)/$(arch)"
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) CGO_ENABLED="0" go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
# GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user