mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-04 05:11:45 +02:00
Compare commits
344 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
83536a83f7 | |||
1183b0ed55 | |||
b00e302f6d | |||
deddb17803 | |||
aa96d831e1 | |||
c6f7f37aaf | |||
63f12dedcf | |||
136d1ecafb | |||
7193defad7 | |||
cf4c57298e | |||
d82a531a41 | |||
7694e317f7 | |||
ed4945ab7e | |||
ce8741bfc8 | |||
7a3db09811 | |||
e73f9b47d3 | |||
c248dacccf | |||
d596d6b843 | |||
6feb2d105d | |||
3a26a5b4d3 | |||
2cdd5654ed | |||
a0d362df4e | |||
334c1ab131 | |||
08d52024ab | |||
a3e16594e8 | |||
cced07ba2d | |||
2003992d75 | |||
71423d98b1 | |||
8ca716c59f | |||
fe48a9a0c3 | |||
ec973eb3bc | |||
7b69b5fa63 | |||
ce4f46cb50 | |||
3454a9b975 | |||
55bc939a37 | |||
1d63b679dc | |||
3df96350a3 | |||
34fab7b3d0 | |||
46817d0664 | |||
1db2ca61fa | |||
0b601406de | |||
b4c771cdee | |||
a486d42351 | |||
90c2199a1b | |||
161c61fac7 | |||
5ffacb1d06 | |||
75ebd0ffbe | |||
dc069f3c57 | |||
e1b512f78f | |||
8854a38f49 | |||
7583a4628c | |||
73c0ea0896 | |||
7dad7c7305 | |||
faa95b4e21 | |||
cb0e13976d | |||
ccd8dcff56 | |||
750656fd7f | |||
d9f515fdba | |||
176249a7d9 | |||
e2a449a7bc | |||
a9695e969e | |||
7ba997dfc2 | |||
d00117e878 | |||
43a84a3f1c | |||
e24f31bdef | |||
fc9240fbac | |||
e0f5431215 | |||
de658a3c6c | |||
73276b1918 | |||
abdb7d4d75 | |||
72299ace15 | |||
b1a14872c3 | |||
df9deb3fbb | |||
9369237229 |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -28,11 +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 ]
|
||||
- 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.
|
||||
|
25
.github/ISSUE_TEMPLATE/help-needed.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/help-needed.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Help Needed
|
||||
about: Something went wrong but I don't know why
|
||||
title: "[HELP]"
|
||||
labels: help wanted
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What happened?**
|
||||
A clear and concise description of what the problem is. Ex. I tried to create a proxy rule but it doesn't work. When I connects to my domain, I see [...]
|
||||
|
||||
**Describe what have you tried**
|
||||
A clear and concise description of what you expect to see and what you have tried to debug it.
|
||||
|
||||
**Describe the networking setup you are using**
|
||||
Here are some example, commonly asked questions from our maintainers:
|
||||
- Are you using the docker build of Zoraxy? [yes (with docker setup & networking config attach) /no]
|
||||
- Your Zoraxy version? [e.g. 3.0.4]
|
||||
- Are you using Cloudflare? [yes/no]
|
||||
- Are your system hosted under a NAT router? [e.g. yes, with subnet is e.g. 192.168.0.0/24 and include port forwarding config if any]
|
||||
- DNS record (if any)
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
43
.github/workflows/docker.yml
vendored
Normal file
43
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
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: Setup building file structure
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
|
||||
- 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 \
|
||||
.
|
21
.gitignore
vendored
21
.gitignore
vendored
@ -30,5 +30,22 @@ src/certs/*
|
||||
src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/ImagePublisher.sh
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
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
|
||||
|
211
CHANGELOG.md
211
CHANGELOG.md
@ -1,3 +1,214 @@
|
||||
# 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
|
||||
|
||||
|
||||
+ Optimized uptime monitor error message [#121](https://github.com/tobychui/zoraxy/issues/121)
|
||||
+ Optimized detection logic for internal proxy target and header rewrite condition for HTTP_HOST [#164](https://github.com/tobychui/zoraxy/issues/164)
|
||||
+ Fixed ovh DNS challenge provider form generator bug [#161](https://github.com/tobychui/zoraxy/issues/161)
|
||||
+ Added permission policy module (not enabled)
|
||||
+ Added single-use cookiejar to uptime monitor request client to handle cookie issues on some poorly written back-end server [#149](https://github.com/tobychui/zoraxy/issues/149)
|
||||
|
||||
|
||||
# v3.0.4 May 18 2024
|
||||
|
||||
## This release tidied up the contribution by [Teifun2](https://github.com/Teifun2) and added a new way to generate DNS challenge based certificate (e.g. wildcards) from Let's Encrypt without changing any environment variables. This also fixes a few previous ACME module EAB settings bug related to concurrent save.
|
||||
|
||||
You can find the DNS challenge settings under TLS / SSL > ACME snippet > Generate New Certificate > (Check the "Use a DNS Challenge" checkbox)
|
||||
|
||||
+ Optimized DNS challenge implementation [thanks to Teifun2](https://github.com/Teifun2) / Issues [#49](https://github.com/tobychui/zoraxy/issues/49) [#79](https://github.com/tobychui/zoraxy/issues/79)
|
||||
+ Removed dependencies on environment variable write and keep all data contained
|
||||
+ Fixed panic on loading certificate generated by Zoraxy v2
|
||||
+ Added automatic form generator for DNS challenge / providers
|
||||
+ Added CA name default value
|
||||
+ Added code generator for acmedns module (storing the DNS challenge provider contents extracted from lego)
|
||||
+ Fixed ACME snippet "Obtain Certificate" concurrent issues in save EAB and DNS credentials
|
||||
|
||||
|
||||
# v3.0.3 Apr 30 2024
|
||||
## Breaking Change
|
||||
|
||||
For users using SMTP with older versions, you might need to update the settings by moving the domains (the part after @ in the username and domain setup field) into the username field.
|
||||
|
||||
+ Updated SMTP UI for non email login username [#129](https://github.com/tobychui/zoraxy/issues/129)
|
||||
+ Fixed ACME cert store reload after cert request [#126](https://github.com/tobychui/zoraxy/issues/126)
|
||||
+ Fixed default rule not applying to default site when default site is set to proxy target [#130](https://github.com/tobychui/zoraxy/issues/130)
|
||||
+ Fixed blacklist-ip not working with CIDR bug
|
||||
+ Fixed minor vdir bug in tailing slash detection and redirect logic
|
||||
+ Added custom mdns name support (-mdnsname flag)
|
||||
+ Added LAN tag in statistic [#131](https://github.com/tobychui/zoraxy/issues/131)
|
||||
|
||||
|
||||
# v3.0.2 Apr 24 2024
|
||||
|
||||
+ Added alias for HTTP proxy host names [#76](https://github.com/tobychui/zoraxy/issues/76)
|
||||
+ Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
|
||||
+ Added HTTP proxy host based access rules [#69](https://github.com/tobychui/zoraxy/issues/69)
|
||||
+ Added EAD Configuration for ACME (by [yeungalan](https://github.com/yeungalan)) [#45](https://github.com/tobychui/zoraxy/issues/45)
|
||||
+ Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
|
||||
+ Fixed panic due to empty domain field in json config [#120](https://github.com/tobychui/zoraxy/issues/120)
|
||||
+ Removed dependencies on management panel css for online font files
|
||||
|
||||
# v3.0.1 Apr 04 2024
|
||||
|
||||
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
|
||||
|
80
README.md
80
README.md
@ -2,9 +2,7 @@
|
||||
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||
|
||||
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
|
||||
### Features
|
||||
|
||||
@ -18,30 +16,43 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (one certificate contains multiple host names)
|
||||
- SNI support (and SAN certs)
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- 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)
|
||||
|
||||
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
[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)
|
||||
|
||||
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 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
|
||||
@ -54,11 +65,11 @@ 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
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server just like a basic home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers. A full "Getting Started" guide can be found [here](https://github.com/tobychui/zoraxy/wiki/Getting-Started).
|
||||
|
||||
#### Linux
|
||||
|
||||
@ -79,33 +90,51 @@ The installation method is same as Linux. If you are using a Raspberry Pi 4 or n
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
|
||||
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)
|
||||
-info
|
||||
Show information about this program in JSON
|
||||
-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
|
||||
-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")
|
||||
Static web server root folder. Only allow change in start paramters (default "./www")
|
||||
-ztauth string
|
||||
ZeroTier authtoken for the local node
|
||||
-ztport int
|
||||
@ -120,7 +149,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
|
||||
|
||||
@ -140,7 +170,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
|
||||
@ -153,26 +183,28 @@ This allows you to have an infinite number of network members in your Global Are
|
||||
## Web SSH
|
||||
|
||||
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
- linux/386 (experimental)
|
||||
|
||||
### Loopback Connection
|
||||
### 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
|
||||
```
|
||||
|
||||
## Sponsor This Project
|
||||
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative license.**
|
||||
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,20 +1,8 @@
|
||||
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
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
COPY entrypoint.sh /opt/zoraxy/
|
||||
|
||||
RUN chmod -R 755 /opt/zoraxy/ &&\
|
||||
chmod +x /opt/zoraxy/entrypoint.sh
|
||||
|
||||
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/
|
||||
|
||||
@ -22,17 +10,59 @@ 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 +x /usr/local/bin/zoraxy
|
||||
FROM docker.io/ubuntu:latest AS build-zerotier
|
||||
|
||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
||||
|
||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||
cd ZeroTierOne-* &&\
|
||||
make &&\
|
||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
FROM docker.io/ubuntu:latest
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
ENV ZEROTIER="false"
|
||||
|
||||
ENV ARGS="-noauth=false"
|
||||
ENV AUTORENEW="86400"
|
||||
ENV CFGUPGRADE="true"
|
||||
ENV DB="auto"
|
||||
ENV DOCKER="true"
|
||||
ENV EARLYRENEW="30"
|
||||
ENV FASTGEOIP="false"
|
||||
ENV MDNS="true"
|
||||
ENV MDNSNAME="''"
|
||||
ENV NOAUTH="false"
|
||||
ENV PORT="8000"
|
||||
ENV SSHLB="false"
|
||||
ENV UPDATE_GEOIP="false"
|
||||
ENV VERSION="false"
|
||||
ENV WEBFM="true"
|
||||
ENV WEBROOT="./www"
|
||||
ENV ZTAUTH=""
|
||||
ENV ZTPORT="9993"
|
||||
|
||||
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
|
||||
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
|
||||
|
||||
|
127
docker/README.md
127
docker/README.md
@ -1,65 +1,104 @@
|
||||
# [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 \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
||||
-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/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
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. |
|
||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||
|
||||
### 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. |
|
||||
| `PORT` | `8000` (Integer) | Management web interface listening port |
|
||||
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
|
||||
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
|
||||
| `VERSION` | `false` (Boolean) | Show version of this server. |
|
||||
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
|
||||
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
|
||||
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
|
||||
| `ZTAUTH` | `""` (String) | ZeroTier authtoken for the local node. |
|
||||
| `ZTPORT` | `9993` (Integer) | ZeroTier controller API port. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
||||
|
||||
### Building
|
||||
|
||||
To build the Docker image:
|
||||
- Check out the repository/branch.
|
||||
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
|
||||
- Run the build command with `docker build -t zoraxy_build .`
|
||||
- You can now use the image `zoraxy_build`
|
||||
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
||||
|
||||
|
@ -1,4 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Zoraxy version $VERSION"
|
||||
|
||||
zoraxy -port=:8000 ${ARGS}
|
||||
trap cleanup TERM INT
|
||||
|
||||
cleanup() {
|
||||
echo "Shutting down..."
|
||||
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
||||
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
||||
exit 0
|
||||
}
|
||||
|
||||
update-ca-certificates
|
||||
echo "CA certificates updated."
|
||||
|
||||
zoraxy -update_geoip=true
|
||||
echo "Updated GeoIP data."
|
||||
|
||||
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" \
|
||||
-port=:"$PORT" \
|
||||
-sshlb="$SSHLB" \
|
||||
-update_geoip="$UPDATE_GEOIP" \
|
||||
-version="$VERSION" \
|
||||
-webfm="$WEBFM" \
|
||||
-webroot="$WEBROOT" \
|
||||
-ztauth="$ZTAUTH" \
|
||||
-ztport="$ZTPORT" \
|
||||
&
|
||||
|
||||
zoraxypid=$!
|
||||
wait $zoraxypid
|
||||
wait $zerotierpid
|
||||
|
||||
|
@ -1 +1 @@
|
||||
zoraxy.arozos.com
|
||||
zoraxy.aroz.org
|
@ -12,19 +12,19 @@
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
|
||||
<!-- 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: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 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.arozos.com/img/og.png">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
@ -80,7 +80,7 @@
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||
<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>
|
||||
|
26
example/README.md
Normal file
26
example/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Example www Folder
|
||||
|
||||
This is an example www folder that contains two sub-folders.
|
||||
|
||||
- `html/`
|
||||
- `templates/`
|
||||
|
||||
The html file contain static resources that will be served by Zoraxy build-in static web server. You can use it as a generic web server with a static site generator like [Hugo](https://gohugo.io/) or use it as a small CDN for serving your scripts / image that commonly use across many of your sites.
|
||||
|
||||
The templates folder contains the template for overriding the build in error or access denied pages. The following templates are supported
|
||||
|
||||
- notfound.html (Default site Not-Found error page)
|
||||
- whitelist.html (Error page when client being blocked by whitelist rule)
|
||||
- blacklist.html (Error page when client being blocked by blacklist rule)
|
||||
|
||||
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
22
example/plugins/build_all.sh
Normal file
22
example/plugins/build_all.sh
Normal file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Iterate over all directories in the current directory
|
||||
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
|
70
example/plugins/debugger/main.go
Normal file
70
example/plugins/debugger/main.go
Normal file
@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.debugger"
|
||||
UI_PATH = "/debug"
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
GlobalCapturePaths: []plugin.CaptureRule{
|
||||
{
|
||||
CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule
|
||||
IncludeSubPaths: true,
|
||||
},
|
||||
},
|
||||
GlobalCaptureIngress: "",
|
||||
AlwaysCapturePaths: []plugin.CaptureRule{},
|
||||
AlwaysCaptureIngress: "",
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/*
|
||||
SubscriptionPath: "/subept",
|
||||
SubscriptionsEvents: []plugin.SubscriptionEvent{
|
||||
*/
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Register the shutdown handler
|
||||
plugin.RegisterShutdownHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Debugger Terminated")
|
||||
})
|
||||
|
||||
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
|
||||
http.HandleFunc("/gcapture", HandleIngressCapture)
|
||||
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 HandleIngressCapture(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "Capture request received")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the debugger"))
|
||||
}
|
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
|
106
example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go
Normal file
106
example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,106 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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, "/") {
|
||||
// Redirect to the index.html
|
||||
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
|
||||
return
|
||||
}
|
||||
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)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
//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)
|
||||
})
|
||||
}
|
198
example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
198
example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,198 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/*
|
||||
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 CaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
IncludeSubPaths bool `json:"include_sub_paths"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Global Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||
This captures the whole traffic of Zoraxy
|
||||
|
||||
*/
|
||||
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
*/
|
||||
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Shutdown handler
|
||||
|
||||
This function will register a shutdown handler for the plugin
|
||||
The shutdown callback will be called when the plugin is shutting down
|
||||
You can use this to clean up resources like closing database connections
|
||||
*/
|
||||
|
||||
func RegisterShutdownHandler(shutdownCallback func()) {
|
||||
// Set up a channel to receive OS signals
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Start a goroutine to listen for signals
|
||||
go func() {
|
||||
<-sigChan
|
||||
shutdownCallback()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
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/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)
|
||||
}
|
||||
|
||||
}
|
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>
|
19
example/plugins/helloworld/zoraxy_plugin/README.txt
Normal file
19
example/plugins/helloworld/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
|
128
example/plugins/helloworld/zoraxy_plugin/embed_webserver.go
Normal file
128
example/plugins/helloworld/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,128 @@
|
||||
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
|
||||
|
||||
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, "/") {
|
||||
// Redirect to the index.html
|
||||
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
|
||||
return
|
||||
}
|
||||
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)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
//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)
|
||||
}()
|
||||
})
|
||||
}
|
174
example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go
Normal file
174
example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,174 @@
|
||||
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 CaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
IncludeSubPaths bool `json:"include_sub_paths"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Global Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||
This captures the whole traffic of Zoraxy
|
||||
|
||||
*/
|
||||
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
*/
|
||||
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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()
|
||||
}
|
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.
81
example/plugins/ztnc/main.go
Normal file
81
example/plugins/ztnc/main.go
Normal file
@ -0,0 +1,81 @@
|
||||
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)
|
||||
|
||||
// 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
|
||||
http.Handle(UI_RELPATH+"/", uiRouter.Handler())
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
80
example/plugins/ztnc/mod/ganserv/authkey.go
Normal file
80
example/plugins/ztnc/mod/ganserv/authkey.go
Normal file
@ -0,0 +1,80 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TryLoadorAskUserForAuthkey() (string, error) {
|
||||
//Check for zt auth token
|
||||
value, exists := os.LookupEnv("ZT_AUTH")
|
||||
if !exists {
|
||||
log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
|
||||
} else {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
authKey := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
if isAdmin() {
|
||||
//Read the secret file directly
|
||||
b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
//Elavate the permission to admin
|
||||
ak, err := readAuthTokenAsAdmin()
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = ak
|
||||
} else {
|
||||
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "linux" {
|
||||
if isAdmin() {
|
||||
//Try to read from source using sudo
|
||||
ak, err := readAuthTokenAsAdmin()
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = strings.TrimSpace(ak)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
//Try read from source
|
||||
b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
authKey = strings.TrimSpace(authKey)
|
||||
|
||||
if authKey == "" {
|
||||
return "", errors.New("Unable to load authkey from file")
|
||||
}
|
||||
|
||||
return authKey, nil
|
||||
}
|
37
example/plugins/ztnc/mod/ganserv/authkeyLinux.go
Normal file
37
example/plugins/ztnc/mod/ganserv/authkeyLinux.go
Normal file
@ -0,0 +1,37 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
)
|
||||
|
||||
func readAuthTokenAsAdmin() (string, error) {
|
||||
if utils.FileExists("./conf/authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(authKey)), nil
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func isAdmin() bool {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return currentUser.Username == "root"
|
||||
}
|
73
example/plugins/ztnc/mod/ganserv/authkeyWin.go
Normal file
73
example/plugins/ztnc/mod/ganserv/authkeyWin.go
Normal file
@ -0,0 +1,73 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Use admin permission to read auth token on Windows
|
||||
func readAuthTokenAsAdmin() (string, error) {
|
||||
//Check if the previous startup already extracted the authkey
|
||||
if utils.FileExists("./conf/authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(authKey)), nil
|
||||
}
|
||||
}
|
||||
|
||||
verb := "runas"
|
||||
exe := "cmd.exe"
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
|
||||
os.WriteFile(output, []byte(""), 0775)
|
||||
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
|
||||
|
||||
verbPtr, _ := syscall.UTF16PtrFromString(verb)
|
||||
exePtr, _ := syscall.UTF16PtrFromString(exe)
|
||||
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
|
||||
argPtr, _ := syscall.UTF16PtrFromString(args)
|
||||
|
||||
var showCmd int32 = 1 //SW_NORMAL
|
||||
|
||||
err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(authKey)), nil
|
||||
}
|
||||
|
||||
// Check if admin on Windows
|
||||
func isAdmin() bool {
|
||||
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
130
example/plugins/ztnc/mod/ganserv/ganserv.go
Normal file
130
example/plugins/ztnc/mod/ganserv/ganserv.go
Normal file
@ -0,0 +1,130 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/database"
|
||||
)
|
||||
|
||||
/*
|
||||
Global Area Network
|
||||
Server side implementation
|
||||
|
||||
This module do a few things to help manage
|
||||
the system GANs
|
||||
|
||||
- Provide DHCP assign to client
|
||||
- Provide a list of connected nodes in the same VLAN
|
||||
- Provide proxy of packet if the target VLAN is online but not reachable
|
||||
|
||||
Also provide HTTP Handler functions for management
|
||||
- Create Network
|
||||
- Update Network Properties (Name / Desc)
|
||||
- Delete Network
|
||||
|
||||
- Authorize Node
|
||||
- Deauthorize Node
|
||||
- Set / Get Network Prefered Subnet Mask
|
||||
- Handle Node ping
|
||||
*/
|
||||
|
||||
type Node struct {
|
||||
Auth bool //If the node is authorized in this network
|
||||
ClientID string //The client ID
|
||||
MAC string //The tap MAC this client is using
|
||||
Name string //Name of the client in this network
|
||||
Description string //Description text
|
||||
ManagedIP net.IP //The IP address assigned by this network
|
||||
LastSeen int64 //Last time it is seen from this host
|
||||
ClientVersion string //Client application version
|
||||
PublicIP net.IP //Public IP address as seen from this host
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
UID string //UUID of the network, must be a 16 char random ASCII string
|
||||
Name string //Name of the network, ASCII only
|
||||
Description string //Description of the network
|
||||
CIDR string //The subnet masked use by this network
|
||||
Nodes []*Node //The nodes currently attached in this network
|
||||
}
|
||||
|
||||
type NetworkManagerOptions struct {
|
||||
Database *database.Database
|
||||
AuthToken string
|
||||
ApiPort int
|
||||
}
|
||||
|
||||
type NetworkMetaData struct {
|
||||
Desc string
|
||||
}
|
||||
|
||||
type MemberMetaData struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type NetworkManager struct {
|
||||
authToken string
|
||||
apiPort int
|
||||
ControllerID string
|
||||
option *NetworkManagerOptions
|
||||
networksMetadata map[string]NetworkMetaData
|
||||
}
|
||||
|
||||
// Create a new GAN manager
|
||||
func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
||||
option.Database.NewTable("ganserv")
|
||||
|
||||
//Load network metadata
|
||||
networkMeta := map[string]NetworkMetaData{}
|
||||
if option.Database.KeyExists("ganserv", "networkmeta") {
|
||||
option.Database.Read("ganserv", "networkmeta", &networkMeta)
|
||||
}
|
||||
|
||||
//Start the zerotier instance if not exists
|
||||
|
||||
//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,
|
||||
ControllerID: "",
|
||||
option: option,
|
||||
networksMetadata: networkMeta,
|
||||
}
|
||||
}
|
||||
|
||||
return &NetworkManager{
|
||||
authToken: option.AuthToken,
|
||||
apiPort: option.ApiPort,
|
||||
ControllerID: instanceInfo.Address,
|
||||
option: option,
|
||||
networksMetadata: networkMeta,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData {
|
||||
md, ok := m.networksMetadata[netid]
|
||||
if !ok {
|
||||
return &NetworkMetaData{}
|
||||
}
|
||||
|
||||
return &md
|
||||
}
|
||||
|
||||
func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) {
|
||||
m.networksMetadata[netid] = *meta
|
||||
m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata)
|
||||
}
|
||||
|
||||
func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData {
|
||||
thisMemberData := MemberMetaData{}
|
||||
m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData)
|
||||
return &thisMemberData
|
||||
}
|
||||
|
||||
func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) {
|
||||
m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta)
|
||||
}
|
504
example/plugins/ztnc/mod/ganserv/handlers.go
Normal file
504
example/plugins/ztnc/mod/ganserv/handlers.go
Normal file
@ -0,0 +1,504 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
)
|
||||
|
||||
func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {
|
||||
if m.ControllerID == "" {
|
||||
//Node id not exists. Check again
|
||||
instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to access node id information")
|
||||
return
|
||||
}
|
||||
|
||||
m.ControllerID = instanceInfo.Address
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(m.ControllerID)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
networkInfo, err := m.createNetwork()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Network created. Assign it the standard network settings
|
||||
err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return the new network ID
|
||||
js, _ := json.Marshal(networkInfo.Nwid)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
networkID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty network id given")
|
||||
return
|
||||
}
|
||||
|
||||
if !m.networkExists(networkID) {
|
||||
utils.SendErrorResponse(w, "network id not exists")
|
||||
return
|
||||
}
|
||||
|
||||
err = m.deleteNetwork(networkID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, _ := utils.GetPara(r, "netid")
|
||||
if netid != "" {
|
||||
targetNetInfo, err := m.getNetworkInfoById(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(targetNetInfo)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
|
||||
} else {
|
||||
// Return the list of networks as JSON
|
||||
networkIds, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
networkInfos := []*NetworkInfo{}
|
||||
for _, id := range networkIds {
|
||||
thisNetInfo, err := m.getNetworkInfoById(id)
|
||||
if err == nil {
|
||||
networkInfos = append(networkInfos, thisNetInfo)
|
||||
}
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(networkInfos)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "network id not given")
|
||||
return
|
||||
}
|
||||
|
||||
if !m.networkExists(netid) {
|
||||
utils.SendErrorResponse(w, "network not eixsts")
|
||||
}
|
||||
|
||||
newName, _ := utils.PostPara(r, "name")
|
||||
newDesc, _ := utils.PostPara(r, "desc")
|
||||
if newName != "" && newDesc != "" {
|
||||
//Strip away html from name and desc
|
||||
re := regexp.MustCompile("<[^>]*>")
|
||||
newName := re.ReplaceAllString(newName, "")
|
||||
newDesc := re.ReplaceAllString(newDesc, "")
|
||||
|
||||
//Set the new network name and desc
|
||||
err = m.setNetworkNameAndDescription(netid, newName, newDesc)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
//Get current name and description
|
||||
name, desc, err := m.getNetworkNameAndDescription(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal([]string{name, desc})
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "netid not given")
|
||||
return
|
||||
}
|
||||
|
||||
targetNetwork, err := m.getNetworkInfoById(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(targetNetwork)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "netid not given")
|
||||
return
|
||||
}
|
||||
cidr, err := utils.PostPara(r, "cidr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "cidr not given")
|
||||
return
|
||||
}
|
||||
ipstart, err := utils.PostPara(r, "ipstart")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "ipstart not given")
|
||||
return
|
||||
}
|
||||
ipend, err := utils.PostPara(r, "ipend")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "ipend not given")
|
||||
return
|
||||
}
|
||||
|
||||
//Validate the CIDR is real, the ip range is within the CIDR range
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid cidr string given")
|
||||
return
|
||||
}
|
||||
|
||||
startIP := net.ParseIP(ipstart)
|
||||
endIP := net.ParseIP(ipend)
|
||||
if startIP == nil || endIP == nil {
|
||||
utils.SendErrorResponse(w, "invalid start or end ip given")
|
||||
return
|
||||
}
|
||||
|
||||
withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP)
|
||||
if !withinRange {
|
||||
utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range")
|
||||
return
|
||||
}
|
||||
|
||||
err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle listing of network members. Set details=true for listing all details
|
||||
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.GetPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "netid is empty")
|
||||
return
|
||||
}
|
||||
|
||||
details, _ := utils.GetPara(r, "detail")
|
||||
|
||||
memberIds, err := m.getNetworkMembers(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
if details == "" {
|
||||
//Only show client ids
|
||||
js, _ := json.Marshal(memberIds)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Show detail members info
|
||||
detailMemberInfo := []*MemberInfo{}
|
||||
for _, thisMemberId := range memberIds {
|
||||
memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId)
|
||||
if err == nil {
|
||||
detailMemberInfo = append(detailMemberInfo, memInfo)
|
||||
}
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(detailMemberInfo)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Authorization of members
|
||||
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
memberid, err := utils.PostPara(r, "memid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "memid not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target memeber exists
|
||||
if !m.memberExistsInNetwork(netid, memberid) {
|
||||
utils.SendErrorResponse(w, "member not exists in given network")
|
||||
return
|
||||
}
|
||||
|
||||
setAuthorized, err := utils.PostPara(r, "auth")
|
||||
if err != nil || setAuthorized == "" {
|
||||
//Get the member authorization state
|
||||
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(memberInfo.Authorized)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if setAuthorized == "true" {
|
||||
m.AuthorizeMember(netid, memberid, true)
|
||||
} else if setAuthorized == "false" {
|
||||
m.AuthorizeMember(netid, memberid, false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Delete or Add IP for a member in a network
|
||||
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
memberid, err := utils.PostPara(r, "memid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "memid not set")
|
||||
return
|
||||
}
|
||||
|
||||
opr, err := utils.PostPara(r, "opr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "opr not defined")
|
||||
return
|
||||
}
|
||||
|
||||
targetip, _ := utils.PostPara(r, "ip")
|
||||
|
||||
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if opr == "add" {
|
||||
if targetip == "" {
|
||||
utils.SendErrorResponse(w, "ip not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidIPAddr(targetip) {
|
||||
utils.SendErrorResponse(w, "ip address not valid")
|
||||
return
|
||||
}
|
||||
|
||||
newIpList := append(memberInfo.IPAssignments, targetip)
|
||||
err = m.setAssignedIps(netid, memberid, newIpList)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
|
||||
} else if opr == "del" {
|
||||
if targetip == "" {
|
||||
utils.SendErrorResponse(w, "ip not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Delete user ip from the list
|
||||
newIpList := []string{}
|
||||
for _, thisIp := range memberInfo.IPAssignments {
|
||||
if thisIp != targetip {
|
||||
newIpList = append(newIpList, thisIp)
|
||||
}
|
||||
}
|
||||
|
||||
err = m.setAssignedIps(netid, memberid, newIpList)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
} else if opr == "get" {
|
||||
js, _ := json.Marshal(memberInfo.IPAssignments)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "unsupported opr type: "+opr)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle naming for members
|
||||
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
memberid, err := utils.PostPara(r, "memid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "memid not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !m.memberExistsInNetwork(netid, memberid) {
|
||||
utils.SendErrorResponse(w, "target member not exists in given network")
|
||||
return
|
||||
}
|
||||
|
||||
//Read memeber data
|
||||
targetMemberData := m.GetMemberMetaData(netid, memberid)
|
||||
|
||||
newname, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
//Send over the member data
|
||||
js, _ := json.Marshal(targetMemberData)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Write member data
|
||||
targetMemberData.Name = newname
|
||||
m.WriteMemeberMetaData(netid, memberid, targetMemberData)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle delete of a given memver
|
||||
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
memberid, err := utils.PostPara(r, "memid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "memid not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if that member is authorized.
|
||||
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "member not exists in given GANet")
|
||||
return
|
||||
}
|
||||
|
||||
if memberInfo.Authorized {
|
||||
//Deauthorized this member before deleting
|
||||
m.AuthorizeMember(netid, memberid, false)
|
||||
}
|
||||
|
||||
//Remove the memeber
|
||||
err = m.deleteMember(netid, memberid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Check if a given network id is a network hosted on this zoraxy node
|
||||
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
|
||||
networks, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
if network == networkId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle server instant joining a given network
|
||||
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if m.memberExistsInNetwork(netid, m.ControllerID) {
|
||||
utils.SendErrorResponse(w, "controller already inside network")
|
||||
return
|
||||
}
|
||||
|
||||
//Join the network
|
||||
err = m.joinNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle server instant leaving a given network
|
||||
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
//Leave the network
|
||||
err = m.leaveNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove it from target network if it is authorized
|
||||
err = m.deleteMember(netid, m.ControllerID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
39
example/plugins/ztnc/mod/ganserv/network.go
Normal file
39
example/plugins/ztnc/mod/ganserv/network.go
Normal file
@ -0,0 +1,39 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Get a random free IP from the pool
|
||||
func (n *Network) GetRandomFreeIP() (net.IP, error) {
|
||||
// Get all IP addresses in the subnet
|
||||
ips, err := GetAllAddressFromCIDR(n.CIDR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter out used IPs
|
||||
usedIPs := make(map[string]bool)
|
||||
for _, node := range n.Nodes {
|
||||
usedIPs[node.ManagedIP.String()] = true
|
||||
}
|
||||
availableIPs := []string{}
|
||||
for _, ip := range ips {
|
||||
if !usedIPs[ip] {
|
||||
availableIPs = append(availableIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// Randomly choose an available IP
|
||||
if len(availableIPs) == 0 {
|
||||
return nil, fmt.Errorf("no available IP")
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randIndex := rand.Intn(len(availableIPs))
|
||||
pickedFreeIP := availableIPs[randIndex]
|
||||
|
||||
return net.ParseIP(pickedFreeIP), nil
|
||||
}
|
55
example/plugins/ztnc/mod/ganserv/network_test.go
Normal file
55
example/plugins/ztnc/mod/ganserv/network_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package ganserv_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/ganserv"
|
||||
)
|
||||
|
||||
func TestGetRandomFreeIP(t *testing.T) {
|
||||
n := ganserv.Network{
|
||||
CIDR: "172.16.0.0/12",
|
||||
Nodes: []*ganserv.Node{
|
||||
{
|
||||
Name: "nodeC1",
|
||||
ManagedIP: net.ParseIP("172.16.1.142"),
|
||||
},
|
||||
{
|
||||
Name: "nodeC2",
|
||||
ManagedIP: net.ParseIP("172.16.5.174"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Call the function for 10 times
|
||||
for i := 0; i < 10; i++ {
|
||||
freeIP, err := n.GetRandomFreeIP()
|
||||
fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP)
|
||||
|
||||
// Assert that no error occurred
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err.Error())
|
||||
}
|
||||
|
||||
// Assert that the returned IP is a valid IPv4 address
|
||||
if freeIP.To4() == nil {
|
||||
t.Errorf("Invalid IP address format: %s", freeIP.String())
|
||||
}
|
||||
|
||||
// Assert that the returned IP is not already used by a node
|
||||
for _, node := range n.Nodes {
|
||||
if freeIP.Equal(node.ManagedIP) {
|
||||
t.Errorf("Returned IP is already in use: %s", freeIP.String())
|
||||
}
|
||||
}
|
||||
|
||||
n.Nodes = append(n.Nodes, &ganserv.Node{
|
||||
Name: "NodeT" + strconv.Itoa(i),
|
||||
ManagedIP: freeIP,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
55
example/plugins/ztnc/mod/ganserv/utils.go
Normal file
55
example/plugins/ztnc/mod/ganserv/utils.go
Normal file
@ -0,0 +1,55 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
//Generate all ip address from a CIDR
|
||||
func GetAllAddressFromCIDR(cidr string) ([]string, error) {
|
||||
ip, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
// remove network address and broadcast address
|
||||
return ips[1 : len(ips)-1], nil
|
||||
}
|
||||
|
||||
func inc(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isValidIPAddr(ipAddr string) bool {
|
||||
ip := net.ParseIP(ipAddr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ipWithinCIDR(ipAddr string, cidr string) bool {
|
||||
// Parse the CIDR string
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the IP address
|
||||
ip := net.ParseIP(ipAddr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the IP address is in the CIDR range
|
||||
return ipNet.Contains(ip)
|
||||
}
|
669
example/plugins/ztnc/mod/ganserv/zerotier.go
Normal file
669
example/plugins/ztnc/mod/ganserv/zerotier.go
Normal file
@ -0,0 +1,669 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
zerotier.go
|
||||
|
||||
This hold the functions that required to communicate with
|
||||
a zerotier instance
|
||||
|
||||
See more on
|
||||
https://docs.zerotier.com/self-hosting/network-controllers/
|
||||
|
||||
*/
|
||||
|
||||
type NodeInfo struct {
|
||||
Address string `json:"address"`
|
||||
Clock int64 `json:"clock"`
|
||||
Config struct {
|
||||
Settings struct {
|
||||
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"`
|
||||
PlanetWorldID int `json:"planetWorldId"`
|
||||
PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"`
|
||||
PublicIdentity string `json:"publicIdentity"`
|
||||
TCPFallbackActive bool `json:"tcpFallbackActive"`
|
||||
Version string `json:"version"`
|
||||
VersionBuild int `json:"versionBuild"`
|
||||
VersionMajor int `json:"versionMajor"`
|
||||
VersionMinor int `json:"versionMinor"`
|
||||
VersionRev int `json:"versionRev"`
|
||||
}
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type NetworkInfo struct {
|
||||
AuthTokens []interface{} `json:"authTokens"`
|
||||
AuthorizationEndpoint string `json:"authorizationEndpoint"`
|
||||
Capabilities []interface{} `json:"capabilities"`
|
||||
ClientID string `json:"clientId"`
|
||||
CreationTime int64 `json:"creationTime"`
|
||||
DNS []interface{} `json:"dns"`
|
||||
EnableBroadcast bool `json:"enableBroadcast"`
|
||||
ID string `json:"id"`
|
||||
IPAssignmentPools []interface{} `json:"ipAssignmentPools"`
|
||||
Mtu int `json:"mtu"`
|
||||
MulticastLimit int `json:"multicastLimit"`
|
||||
Name string `json:"name"`
|
||||
Nwid string `json:"nwid"`
|
||||
Objtype string `json:"objtype"`
|
||||
Private bool `json:"private"`
|
||||
RemoteTraceLevel int `json:"remoteTraceLevel"`
|
||||
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
|
||||
Revision int `json:"revision"`
|
||||
Routes []interface{} `json:"routes"`
|
||||
Rules []struct {
|
||||
Not bool `json:"not"`
|
||||
Or bool `json:"or"`
|
||||
Type string `json:"type"`
|
||||
} `json:"rules"`
|
||||
RulesSource string `json:"rulesSource"`
|
||||
SsoEnabled bool `json:"ssoEnabled"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
V4AssignMode struct {
|
||||
Zt bool `json:"zt"`
|
||||
} `json:"v4AssignMode"`
|
||||
V6AssignMode struct {
|
||||
SixPlane bool `json:"6plane"`
|
||||
Rfc4193 bool `json:"rfc4193"`
|
||||
Zt bool `json:"zt"`
|
||||
} `json:"v6AssignMode"`
|
||||
}
|
||||
|
||||
type MemberInfo struct {
|
||||
ActiveBridge bool `json:"activeBridge"`
|
||||
Address string `json:"address"`
|
||||
AuthenticationExpiryTime int `json:"authenticationExpiryTime"`
|
||||
Authorized bool `json:"authorized"`
|
||||
Capabilities []interface{} `json:"capabilities"`
|
||||
CreationTime int64 `json:"creationTime"`
|
||||
ID string `json:"id"`
|
||||
Identity string `json:"identity"`
|
||||
IPAssignments []string `json:"ipAssignments"`
|
||||
LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"`
|
||||
LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"`
|
||||
LastAuthorizedTime int `json:"lastAuthorizedTime"`
|
||||
LastDeauthorizedTime int `json:"lastDeauthorizedTime"`
|
||||
NoAutoAssignIps bool `json:"noAutoAssignIps"`
|
||||
Nwid string `json:"nwid"`
|
||||
Objtype string `json:"objtype"`
|
||||
RemoteTraceLevel int `json:"remoteTraceLevel"`
|
||||
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
|
||||
Revision int `json:"revision"`
|
||||
SsoExempt bool `json:"ssoExempt"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
VMajor int `json:"vMajor"`
|
||||
VMinor int `json:"vMinor"`
|
||||
VProto int `json:"vProto"`
|
||||
VRev int `json:"vRev"`
|
||||
}
|
||||
|
||||
// Get the zerotier node info from local service
|
||||
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
|
||||
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("X-ZT1-AUTH", token)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Read from zerotier service instance
|
||||
|
||||
defer resp.Body.Close()
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Parse the payload into struct
|
||||
thisInstanceInfo := NodeInfo{}
|
||||
err = json.Unmarshal(payload, &thisInstanceInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &thisInstanceInfo, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Network Functions
|
||||
*/
|
||||
//Create a zerotier network
|
||||
func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
|
||||
url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID)
|
||||
|
||||
data := []byte(`{}`)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
networkInfo := NetworkInfo{}
|
||||
err = json.Unmarshal(payload, &networkInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &networkInfo, nil
|
||||
}
|
||||
|
||||
// List network details
|
||||
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
|
||||
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", m.authToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
thisNetworkInfo := NetworkInfo{}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(payload, &thisNetworkInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &thisNetworkInfo, nil
|
||||
}
|
||||
|
||||
func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error {
|
||||
payloadBytes, err := json.Marshal(newNetworkInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payloadBuffer := bytes.NewBuffer(payloadBytes)
|
||||
|
||||
// Create the HTTP request
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/"
|
||||
req, err := http.NewRequest("POST", url, payloadBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", m.authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Send the HTTP request
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Print the response status code
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List network IDs
|
||||
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", m.authToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return []string{}, errors.New("network error")
|
||||
}
|
||||
|
||||
networkIds := []string{}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(payload, &networkIds)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
return networkIds, nil
|
||||
}
|
||||
|
||||
// wrapper for checking if a network id exists
|
||||
func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
networkIds, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, thisid := range networkIds {
|
||||
if thisid == networkId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// delete a network
|
||||
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
client := &http.Client{}
|
||||
|
||||
// Create a new DELETE request
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the required authorization header
|
||||
req.Header.Set("X-Zt1-Auth", m.authToken)
|
||||
|
||||
// Send the request and get the response
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close the response body when we're done
|
||||
defer resp.Body.Close()
|
||||
s, err := io.ReadAll(resp.Body)
|
||||
fmt.Println(string(s), err, resp.StatusCode)
|
||||
|
||||
// Print the response status code
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configure network
|
||||
// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
data := map[string]interface{}{
|
||||
"ipAssignmentPools": []map[string]string{
|
||||
{
|
||||
"ipRangeStart": ipRangeStart,
|
||||
"ipRangeEnd": ipRangeEnd,
|
||||
},
|
||||
},
|
||||
"routes": []map[string]interface{}{
|
||||
{
|
||||
"target": routeTarget,
|
||||
"via": nil,
|
||||
},
|
||||
},
|
||||
"v4AssignMode": "zt",
|
||||
"private": true,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
// Print the response status code
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid
|
||||
data := map[string]interface{}{
|
||||
"ipAssignments": newIps,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
// Print the response status code
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error {
|
||||
// Convert string to rune slice
|
||||
r := []rune(name)
|
||||
|
||||
// Loop over runes and remove non-ASCII characters
|
||||
for i, v := range r {
|
||||
if v > 127 {
|
||||
r[i] = ' '
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to string and trim whitespace
|
||||
name = strings.TrimSpace(string(r))
|
||||
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/"
|
||||
data := map[string]interface{}{
|
||||
"name": name,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
// Print the response status code
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
meta := m.GetNetworkMetaData(netid)
|
||||
if meta != nil {
|
||||
meta.Desc = desc
|
||||
m.WriteNetworkMetaData(netid, meta)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) {
|
||||
//Get name from network info
|
||||
netinfo, err := m.getNetworkInfoById(netid)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
name := netinfo.Name
|
||||
|
||||
//Get description from meta
|
||||
desc := ""
|
||||
networkMeta := m.GetNetworkMetaData(netid)
|
||||
if networkMeta != nil {
|
||||
desc = networkMeta.Desc
|
||||
}
|
||||
|
||||
return name, desc, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Member functions
|
||||
*/
|
||||
|
||||
func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member"
|
||||
reqBody := bytes.NewBuffer([]byte{})
|
||||
req, err := http.NewRequest("GET", url, reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to get network members")
|
||||
}
|
||||
|
||||
memberList := map[string]int{}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(payload, &memberList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
members := make([]string, 0, len(memberList))
|
||||
for k := range memberList {
|
||||
members = append(members, k)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool {
|
||||
//Get a list of member
|
||||
memberids, err := m.getNetworkMembers(netid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, thisMemberId := range memberids {
|
||||
if thisMemberId == memid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Get a network memeber info by netid and memberid
|
||||
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", m.authToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
thisMemeberInfo := &MemberInfo{}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(payload, &thisMemeberInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return thisMemeberInfo, nil
|
||||
}
|
||||
|
||||
// Set the authorization state of a member
|
||||
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
||||
payload := []byte(`{"authorized": true}`)
|
||||
if !setAuthorized {
|
||||
payload = []byte(`{"authorized": false}`)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-ZT1-AUTH", m.authToken)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a member from the network
|
||||
func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to join a given network
|
||||
func (m *NetworkManager) joinNetwork(netid string) error {
|
||||
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to leave a given network
|
||||
func (m *NetworkManager) leaveNetwork(netid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
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
|
128
example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
Normal file
128
example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,128 @@
|
||||
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
|
||||
|
||||
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, "/") {
|
||||
// Redirect to the index.html
|
||||
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
|
||||
return
|
||||
}
|
||||
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)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
//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)
|
||||
}()
|
||||
})
|
||||
}
|
174
example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
174
example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,174 @@
|
||||
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 CaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
IncludeSubPaths bool `json:"include_sub_paths"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Global Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
|
||||
This captures the whole traffic of Zoraxy
|
||||
|
||||
*/
|
||||
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
|
||||
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
*/
|
||||
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
|
||||
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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)
|
||||
}
|
747
example/plugins/ztnc/web/details.html
Normal file
747
example/plugins/ztnc/web/details.html
Normal file
@ -0,0 +1,747 @@
|
||||
<!-- 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;">
|
||||
<button onclick='$("#gannetDetailEdit").slideToggle("fast");' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
|
||||
<h1 class="ui header">
|
||||
<span class="ganetID"></span>
|
||||
<div class="sub header ganetName"></div>
|
||||
</h1>
|
||||
<div class="ui divider"></div>
|
||||
<p><span class="ganetDesc"></span></p>
|
||||
|
||||
</div>
|
||||
<div id="gannetDetailEdit" class="ui form" style="margin-top: 1em; display:none;">
|
||||
<div class="ui divider"></div>
|
||||
<p>You can change the network name and description below. <br>The name and description is only for easy management purpose and will not effect the network operation.</p>
|
||||
<div class="field">
|
||||
<label>Network Name</label>
|
||||
<input type="text" id="gaNetNameInput" placeholder="">
|
||||
</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>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h2>Settings</h2>
|
||||
<div class="" style="overflow-x: auto;">
|
||||
<table class="ui basic celled unstackable table" style="min-width: 560px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4">IPv4 Auto-Assign</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ganetRangeTable">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br>
|
||||
<div class="ui form">
|
||||
<h3>Custom IP Range</h3>
|
||||
<p>Manual IP Range Configuration. The IP range must be within the selected CIDR range.
|
||||
<br>Use <code>Utilities > IP to CIDR tool</code> if you are not too familiar with CIDR notations.</p>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>IP Start</label>
|
||||
<input type="text" class="ganIpStart" placeholder="">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>IP End</label>
|
||||
<input type="text" class="ganIpEnd" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="setNetworkRange();" class="ui basic button"><i class="ui blue save icon"></i> Save Settings</button>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h2>Members</h2>
|
||||
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
|
||||
<div class="ui checkbox" style="margin-bottom: 1em;">
|
||||
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);" checked>
|
||||
<label>Show Unauthorized Members</label>
|
||||
</div>
|
||||
<div class="" style="overflow-x: auto;">
|
||||
<table class="ui celled unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Auth</th>
|
||||
<th>Address</th>
|
||||
<th>Name</th>
|
||||
<th>Managed IP</th>
|
||||
<th>Authorized Since</th>
|
||||
<th>Version</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="networkMemeberTable">
|
||||
<tr>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Add Controller as Member</h4>
|
||||
<p>Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.</p>
|
||||
<button class="ui basic small button addControllerToNetworkBtn" onclick="ganAddControllerToNetwork(this);"><i class="green add icon"></i> Add Controller as Member</button>
|
||||
<button class="ui basic small button removeControllerFromNetworkBtn" onclick="ganRemoveControllerFromNetwork(this);"><i class="red sign-out icon"></i> Remove Controller from Member</button>
|
||||
<br><br>
|
||||
</div>
|
||||
<script>
|
||||
$(".checkbox").checkbox();
|
||||
var currentGANetID = "";
|
||||
var currentGANNetMemeberListener = undefined;
|
||||
var currentGaNetDetails = {};
|
||||
var currentGANMemberList = [];
|
||||
var netRanges = {
|
||||
"10.147.17.*": "10.147.17.0/24",
|
||||
"10.147.18.*": "10.147.18.0/24",
|
||||
"10.147.19.*": "10.147.19.0/24",
|
||||
"10.147.20.*": "10.147.20.0/24",
|
||||
"10.144.*.*": "10.144.0.0/16",
|
||||
"10.241.*.*": "10.241.0.0/16",
|
||||
"10.242.*.*": "10.242.0.0/16",
|
||||
"10.243.*.*": "10.243.0.0/16",
|
||||
"10.244.*.*": "10.244.0.0/16",
|
||||
"172.22.*.*": "172.22.0.0/15",
|
||||
"172.23.*.*": "172.23.0.0/16",
|
||||
"172.24.*.*": "172.24.0.0/14",
|
||||
"172.25.*.*": "172.25.0.0/16",
|
||||
"172.26.*.*": "172.26.0.0/15",
|
||||
"172.27.*.*": "172.27.0.0/16",
|
||||
"172.28.*.*": "172.28.0.0/15",
|
||||
"172.29.*.*": "172.29.0.0/16",
|
||||
"172.30.*.*": "172.30.0.0/15",
|
||||
"192.168.191.*": "192.168.191.0/24",
|
||||
"192.168.192.*": "192.168.192.0/24",
|
||||
"192.168.193.*": "192.168.193.0/24",
|
||||
"192.168.194.*": "192.168.194.0/24",
|
||||
"192.168.195.*": "192.168.195.0/24",
|
||||
"192.168.196.*": "192.168.196.0/24"
|
||||
}
|
||||
|
||||
function generateIPRangeTable(netRanges) {
|
||||
$("#ganetRangeTable").empty();
|
||||
const tableBody = document.getElementById('ganetRangeTable');
|
||||
const cidrs = Object.values(netRanges);
|
||||
|
||||
// Set the number of rows and columns to display in the table
|
||||
const numRows = 6;
|
||||
const numCols = 4;
|
||||
|
||||
let row = document.createElement('tr');
|
||||
let col = 0;
|
||||
for (let i = 0; i < cidrs.length; i++) {
|
||||
if (col >= numCols) {
|
||||
tableBody.appendChild(row);
|
||||
row = document.createElement('tr');
|
||||
col = 0;
|
||||
}
|
||||
|
||||
const td = document.createElement('td');
|
||||
td.setAttribute('class', `clickable iprange`);
|
||||
td.setAttribute('CIDR', cidrs[i]);
|
||||
td.innerHTML = cidrs[i];
|
||||
let thisCidr = cidrs[i];
|
||||
td.onclick = function(){
|
||||
selectNetworkRange(thisCidr, td);
|
||||
};
|
||||
|
||||
row.appendChild(td);
|
||||
col++;
|
||||
}
|
||||
|
||||
// Add any remaining cells to the table
|
||||
if (col > 0) {
|
||||
for (let i = col; i < numCols; i++) {
|
||||
row.appendChild(document.createElement('td'));
|
||||
}
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightCurrentGANetCIDR(){
|
||||
var currentCIDR = currentGaNetDetails.routes[0].target;
|
||||
$(".iprange").each(function(){
|
||||
if ($(this).attr("CIDR") == currentCIDR){
|
||||
$(this).addClass("active");
|
||||
populateStartEndIpByCidr(currentCIDR);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function populateStartEndIpByCidr(cidr){
|
||||
function cidrToRange(cidr) {
|
||||
var range = [2];
|
||||
cidr = cidr.split('/');
|
||||
var start = ip2long(cidr[0]);
|
||||
range[0] = long2ip(start);
|
||||
range[1] = long2ip(Math.pow(2, 32 - cidr[1]) + start - 1);
|
||||
return range;
|
||||
}
|
||||
var cidrRange = cidrToRange(cidr);
|
||||
$(".ganIpStart").val(cidrRange[0]);
|
||||
$(".ganIpEnd").val(cidrRange[1]);
|
||||
}
|
||||
|
||||
function selectNetworkRange(cidr, object){
|
||||
populateStartEndIpByCidr(cidr);
|
||||
|
||||
$(".iprange.active").removeClass("active");
|
||||
$(object).addClass("active");
|
||||
}
|
||||
|
||||
function setNetworkRange(){
|
||||
var ipstart = $(".ganIpStart").val().trim();
|
||||
var ipend = $(".ganIpEnd").val().trim();
|
||||
|
||||
if (ipstart == ""){
|
||||
$(".ganIpStart").parent().addClass("error");
|
||||
}else{
|
||||
$(".ganIpStart").parent().removeClass("error");
|
||||
}
|
||||
|
||||
if (ipend == ""){
|
||||
$(".ganIpEnd").parent().addClass("error");
|
||||
}else{
|
||||
$(".ganIpEnd").parent().removeClass("error");
|
||||
}
|
||||
|
||||
//Get CIDR from selected range group
|
||||
var cidr = $(".iprange.active").attr("cidr");
|
||||
|
||||
$.cjax({
|
||||
url: "./api/gan/network/setRange",
|
||||
metohd: "POST",
|
||||
data:{
|
||||
netid: currentGANetID,
|
||||
cidr: cidr,
|
||||
ipstart: ipstart,
|
||||
ipend: ipend
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000)
|
||||
}else{
|
||||
msgbox("Network Range Updated")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function saveNameAndDesc(object=undefined){
|
||||
var name = $("#gaNetNameInput").val();
|
||||
var desc = $("#gaNetDescInput").val();
|
||||
if (object != undefined){
|
||||
$(object).addClass("loading");
|
||||
}
|
||||
$.cjax({
|
||||
url: "./api/gan/network/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
name: name,
|
||||
desc: desc,
|
||||
},
|
||||
success: function(data){
|
||||
initNetNameAndDesc();
|
||||
if (object != undefined){
|
||||
$(object).removeClass("loading");
|
||||
msgbox("Network Metadata Updated");
|
||||
}
|
||||
$("#gannetDetailEdit").slideUp("fast");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initNetNameAndDesc(){
|
||||
//Get the details of the net
|
||||
$.get("./api/gan/network/name?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
$("#gaNetNameInput").val(data[0]);
|
||||
$(".ganetName").html(data[0]);
|
||||
$("#gaNetDescInput").val(data[1]);
|
||||
$(".ganetDesc").text(data[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initNetDetails(){
|
||||
//Get the details of the net
|
||||
$.get("./api/gan/network/list?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
currentGaNetDetails = data;
|
||||
highlightCurrentGANetCIDR();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Handle delete IP from memeber
|
||||
function deleteIpFromMemeber(memberid, ip){
|
||||
$.cjax({
|
||||
url: "./api/gan/members/ip",
|
||||
metohd: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
memid: memberid,
|
||||
opr: "del",
|
||||
ip: ip,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP removed from member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addIpToMemeberFromInput(memberid, newip){
|
||||
function isValidIPv4Address(address) {
|
||||
// Split the address into its 4 components
|
||||
const parts = address.split('.');
|
||||
|
||||
// Check that there are 4 components
|
||||
if (parts.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that each component is a number between 0 and 255
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const part = parseInt(parts[i], 10);
|
||||
if (isNaN(part) || part < 0 || part > 255) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The address is valid
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isValidIPv4Address(newip)){
|
||||
msgbox(newip + " is not a valid IPv4 address", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
$.cjax({
|
||||
url: "./api/gan/members/ip",
|
||||
metohd: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
memid: memberid,
|
||||
opr: "add",
|
||||
ip: newip,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP added to member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Member table populate
|
||||
function renderMemeberTable(forceUpdate = false) {
|
||||
$.ajax({
|
||||
url: './api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
let tableBody = $('#networkMemeberTable');
|
||||
if (tableBody.length == 0){
|
||||
return;
|
||||
}
|
||||
data.sort((a, b) => a.address.localeCompare(b.address));
|
||||
//Check if the new object equal to the old one
|
||||
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
|
||||
//Do not need to update it
|
||||
return;
|
||||
}
|
||||
tableBody.empty();
|
||||
currentGANMemberList = data;
|
||||
|
||||
var authroziedCount = 0;
|
||||
data.forEach((member) => {
|
||||
let lastAuthTime = new Date(member.lastAuthorizedTime).toLocaleString();
|
||||
if (member.lastAuthorizedTime == 0){
|
||||
lastAuthTime = "Never";
|
||||
}
|
||||
|
||||
let version = `${member.vMajor}.${member.vMinor}.${member.vProto}.${member.vRev}`;
|
||||
if (member.vMajor == -1){
|
||||
version = "Unknown";
|
||||
}
|
||||
|
||||
let authorizedCheckbox = `<div class="ui fitted checkbox">
|
||||
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);">
|
||||
<label></label>
|
||||
</div>`;
|
||||
if (member.authorized){
|
||||
authorizedCheckbox = `<div class="ui fitted checkbox">
|
||||
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);" checked="">
|
||||
<label></label>
|
||||
</div>`
|
||||
}
|
||||
|
||||
let rowClass = "authorized";
|
||||
let unauthorizedStyle = "";
|
||||
if (!$("#showUnauthorizedMembers")[0].checked && !member.authorized){
|
||||
unauthorizedStyle = "display:none;";
|
||||
}
|
||||
if (!member.authorized){
|
||||
rowClass = "unauthorized";
|
||||
}else{
|
||||
authroziedCount++;
|
||||
}
|
||||
|
||||
let assignedIp = "";
|
||||
|
||||
if (member.ipAssignments.length == 0){
|
||||
assignedIp = "Not assigned"
|
||||
}else{
|
||||
assignedIp = `<div class="ui list">`
|
||||
member.ipAssignments.forEach(function(thisIp){
|
||||
assignedIp += `<div class="item" style="width: 100%;">${thisIp} <a style="cursor:pointer; float: right;" title="Remove IP" onclick="deleteIpFromMemeber('${member.address}','${thisIp}');"><i class="red remove icon"></i></a></div>`;
|
||||
})
|
||||
assignedIp += `</div>`
|
||||
}
|
||||
const row = $(`<tr class="GANetMemberEntity ${rowClass}" style="${unauthorizedStyle}">`);
|
||||
row.append($(`<td class="GANetMember ${rowClass}" style="text-align: center;">`).html(authorizedCheckbox));
|
||||
row.append($('<td>').text(member.address));
|
||||
row.append($('<td>').html(`<span class="memberName" addr="${member.address}"></span> <a style="cursor:pointer; float: right;" title="Edit Memeber Name" onclick="renameMember('${member.address}');"><i class="grey edit icon"></i></a>`));
|
||||
row.append($('<td>').html(`${assignedIp}
|
||||
<div class="ui action mini fluid input" style="min-width: 200px;">
|
||||
<input type="text" placeholder="IPv4" onchange="$(this).val($(this).val().trim());">
|
||||
<button onclick="addIpToMemeberFromInput('${member.address}',$(this).parent().find('input').val());" class="ui basic icon button">
|
||||
<i class="add icon"></i>
|
||||
</button>
|
||||
</div>`));
|
||||
row.append($('<td>').text(lastAuthTime));
|
||||
row.append($('<td>').text(version));
|
||||
row.append($(`<td title="Deauthorize & Delete Memeber" style="text-align: center;" onclick="handleMemberDelete('${member.address}');">`).html(`<button class="ui basic mini icon button"><i class="red remove icon"></i></button>`));
|
||||
|
||||
tableBody.append(row);
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
tableBody.append(`<tr>
|
||||
<td colspan="7"><i class="green check circle icon"></i> No member has joined this network yet.</td>
|
||||
</tr>`);
|
||||
}
|
||||
|
||||
if (data.length > 0 && authroziedCount == 0 && !$("#showUnauthorizedMembers")[0].checked){
|
||||
//All nodes are unauthorized. Show tips to enable unauthorize display
|
||||
tableBody.append(`<tr>
|
||||
<td colspan="7"><i class="yellow exclamation circle icon"></i> Unauthorized nodes detected. Enable "Show Unauthorized Member" to change member access permission.</td>
|
||||
</tr>`);
|
||||
}
|
||||
|
||||
initNameForMembers();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('Error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initNameForMembers(){
|
||||
$(".memberName").each(function(){
|
||||
let addr = $(this).attr("addr");
|
||||
let targetDOM = $(this);
|
||||
$.cjax({
|
||||
url: "./api/gan/members/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
memid: addr,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
$(targetDOM).text("N/A");
|
||||
}else{
|
||||
$(targetDOM).text(data.Name);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function renameMember(targetMemberAddr){
|
||||
if (targetMemberAddr == ""){
|
||||
msgbox("Member address cannot be empty", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
|
||||
if (newname != null && newname.trim() != "") {
|
||||
$.cjax({
|
||||
url: "./api/gan/members/name",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid: currentGANetID,
|
||||
memid: targetMemberAddr,
|
||||
name: newname
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Member Name Updated");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Helper function to check if two objects are equal recursively
|
||||
function objectEqual(obj1, obj2) {
|
||||
// compare types
|
||||
if (typeof obj1 !== typeof obj2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// compare values
|
||||
if (typeof obj1 !== 'object' || obj1 === null) {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
// compare keys
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!keys2.includes(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// recursively compare values
|
||||
if (!objectEqual(obj1[key], obj2[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function changeUnauthorizedVisibility(visable){
|
||||
if(visable){
|
||||
$(".GANetMemberEntity.unauthorized").show();
|
||||
}else{
|
||||
$(".GANetMemberEntity.unauthorized").hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMemberAuth(object){
|
||||
let targetMemberAddr = $(object).attr("addr");
|
||||
let isAuthed = object.checked;
|
||||
$.cjax({
|
||||
url: "./api/gan/members/authorize",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
memid: targetMemberAddr,
|
||||
auth: isAuthed
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
if (isAuthed){
|
||||
msgbox("Member Authorized");
|
||||
}else{
|
||||
msgbox("Member Deauthorized");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleMemberDelete(addr){
|
||||
if (confirm("Confirm delete member " + addr + " ?")){
|
||||
$.cjax({
|
||||
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");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Add and remove this controller node to network as member
|
||||
function ganAddControllerToNetwork(){
|
||||
$(".addControllerToNetworkBtn").addClass("disabled");
|
||||
$(".addControllerToNetworkBtn").addClass("loading");
|
||||
|
||||
$.cjax({
|
||||
url: "./api/gan/network/join",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
},
|
||||
success: function(data){
|
||||
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller joint " + currentGANetID);
|
||||
}
|
||||
setTimeout(function(){
|
||||
renderMemeberTable(true);
|
||||
}, 3000)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ganRemoveControllerFromNetwork(){
|
||||
$(".removeControllerFromNetworkBtn").addClass("disabled");
|
||||
$(".removeControllerFromNetworkBtn").addClass("loading");
|
||||
|
||||
$.cjax({
|
||||
url: "./api/gan/network/leave",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller left " + currentGANetID);
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
$(".removeControllerFromNetworkBtn").removeClass("disabled");
|
||||
$(".removeControllerFromNetworkBtn").removeClass("loading");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Entry points
|
||||
function initGanetDetails(ganetId){
|
||||
currentGANetID = ganetId;
|
||||
$(".ganetID").text(ganetId);
|
||||
initNetNameAndDesc(ganetId);
|
||||
generateIPRangeTable(netRanges);msgbox
|
||||
initNetDetails();
|
||||
renderMemeberTable(true);
|
||||
|
||||
//Setup a listener to listen for member list change
|
||||
if (currentGANNetMemeberListener == undefined){
|
||||
currentGANNetMemeberListener = setInterval(function(){
|
||||
if ($('#networkMemeberTable').length > 0 && currentGANetID){
|
||||
renderMemeberTable();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Exit point
|
||||
function exitToGanList(){
|
||||
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>
|
262
example/plugins/ztnc/web/index.html
Normal file
262
example/plugins/ztnc/web/index.html
Normal file
@ -0,0 +1,262 @@
|
||||
<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 == ""){
|
||||
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){
|
||||
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){
|
||||
msgbox(response.error, false, 5000);
|
||||
}else{
|
||||
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);
|
||||
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){
|
||||
if (confirm("Confirm remove Network " + netid + " PERMANENTLY ?"))
|
||||
$.cjax({
|
||||
url: "./api/gan/network/remove",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id: netid,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
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>
|
229
example/www/html/index.html
Normal file
229
example/www/html/index.html
Normal file
@ -0,0 +1,229 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Zoraxy Firework!</title>
|
||||
<style>
|
||||
body{
|
||||
margin: 0 !important;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js" integrity="sha512-aNMyYYxdIxIaot0Y1/PLuEu3eipGCmsEUBrUq+7aVyPGMFH8z0eTP0tkqAvv34fzN6z+201d3T8HPb1svWSKHQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script>
|
||||
var c = document.getElementById("c");
|
||||
var ctx = c.getContext("2d");
|
||||
var cH;
|
||||
var cW;
|
||||
var bgColor = "#FF6138";
|
||||
var animations = [];
|
||||
var circles = [];
|
||||
|
||||
var colorPicker = (function() {
|
||||
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#FCFCFC", "#282741"];
|
||||
var index = 0;
|
||||
function next() {
|
||||
index = index++ < colors.length-1 ? index : 0;
|
||||
return colors[index];
|
||||
}
|
||||
function current() {
|
||||
return colors[index]
|
||||
}
|
||||
return {
|
||||
next: next,
|
||||
current: current
|
||||
}
|
||||
})();
|
||||
|
||||
function removeAnimation(animation) {
|
||||
var index = animations.indexOf(animation);
|
||||
if (index > -1) animations.splice(index, 1);
|
||||
}
|
||||
|
||||
function calcPageFillRadius(x, y) {
|
||||
var l = Math.max(x - 0, cW - x);
|
||||
var h = Math.max(y - 0, cH - y);
|
||||
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
|
||||
}
|
||||
|
||||
function addClickListeners() {
|
||||
document.addEventListener("touchstart", handleEvent);
|
||||
document.addEventListener("mousedown", handleEvent);
|
||||
};
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.touches) {
|
||||
e.preventDefault();
|
||||
e = e.touches[0];
|
||||
}
|
||||
var currentColor = colorPicker.current();
|
||||
var nextColor = colorPicker.next();
|
||||
var targetR = calcPageFillRadius(e.pageX, e.pageY);
|
||||
var rippleSize = Math.min(200, (cW * .4));
|
||||
var minCoverDuration = 750;
|
||||
|
||||
var pageFill = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: nextColor
|
||||
});
|
||||
var fillAnimation = anime({
|
||||
targets: pageFill,
|
||||
r: targetR,
|
||||
duration: Math.max(targetR / 2 , minCoverDuration ),
|
||||
easing: "easeOutQuart",
|
||||
complete: function(){
|
||||
bgColor = pageFill.fill;
|
||||
removeAnimation(fillAnimation);
|
||||
}
|
||||
});
|
||||
|
||||
var ripple = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: currentColor,
|
||||
stroke: {
|
||||
width: 3,
|
||||
color: currentColor
|
||||
},
|
||||
opacity: 1
|
||||
});
|
||||
var rippleAnimation = anime({
|
||||
targets: ripple,
|
||||
r: rippleSize,
|
||||
opacity: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: 900,
|
||||
complete: removeAnimation
|
||||
});
|
||||
|
||||
var particles = [];
|
||||
for (var i=0; i<32; i++) {
|
||||
var particle = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
fill: currentColor,
|
||||
r: anime.random(24, 48)
|
||||
})
|
||||
particles.push(particle);
|
||||
}
|
||||
var particlesAnimation = anime({
|
||||
targets: particles,
|
||||
x: function(particle){
|
||||
return particle.x + anime.random(rippleSize, -rippleSize);
|
||||
},
|
||||
y: function(particle){
|
||||
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
|
||||
},
|
||||
r: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: anime.random(1000,1300),
|
||||
complete: removeAnimation
|
||||
});
|
||||
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
|
||||
}
|
||||
|
||||
function extend(a, b){
|
||||
for(var key in b) {
|
||||
if(b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var Circle = function(opts) {
|
||||
extend(this, opts);
|
||||
}
|
||||
|
||||
Circle.prototype.draw = function() {
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
|
||||
if (this.stroke) {
|
||||
ctx.strokeStyle = this.stroke.color;
|
||||
ctx.lineWidth = this.stroke.width;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
var animate = anime({
|
||||
duration: Infinity,
|
||||
update: function() {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, cW, cH);
|
||||
animations.forEach(function(anim) {
|
||||
anim.animatables.forEach(function(animatable) {
|
||||
animatable.target.draw();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var resizeCanvas = function() {
|
||||
cW = window.innerWidth;
|
||||
cH = window.innerHeight;
|
||||
c.width = cW * devicePixelRatio;
|
||||
c.height = cH * devicePixelRatio;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
};
|
||||
|
||||
(function init() {
|
||||
resizeCanvas();
|
||||
if (window.CP) {
|
||||
// CodePen's loop detection was causin' problems
|
||||
// and I have no idea why, so...
|
||||
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 6000;
|
||||
}
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
addClickListeners();
|
||||
if (!!window.location.pathname.match(/fullcpgrid/)) {
|
||||
startFauxClicking();
|
||||
}
|
||||
handleInactiveUser();
|
||||
})();
|
||||
|
||||
function handleInactiveUser() {
|
||||
var inactive = setTimeout(function(){
|
||||
fauxClick(cW/2, cH/2);
|
||||
}, 2000);
|
||||
|
||||
function clearInactiveTimeout() {
|
||||
clearTimeout(inactive);
|
||||
document.removeEventListener("mousedown", clearInactiveTimeout);
|
||||
document.removeEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", clearInactiveTimeout);
|
||||
document.addEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
function startFauxClicking() {
|
||||
setTimeout(function(){
|
||||
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
|
||||
startFauxClicking();
|
||||
}, anime.random(200, 900));
|
||||
}
|
||||
|
||||
function fauxClick(x, y) {
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX = x;
|
||||
fauxClick.pageY = y;
|
||||
document.dispatchEvent(fauxClick);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
52
example/www/templates/blacklist.html
Normal file
52
example/www/templates/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
42
example/www/templates/notfound.html
Normal file
42
example/www/templates/notfound.html
Normal file
File diff suppressed because one or more lines are too long
52
example/www/templates/whitelist.html
Normal file
52
example/www/templates/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
Binary file not shown.
BIN
img/title.png
BIN
img/title.png
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 69 KiB |
BIN
img/title.psd
BIN
img/title.psd
Binary file not shown.
@ -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
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -230,7 +231,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rule.AddCountryCodeToBlackList(countryCode, comment)
|
||||
//Check if the country code contains comma, if yes, split it
|
||||
if strings.Contains(countryCode, ",") {
|
||||
codes := strings.Split(countryCode, ",")
|
||||
for _, code := range codes {
|
||||
code = strings.TrimSpace(code)
|
||||
rule.AddCountryCodeToBlackList(code, comment)
|
||||
}
|
||||
} else {
|
||||
countryCode = strings.TrimSpace(countryCode)
|
||||
rule.AddCountryCodeToBlackList(countryCode, comment)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -254,7 +265,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromBlackList(countryCode)
|
||||
//Check if the country code contains comma, if yes, split it
|
||||
if strings.Contains(countryCode, ",") {
|
||||
codes := strings.Split(countryCode, ",")
|
||||
for _, code := range codes {
|
||||
code = strings.TrimSpace(code)
|
||||
rule.RemoveCountryCodeFromBlackList(code)
|
||||
}
|
||||
} else {
|
||||
countryCode = strings.TrimSpace(countryCode)
|
||||
rule.RemoveCountryCodeFromBlackList(countryCode)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -397,7 +418,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
//Check if the country code contains comma, if yes, split it
|
||||
if strings.Contains(countryCode, ",") {
|
||||
codes := strings.Split(countryCode, ",")
|
||||
for _, code := range codes {
|
||||
code = strings.TrimSpace(code)
|
||||
rule.AddCountryCodeToWhitelist(code, comment)
|
||||
}
|
||||
} else {
|
||||
countryCode = strings.TrimSpace(countryCode)
|
||||
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -420,7 +451,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
//Check if the country code contains comma, if yes, split it
|
||||
if strings.Contains(countryCode, ",") {
|
||||
codes := strings.Split(countryCode, ",")
|
||||
for _, code := range codes {
|
||||
code = strings.TrimSpace(code)
|
||||
rule.RemoveCountryCodeFromWhitelist(code)
|
||||
}
|
||||
} else {
|
||||
countryCode = strings.TrimSpace(countryCode)
|
||||
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -505,3 +546,39 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// List all quick ban ip address
|
||||
func handleListQuickBan(w http.ResponseWriter, r *http.Request) {
|
||||
currentSummary := statisticCollector.GetCurrentDailySummary()
|
||||
type quickBanEntry struct {
|
||||
IpAddr string
|
||||
Count int
|
||||
CountryCode string
|
||||
}
|
||||
result := []quickBanEntry{}
|
||||
currentSummary.RequestClientIp.Range(func(key, value interface{}) bool {
|
||||
ip := key.(string)
|
||||
count := value.(int)
|
||||
thisEntry := quickBanEntry{
|
||||
IpAddr: ip,
|
||||
Count: count,
|
||||
}
|
||||
|
||||
//Get the country code
|
||||
geoinfo, err := geodbStore.ResolveCountryCodeFromIP(ip)
|
||||
if err == nil {
|
||||
thisEntry.CountryCode = geoinfo.CountryIsoCode
|
||||
}
|
||||
|
||||
result = append(result, thisEntry)
|
||||
return true
|
||||
})
|
||||
|
||||
//Sort result based on count
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Count > result[j].Count
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
89
src/acme.go
89
src/acme.go
@ -38,7 +38,21 @@ func initACME() *acme.ACMEHandler {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
|
||||
}
|
||||
|
||||
// Restart ACME handler and auto renewer
|
||||
func restartACMEHandler() {
|
||||
SystemWideLogger.Println("Restarting ACME handler")
|
||||
//Clos the current handler and auto renewer
|
||||
acmeHandler.Close()
|
||||
acmeAutoRenewer.Close()
|
||||
acmeDeregisterSpecialRoutingRule()
|
||||
|
||||
//Reinit the handler with a new random port
|
||||
acmeHandler = initACME()
|
||||
|
||||
acmeRegisterSpecialRoutingRule()
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
@ -82,40 +96,69 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
}
|
||||
}
|
||||
|
||||
// remove the special routing rule for ACME
|
||||
func acmeDeregisterSpecialRoutingRule() {
|
||||
SystemWideLogger.Println("Removing ACME routing rule")
|
||||
dynamicProxyRouter.RemoveRoutingRule("acme-autorenew")
|
||||
}
|
||||
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
requireRestorePort80 := false
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Check if port 80 is enabled
|
||||
if !dynamicProxyRouter.Option.ListenOnPort80 {
|
||||
//Enable port 80 temporarily
|
||||
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
requireRestorePort80 = true
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
isForceHttpsRedirectEnabledOriginally = true
|
||||
}
|
||||
|
||||
} else if dynamicProxyRouter.Option.Port == 80 {
|
||||
//Go ahead
|
||||
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
isForceHttpsRedirectEnabledOriginally = true
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
return
|
||||
}
|
||||
|
||||
} else if dynamicProxyRouter.Option.Port == 80 {
|
||||
//Go ahead
|
||||
|
||||
} else {
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
time.Sleep(3 * time.Second)
|
||||
//Add a 2 second delay to make sure everything is settle down
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
//Update the TLS cert store buffer
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if requireRestorePort80 {
|
||||
//Restore port 80 listener
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||
}
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
|
||||
|
314
src/api.go
314
src/api.go
@ -5,8 +5,11 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||
"imuslab.com/zoraxy/mod/ipscan"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -16,34 +19,11 @@ import (
|
||||
API.go
|
||||
|
||||
This file contains all the API called by the web management interface
|
||||
|
||||
*/
|
||||
|
||||
var requireAuth = true
|
||||
|
||||
func initAPIs() {
|
||||
|
||||
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
||||
AuthAgent: authAgent,
|
||||
RequireAuth: requireAuth,
|
||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.FS(webres))
|
||||
if development {
|
||||
fs = http.FileServer(http.Dir("web/"))
|
||||
}
|
||||
//Add a layer of middleware for advance control
|
||||
advHandler := FSHandler(fs)
|
||||
http.Handle("/", advHandler)
|
||||
|
||||
//Authentication APIs
|
||||
registerAuthAPIs(requireAuth)
|
||||
|
||||
//Reverse proxy
|
||||
// Register the APIs for HTTP proxy management functions
|
||||
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
/* Reverse Proxy Settings & Status */
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
@ -54,55 +34,80 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
|
||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||
//Reverse proxy virtual directory APIs
|
||||
/* Reverse proxy upstream (load balance) */
|
||||
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
|
||||
/* Reverse proxy virtual directory */
|
||||
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||
//Reverse proxy user define header apis
|
||||
/* Reverse proxy user-defined header */
|
||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||
//Reverse proxy auth related APIs
|
||||
authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState)
|
||||
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
|
||||
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
|
||||
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
|
||||
authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior)
|
||||
/* Reverse proxy auth related */
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
|
||||
}
|
||||
|
||||
//TLS / SSL config
|
||||
// Register the APIs for TLS / SSL certificate management functions
|
||||
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
|
||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
}
|
||||
|
||||
//Redirection config
|
||||
// Register the APIs for Authentication handlers like Authelia and OAUTH2
|
||||
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
|
||||
}
|
||||
|
||||
// Register the APIs for redirection rules management functions
|
||||
func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||
}
|
||||
|
||||
//Access Rules API
|
||||
// Register the APIs for access rules management functions
|
||||
func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
|
||||
/* Access Rules Settings & Status */
|
||||
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
||||
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
||||
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||
//Blacklist APIs
|
||||
/* Blacklist */
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
//Whitelist APIs
|
||||
/* Whitelist */
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
||||
@ -110,20 +115,37 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
||||
|
||||
//Path Blocker APIs
|
||||
/* Quick Ban List */
|
||||
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
|
||||
}
|
||||
|
||||
// Register the APIs for path blocking rules management functions, WIP
|
||||
func RegisterPathRuleAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
|
||||
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
|
||||
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
|
||||
}
|
||||
|
||||
//Statistic & uptime monitoring API
|
||||
// Register the APIs statistic anlysis and uptime monitoring functions
|
||||
func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
|
||||
/* Traffic Summary */
|
||||
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
||||
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
|
||||
authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
|
||||
/* Zoraxy Analytic */
|
||||
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
||||
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
||||
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
||||
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
/* UpTime Monitor */
|
||||
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
||||
}
|
||||
|
||||
//Global Area Network APIs
|
||||
// Register the APIs for Global Area Network management functions, Will be moving to plugin soon
|
||||
func RegisterGANAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
|
||||
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
|
||||
@ -138,30 +160,70 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
||||
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||
}
|
||||
|
||||
//TCP Proxy
|
||||
authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/start", tcpProxyManager.HandleStartProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
|
||||
// Register the APIs for Stream (TCP / UDP) Proxy management functions
|
||||
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
|
||||
}
|
||||
|
||||
//mDNS APIs
|
||||
// Register the APIs for mDNS service management functions
|
||||
func RegisterMDNSAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
||||
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
|
||||
}
|
||||
|
||||
//Zoraxy Analytic
|
||||
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
||||
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
||||
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
||||
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
// Register the APIs for ACME and Auto Renewer management functions
|
||||
func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
|
||||
/* ACME Core */
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
/* Auto Renewer */
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
|
||||
/* ACME Wizard */
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck)
|
||||
}
|
||||
|
||||
//Network utilities
|
||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
||||
// Register the APIs for Static Web Server management functions
|
||||
func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
|
||||
/* Static Web Server Controls */
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||
/* File Manager */
|
||||
if *allowWebFileManager {
|
||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
|
||||
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
|
||||
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
|
||||
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
|
||||
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
|
||||
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
|
||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// Register the APIs for Network Utilities functions
|
||||
func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
|
||||
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
|
||||
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
||||
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
||||
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||
@ -174,93 +236,49 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
||||
|
||||
//Account Reset
|
||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
||||
|
||||
//ACME & Auto Renewer
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Static Web Server
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||
if *allowWebFileManager {
|
||||
//Web Directory Manager file operation functions
|
||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
|
||||
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
|
||||
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
|
||||
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
|
||||
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
|
||||
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
|
||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
||||
}
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
}
|
||||
|
||||
// Function to renders Auth related APIs
|
||||
func registerAuthAPIs(requireAuth bool) {
|
||||
//Auth APIs
|
||||
http.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||
http.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||
http.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
||||
func RegisterPluginAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins)
|
||||
authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin)
|
||||
authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin)
|
||||
authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon)
|
||||
}
|
||||
|
||||
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||
func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
||||
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
||||
if requireAuth {
|
||||
authAgent.CheckLogin(w, r)
|
||||
} else {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
|
||||
targetMux.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(username)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||
uc := authAgent.GetUserCounts()
|
||||
js, _ := json.Marshal(uc)
|
||||
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(authAgent.GetUserCounts())
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
if authAgent.GetUserCounts() == 0 {
|
||||
//Allow register root admin
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
|
||||
|
||||
})
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
|
||||
} else {
|
||||
//This function is disabled
|
||||
utils.SendErrorResponse(w, "Root management account already exists")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
|
||||
targetMux.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
@ -295,5 +313,61 @@ func registerAuthAPIs(requireAuth bool) {
|
||||
authAgent.UnregisterUser(username)
|
||||
authAgent.CreateUserAccount(username, newPassword, "")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/* Register all the APIs */
|
||||
func initAPIs(targetMux *http.ServeMux) {
|
||||
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
||||
AuthAgent: authAgent,
|
||||
RequireAuth: requireAuth,
|
||||
TargetMux: targetMux,
|
||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.FS(webres))
|
||||
if DEVELOPMENT_BUILD {
|
||||
fs = http.FileServer(http.Dir("web/"))
|
||||
}
|
||||
//Add a layer of middleware for advance control
|
||||
advHandler := FSHandler(fs)
|
||||
targetMux.Handle("/", advHandler)
|
||||
|
||||
//Register the APIs
|
||||
RegisterAuthAPIs(requireAuth, targetMux)
|
||||
RegisterHTTPProxyAPIs(authRouter)
|
||||
RegisterTLSAPIs(authRouter)
|
||||
RegisterAuthenticationHandlerAPIs(authRouter)
|
||||
RegisterRedirectionAPIs(authRouter)
|
||||
RegisterAccessRuleAPIs(authRouter)
|
||||
RegisterPathRuleAPIs(authRouter)
|
||||
RegisterStatisticalAPIs(authRouter)
|
||||
RegisterGANAPIs(authRouter)
|
||||
RegisterStreamProxyAPIs(authRouter)
|
||||
RegisterMDNSAPIs(authRouter)
|
||||
RegisterNetworkUtilsAPIs(authRouter)
|
||||
RegisterACMEAndAutoRenewerAPIs(authRouter)
|
||||
RegisterStaticWebServerAPIs(authRouter)
|
||||
RegisterPluginAPIs(authRouter)
|
||||
|
||||
//Account Reset
|
||||
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
||||
|
||||
//Docker UX Optimizations
|
||||
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
|
||||
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
|
||||
|
||||
//Others
|
||||
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
||||
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
||||
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
}
|
||||
|
82
src/cert.go
82
src/cert.go
@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -46,6 +47,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
UseDNS bool
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
@ -81,12 +83,19 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||
if err == nil {
|
||||
useDNSValidation = certInfo.UseDNS
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
UseDNS: useDNSValidation,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
@ -168,32 +177,36 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Handle front-end toggling TLS mode
|
||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
currentTlsSetting := false
|
||||
currentTlsSetting := true //Default to true
|
||||
if dynamicProxyRouter.Option != nil {
|
||||
currentTlsSetting = dynamicProxyRouter.Option.UseTls
|
||||
}
|
||||
if sysdb.KeyExists("settings", "usetls") {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
newState, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//No setting. Get the current status
|
||||
if r.Method == http.MethodGet {
|
||||
//Get the current status
|
||||
js, _ := json.Marshal(currentTlsSetting)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
} else if r.Method == http.MethodPost {
|
||||
newState, err := utils.PostBool(r, "set")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "new state not set or invalid")
|
||||
return
|
||||
}
|
||||
if newState {
|
||||
sysdb.Write("settings", "usetls", true)
|
||||
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(true)
|
||||
} else if newState == "false" {
|
||||
} else {
|
||||
sysdb.Write("settings", "usetls", false)
|
||||
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
} else {
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,6 +237,51 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle download of the selected certificate
|
||||
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||
// get the certificate name
|
||||
certname, err := utils.GetPara(r, "certname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid certname given")
|
||||
return
|
||||
}
|
||||
certname = filepath.Base(certname) //prevent path escape
|
||||
|
||||
// check if the cert exists
|
||||
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
|
||||
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
|
||||
|
||||
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
||||
//Zip them and serve them via http download
|
||||
seeking, _ := utils.GetBool(r, "seek")
|
||||
if seeking {
|
||||
//This request only check if the key exists. Do not provide download
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
//Serve both file in zip
|
||||
zipTmpFolder := "./tmp/download"
|
||||
os.MkdirAll(zipTmpFolder, 0775)
|
||||
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
||||
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
||||
|
||||
// Serve the zip file
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
http.ServeFile(w, r, zipFileName)
|
||||
} else {
|
||||
//Not both key exists
|
||||
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upload of the certificate
|
||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// check if request method is POST
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -47,18 +48,23 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
}
|
||||
|
||||
//Parse it into dynamic proxy endpoint
|
||||
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||
thisConfigEndpoint := dynamicproxy.GetDefaultProxyEndpoint()
|
||||
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Make sure the tags are not nil
|
||||
if thisConfigEndpoint.Tags == nil {
|
||||
thisConfigEndpoint.Tags = []string{}
|
||||
}
|
||||
|
||||
//Matching domain not set. Assume root
|
||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||
}
|
||||
|
||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||
//This is a root config file
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
@ -67,7 +73,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||
|
||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
|
||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
|
||||
//This is a host config file
|
||||
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
@ -79,7 +85,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
return errors.New("not supported proxy type")
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.ActiveOrigins)+" routing rule loaded", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -96,7 +102,7 @@ func filterProxyConfigFilename(filename string) string {
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//Get filename for saving
|
||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
@ -128,21 +134,23 @@ func RemoveReverseProxyConfig(endpoint string) error {
|
||||
// Get the default root config that point to the internal static web server
|
||||
// this will be used if root config is not found (new deployment / missing root.config file)
|
||||
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
//Get the default proxy endpoint
|
||||
rootProxyEndpointConfig := dynamicproxy.GetDefaultProxyEndpoint()
|
||||
rootProxyEndpointConfig.ProxyType = dynamicproxy.ProxyTypeRoot
|
||||
rootProxyEndpointConfig.RootOrMatchingDomain = "/"
|
||||
rootProxyEndpointConfig.ActiveOrigins = []*loadbalance.Upstream{
|
||||
{
|
||||
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
|
||||
RequireTLS: false,
|
||||
SkipCertValidations: false,
|
||||
Weight: 0,
|
||||
},
|
||||
}
|
||||
rootProxyEndpointConfig.DefaultSiteOption = dynamicproxy.DefaultSite_InternalStaticWebServer
|
||||
rootProxyEndpointConfig.DefaultSiteValue = ""
|
||||
|
||||
//Default settings
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
|
||||
ProxyType: dynamicproxy.ProxyType_Root,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
|
||||
RequireTLS: false,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: false,
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||
DefaultSiteValue: "",
|
||||
})
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&rootProxyEndpointConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -155,29 +163,32 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
*/
|
||||
|
||||
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
|
||||
includeSysDBRaw, _ := utils.GetPara(r, "includeDB")
|
||||
includeSysDB := false
|
||||
if includeSysDBRaw == "true" {
|
||||
//Include the system database in backup snapshot
|
||||
//Temporary set it to read only
|
||||
sysdb.ReadOnly = true
|
||||
includeSysDB = true
|
||||
}
|
||||
|
||||
// Specify the folder path to be zipped
|
||||
folderPath := "./conf/"
|
||||
if !utils.FileExists("./conf") {
|
||||
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
||||
return
|
||||
}
|
||||
folderPath := "./conf"
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
// Set the Content-Disposition header to specify the file name
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
|
||||
// Set the Content-Disposition header to specify the file name, add timestamp to the filename
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"")
|
||||
|
||||
// Create a zip writer
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add files to the zip
|
||||
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
err := filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -220,7 +231,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open("sys.db")
|
||||
file, err := os.Open("./sys.db")
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
|
||||
return
|
||||
@ -234,8 +245,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Restore sysdb state
|
||||
sysdb.ReadOnly = false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -271,6 +280,8 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
targetDir := "./conf"
|
||||
if utils.FileExists(targetDir) {
|
||||
//Backup the old config to old
|
||||
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
||||
//os.Rename(*path_conf, backupPath)
|
||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||
}
|
||||
|
||||
|
154
src/def.go
Normal file
154
src/def.go
Normal file
@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
Type and flag definations
|
||||
|
||||
This file contains all the type and flag definations
|
||||
Author: tobychui
|
||||
*/
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/auth/sso/authelia"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/info/logviewer"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
"imuslab.com/zoraxy/mod/plugins"
|
||||
"imuslab.com/zoraxy/mod/sshprox"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
"imuslab.com/zoraxy/mod/streamproxy"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.1.9"
|
||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||
|
||||
/* System Constants */
|
||||
TMP_FOLDER = "./tmp"
|
||||
WEBSERV_DEFAULT_PORT = 5487
|
||||
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
|
||||
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
|
||||
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
|
||||
MDNS_IDENTIFY_VENDOR = "imuslab.com"
|
||||
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
|
||||
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
|
||||
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
|
||||
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
|
||||
CSRF_COOKIENAME = "zoraxy_csrf"
|
||||
LOG_PREFIX = "zr"
|
||||
LOG_EXTENSION = ".log"
|
||||
|
||||
/* Configuration Folder Storage Path Constants */
|
||||
CONF_HTTP_PROXY = "./conf/proxy"
|
||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||
CONF_CERT_STORE = "./conf/certs"
|
||||
CONF_REDIRECTION = "./conf/redirect"
|
||||
CONF_ACCESS_RULE = "./conf/access"
|
||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||
)
|
||||
|
||||
/* System Startup Flags */
|
||||
var (
|
||||
webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
databaseBackend = flag.String("db", "auto", "Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV")
|
||||
noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
showver = flag.Bool("version", false, "Show version of this server")
|
||||
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
|
||||
ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
|
||||
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
|
||||
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
|
||||
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||
|
||||
/* Default Configuration Flags */
|
||||
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
|
||||
defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default")
|
||||
|
||||
/* Path Configuration Flags */
|
||||
//path_database = flag.String("dbpath", "./sys.db", "Database path")
|
||||
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
|
||||
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
|
||||
path_logFile = flag.String("log", "./log", "Log folder path")
|
||||
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
|
||||
|
||||
/* Maintaince Function Flags */
|
||||
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
|
||||
)
|
||||
|
||||
/* Global Variables and Handlers */
|
||||
var (
|
||||
nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup
|
||||
bootTime = time.Now().Unix()
|
||||
requireAuth = true //Require authentication for webmin panel, override from flag
|
||||
|
||||
/*
|
||||
Binary Embedding File System
|
||||
*/
|
||||
//go:embed web/*
|
||||
webres embed.FS
|
||||
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
|
||||
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
|
||||
|
||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
||||
accessController *access.Controller //Access controller, handle black list and white list
|
||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||
webSshManager *sshprox.Manager //Web SSH connection service
|
||||
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//Authentication Provider
|
||||
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
LogViewer *logviewer.Viewer //Log viewer HTTP handlers
|
||||
)
|
@ -25,12 +25,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "port must be a valid integer")
|
||||
@ -76,7 +70,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
//Set the email sender properties
|
||||
thisEmailSender := email.Sender{
|
||||
Hostname: strings.TrimSpace(hostname),
|
||||
Domain: strings.TrimSpace(domain),
|
||||
Port: port,
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
@ -206,7 +199,7 @@ var (
|
||||
)
|
||||
|
||||
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if EmailSender.Username == "" || EmailSender.Domain == "" {
|
||||
if EmailSender.Username == "" {
|
||||
//Reset account not setup
|
||||
utils.SendErrorResponse(w, "Reset account not setup.")
|
||||
return
|
||||
@ -279,17 +272,14 @@ func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Delete the user account
|
||||
authAgent.UnregisterUser(username)
|
||||
|
||||
//Ok. Set the new password
|
||||
err = authAgent.CreateUserAccount(username, newPassword, "")
|
||||
if err != nil {
|
||||
// Un register the user account
|
||||
if err := authAgent.UnregisterUser(username); err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
//Ok. Set the new password
|
||||
if err := authAgent.CreateUserAccount(username, newPassword, ""); err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
204
src/go.mod
204
src/go.mod
@ -1,34 +1,216 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/go-session/session v3.1.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
||||
github.com/tidwall/buntdb v1.1.2 // indirect
|
||||
github.com/tidwall/gjson v1.12.1 // indirect
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.112.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-resty/resty/v2 v2.16.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/linode/linodego v1.44.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.10.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.8 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
google.golang.org/api v0.214.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
1048
src/go.sum
1048
src/go.sum
File diff suppressed because it is too large
Load Diff
180
src/main.go
180
src/main.go
@ -1,7 +1,36 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
______
|
||||
|___ /
|
||||
/ / ___ _ __ __ ___ ___ _
|
||||
/ / / _ \| '__/ _` \ \/ / | | |
|
||||
/ /_| (_) | | | (_| |> <| |_| |
|
||||
/_____\___/|_| \__,_/_/\_\\__, |
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
Zoraxy - A general purpose HTTP reverse proxy and forwarding tool
|
||||
Author: tobychui
|
||||
License: AGPLv3
|
||||
|
||||
--------------------------------------------
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, version 3 of the License or any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -12,88 +41,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"github.com/gorilla/csrf"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
"imuslab.com/zoraxy/mod/sshprox"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
"imuslab.com/zoraxy/mod/tcpprox"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/update"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
// General flags
|
||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
|
||||
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
|
||||
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "3.0.2"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
|
||||
/*
|
||||
Binary Embedding File System
|
||||
*/
|
||||
//go:embed web/*
|
||||
webres embed.FS
|
||||
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
||||
accessController *access.Controller //Access controller, handle black list and white list
|
||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||
webSshManager *sshprox.Manager //Web SSH connection service
|
||||
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
/* SIGTERM handler, do shutdown sequences before closing */
|
||||
func SetupCloseHandler() {
|
||||
c := make(chan os.Signal, 2)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-c
|
||||
ShutdownSeq()
|
||||
@ -101,47 +58,35 @@ func SetupCloseHandler() {
|
||||
}()
|
||||
}
|
||||
|
||||
func ShutdownSeq() {
|
||||
fmt.Println("- Shutting down " + name)
|
||||
fmt.Println("- Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
fmt.Println("- Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
if mdnsTickerStop != nil {
|
||||
fmt.Println("- Stopping mDNS Discoverer (might take a few minutes)")
|
||||
// Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
}
|
||||
|
||||
mdnsScanner.Close()
|
||||
fmt.Println("- Closing Certificates Auto Renewer")
|
||||
acmeAutoRenewer.Close()
|
||||
//Remove the tmp folder
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
fmt.Println("- Closing system wide logger")
|
||||
SystemWideLogger.Close()
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
|
||||
/* Maintaince Function Modes */
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
|
||||
os.Exit(0)
|
||||
}
|
||||
if *geoDbUpdate {
|
||||
geodb.DownloadGeoDBUpdate("./conf/geodb")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
/* Main Zoraxy Routines */
|
||||
if !utils.ValidateListeningAddress(*webUIPort) {
|
||||
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *enableAutoUpdate {
|
||||
fmt.Println("Checking required config update")
|
||||
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
|
||||
}
|
||||
|
||||
SetupCloseHandler()
|
||||
|
||||
//Read or create the system uuid
|
||||
uuidRecord := "./sys.uuid"
|
||||
uuidRecord := *path_uuid
|
||||
if !utils.FileExists(uuidRecord) {
|
||||
newSystemUUID := uuid.New().String()
|
||||
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
|
||||
@ -153,12 +98,22 @@ func main() {
|
||||
}
|
||||
nodeUUID = string(uuidBytes)
|
||||
|
||||
//Startup all modules
|
||||
//Create a new webmin mux and csrf middleware layer
|
||||
webminPanelMux = http.NewServeMux()
|
||||
csrfMiddleware = csrf.Protect(
|
||||
[]byte(nodeUUID),
|
||||
csrf.CookieName(CSRF_COOKIENAME),
|
||||
csrf.Secure(false),
|
||||
csrf.Path("/"),
|
||||
csrf.SameSite(csrf.SameSiteLaxMode),
|
||||
)
|
||||
|
||||
//Startup all modules, see start.go
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs()
|
||||
initAPIs(webminPanelMux)
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
@ -170,11 +125,10 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, nil)
|
||||
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -69,6 +69,9 @@ func NewAccessController(options *Options) (*Controller, error) {
|
||||
Options: options,
|
||||
}
|
||||
|
||||
//Assign default access rule parent
|
||||
thisController.DefaultAccessRule.parent = &thisController
|
||||
|
||||
//Load all acccess rules from file
|
||||
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
|
||||
if err != nil {
|
||||
@ -113,6 +116,7 @@ func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
|
||||
// Load access rules to runtime, require rule ID
|
||||
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
|
||||
if accessRuleID == "default" || accessRuleID == "" {
|
||||
|
||||
return c.DefaultAccessRule, nil
|
||||
}
|
||||
//Load from sync.Map, should be O(1)
|
||||
|
@ -2,6 +2,8 @@ package access
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -71,5 +73,22 @@ func (s *AccessRule) GetAllBlacklistedIp() []string {
|
||||
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
|
||||
IPBlacklist := *s.BlackListIP
|
||||
_, ok := IPBlacklist[ipAddr]
|
||||
return ok
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for CIDR
|
||||
for ipOrCIDR, _ := range IPBlacklist {
|
||||
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -22,17 +21,29 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
var defaultNameservers = []string{
|
||||
"8.8.8.8:53", // Google DNS
|
||||
"8.8.4.4:53", // Google DNS
|
||||
"1.1.1.1:53", // Cloudflare DNS
|
||||
"1.0.0.1:53", // Cloudflare DNS
|
||||
}
|
||||
|
||||
type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
AcmeName string `json:"acme_name"` //ACME provider name
|
||||
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
|
||||
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
|
||||
UseDNS bool `json:"dns"` //Use DNS challenge
|
||||
PropTimeout int `json:"prop_time"` //Propagation timeout
|
||||
DNSServers []string `json:"dnsServers"` // DNS servers
|
||||
}
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
@ -67,25 +78,38 @@ type ACMEHandler struct {
|
||||
DefaultAcmeServer string
|
||||
Port string
|
||||
Database *database.Database
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
// NewACME creates a new ACMEHandler instance.
|
||||
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
|
||||
func NewACME(defaultAcmeServer string, port string, database *database.Database, logger *logger.Logger) *ACMEHandler {
|
||||
return &ACMEHandler{
|
||||
DefaultAcmeServer: acmeServer,
|
||||
DefaultAcmeServer: defaultAcmeServer,
|
||||
Port: port,
|
||||
Database: database,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ACMEHandler) Logf(message string, err error) {
|
||||
a.Logger.PrintAndLog("ACME", message, err)
|
||||
}
|
||||
|
||||
// Close closes the ACMEHandler.
|
||||
// ACME Handler does not need to close anything
|
||||
// Function defined for future compatibility
|
||||
func (a *ACMEHandler) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for the specified domains.
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) {
|
||||
log.Println("[ACME] Obtaining certificate...")
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int, dnsServers string) (bool, error) {
|
||||
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
|
||||
|
||||
// generate private key
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Private key generation failed", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
@ -101,7 +125,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
// skip TLS verify if need
|
||||
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
|
||||
if skipTLS {
|
||||
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server")
|
||||
a.Logf("Ignoring TLS/SSL Verification Error for ACME Server", nil)
|
||||
config.HTTPClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
@ -116,6 +140,11 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to Let's Encrypt if it is not set
|
||||
if caName == "" {
|
||||
caName = "Let's Encrypt"
|
||||
}
|
||||
|
||||
// setup the custom ACME url endpoint.
|
||||
if caUrl != "" {
|
||||
config.CADirURL = caUrl
|
||||
@ -123,16 +152,16 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
// if not custom ACME url, load it from ca.json
|
||||
if caName == "custom" {
|
||||
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
|
||||
a.Logf("Using Custom ACME "+caUrl+" for CA Directory URL", nil)
|
||||
} else {
|
||||
caLinkOverwrite, err := loadCAApiServerFromName(caName)
|
||||
if err == nil {
|
||||
config.CADirURL = caLinkOverwrite
|
||||
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
|
||||
a.Logf("Using "+caLinkOverwrite+" for CA Directory URL", nil)
|
||||
} else {
|
||||
// (caName == "" || caUrl == "") will use default acme
|
||||
config.CADirURL = a.DefaultAcmeServer
|
||||
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
|
||||
a.Logf("Using Default ACME "+a.DefaultAcmeServer+" for CA Directory URL", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,15 +169,74 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to spawn new ACME client from current config", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Load certificate info from JSON file
|
||||
certInfo, err := LoadCertInfoJSON(fmt.Sprintf("./conf/certs/%s.json", certificateName))
|
||||
if err == nil {
|
||||
useDNS = certInfo.UseDNS
|
||||
if dnsServers == "" && certInfo.DNSServers != nil && len(certInfo.DNSServers) > 0 {
|
||||
dnsServers = strings.Join(certInfo.DNSServers, ",")
|
||||
}
|
||||
propagationTimeout = certInfo.PropTimeout
|
||||
}
|
||||
|
||||
// Clean DNS servers
|
||||
dnsNameservers := strings.Split(dnsServers, ",")
|
||||
for i := range dnsNameservers {
|
||||
dnsNameservers[i] = strings.TrimSpace(dnsNameservers[i])
|
||||
}
|
||||
|
||||
// setup how to receive challenge
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
if useDNS {
|
||||
if !a.Database.TableExists("acme") {
|
||||
a.Database.NewTable("acme")
|
||||
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -1)")
|
||||
}
|
||||
|
||||
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
|
||||
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -2)")
|
||||
}
|
||||
|
||||
var dnsCredentials string
|
||||
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
|
||||
if err != nil {
|
||||
a.Logf("Read DNS credential failed", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
var dnsProvider string
|
||||
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
|
||||
if err != nil {
|
||||
a.Logf("Read DNS Provider failed", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
|
||||
if err != nil {
|
||||
a.Logf("Unable to resolve DNS challenge provider", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(dnsNameservers) > 0 && dnsNameservers[0] != "" {
|
||||
a.Logf("Using DNS servers: "+strings.Join(dnsNameservers, ", "), nil)
|
||||
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dnsNameservers))
|
||||
} else {
|
||||
// Use default DNS-01 nameservers if dnsServers is empty
|
||||
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(defaultNameservers))
|
||||
}
|
||||
if err != nil {
|
||||
a.Logf("Failed to resolve DNS01 Provider", err)
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
a.Logf("Failed to resolve HTTP01 Provider", err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
@ -162,7 +250,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
var reg *registration.Resource
|
||||
// New users will need to register
|
||||
if client.GetExternalAccountRequired() {
|
||||
log.Println("External Account Required for this ACME Provider.")
|
||||
a.Logf("External Account Required for this ACME Provider", nil)
|
||||
// IF KID and HmacEncoded is overidden
|
||||
|
||||
if !a.Database.TableExists("acme") {
|
||||
@ -177,20 +265,18 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
var kid string
|
||||
var hmacEncoded string
|
||||
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to read kid from database", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to read HMAC from database", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
|
||||
a.Logf("EAB Credential retrieved: "+kid+" / "+hmacEncoded, nil)
|
||||
if kid != "" && hmacEncoded != "" {
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
@ -199,14 +285,14 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Register with external account binder failed", err)
|
||||
return false, err
|
||||
}
|
||||
//return false, errors.New("External Account Required for this ACME Provider.")
|
||||
} else {
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Unable to register client", err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
@ -219,7 +305,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Obtain certificate failed", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
@ -227,31 +313,34 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
// private key, and a certificate URL.
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to write public key to disk", err)
|
||||
return false, err
|
||||
}
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to write private key to disk", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Save certificate's ACME info for renew usage
|
||||
certInfo := &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
certInfo = &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
PropTimeout: propagationTimeout,
|
||||
DNSServers: dnsNameservers,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Marshal certificate renew config failed", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to write certificate renew config to file", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
@ -269,7 +358,7 @@ func (a *ACMEHandler) CheckCertificate() []string {
|
||||
expiredCerts := []string{}
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
a.Logf("Failed to load certificate folder", err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
@ -353,6 +442,8 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
//Make sure the wildcard * do not goes into the filename
|
||||
filename = strings.ReplaceAll(filename, "*", "_")
|
||||
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
@ -364,14 +455,14 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
ca, err := utils.PostPara(r, "ca")
|
||||
if err != nil {
|
||||
log.Println("[INFO] CA not set. Using default")
|
||||
a.Logf("CA not set. Using default", nil)
|
||||
ca, caUrl = "", ""
|
||||
}
|
||||
|
||||
if ca == "custom" {
|
||||
caUrl, err = utils.PostPara(r, "caURL")
|
||||
if err != nil {
|
||||
log.Println("[INFO] Custom CA set but no URL provide, Using default")
|
||||
a.Logf("Custom CA set but no URL provide, Using default", nil)
|
||||
ca, caUrl = "", ""
|
||||
}
|
||||
}
|
||||
@ -391,8 +482,55 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
skipTLS = true
|
||||
}
|
||||
|
||||
var dns bool
|
||||
|
||||
if dnsString, err := utils.PostPara(r, "dns"); err != nil {
|
||||
dns = false
|
||||
} else if dnsString != "true" {
|
||||
dns = false
|
||||
} else {
|
||||
dns = true
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
|
||||
|
||||
// Default propagation timeout is 300 seconds
|
||||
propagationTimeout := 300
|
||||
if dns {
|
||||
ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
|
||||
if err == nil {
|
||||
propagationTimeout, err = strconv.Atoi(ppgTimeout)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid propagation timeout value")
|
||||
return
|
||||
}
|
||||
if propagationTimeout < 60 {
|
||||
//Minimum propagation timeout is 60 seconds
|
||||
propagationTimeout = 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Clean spaces in front or behind each domain
|
||||
cleanedDomains := []string{}
|
||||
for _, domain := range domains {
|
||||
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
|
||||
}
|
||||
|
||||
// Extract DNS servers from the request
|
||||
var dnsServers []string
|
||||
dnsServersPara, err := utils.PostPara(r, "dnsServers")
|
||||
if err == nil && dnsServersPara != "" {
|
||||
dnsServers = strings.Split(dnsServersPara, ",")
|
||||
for i := range dnsServers {
|
||||
dnsServers[i] = strings.TrimSpace(dnsServers[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Convert DNS servers slice to a single string
|
||||
dnsServersString := strings.Join(dnsServers, ",")
|
||||
|
||||
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout, dnsServersString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
@ -404,7 +542,7 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
func jsonEscape(i string) string {
|
||||
b, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
log.Println("Unable to escape json data: " + err.Error())
|
||||
//log.Println("Unable to escape json data: " + err.Error())
|
||||
return i
|
||||
}
|
||||
s := string(b)
|
||||
@ -424,7 +562,7 @@ func IsPortInUse(port int) bool {
|
||||
}
|
||||
|
||||
// Load cert information from json file
|
||||
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
certInfoBytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -435,5 +573,10 @@ func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clean DNS servers
|
||||
for i := range certInfo.DNSServers {
|
||||
certInfo.DNSServers[i] = strings.TrimSpace(certInfo.DNSServers[i])
|
||||
}
|
||||
|
||||
return certInfo, nil
|
||||
}
|
||||
|
56
src/mod/acme/acme_dns.go
Normal file
56
src/mod/acme/acme_dns.go
Normal file
@ -0,0 +1,56 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
// Preprocessor function to get DNS challenge provider by name
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
|
||||
//Unpack the dnsCredentials (json string) to map
|
||||
var dnsCredentialsMap map[string]interface{}
|
||||
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Clear the PollingInterval and PropagationTimeout field and conert to int
|
||||
userDefinedPollingInterval := 2
|
||||
if dnsCredentialsMap["PollingInterval"] != nil {
|
||||
userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
|
||||
delete(dnsCredentialsMap, "PollingInterval")
|
||||
convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
|
||||
if err == nil {
|
||||
userDefinedPollingInterval = convertedPollingInterval
|
||||
}
|
||||
}
|
||||
|
||||
userDefinedPropagationTimeout := ppgTimeout
|
||||
if dnsCredentialsMap["PropagationTimeout"] != nil {
|
||||
userDefinedPropagationTimeoutRaw := dnsCredentialsMap["PropagationTimeout"].(string)
|
||||
delete(dnsCredentialsMap, "PropagationTimeout")
|
||||
convertedPropagationTimeout, err := strconv.Atoi(userDefinedPropagationTimeoutRaw)
|
||||
if err == nil {
|
||||
//Overwrite the default propagation timeout if it is requeted from UI
|
||||
userDefinedPropagationTimeout = convertedPropagationTimeout
|
||||
}
|
||||
}
|
||||
|
||||
//Restructure dnsCredentials string from map
|
||||
dnsCredentialsBytes, err := json.Marshal(dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsCredentials = string(dnsCredentialsBytes)
|
||||
|
||||
//Using acmedns CICD pipeline generated datatype to optain the DNS provider
|
||||
return acmedns.GetDNSProviderByJsonConfig(
|
||||
dnsProvider,
|
||||
dnsCredentials,
|
||||
int64(userDefinedPropagationTimeout),
|
||||
int64(userDefinedPollingInterval),
|
||||
)
|
||||
}
|
1275
src/mod/acme/acmedns/acmedns.go
Normal file
1275
src/mod/acme/acmedns/acmedns.go
Normal file
File diff suppressed because it is too large
Load Diff
27
src/mod/acme/acmedns/acmedns_test.go
Normal file
27
src/mod/acme/acmedns/acmedns_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package acmedns_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
// Test if the structure of ACME DNS config can be reflected from lego source code definations
|
||||
func TestACMEDNSConfigStructureReflector(t *testing.T) {
|
||||
providers := []string{
|
||||
"gandi",
|
||||
"cloudflare",
|
||||
"azure",
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
strcture, err := acmedns.GetProviderConfigStructure(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(strcture)
|
||||
}
|
||||
|
||||
}
|
3581
src/mod/acme/acmedns/providers.json
Normal file
3581
src/mod/acme/acmedns/providers.json
Normal file
File diff suppressed because it is too large
Load Diff
80
src/mod/acme/acmedns/providerutils.go
Normal file
80
src/mod/acme/acmedns/providerutils.go
Normal file
@ -0,0 +1,80 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
//go:embed providers.json
|
||||
var providers []byte //A list of providers generated by acmedns code-generator
|
||||
|
||||
type ConfigTemplate struct {
|
||||
Name string `json:"Name"`
|
||||
ConfigableFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"ConfigableFields"`
|
||||
HiddenFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"HiddenFields"`
|
||||
}
|
||||
|
||||
// Return a map of string => datatype
|
||||
func GetProviderConfigStructure(providerName string) (map[string]string, error) {
|
||||
//Load the target config template from embedded providers.json
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
|
||||
targetConfigTemplate, ok := configTemplateMap[providerName]
|
||||
if !ok {
|
||||
return map[string]string{}, errors.New("provider not supported")
|
||||
}
|
||||
|
||||
results := map[string]string{}
|
||||
for _, field := range targetConfigTemplate.ConfigableFields {
|
||||
results[field.Title] = field.Datatype
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// HandleServeProvidersJson return the list of supported providers as json
|
||||
func HandleServeProvidersJson(w http.ResponseWriter, r *http.Request) {
|
||||
providerName, _ := utils.GetPara(r, "name")
|
||||
if providerName == "" {
|
||||
//Send the current list of providers
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load DNS provider")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse the provider names into an array
|
||||
providers := []string{}
|
||||
for providerName, _ := range configTemplateMap {
|
||||
providers = append(providers, providerName)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(providers)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
//Get the config for that provider
|
||||
confTemplate, err := GetProviderConfigStructure(providerName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(confTemplate)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
@ -75,6 +75,15 @@ func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
|
||||
httpServerReachable := isHTTPServerAvailable(domain)
|
||||
js, _ := json.Marshal(httpServerReachable)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if stepNo == 10 {
|
||||
//Resolve public Ip address for tour
|
||||
publicIp, err := getPublicIPAddress()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
js, _ := json.Marshal(publicIp)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid step number")
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -26,6 +26,7 @@ type AutoRenewConfig struct {
|
||||
Email string //Email for acme
|
||||
RenewAll bool //Renew all or selective renew with the slice below
|
||||
FilesToRenew []string //If RenewAll is false, renew these certificate files
|
||||
DNSServers string // DNS servers
|
||||
}
|
||||
|
||||
type AutoRenewer struct {
|
||||
@ -34,7 +35,9 @@ type AutoRenewer struct {
|
||||
AcmeHandler *ACMEHandler
|
||||
RenewerConfig *AutoRenewConfig
|
||||
RenewTickInterval int64
|
||||
EarlyRenewDays int //How many days before cert expire to renew certificate
|
||||
TickerstopChan chan bool
|
||||
Logger *logger.Logger //System wide logger
|
||||
}
|
||||
|
||||
type ExpiredCerts struct {
|
||||
@ -44,11 +47,15 @@ type ExpiredCerts struct {
|
||||
|
||||
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
|
||||
// Set renew check interval to 0 for auto (1 day)
|
||||
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
|
||||
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
|
||||
if renewCheckInterval == 0 {
|
||||
renewCheckInterval = 86400 //1 day
|
||||
}
|
||||
|
||||
if earlyRenewDays == 0 {
|
||||
earlyRenewDays = 30
|
||||
}
|
||||
|
||||
//Load the config file. If not found, create one
|
||||
if !utils.FileExists(config) {
|
||||
//Create one
|
||||
@ -82,8 +89,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
AcmeHandler: AcmeHandler,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
EarlyRenewDays: earlyRenewDays,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
|
||||
|
||||
if thisRenewer.RenewerConfig.Enabled {
|
||||
//Start the renew ticker
|
||||
thisRenewer.StartAutoRenewTicker()
|
||||
@ -95,6 +106,10 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
return &thisRenewer, nil
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) Logf(message string, err error) {
|
||||
a.Logger.PrintAndLog("cert-renew", message, err)
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) StartAutoRenewTicker() {
|
||||
//Stop the previous ticker if still running
|
||||
if a.TickerstopChan != nil {
|
||||
@ -113,7 +128,7 @@ func (a *AutoRenewer) StartAutoRenewTicker() {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Println("Check and renew certificates in progress")
|
||||
a.Logf("Check and renew certificates in progress", nil)
|
||||
a.CheckAndRenewCertificates()
|
||||
}
|
||||
}
|
||||
@ -135,7 +150,7 @@ func (a *AutoRenewer) StopAutoRenewTicker() {
|
||||
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
|
||||
// opr = setAuto -> Set to use auto detect certificates and renew
|
||||
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
|
||||
opr, err := utils.GetPara(r, "opr")
|
||||
opr, err := utils.PostPara(r, "opr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Operation not set")
|
||||
return
|
||||
@ -165,6 +180,8 @@ func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.R
|
||||
a.RenewerConfig.RenewAll = true
|
||||
a.saveRenewConfigToFile()
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid operation given")
|
||||
}
|
||||
|
||||
}
|
||||
@ -208,42 +225,52 @@ func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// HandleAutoRenewEnable get and set the auto renew enable state
|
||||
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
|
||||
val, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
if r.Method == http.MethodGet {
|
||||
js, _ := json.Marshal(a.RenewerConfig.Enabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if val == "true" {
|
||||
} else if r.Method == http.MethodPost {
|
||||
val, err := utils.PostBool(r, "enable")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty enable state")
|
||||
}
|
||||
if val {
|
||||
//Check if the email is not empty
|
||||
if a.RenewerConfig.Email == "" {
|
||||
utils.SendErrorResponse(w, "Email is not set")
|
||||
return
|
||||
}
|
||||
|
||||
a.RenewerConfig.Enabled = true
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew enabled")
|
||||
a.Logf("ACME auto renew enabled", nil)
|
||||
a.StartAutoRenewTicker()
|
||||
} else {
|
||||
a.RenewerConfig.Enabled = false
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew disabled")
|
||||
a.Logf("ACME auto renew disabled", nil)
|
||||
a.StopAutoRenewTicker()
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
email, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current email to user
|
||||
js, _ := json.Marshal(a.RenewerConfig.Email)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
} else if r.Method == http.MethodPost {
|
||||
email, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty email given")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the email is valid
|
||||
_, err := mail.ParseAddress(email)
|
||||
_, err = mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -252,8 +279,11 @@ func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
|
||||
//Set the new config
|
||||
a.RenewerConfig.Email = email
|
||||
a.saveRenewConfigToFile()
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// Check and renew certificates. This check all the certificates in the
|
||||
@ -263,7 +293,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
certFolder := a.CertFolder
|
||||
files, err := os.ReadDir(certFolder)
|
||||
if err != nil {
|
||||
log.Println("Unable to renew certificates: " + err.Error())
|
||||
a.Logf("Read certificate store failed", err)
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
@ -277,13 +307,12 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
||||
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -305,13 +334,12 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
||||
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -327,6 +355,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
return a.renewExpiredDomains(expiredCertList)
|
||||
}
|
||||
|
||||
// Close the auto renewer
|
||||
func (a *AutoRenewer) Close() {
|
||||
if a.TickerstopChan != nil {
|
||||
a.TickerstopChan <- true
|
||||
@ -338,29 +367,41 @@ func (a *AutoRenewer) Close() {
|
||||
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
|
||||
renewedCertFiles := []string{}
|
||||
for _, expiredCert := range certs {
|
||||
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
|
||||
a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
|
||||
fileName := filepath.Base(expiredCert.Filepath)
|
||||
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
||||
|
||||
// Load certificate info for ACME detail
|
||||
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
|
||||
certInfo, err := loadCertInfoJSON(certInfoFilename)
|
||||
certInfo, err := LoadCertInfoJSON(certInfoFilename)
|
||||
if err != nil {
|
||||
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
|
||||
a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
|
||||
|
||||
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
|
||||
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
|
||||
a.Logf("Extract issuer name for cert error, using default ca", err)
|
||||
certInfo = &CertificateInfoJSON{}
|
||||
} else {
|
||||
certInfo = &CertificateInfoJSON{AcmeName: CAName}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
|
||||
//For upgrading config from older version of Zoraxy which don't have timeout
|
||||
if certInfo.PropTimeout == 0 {
|
||||
//Set default timeout
|
||||
certInfo.PropTimeout = 300
|
||||
}
|
||||
|
||||
// Extract DNS servers from the certificate info if available
|
||||
var dnsServers string
|
||||
if len(certInfo.DNSServers) > 0 {
|
||||
dnsServers = strings.Join(certInfo.DNSServers, ",")
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
|
||||
if err != nil {
|
||||
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
||||
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
||||
} else {
|
||||
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
|
||||
a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
|
||||
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
|
||||
}
|
||||
}
|
||||
@ -404,3 +445,40 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
||||
// Handle update auto renew DNS configuration
|
||||
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsProvider not set")
|
||||
return
|
||||
}
|
||||
|
||||
dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsCredentials not set")
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := utils.PostPara(r, "filename")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "filename not set")
|
||||
return
|
||||
}
|
||||
|
||||
dnsServers, err := utils.PostPara(r, "dnsServers")
|
||||
if err != nil {
|
||||
dnsServers = ""
|
||||
}
|
||||
|
||||
if !a.AcmeHandler.Database.TableExists("acme") {
|
||||
a.AcmeHandler.Database.NewTable("acme")
|
||||
}
|
||||
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package acme
|
||||
/*
|
||||
CA.go
|
||||
|
||||
This script load CA defination from embedded ca.json
|
||||
This script load CA definition from embedded ca.json
|
||||
*/
|
||||
import (
|
||||
_ "embed"
|
||||
@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CA Defination, load from embeded json when startup
|
||||
// CA definition, load from embeded json when startup
|
||||
type CaDef struct {
|
||||
Production map[string]string
|
||||
Test map[string]string
|
||||
|
@ -5,14 +5,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get the issuer name from pem file
|
||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||
// Read the PEM file
|
||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
||||
pemData, err := os.ReadFile(pemFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -81,13 +81,14 @@ func CertIsExpired(certBytes []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func CertExpireSoon(certBytes []byte) bool {
|
||||
// CertExpireSoon check if the given cert bytes will expires within the given number of days from now
|
||||
func CertExpireSoon(certBytes []byte, numberOfDays int) bool {
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
expirationDate := cert.NotAfter
|
||||
threshold := 14 * 24 * time.Hour // 14 days
|
||||
threshold := time.Duration(numberOfDays) * 24 * time.Hour
|
||||
|
||||
timeRemaining := time.Until(expirationDate)
|
||||
if timeRemaining <= threshold {
|
||||
|
@ -14,10 +14,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"encoding/hex"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
db "imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -27,6 +27,7 @@ type AuthAgent struct {
|
||||
SessionStore *sessions.CookieStore
|
||||
Database *db.Database
|
||||
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
type AuthEndpoints struct {
|
||||
@ -37,12 +38,12 @@ type AuthEndpoints struct {
|
||||
Autologin string
|
||||
}
|
||||
|
||||
//Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
// Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
store := sessions.NewCookieStore(key)
|
||||
err := sysdb.NewTable("auth")
|
||||
if err != nil {
|
||||
log.Println("Failed to create auth database. Terminating.")
|
||||
systemLogger.Println("Failed to create auth database. Terminating.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -52,13 +53,14 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
|
||||
SessionStore: store,
|
||||
Database: sysdb,
|
||||
LoginRedirectionHandler: loginRedirectionHandler,
|
||||
Logger: systemLogger,
|
||||
}
|
||||
|
||||
//Return the authAgent
|
||||
return &newAuthAgent
|
||||
}
|
||||
|
||||
func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
func GetSessionKey(sysdb *db.Database, logger *logger.Logger) (string, error) {
|
||||
sysdb.NewTable("auth")
|
||||
sessionKey := ""
|
||||
if !sysdb.KeyExists("auth", "sessionkey") {
|
||||
@ -66,9 +68,9 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
rand.Read(key)
|
||||
sessionKey = string(key)
|
||||
sysdb.Write("auth", "sessionkey", sessionKey)
|
||||
log.Println("[Auth] New authentication session key generated")
|
||||
logger.PrintAndLog("auth", "New authentication session key generated", nil)
|
||||
} else {
|
||||
log.Println("[Auth] Authentication session key loaded from database")
|
||||
logger.PrintAndLog("auth", "Authentication session key loaded from database", nil)
|
||||
err := sysdb.Read("auth", "sessionkey", &sessionKey)
|
||||
if err != nil {
|
||||
return "", errors.New("database read error. Is the database file corrupted?")
|
||||
@ -77,7 +79,7 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
return sessionKey, nil
|
||||
}
|
||||
|
||||
//This function will handle an http request and redirect to the given login address if not logged in
|
||||
// This function will handle an http request and redirect to the given login address if not logged in
|
||||
func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
|
||||
if a.CheckAuth(r) {
|
||||
//User already logged in
|
||||
@ -88,14 +90,14 @@ func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, hand
|
||||
}
|
||||
}
|
||||
|
||||
//Handle login request, require POST username and password
|
||||
// Handle login request, require POST username and password
|
||||
func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Get username from request using POST mode
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
//Username not defined
|
||||
log.Println("[Auth] " + r.RemoteAddr + " trying to login with username: " + username)
|
||||
a.Logger.PrintAndLog("auth", r.RemoteAddr+" trying to login with username: "+username, nil)
|
||||
utils.SendErrorResponse(w, "Username not defined or empty.")
|
||||
return
|
||||
}
|
||||
@ -124,11 +126,11 @@ func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
a.LoginUserByRequest(w, r, username, rememberme)
|
||||
|
||||
//Print the login message to console
|
||||
log.Println(username + " logged in.")
|
||||
a.Logger.PrintAndLog("auth", username+" logged in.", nil)
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
//Password incorrect
|
||||
log.Println(username + " login request rejected: " + rejectionReason)
|
||||
a.Logger.PrintAndLog("auth", username+" login request rejected: "+rejectionReason, nil)
|
||||
|
||||
utils.SendErrorResponse(w, rejectionReason)
|
||||
return
|
||||
@ -140,14 +142,14 @@ func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string
|
||||
return succ
|
||||
}
|
||||
|
||||
//validate the username and password, return reasons if the auth failed
|
||||
// validate the username and password, return reasons if the auth failed
|
||||
func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
|
||||
hashedPassword := Hash(password)
|
||||
var passwordInDB string
|
||||
err := a.Database.Read("auth", "passhash/"+username, &passwordInDB)
|
||||
if err != nil {
|
||||
//User not found or db exception
|
||||
log.Println("[Auth] " + username + " login with incorrect password")
|
||||
a.Logger.PrintAndLog("auth", username+" login with incorrect password", nil)
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
|
||||
@ -158,7 +160,7 @@ func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, passw
|
||||
}
|
||||
}
|
||||
|
||||
//Login the user by creating a valid session for this user
|
||||
// Login the user by creating a valid session for this user
|
||||
func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
|
||||
@ -181,11 +183,15 @@ func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, u
|
||||
session.Save(r, w)
|
||||
}
|
||||
|
||||
//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
|
||||
// Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
|
||||
func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := a.GetUserName(w, r)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "user not logged in")
|
||||
return
|
||||
}
|
||||
if username != "" {
|
||||
log.Println(username + " logged out.")
|
||||
a.Logger.PrintAndLog("auth", username+" logged out", nil)
|
||||
}
|
||||
// Revoke users authentication
|
||||
err = a.Logout(w, r)
|
||||
@ -194,7 +200,7 @@ func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
@ -204,11 +210,11 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
session.Values["authenticated"] = false
|
||||
session.Values["username"] = nil
|
||||
session.Save(r, w)
|
||||
return nil
|
||||
session.Options.MaxAge = -1
|
||||
return session.Save(r, w)
|
||||
}
|
||||
|
||||
//Get the current session username from request
|
||||
// Get the current session username from request
|
||||
func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
@ -220,7 +226,7 @@ func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string,
|
||||
}
|
||||
}
|
||||
|
||||
//Get the current session user email from request
|
||||
// Get the current session user email from request
|
||||
func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
@ -239,7 +245,7 @@ func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the user has logged in, return true / false in JSON
|
||||
// Check if the user has logged in, return true / false in JSON
|
||||
func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if a.CheckAuth(r) {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
@ -248,7 +254,7 @@ func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
//Handle new user register. Require POST username, password, group.
|
||||
// Handle new user register. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
@ -291,10 +297,10 @@ func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callb
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] New user " + newusername + " added to system.")
|
||||
a.Logger.PrintAndLog("auth", "New user "+newusername+" added to system.", nil)
|
||||
}
|
||||
|
||||
//Handle new user register without confirmation email. Require POST username, password, group.
|
||||
// Handle new user register without confirmation email. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
@ -324,15 +330,16 @@ func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Re
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] Admin account created: " + newusername)
|
||||
a.Logger.PrintAndLog("auth", "Admin account created: "+newusername, nil)
|
||||
}
|
||||
|
||||
//Check authentication from request header's session value
|
||||
// Check authentication from request header's session value
|
||||
func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
session, err := a.SessionStore.Get(r, a.SessionName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
return false
|
||||
@ -340,8 +347,8 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//Handle de-register of users. Require POST username.
|
||||
//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
|
||||
// Handle de-register of users. Require POST username.
|
||||
// THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
|
||||
func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
||||
//Check if the user is logged in
|
||||
if !a.CheckAuth(r) {
|
||||
@ -365,7 +372,7 @@ func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] User " + username + " has been removed from the system.")
|
||||
a.Logger.PrintAndLog("auth", "User "+username+" has been removed from the system", nil)
|
||||
}
|
||||
|
||||
func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
@ -381,7 +388,7 @@ func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the number of users in the system
|
||||
// Get the number of users in the system
|
||||
func (a *AuthAgent) GetUserCounts() int {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
usercount := 0
|
||||
@ -393,12 +400,12 @@ func (a *AuthAgent) GetUserCounts() int {
|
||||
}
|
||||
|
||||
if usercount == 0 {
|
||||
log.Println("There are no user in the database.")
|
||||
a.Logger.PrintAndLog("auth", "There are no user in the database", nil)
|
||||
}
|
||||
return usercount
|
||||
}
|
||||
|
||||
//List all username within the system
|
||||
// List all username within the system
|
||||
func (a *AuthAgent) ListUsers() []string {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
results := []string{}
|
||||
@ -411,7 +418,7 @@ func (a *AuthAgent) ListUsers() []string {
|
||||
return results
|
||||
}
|
||||
|
||||
//Check if the given username exists
|
||||
// Check if the given username exists
|
||||
func (a *AuthAgent) UserExists(username string) bool {
|
||||
userpasswordhash := ""
|
||||
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
|
||||
@ -421,7 +428,7 @@ func (a *AuthAgent) UserExists(username string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//Update the session expire time given the request header.
|
||||
// Update the session expire time given the request header.
|
||||
func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
if session.Values["authenticated"].(bool) {
|
||||
@ -446,7 +453,7 @@ func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
//Create user account
|
||||
// Create user account
|
||||
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
|
||||
//Check user already exists
|
||||
if a.UserExists(newusername) {
|
||||
@ -470,7 +477,7 @@ func (a *AuthAgent) CreateUserAccount(newusername string, password string, email
|
||||
return nil
|
||||
}
|
||||
|
||||
//Hash the given raw string into sha512 hash
|
||||
// Hash the given raw string into sha512 hash
|
||||
func Hash(raw string) string {
|
||||
h := sha512.New()
|
||||
h.Write([]byte(raw))
|
||||
|
@ -2,7 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -10,7 +10,7 @@ type RouterOption struct {
|
||||
AuthAgent *AuthAgent
|
||||
RequireAuth bool //This router require authentication
|
||||
DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected
|
||||
|
||||
TargetMux *http.ServeMux
|
||||
}
|
||||
|
||||
type RouterDef struct {
|
||||
@ -28,24 +28,38 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef {
|
||||
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
//Check if the endpoint already registered
|
||||
if _, exist := router.endpoints[endpoint]; exist {
|
||||
log.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
authAgent := router.option.AuthAgent
|
||||
|
||||
//OK. Register handler
|
||||
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
if router.option.TargetMux == nil {
|
||||
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
} else {
|
||||
router.option.TargetMux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
router.endpoints[endpoint] = handler
|
||||
|
||||
|
136
src/mod/auth/sso/authelia/authelia.go
Normal file
136
src/mod/auth/sso/authelia/authelia.go
Normal file
@ -0,0 +1,136 @@
|
||||
package authelia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AutheliaRouterOptions struct {
|
||||
UseHTTPS bool //If the Authelia server is using HTTPS
|
||||
AutheliaURL string //The URL of the Authelia server
|
||||
Logger *logger.Logger
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type AutheliaRouter struct {
|
||||
options *AutheliaRouterOptions
|
||||
}
|
||||
|
||||
// NewAutheliaRouter creates a new AutheliaRouter object
|
||||
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
|
||||
options.Database.NewTable("authelia")
|
||||
|
||||
//Read settings from database, if exists
|
||||
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
|
||||
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
|
||||
|
||||
return &AutheliaRouter{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
|
||||
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"useHTTPS": ar.options.UseHTTPS,
|
||||
"autheliaURL": ar.options.AutheliaURL,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the settings
|
||||
autheliaURL, err := utils.PostPara(r, "autheliaURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "autheliaURL not found")
|
||||
return
|
||||
}
|
||||
|
||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
||||
if err != nil {
|
||||
useHTTPS = false
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.AutheliaURL = autheliaURL
|
||||
ar.options.UseHTTPS = useHTTPS
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
|
||||
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// handleAutheliaAuth is the internal handler for Authelia authentication
|
||||
// Set useHTTPS to true if your authelia server is using HTTPS
|
||||
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
|
||||
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
client := &http.Client{}
|
||||
|
||||
if ar.options.AutheliaURL == "" {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
return errors.New("authelia URL not set")
|
||||
}
|
||||
protocol := "http"
|
||||
if ar.options.UseHTTPS {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
|
||||
//Remove tailing slash if any
|
||||
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
|
||||
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
|
||||
}
|
||||
|
||||
//Make a request to Authelia to verify the request
|
||||
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
|
||||
|
||||
// Copy cookies from the incoming request
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Making the verification request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -9,17 +9,39 @@ package database
|
||||
*/
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database/dbinc"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
|
||||
Tables sync.Map
|
||||
ReadOnly bool
|
||||
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, readOnlyMode bool) (*Database, error) {
|
||||
return newDatabase(dbfile, readOnlyMode)
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
@ -29,39 +51,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
err := sysdb.DropTable("MyTable")
|
||||
*/
|
||||
|
||||
func (d *Database) UpdateReadWriteMode(readOnly bool) {
|
||||
d.ReadOnly = readOnly
|
||||
}
|
||||
|
||||
//Dump the whole db into a log file
|
||||
func (d *Database) Dump(filename string) ([]string, error) {
|
||||
return d.dump(filename)
|
||||
}
|
||||
|
||||
//Create a new table
|
||||
// Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
return d.newTable(tableName)
|
||||
}
|
||||
|
||||
//Check is table exists
|
||||
// Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
return d.tableExists(tableName)
|
||||
}
|
||||
|
||||
//Drop the given table
|
||||
// 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:
|
||||
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);
|
||||
|
||||
err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
*/
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
return d.write(tableName, key, value)
|
||||
@ -81,14 +97,21 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
|
||||
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
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
*/
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
return d.delete(tableName, key)
|
||||
@ -115,6 +138,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
return d.listTable(tableName)
|
||||
}
|
||||
|
||||
/*
|
||||
Close the database connection
|
||||
*/
|
||||
func (d *Database) Close() {
|
||||
d.close()
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user