155 Commits
2.3 ... 3.0.1

Author SHA1 Message Date
05297d854b Merge pull request #117 from tobychui/v3.0.1
V3.0.1 Updates
- Added regex support for redirect (slow, don't use it unless you really needs it)
- Added new dpcore implementations for faster proxy speed
- Added support for CF-Connecting-IP to X-Real-IP auto rewrite
- Added better 404 page
- Added option to bypass websocket origin check
- Updated project homepage design
- Fixed recursive port detection logic
- Fixed UserAgent in resp bug
- Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped)
2024-04-04 15:02:10 +08:00
0d7bce4d30 Updated to go 1.22 2024-04-04 14:48:17 +08:00
8db95dddc6 Added regexp redirect support 2024-04-04 14:24:38 +08:00
05daeded37 Updated dpcore
- Added immediate flush for buffer (i.e. no chunked encoding)
- Added support for stream mode proxy sniffing logic
2024-04-01 21:36:11 +08:00
8ce6471be5 Updated homepage design 2024-03-30 16:26:03 +08:00
e242c9288f Fixed recursive port detection logic
+ Fixed recursive port detection logic
+ Added support for CF-Connecting-IP #114
2024-03-30 15:52:10 +08:00
c55a29e7cf Better 404 page and rules disable toggle 2024-03-16 20:45:05 +08:00
6af047430c Added UI for WebSocket Origin Check bypass 2024-03-12 14:03:31 +08:00
200c924acd 3.0.1 init commit
- Removed Go HTTP client UA
- Added optional bypass of websocket origin check #107
- Added basic forward proxy for debug
- Fixed UI error in network utils tab
2024-03-10 14:49:18 +08:00
9b2168466c Update access.html
Fixed issue #103
2024-02-24 14:04:33 +08:00
7ae48bf370 Update README.md
Added quick download links for Github noobs
2024-02-20 22:00:58 +08:00
ee3d76fb96 Fix: Failure to build upon release
If we need these back, we can just re-add them.
2024-02-18 14:20:34 -05:00
40d192524b Merge pull request #102 from Morethanevil/main
Update CHANGELOG.md
2024-02-18 21:08:50 +08:00
c659e05005 Update CHANGELOG.md
Updated Changelog
2024-02-18 13:36:35 +01:00
676a45c222 Merge pull request #101 from tobychui/v3.0.0
V3.0.0 Updates
2024-02-18 20:00:20 +08:00
1da0761b13 Update vdir.html
Fixed minor issue on vdir not loading on start
2024-02-18 19:59:10 +08:00
32939874f2 Fix: Remove override for OpenSSL 2024-02-17 12:24:31 -05:00
43a4bf389a Updated README 2024-02-17 21:07:28 +08:00
33c7c5fa00 Custom header support
+ Added custom header
+ Removed unused files
2024-02-17 20:28:19 +08:00
216b53f224 Updated GAN features
+ Added add controller as memeber feature
+ Deprecated aroz subservice support
2024-02-16 21:16:14 +08:00
059b0a2e1c Update 1.png 2024-02-16 16:00:17 +08:00
3ab952f168 Update main.css
Fixed text color on rule instruction bug
2024-02-16 15:51:56 +08:00
4f676d6770 Updated system screenshots 2024-02-16 15:51:23 +08:00
e980bc847b Updated a lot of stuffs
+ Added comments for whitelist
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header
+ Added X-real-Ip header
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
2024-02-16 15:44:09 +08:00
174efc9080 Added per host vdir implementation 2024-02-14 22:52:56 +08:00
3228789375 Restructured proxy routing logic
- Moved virtual directory into host routing object
- Generalized root and hosts routing struct
- Optimized UI
2024-02-13 21:46:43 +08:00
36e461795a Added zoraxy start paramters for reference 2024-01-30 15:22:59 +08:00
d6e7641364 Merge pull request #88 from PassiveLemon/EnglishCorrection
English correction
2024-01-01 21:38:42 +08:00
15cebd6e06 Update README.md 2023-12-04 21:03:45 -05:00
e9a074d4d1 Merge branch 'EnglishCorrection' of https://github.com/PassiveLemon/zoraxy-dev into EnglishCorrection 2023-12-04 21:01:28 -05:00
4b7fd39e57 Update web root 2023-12-04 20:59:50 -05:00
fa005f1327 Update README.md 2023-12-04 20:59:25 -05:00
c7a9f40baa Update README.md 2023-12-04 20:44:42 -05:00
d5b9726158 Update index.html 2023-12-04 20:44:41 -05:00
f659e66cf7 Update CHANGELOG.md 2023-12-04 20:44:39 -05:00
801bdbf298 Fix: main.yml
Some things somehow passed by me and needed fixing.
Updated the actions in the process.
2023-11-28 23:16:51 -05:00
09da93cfb3 Merge pull request #70 from PassiveLemon/Ghcr
Update container (2.6.8)
2023-11-28 18:46:42 +08:00
70ace02e80 Revert port update 2023-11-27 20:18:24 -05:00
1f758e953d Fix: OpenSSL CVE 2023-11-26 16:16:31 -05:00
ffad2cab81 Comment out GHCR 2023-11-25 12:20:52 -05:00
dbb10644de Update to new port 2023-11-25 12:14:26 -05:00
4848392185 Merge branch 'tobychui:main' into Ghcr 2023-11-25 12:09:43 -05:00
956f4ac30f Merge pull request #85 from Morethanevil/patch-7
Update CHANGELOG.md
2023-11-25 23:07:14 +08:00
c09ff28fd5 Update CHANGELOG.md 2023-11-25 16:00:54 +01:00
20cf290d37 Merge pull request #84 from tobychui/2.6.8
Update v2.6.8
2023-11-25 22:25:22 +08:00
4ca0fcc6d1 Update main.go
Swap to use embedded web fs
2023-11-25 16:38:15 +08:00
ce4ce72820 Added optional TLS bypass mechanism
+ Added opt-out for subdomains for global TLS settings #44
+ Optimized subdomain / vdir editing interface
2023-11-25 15:54:28 +08:00
e363d55899 Fixed #77 and added systemwide logger
+ Added system wide logger (wip)
+ Fixed issue #77 uptime monitor bug
+ Added backend options for bypass global TLS (or allow per rule TLS settings, wip)
+
2023-11-25 12:50:30 +08:00
172479e4fb Added default HTTP/2 Mode
+ Added automatic HTTP/2 switch to TLS mode (Related to #40)
2023-10-20 13:44:47 +08:00
156fa5dace Bug fix
+ Added potential fix for #67
+ Update version number
+ Changed default static web port to 5487 so it is even more unlikely to be used
+
2023-10-20 11:01:28 +08:00
4d40e0aa38 Publish to GHCR 2023-10-03 16:36:53 -04:00
045e66b631 Revert "Publish to GitHub Container Registry"
This reverts commit 23bdaa1517.
2023-10-03 16:35:10 -04:00
62e60d78de Merge branch 'tobychui:main' into main 2023-10-03 16:34:43 -04:00
23bdaa1517 Publish to GitHub Container Registry 2023-10-03 16:34:23 -04:00
50f222cced Merge pull request #68 from Morethanevil/patch-6
Update CHANGELOG.md
2023-09-26 21:30:23 +08:00
640e1adf96 Update CHANGELOG.md
Updated to latest version
2023-09-26 09:28:57 +02:00
d4bb84180c Merge pull request #66 from tobychui/2.6.7
v2.6.7 Update
2023-09-26 11:24:09 +08:00
bda47fc36b Added default Ca features
+ Added default CA feature
+ Fixed RWD issue in TLS cert table
+ Optimized ACME UI in the TLS page
2023-09-25 20:54:50 +08:00
fd6ba56143 Added web directory manager
+ Added web directory manager
+ Added dummy service expose proxy page
+ Moved ACME and renew to a new section in TLS management page
2023-09-24 23:44:48 +08:00
b63a0fc246 Added static web server and black / whitelist template
- Added static web server
- Added static web server default index
- Added embeded templates to blacklist / whitelist
- Added wip Web Directory Manager

Place the templates at ./www/templates/blacklist.html or whitelist.html to replace the build in embedded template for access control 403 error
-
2023-09-14 16:15:40 +08:00
ed92cccf0e Merge pull request #65 from daluntw/2.6.7
Fix the out of range problem when certificate auto renew
2023-09-13 17:21:29 +08:00
95892802fd use issuer org as failover for json file not exist 2023-09-13 04:28:33 +00:00
8a5004e828 handle buypass issuer not match 2023-09-13 04:27:11 +00:00
c6c523e005 prevent out of range when check issuer exist 2023-09-13 00:39:29 +00:00
a692ec818d Static web server
- Fixed arm64 build bug in Make file
- Added wip static web server
2023-09-12 16:41:52 +08:00
c65f780613 Merge pull request #64 from daluntw/multidomain_fix
Fix multidomain UI handle incorrectly
2023-09-10 17:42:07 +08:00
507c2ab468 Updated 2.6.7 init 2023-09-09 12:28:24 +08:00
1180da8d11 fix multidomain missing logic 2023-09-08 23:18:47 +00:00
83f574e3ab Merge pull request #60 from Morethanevil/main
Update CHANGELOG.md
2023-08-30 22:47:42 +08:00
60837f307d Update CHANGELOG.md 2023-08-30 16:34:43 +02:00
50d5dedabe Merge pull request #59 from tobychui/2.6.6-experimental
2.6.6
2023-08-30 15:36:50 +08:00
f15c774c70 Updated go mod
+ Updated go mod to latest version
+ Fixed minor UX problem pn acme snippet
2023-08-30 12:16:53 +08:00
069f4805f6 Merge pull request #57 from daluntw/dev-custom-acme
Add Skip TLS Verify Feature For ACME Server
2023-08-29 10:06:52 +08:00
eb98624a6a fix naming convention 2023-08-28 03:35:20 +00:00
6a0c7cf499 add skipTLS for custom acme server 2023-08-28 03:31:33 +00:00
73ab9ca778 Optimized memory usage and root routing
+ Added unset subdomain custom redirection feature #46
+ Optimized memory usage by space time tradeoff in geoip lookup to fix #52
+ Replaced all stori/go.uuid to google/uuid for security reasons #55
2023-08-27 10:18:49 +08:00
9f9e0750e1 Update README.md 2023-08-26 15:05:53 +08:00
5664965491 Update README.md
Added donation link (wip)
2023-08-26 11:59:51 +08:00
db4016e79f Merge pull request #54 from PassiveLemon/Docker-Rework
Completely rework the container
2023-08-25 11:03:38 +08:00
f84c4370cf doc: update readme 2023-08-24 17:59:35 -04:00
b39cb6391b Completely rework the container 2023-08-24 17:48:57 -04:00
4f7f60188f Added basic auth exception paths
Added feature request from #25
2023-08-22 23:46:54 +08:00
dce58343db Merge pull request #48 from daluntw/dev-custom-acme
Add custom ACME server feature in backend
2023-08-22 14:05:42 +08:00
415838ad39 add frontend support 2023-08-21 13:19:33 +00:00
ce0b1a7585 fix missing space 2023-08-21 12:55:14 +00:00
352995e852 add custom acme feature in backend 2023-08-20 21:01:33 +00:00
a3d55a3274 Patching redirection bug
+ Added wip basic auth editor custom exception rules
+ Added custom logic to handle apache screw up redirect header
2023-08-20 14:50:25 +08:00
70adadf129 bug fixes for 2.6.5
+ Added potential fixes for #34 and #39
+ Added better error handling for #43
2023-08-17 14:08:48 +08:00
d42ac8a146 Merge pull request #36 from tobychui/Docker-2.1.0
Docker 2.1.0
2023-07-29 00:00:09 +08:00
f304ff8862 feature: network connectivity test 2023-07-28 10:23:20 -04:00
7d91e02dc9 feature: golang specific image
smaller size
2023-07-28 10:22:46 -04:00
dae510ae0a doc: VERSION 2023-07-28 10:22:22 -04:00
cd382a78a5 Merge pull request #35 from Morethanevil/patch-5
Update CHANGELOG.md
2023-07-27 00:22:57 +08:00
987de4a7be Update CHANGELOG.md 2023-07-26 17:21:09 +02:00
52d3b2f8c2 Fixed memory leaking
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in acme handler logic
+ Added "do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
2023-07-26 19:17:43 +08:00
5038429a70 Update README.md 2023-07-20 22:04:16 -04:00
2acbf0f3f5 Merge pull request #31 from tobychui/DockerMerge
Docker 2.0.0
2023-07-20 10:30:55 +08:00
aed703e260 Merge branch 'main' into DockerMerge 2023-07-20 10:30:07 +08:00
5ece7c0da4 2.0.0 init
Completely revamped the container to support the new external configuration files.
2023-07-19 16:39:51 -04:00
7eda6ba501 Merge pull request #29 from Morethanevil/patch-4
Updated change log
2023-07-19 11:42:13 +08:00
2da5ef048f Update CHANGELOG.md 2023-07-19 05:14:42 +02:00
6c48939316 Update CHANGELOG.md 2023-07-19 05:10:08 +02:00
544894bbba Merge pull request #28 from tobychui/autorenew_experimental
Auto-renew with ACME
2023-07-19 09:43:23 +08:00
153d056bdf ACME compatibility fix for /.well-known/
+ Updated acme well known take-over regrex
+ Added experimental config export and import
+ Added unit test for location rewrite in dpcore
+ Moved all config files to ./conf and original proxy files to ./conf/proxy
+ Minor optimization on UI regarding TLS verification logo on subdomain and vdir list
2023-07-12 21:42:09 +08:00
12c1118af9 Merge pull request #27 from tobychui/main
Update auto-renew branch to catch up with main
2023-07-09 14:43:07 +08:00
67ba143999 Merge branch 'autorenew_experimental' into main 2023-07-09 14:41:34 +08:00
0a8a821394 Update main.go
Switched dev mode to false (Note: The advance routing rule system is still work in progress, do not use them)
2023-07-06 11:03:41 +08:00
36b17ce4cf acme and redirection patch
+ Added experimental fix for redirection tailing problem
+ Added acme widget for first time users to setup https
2023-07-06 11:01:33 +08:00
519372069f Update README.md 2023-07-04 07:58:40 -04:00
2f14d6f271 Fixed minor bugs in renew policy toggle 2023-06-29 18:59:24 +08:00
44ac7144ec Added access control bypass for /.well-known router 2023-06-23 23:45:49 +08:00
741d3f8de1 Minor fix to renew now function 2023-06-23 23:22:19 +08:00
23eca5afae Added experimental acme renew from Let's Encrypt 2023-06-23 23:09:10 +08:00
050fab9481 Merge pull request #24 from tobychui/DockerMerge
Updates + Changes
2023-06-22 11:12:26 +08:00
3fc92bac27 Updates + Changes
Some code improvements. If the version ever changes, it will download the new binary if it doesn't already exist. Old ones will be kept however.

Some general changes to documentation.
2023-06-21 13:04:32 -04:00
594f75da97 Update README.md 2023-06-21 23:59:24 +08:00
3fbf246fb4 Added docker file (Fork from PassiveLemon) 2023-06-21 19:45:58 +08:00
828af6263d Merge pull request #23 from Morethanevil/patch-3
Update CHANGELOG.md
2023-06-16 21:46:32 +08:00
ab42cec31f Update CHANGELOG.md 2023-06-15 19:23:45 +02:00
a8bf07dbba Update start.go
Patched the rule folder name
2023-06-16 00:54:55 +08:00
48dc85ea3e Updates 2.6.4
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming acme integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
+
2023-06-16 00:48:39 +08:00
a73a7944ec Merge pull request #19 from Morethanevil/patch-2
Update CHANGELOG.md
2023-06-09 00:22:33 +08:00
d187124db6 Quick fix on the edit not working bug 2023-06-09 00:01:11 +08:00
0dd9e5d73c Update CHANGELOG.md 2023-06-08 17:05:41 +02:00
5e7599756f Updates 2.6.3
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse #7
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
2023-06-08 21:42:03 +08:00
5db50c1ca2 Merge pull request #16 from Morethanevil/main-1
Create CHANGELOG.md
2023-06-06 14:38:03 +08:00
884507b45a Create CHANGELOG.md
Create Changelog.md for users to see progress of development
2023-06-05 19:18:28 +02:00
2574d0504e Updates v2.6.2
+ Added advance stats operation tab
+ Added statistic reset #13
+ Added statistic export to csv and json (please use json)
+ Make subdomain clickable (not vdir) #12
+ Added TCP Proxy
+ Updates SMTP setup UI to make it more straight forward to setup
2023-06-04 23:59:56 +08:00
9535abe314 Update README.md 2023-06-03 18:08:36 +08:00
8e6a60f684 Added two screenshots for attractiveness 2023-06-03 18:08:03 +08:00
ead26ea16d Merge pull request #15 from Morethanevil/patch-1
Update README.md
2023-06-03 17:53:23 +08:00
3d66c01d7b Update README.md 2023-06-03 11:13:10 +02:00
20fd8e9a49 Updates v2.6.1
+ Added reverse proxy TLS skip verification
+ Added basic auth
+ Edit proxy settings
+ Whitelist
+ TCP Proxy (experimental)
+ Info (Utilities page)
2023-05-31 22:22:47 +08:00
5952a1b55f Updates v2.6 experimental build
+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore
2023-05-27 11:12:34 +08:00
edd19e2b30 Added FAQ section 2023-05-25 14:10:32 +08:00
7ad64de145 Update README.md
Added docker file link by @cyb3rdoc
2023-05-25 14:07:02 +08:00
dd08d003a6 Update index.html 2023-05-24 16:22:52 +08:00
f4f2b55f14 Added more homepage code 2023-05-24 16:15:39 +08:00
01daf5541a Merge pull request #3 from ArgonQQ/patch-1
fix: typo in readme.md
2023-05-23 22:49:42 +08:00
b1d0a79131 Added opengraph 2023-05-23 22:03:33 +08:00
23e289eaff Updated project homepage (WIP) 2023-05-23 22:01:46 +08:00
e71d4154d6 fix: typo in readme.md 2023-05-23 13:35:29 +02:00
80f566e312 Added build from src instruction 2023-05-23 11:46:34 +08:00
530a99a544 Added docker repo link 2023-05-23 10:45:44 +08:00
ba347c5fa1 Added missing web folder 2023-05-23 01:29:59 +08:00
d51cb6efdf Update README.md
Updated license and more web ssh details
2023-05-22 23:06:49 +08:00
c07d5f85df v2 init commit 2023-05-22 23:05:59 +08:00
5ac0fdde1d Create LICENSE 2023-05-22 22:57:51 +08:00
55ffcf2eb6 Create CNAME 2023-05-15 21:15:42 +08:00
042df04bf4 Delete CNAME 2023-05-15 21:15:39 +08:00
ba1a9e9ef7 Create CNAME 2023-05-15 21:14:10 +08:00
7c9dcb88fc Create index.html 2023-05-15 21:12:22 +08:00
2eaaf4905f Merge branch 'main' of https://github.com/tobychui/zoraxy 2023-05-15 21:11:42 +08:00
630ff9c59f Update social_banner.psd 2023-05-15 21:11:40 +08:00
7bb24f9f61 Update issue templates 2023-05-14 12:29:29 +08:00
474 changed files with 530079 additions and 72 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser (if it is a bug appears on the UI section of the system):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Host Environment (please complete the following information):**
- Arch: [e.g. arm64]
- Device: [e.g. Bananapi R2 PRO]
- OS: [e.g. Armbian]
- Version [e.g. 23.02 Bullseye ]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[ENHANCEMENTS]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

41
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,41 @@
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 \
.

6
.gitignore vendored
View File

@ -27,4 +27,8 @@ src/conf/*
src/ReverseProxy_*_*
src/Zoraxy_*_*
src/certs/*
src/rules/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
src/mod/acme/test/stackoverflow.pem

113
CHANGELOG.md Normal file
View File

@ -0,0 +1,113 @@
# v3.0.0 Feb 18 2024
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!
+ Added comments for whitelist [#97](https://github.com/tobychui/zoraxy/issues/97)
+ Added force-renew for certificates [#92](https://github.com/tobychui/zoraxy/issues/92)
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header / Added X-real-Ip header [#93](https://github.com/tobychui/zoraxy/issues/93)
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
+ Added "Add controller as member" feature to Global Area Network editor
+ Added custom header
+ Deprecated aroz subservice support
+ Updated visuals, improving logical structure, less depressing colors [#95](https://github.com/tobychui/zoraxy/issues/95)
+ Added virtual directory into host routing object (each host now got its own sets of virtual directories)
+ Added support for wildcard host names (e.g. *.example.com)
+ Added best-fit selection for wildcard matching rules (e.g. *.a.example.com > *.example.com in routing)
+ Generalized root and hosts routing struct (no more conversion between runtime & save record object
+ Added "Default Site" to replace "Proxy Root" interface
+ Added Redirect & 404 page for "Default Site"
# v2.6.8 Nov 25 2023
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)
+ Optimized subdomain / vdir editing interface
+ Added system-wide logger (Work in progress)
+ Fixed issue for uptime monitor bug [#77](https://github.com/tobychui/zoraxy/issues/77)
+ Changed default static web port to 5487 (prevent already in use)
+ Added automatic HTTP/2 to TLS mode
+ Bug fix for webserver autostart [67](https://github.com/tobychui/zoraxy/issues/67)
# v2.6.7 Sep 26 2023
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
+ Web Directory Manager (see static webserver tab)
+ Added static web server and black / whitelist template [#38](https://github.com/tobychui/zoraxy/issues/38)
+ Added default / preferred CA features for ACME [#47](https://github.com/tobychui/zoraxy/issues/47)
+ Optimized TLS/SSL page and added dedicated section for ACME related features
+ Bugfixes [#61](https://github.com/tobychui/zoraxy/issues/61) [#58](https://github.com/tobychui/zoraxy/issues/58)
# v2.6.6 Aug 30 2023
+ Added basic auth editor custom exception rules
+ Fixed redirection bug under another reverse proxy and Apache location headers [#39](https://github.com/tobychui/zoraxy/issues/39)
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
+ Added custom ACME feature in backend, thx [@daluntw](https://github.com/daluntw)
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
+ Introduce new start parameter `-fastgeoip=true`: see [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
# v2.6.5.1 Jul 26 2023
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in ACME handler logic
+ Added "Do you want to get a TLS certificate for this subdomain?" dialogue when a new subdomain proxy rule is created
# v2.6.5 Jul 19 2023
+ Added Import / Export-Feature
+ Moved configuration files to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
+ Added Whois
# v2.6.4 Jun 15 2023
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming ACME integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
# v2.6.3 Jun 8 2023
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for issue [#7](https://github.com/tobychui/zoraxy/issues/7)
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
# v2.6.2 Jun 4 2023
+ Added advance stats operation tab
+ Added statistic reset [#13](https://github.com/tobychui/zoraxy/issues/13)
+ Added statistic export to csv and json (please use json)
+ Make subdomain clickable (not vdir) [#12](https://github.com/tobychui/zoraxy/issues/12)
+ Added TCP Proxy
+ Updates SMTP setup UI to make it more straight forward to setup
# v2.6.1 May 31 2023
+ Added reverse proxy TLS skip verification
+ Added basic auth
+ Edit proxy settings
+ Whitelist
+ TCP Proxy (experimental)
+ Info (Utilities page)
# v2.6 May 27 2023
+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referrer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore

661
LICENSE Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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, either version 3 of the License, or
(at your option) 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/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

160
README.md
View File

@ -2,24 +2,28 @@
# Zoraxy
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
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)*
### Features
- Simple to use interface with detail in-system instructions
- Reverse Proxy
- Subdomain Reverse Proxy
- Virtual Directory Reverse Proxy
- Virtual Directory
- Basic Auth
- Custom Headers
- Redirection Rules
- TLS / SSL setup and deploy
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners)
- ACME features like auto-renew to serve your sites in http**s**
- SNI support (one certificate contains multiple host names)
- 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
- Integrated Up-time Monitor
- Web-SSH Terminal
- Utilities
- CIDR IP converters
- mDNS Scanner
- IP Scanner
@ -27,15 +31,33 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- Basic single-admin management mode
- External permission management system for easy system integration
- SMTP config for password reset
## 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/)
## Build from Source
Requires Go 1.22 or higher
```bash
git clone https://github.com/tobychui/zoraxy
cd ./zoraxy/src/
go mod tidy
go build
sudo ./zoraxy -port=:8000
```
## Usage
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
### Standalone Mode
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start 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 home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
#### Linux
@ -49,15 +71,49 @@ Download the binary executable and double click the binary file to start it.
#### Raspberry Pi
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
The installation method is same as Linux. If you are using a Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
#### Other ARM SBCs or Android phone with Termux
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
```
Usage of zoraxy:
-autorenew int
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-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)
-mdns
Enable mDNS scanner and transponder (default true)
-noauth
Disable authentication for management interface
-port string
Management web interface listening port (default ":8000")
-sshlb
Allow loopback web ssh connection (DANGER)
-version
Show version of this server
-webfm
Enable web file manager for static web server root folder (default true)
-webroot string
Static web server root folder. Only allow chnage in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
ZeroTier controller API port (default 9993)
```
### External Permission Management Mode
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag
If you already have an upstream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag:
```bash
./zoraxy -noauth=true
@ -65,95 +121,57 @@ If you already have a up-stream reverse proxy server in place with permission ma
*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.*
#### Use with ArozOS
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
```bash
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
cd ~/arozos/subservices
mkdir zoraxy
cd ./zoraxy
# Download the release binary from Github release
wget {binary executable link from release page}
# Set permission. Change this if required
sudo chmod 775 -R ./
# Start zoraxy to see if the downloaded arch is correct.
./zoraxy
# After the unzip done, press Ctrl + C to kill it
# Rename it to valid arozos subservice binary format
mv ./zoraxy zoraxy_linux_amd64
# If you are using SBCs with different CPU arch, use the following names
# mv ./zoraxy zoraxy_linux_arm
# mv ./zoraxy zoraxy_linux_arm64
# Restart arozos
sudo systemctl restart arozos
```
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
## Screenshots
![](img/screenshots/0_1.png)
![](img/screenshots/0_2.png)
![](img/screenshots/1.png)
![](img/screenshots/2.png)
![](img/screenshots/3.png)
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
![](img/screenshots/4.png)
## FAQ
![](img/screenshots/5.png)
![](img/screenshots/7.png)
![](img/screenshots/8.png)
![](img/screenshots/9.png)
![](img/screenshots/10_1.png)
![](img/screenshots/10_2.png)
There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychui/zoraxy/wiki/FAQ---Frequently-Asked-Questions)!
## Global Area Network Controller
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
Assuming you already have a valid license, to use Zoraxy with ZeroTier, 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 at correct location in your host.
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 wierd 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
```
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
This allows you to have infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
## Web.SSH
## Web SSH
Web SSH currently only support Linux based OS.
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 web ssh connection, by default, is disabled. This means that if you are trying to connect to address like 127.0.0.1 or localhost, the system will reject your connection due to security issues. To enable loopback for testing or development purpose, use the following flags to override the loopback checking.
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:
```
```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 Github repo is for storing the release binary and collecting issue only. **This project is not open source and the provided binaries are for function review purpose only.** For business users, please contact toby@imuslab.com for commercial licensing details.
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.**

38
docker/Dockerfile Normal file
View File

@ -0,0 +1,38 @@
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
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/
WORKDIR /opt/zoraxy/source/
RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\
rm -r /opt/zoraxy/source/
RUN chmod +x /usr/local/bin/zoraxy
WORKDIR /opt/zoraxy/config/
ENV VERSION=$VERSION
ENV ARGS="-noauth=false"
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1

65
docker/README.md Normal file
View File

@ -0,0 +1,65 @@
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](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>
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>
### 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
```
### Using Docker Compose </br>
```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:
image: zoraxydocker/zoraxy:latest
container_name: zoraxy
ports:
- 80:80
- 443:443
- 8005:8000/tcp
volumes:
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/
environment:
ARGS: '-noauth=false'
```

4
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
echo "Zoraxy version $VERSION"
zoraxy -port=:8000 ${ARGS}

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
zoraxy.arozos.com

BIN
docs/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
docs/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1 @@
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m772-635-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333-194l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Zm0-148 48-96 98-43-98-43-48-96-47 96-99 43 99 43 47 96Zm0-139Z"/></svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1 @@
<svg fill="#ff7a7a" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M280-453h400v-60H280v60ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>

After

Width:  |  Height:  |  Size: 448 B

1
docs/img/icons/code.svg Normal file
View File

@ -0,0 +1 @@
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M320-242 80-482l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"/></svg>

After

Width:  |  Height:  |  Size: 227 B

1
docs/img/icons/gan.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="#919191" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M120-80v-270h120v-160h210v-100H330v-270h300v270H510v100h210v160h120v270H540v-270h120v-100H300v100h120v270H120Zm270-590h180v-150H390v150ZM180-140h180v-150H180v150Zm420 0h180v-150H600v150ZM480-670ZM360-290Zm240 0Z"/></svg>

After

Width:  |  Height:  |  Size: 332 B

1
docs/img/icons/home.svg Normal file
View File

@ -0,0 +1 @@
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@ -0,0 +1 @@
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M356-120H180q-24 0-42-18t-18-42v-176q44-5 75.5-34.5T227-463q0-43-31.5-72.5T120-570v-176q0-24 18-42t42-18h177q11-40 39.5-67t68.5-27q40 0 68.5 27t39.5 67h173q24 0 42 18t18 42v173q40 11 65.5 41.5T897-461q0 40-25.5 67T806-356v176q0 24-18 42t-42 18H570q-5-48-35.5-77.5T463-227q-41 0-71.5 29.5T356-120Zm-176-60h130q25-61 69.888-84 44.888-23 83-23T546-264q45 23 70 84h130v-235h45q20 0 33-13t13-33q0-20-13-33t-33-13h-45v-239H511v-48q0-20-13-33t-33-13q-20 0-33 13t-13 33v48H180v130q48.15 17.817 77.575 59.686Q287-514.445 287-462.777 287-412 257.5-370T180-310v130Zm329-330Z"/></svg>

After

Width:  |  Height:  |  Size: 688 B

1
docs/img/icons/proxy.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#fcba03"><path d="M273-160 80-353l193-193 42 42-121 121h316v60H194l121 121-42 42Zm414-254-42-42 121-121H450v-60h316L645-758l42-42 193 193-193 193Z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#0388fc" height="48" viewBox="0 -960 960 960" width="48"><path d="M700-160v-410H275l153 153-42 43-226-226 226-226 42 42-153 154h485v470h-60Z"/></svg>

After

Width:  |  Height:  |  Size: 195 B

1
docs/img/icons/scan.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="#83f2c4" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M197-197q-54-54-85.5-126.5T80-480q0-84 31.5-156.5T197-763l43 43q-46 46-73 107.5T140-480q0 71 26.5 132T240-240l-43 43Zm113-113q-32-32-51-75.5T240-480q0-51 19-94.5t51-75.5l43 43q-24 24-38.5 56.5T300-480q0 38 14 70t39 57l-43 43Zm170-90q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm170 90-43-43q24-24 38.5-56.5T660-480q0-38-14-70t-39-57l43-43q32 32 51 75.5t19 94.5q0 50-19 93.5T650-310Zm113 113-43-43q46-46 73-107.5T820-480q0-71-26.5-132T720-720l43-43q54 55 85.5 127.5T880-480q0 83-31.5 155.5T763-197Z"/></svg>

After

Width:  |  Height:  |  Size: 667 B

View File

@ -0,0 +1 @@
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M345-377h391L609-548 506-413l-68-87-93 123Zm-85 177q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z"/></svg>

After

Width:  |  Height:  |  Size: 355 B

1
docs/img/icons/stats.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="#edf230" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M109.912-150Q81-150 60.5-170.589 40-191.177 40-220.089 40-249 60.494-269.5t49.273-20.5q5.233 0 10.233.5 5 .5 13 2.5l200-200q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q371.177-580 400.089-580 429-580 449.5-559.366t20.5 49.61Q470-508 467-487l110 110q8-2 13-2.5t10-.5q5 0 10 .5t13 2.5l160-160q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q821.177-630 850.089-630 879-630 899.5-609.411q20.5 20.588 20.5 49.5Q920-531 899.506-510.5T850.233-490Q845-490 840-490.5q-5-.5-13-2.5L667-333q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q628.823-240 599.911-240 571-240 550.5-260.494T530-309.767q0-5.233.5-10.233.5-5 2.5-13L423-443q-8 2-13 2.5t-10.25.5q-1.75 0-22.75-3L177-243q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q138.823-150 109.912-150ZM160-592l-20.253-43.747L96-656l43.747-20.253L160-720l20.253 43.747L224-656l-43.747 20.253L160-592Zm440-51-30.717-66.283L503-740l66.283-30.717L600-837l30.717 66.283L697-740l-66.283 30.717L600-643Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H140Zm0-60h680v-436H140v436Zm160-72-42-42 103-104-104-104 43-42 146 146-146 146Zm190 4v-60h220v60H490Z"/></svg>

After

Width:  |  Height:  |  Size: 300 B

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
docs/img/og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/img/og.psd Normal file

Binary file not shown.

BIN
docs/img/screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
docs/img/screenshots/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
docs/img/screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
docs/img/screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
docs/img/screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
docs/img/screenshots/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/img/screenshots/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
docs/img/screenshots/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
docs/img/screenshots/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
docs/img/screenshots/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

BIN
docs/img/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/img/title.psd Normal file

Binary file not shown.

386
docs/index.html Normal file
View File

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

237
docs/style.css Normal file
View File

@ -0,0 +1,237 @@
body{
background: #ffffff !important;
margin: 0;
padding: 0;
overflow-y: hidden;
}
:root{
--themeTextColor: #404040;
--themeSkyblueColor: #a9d1f3;
--themeSkyblueColorDecondary: #8eb9df;
}
.main {
display: flex;
height: 100vh;
}
.left-menu {
width: 80px;
min-width: 80px;
background-color: #fcfcfc;
min-height: 100vh;
padding-top: 1.5em;
}
.iconWrapper{
padding: 1em;
border-bottom: 1px solid #f6f6f6;
}
.right-content {
flex-grow: 1;
position: relative;
max-height: 100%;
overflow-y: auto;
}
.ui.black.button{
background-color: var(--themeTextColor) !important;
}
/* Menu items */
.menu-item{
display: block;
padding: 0.4em;
padding-top: 1.2em;
padding-bottom: 1.2em;
text-align: center;
border-bottom: 1px solid #f6f6f6;
width: 100%;
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
}
.menu-item.active{
background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
}
.menu-item .item-icon{
fill: #fcfcfc;
}
.menu-item:hover{
background: rgba(35,35,35,0.1);
}
.menu-item img{
width: 30px;
display: inline-block;
vertical-align: middle;
}
/* Head banner */
.herotext{
padding-top: 15em;
padding-left: 8vw;
min-height: 100vh;
}
.bannerHeader{
font-size: 8em;
font-weight: 600;
color: white;
}
.bannerSubheader{
font-weight: 400;
font-size: 1.2em;
color: #ebebeb;
margin-top: -20px;
}
.bannerHeaderWrapper{
text-align: center;
display: inline-block;
}
#home{
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
}
#home .table th, #home .table h4{
color: white;
}
#home .table h4 .content, #home .table h4 .sub.header{
color: white;
}
#home .table td a{
color: #d6ddff;
}
/* features */
#features{
padding-top: 4em;
padding-bottom: 4em;
background-color: white;
}
/* screenshots */
.screenshot{
transition: transform ease-in-out 0.1s;
box-shadow: 3px 3px 5px 0px rgba(51,51,51,0.14);
margin-bottom: 1em;
}
.screenshot:hover {
transform: scale(1.1); /* (150% zoom - Note: if the zoom is too large, it will go outside of the viewport) */
}
/* RWD */
@media (max-width:960px) {
/* Menu RWD */
.left-menu {
width: 50px;
min-width: 50px;
}
.iconWrapper{
padding: 0.2em;
border-bottom: 1px solid #f6f6f6;
}
.menu-item{
padding: 0.3em;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.menu-item img{
width: 26px;
}
/* head banner RWD */
.headbanner{
opacity: 0.1;
}
.herotext{
padding-left: 0;
text-align: center;
}
.bannerSubheader{
font-size: 1.2em;
}
.bannerHeader{
font-size: 5em;
}
.bannerHeaderWrapper{
display: inline;
width: 100%;
}
.herotext .ui.collapsing.table{
width: 100%;
}
}
/*
Waves CSS
*/
#wavesWrapper{
position: absolute;
bottom: 0;
width: 100%;
left: 0;
}
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

61
src/Makefile Normal file
View File

@ -0,0 +1,61 @@
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64
temp = $(subst /, ,$@)
os = $(word 1, $(temp))
arch = $(word 2, $(temp))
#all: web.tar.gz $(PLATFORMS) fixwindows zoraxy_file_checksum.sha1
all: clear_old $(PLATFORMS) fixwindows
binary: $(PLATFORMS)
hash: zoraxy_file_checksum.sha1
web: web.tar.gz
clean:
rm -f zoraxy_*_*
rm -f web.tar.gz
$(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) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
fixwindows:
-mv ./dist/zoraxy_windows_amd64 ./dist/zoraxy_windows_amd64.exe
# -mv ./dist/zoraxy_windows_arm64 ./dist/zoraxy_windows_arm64.exe
clear_old:
-rm -rf ./dist/
-mkdir ./dist/
web.tar.gz:
@echo "Removing old build resources, if exists"
-rm -rf ./dist/
-mkdir ./dist/
@echo "Moving subfolders to build folder"
-cp -r ./web ./dist/web/
-cp -r ./system ./dist/system/
@ echo "Remove sensitive information"
-rm -rf ./dist/certs/
-rm -rf ./dist/conf/
-rm -rf ./dist/rules/
@echo "Creating tarball for all required files"
-rm ./dist/web.tar.gz
-cd ./dist/ && tar -czf ./web.tar.gz system/ web/
@echo "Clearing up unzipped folder structures"
-rm -rf "./dist/web"
-rm -rf "./dist/system"
zoraxy_file_checksum.sha1:
@echo "Generating the checksum, if sha1sum installed"
-sha1sum ./dist/web.tar.gz > ./dist/zoraxy_file_checksum.sha1

205
src/accesslist.go Normal file
View File

@ -0,0 +1,205 @@
package main
import (
"encoding/json"
"net/http"
"github.com/microcosm-cc/bluemonday"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/utils"
)
/*
accesslist.go
This script file is added to extend the
reverse proxy function to include
banning / whitelist a specific IP address or country code
*/
/*
Blacklist Related
*/
// List a of blacklisted ip address or country code
func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
bltype, err := utils.GetPara(r, "type")
if err != nil {
bltype = "country"
}
resulst := []string{}
if bltype == "country" {
resulst = geodbStore.GetAllBlacklistedCountryCode()
} else if bltype == "ip" {
resulst = geodbStore.GetAllBlacklistedIp()
}
js, _ := json.Marshal(resulst)
utils.SendJSONResponse(w, string(js))
}
func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
countryCode, err := utils.PostPara(r, "cc")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty country code")
return
}
geodbStore.AddCountryCodeToBlackList(countryCode)
utils.SendOK(w)
}
func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
countryCode, err := utils.PostPara(r, "cc")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty country code")
return
}
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
utils.SendOK(w)
}
func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
ipAddr, err := utils.PostPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty ip address")
return
}
geodbStore.AddIPToBlackList(ipAddr)
}
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
ipAddr, err := utils.PostPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty ip address")
return
}
geodbStore.RemoveIPFromBlackList(ipAddr)
utils.SendOK(w)
}
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable")
if err != nil {
//Return the current enabled state
currentEnabled := geodbStore.BlacklistEnabled
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enable == "true" {
geodbStore.ToggleBlacklist(true)
} else if enable == "false" {
geodbStore.ToggleBlacklist(false)
} else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return
}
utils.SendOK(w)
}
}
/*
Whitelist Related
*/
func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype, err := utils.GetPara(r, "type")
if err != nil {
bltype = "country"
}
resulst := []*geodb.WhitelistEntry{}
if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode()
} else if bltype == "ip" {
resulst = geodbStore.GetAllWhitelistedIp()
}
js, _ := json.Marshal(resulst)
utils.SendJSONResponse(w, string(js))
}
func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
countryCode, err := utils.PostPara(r, "cc")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty country code")
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w)
}
func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
countryCode, err := utils.PostPara(r, "cc")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty country code")
return
}
geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
utils.SendOK(w)
}
func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
ipAddr, err := utils.PostPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty ip address")
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
geodbStore.AddIPToWhiteList(ipAddr, comment)
}
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
ipAddr, err := utils.PostPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty ip address")
return
}
geodbStore.RemoveIPFromWhiteList(ipAddr)
utils.SendOK(w)
}
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable")
if err != nil {
//Return the current enabled state
currentEnabled := geodbStore.WhitelistEnabled
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enable == "true" {
geodbStore.ToggleWhitelist(true)
} else if enable == "false" {
geodbStore.ToggleWhitelist(false)
} else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return
}
utils.SendOK(w)
}
}

139
src/acme.go Normal file
View File

@ -0,0 +1,139 @@
package main
import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"regexp"
"strconv"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
)
/*
acme.go
This script handle special routing required for acme auto cert renew functions
*/
// Helper function to generate a random port above a specified value
func getRandomPort(minPort int) int {
return rand.Intn(65535-minPort) + minPort
}
// init the new ACME instance
func initACME() *acme.ACMEHandler {
SystemWideLogger.Println("Starting ACME handler")
rand.Seed(time.Now().UnixNano())
// Generate a random port above 30000
port := getRandomPort(30000)
// Check if the port is already in use
for acme.IsPortInUse(port) {
port = getRandomPort(30000)
}
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
}
// create the special routing rule for ACME
func acmeRegisterSpecialRoutingRule() {
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
ID: "acme-autorenew",
MatchRule: func(r *http.Request) bool {
found, _ := regexp.MatchString("/.well-known/acme-challenge/*", r.RequestURI)
return found
},
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequest(http.MethodGet, "http://localhost:"+acmeHandler.Getport()+r.RequestURI, nil)
req.Host = r.Host
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
return
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
return
}
resBody, err := io.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
fmt.Printf("error reading: %s\n", err)
return
}
w.Write(resBody)
},
Enabled: true,
UseSystemAccessControl: false,
})
if err != nil {
SystemWideLogger.PrintAndLog("ACME", "Unable register temp port for DNS resolver", err)
}
}
// 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)
} else {
//Set this to true, so after renew, do not turn it off
isForceHttpsRedirectEnabledOriginally = true
}
} 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)
// 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)
}
}
}
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
func HandleACMEPreferredCA(w http.ResponseWriter, r *http.Request) {
ca, err := utils.PostPara(r, "set")
if err != nil {
//Return the current ca to user
prefCA := "Let's Encrypt"
sysdb.Read("acmepref", "prefca", &prefCA)
js, _ := json.Marshal(prefCA)
utils.SendJSONResponse(w, string(js))
} else {
//Check if the CA is supported
acme.IsSupportedCA(ca)
//Set the new config
sysdb.Write("acmepref", "prefca", ca)
SystemWideLogger.Println("Updating prefered ACME CA to " + ca)
utils.SendOK(w)
}
}

291
src/api.go Normal file
View File

@ -0,0 +1,291 @@
package main
import (
"encoding/json"
"net/http"
"net/http/pprof"
"imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
)
/*
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
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", 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
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
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/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
//TLS / SSL config
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
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
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
//Blacklist APIs
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
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
//Path Blocker APIs
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
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
//Global Area Network APIs
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
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)
//mDNS APIs
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)
//Network utilities
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet)
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
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/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) {
if requireAuth {
authAgent.CheckLogin(w, r)
} else {
utils.SendJSONResponse(w, "true")
}
})
http.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)
utils.SendJSONResponse(w, string(js))
})
http.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) {
})
} 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) {
username, err := authAgent.GetUserName(w, r)
if err != nil {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
return
}
oldPassword, err := utils.PostPara(r, "oldPassword")
if err != nil {
utils.SendErrorResponse(w, "empty current password")
return
}
newPassword, err := utils.PostPara(r, "newPassword")
if err != nil {
utils.SendErrorResponse(w, "empty new password")
return
}
confirmPassword, _ := utils.PostPara(r, "confirmPassword")
if newPassword != confirmPassword {
utils.SendErrorResponse(w, "confirm password not match")
return
}
//Check if the old password correct
oldPasswordCorrect, _ := authAgent.ValidateUsernameAndPasswordWithReason(username, oldPassword)
if !oldPasswordCorrect {
utils.SendErrorResponse(w, "Invalid current password given")
return
}
//Change the password of the root user
authAgent.UnregisterUser(username)
authAgent.CreateUserAccount(username, newPassword, "")
})
}

308
src/cert.go Normal file
View File

@ -0,0 +1,308 @@
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
// Check if the default certificates is correctly setup
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
type CheckResult struct {
DefaultPubExists bool
DefaultPriExists bool
}
pub, pri := tlsCertManager.DefaultCertExistsSep()
js, _ := json.Marshal(CheckResult{
pub,
pri,
})
utils.SendJSONResponse(w, string(js))
}
// Return a list of domains where the certificates covers
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
filenames, err := tlsCertManager.ListCertDomains()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
showDate, _ := utils.GetPara(r, "date")
if showDate == "true" {
type CertInfo struct {
Domain string
LastModifiedDate string
ExpireDate string
RemainingDays int
}
results := []*CertInfo{}
for _, filename := range filenames {
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath)
if err != nil {
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
return
}
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
expiredIn := 0
if err != nil {
//Unable to load this file
continue
} else {
//Cert loaded. Check its expire time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
duration := cert.NotAfter.Sub(time.Now())
// Convert the duration to days
expiredIn = int(duration.Hours() / 24)
}
}
}
thisCertInfo := CertInfo{
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
}
results = append(results, &thisCertInfo)
}
js, _ := json.Marshal(results)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
} else {
response, err := json.Marshal(filenames)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
}
// List all certificates and map all their domains to the cert filename
func handleListDomains(w http.ResponseWriter, r *http.Request) {
filenames, err := os.ReadDir("./conf/certs/")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
certnameToDomainMap := map[string]string{}
for _, filename := range filenames {
if filename.IsDir() {
continue
}
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBtyes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
for _, dnsName := range cert.DNSNames {
certnameToDomainMap[dnsName] = certname
}
certnameToDomainMap[cert.Subject.CommonName] = certname
}
}
}
}
requireCompact, _ := utils.GetPara(r, "compact")
if requireCompact == "true" {
result := make(map[string][]string)
for key, value := range certnameToDomainMap {
if _, ok := result[value]; !ok {
result[value] = make([]string, 0)
}
result[value] = append(result[value], key)
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
return
}
js, _ := json.Marshal(certnameToDomainMap)
utils.SendJSONResponse(w, string(js))
}
// Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := false
if sysdb.KeyExists("settings", "usetls") {
sysdb.Read("settings", "usetls", &currentTlsSetting)
}
newState, err := utils.PostPara(r, "set")
if err != nil {
//No setting. Get the current status
js, _ := json.Marshal(currentTlsSetting)
utils.SendJSONResponse(w, string(js))
} else {
if newState == "true" {
sysdb.Write("settings", "usetls", true)
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(true)
} else if newState == "false" {
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)
}
}
// Handle the GET and SET of reverse proxy TLS versions
func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
newState, err := utils.PostPara(r, "set")
if err != nil {
//GET
var reqLatestTLS bool = false
if sysdb.KeyExists("settings", "forceLatestTLS") {
sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS)
}
js, _ := json.Marshal(reqLatestTLS)
utils.SendJSONResponse(w, string(js))
} else {
if newState == "true" {
sysdb.Write("settings", "forceLatestTLS", true)
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true)
} else if newState == "false" {
sysdb.Write("settings", "forceLatestTLS", false)
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false)
} else {
utils.SendErrorResponse(w, "invalid state given")
}
}
}
// Handle upload of the certificate
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// get the key type
keytype, err := utils.GetPara(r, "ktype")
overWriteFilename := ""
if err != nil {
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
return
}
// get the domain
domain, err := utils.GetPara(r, "domain")
if err != nil {
//Assume localhost
domain = "default"
}
if keytype == "pub" {
overWriteFilename = domain + ".pem"
} else if keytype == "pri" {
overWriteFilename = domain + ".key"
} else {
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
return
}
// parse multipart form data
err = r.ParseMultipartForm(10 << 20) // 10 MB
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
// get file from form data
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to get file", http.StatusBadRequest)
return
}
defer file.Close()
// create file in upload directory
os.MkdirAll("./conf/certs", 0775)
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return
}
defer f.Close()
// copy file contents to destination file
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
//Update cert list
tlsCertManager.UpdateLoadedCertList()
// send response
fmt.Fprintln(w, "File upload successful!")
}
// Handle cert remove
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "invalid domain given")
return
}
err = tlsCertManager.RemoveCert(domain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
}
}

350
src/config.go Normal file
View File

@ -0,0 +1,350 @@
package main
import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
)
/*
Reverse Proxy Configs
The following section handle
the reverse proxy configs
*/
type Record struct {
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
BypassGlobalTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
}
/*
Load Reverse Proxy Config from file and append it to current runtime proxy router
*/
func LoadReverseProxyConfig(configFilepath string) error {
//Load the config file from disk
endpointConfig, err := os.ReadFile(configFilepath)
if err != nil {
return err
}
//Parse it into dynamic proxy endpoint
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
if err != nil {
return err
}
//Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
} else {
return errors.New("not supported proxy type")
}
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
return nil
}
func filterProxyConfigFilename(filename string) string {
//Filter out wildcard characters
filename = strings.ReplaceAll(filename, "*", "(ST)")
filename = strings.ReplaceAll(filename, "?", "(QM)")
filename = strings.ReplaceAll(filename, "[", "(OB)")
filename = strings.ReplaceAll(filename, "]", "(CB)")
filename = strings.ReplaceAll(filename, "#", "(HT)")
return filepath.ToSlash(filename)
}
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
//Save config to file
js, err := json.MarshalIndent(endpoint, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, js, 0775)
}
func RemoveReverseProxyConfig(endpoint string) error {
filename := filepath.Join("./conf/proxy/", endpoint+".config")
if endpoint == "/" {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
if !utils.FileExists(filename) {
return errors.New("target endpoint not exists")
}
return os.Remove(filename)
}
// 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) {
//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: "",
})
if err != nil {
return nil, err
}
return rootProxyEndpoint, nil
}
/*
Importer and Exporter of Zoraxy proxy config
*/
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
includeSysDBRaw, err := 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/"
// 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\"")
// 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 {
if err != nil {
return err
}
if folderPath == filePath {
//Skip root folder
return nil
}
// Create a new file in the zip
if !utils.IsDir(filePath) {
zipFile, err := zipWriter.Create(filePath)
if err != nil {
return err
}
// Open the file on disk
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
return err
}
}
return nil
})
if includeSysDB {
//Also zip in the sysdb
zipFile, err := zipWriter.Create("sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to zip sysdb", err)
return
}
// Open the file on disk
file, err := os.Open("sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
SystemWideLogger.Println(err)
return
}
//Restore sysdb state
sysdb.ReadOnly = false
}
if err != nil {
// Handle the error and send an HTTP response with the error message
http.Error(w, fmt.Sprintf("Failed to zip folder: %v", err), http.StatusInternalServerError)
return
}
}
func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
// Check if the request is a POST with a file upload
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusBadRequest)
return
}
// Max file size limit (10 MB in this example)
r.ParseMultipartForm(10 << 20)
// Get the uploaded file
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to retrieve uploaded file", http.StatusInternalServerError)
return
}
defer file.Close()
if filepath.Ext(handler.Filename) != ".zip" {
http.Error(w, "Upload file is not a zip file", http.StatusInternalServerError)
return
}
// Create the target directory to unzip the files
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}
err = os.MkdirAll(targetDir, os.ModePerm)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError)
return
}
// Open the zip file
zipReader, err := zip.NewReader(file, handler.Size)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open zip file: %v", err), http.StatusInternalServerError)
return
}
restoreDatabase := false
// Extract each file from the zip archive
for _, zipFile := range zipReader.File {
// Open the file in the zip archive
rc, err := zipFile.Open()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open file in zip: %v", err), http.StatusInternalServerError)
return
}
defer rc.Close()
// Create the corresponding file on disk
zipFile.Name = strings.ReplaceAll(zipFile.Name, "../", "")
fmt.Println("Restoring: " + strings.ReplaceAll(zipFile.Name, "\\", "/"))
if zipFile.Name == "sys.db" {
//Sysdb replacement. Close the database and restore
sysdb.Close()
restoreDatabase = true
} else if !strings.HasPrefix(strings.ReplaceAll(zipFile.Name, "\\", "/"), "conf/") {
//Malformed zip file.
http.Error(w, fmt.Sprintf("Invalid zip file structure or version too old"), http.StatusInternalServerError)
return
}
//Check if parent dir exists
if !utils.FileExists(filepath.Dir(zipFile.Name)) {
os.MkdirAll(filepath.Dir(zipFile.Name), 0775)
}
//Create the file
newFile, err := os.Create(zipFile.Name)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError)
return
}
defer newFile.Close()
// Copy the file contents from the zip to the new file
_, err = io.Copy(newFile, rc)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to extract file from zip: %v", err), http.StatusInternalServerError)
return
}
}
// Send a success response
w.WriteHeader(http.StatusOK)
SystemWideLogger.Println("Configuration restored")
fmt.Fprintln(w, "Configuration restored")
if restoreDatabase {
go func() {
SystemWideLogger.Println("Database altered. Restarting in 3 seconds...")
time.Sleep(3 * time.Second)
os.Exit(0)
}()
}
}

298
src/emails.go Normal file
View File

@ -0,0 +1,298 @@
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/utils"
)
/*
SMTP Settings and Test Email Handlers
*/
func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
hostname, err := utils.PostPara(r, "hostname")
if err != nil {
utils.SendErrorResponse(w, "hostname cannot be empty")
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")
return
}
port, err := strconv.Atoi(portString)
if err != nil {
utils.SendErrorResponse(w, "port must be a valid integer")
return
}
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "username cannot be empty")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
//Empty password. Use old one if exists
oldConfig := loadSMTPConfig()
if oldConfig.Password == "" {
utils.SendErrorResponse(w, "password cannot be empty")
return
} else {
password = oldConfig.Password
}
}
senderAddr, err := utils.PostPara(r, "senderAddr")
if err != nil {
utils.SendErrorResponse(w, "senderAddr cannot be empty")
return
}
adminAddr, err := utils.PostPara(r, "adminAddr")
if err != nil {
utils.SendErrorResponse(w, "adminAddr cannot be empty")
return
}
//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),
SenderAddr: strings.TrimSpace(senderAddr),
}
//Write this into database
setSMTPConfig(&thisEmailSender)
//Update the current EmailSender
EmailSender = &thisEmailSender
//Set the admin address of password reset
setSMTPAdminAddress(adminAddr)
//Reply ok
utils.SendOK(w)
}
func HandleSMTPGet(w http.ResponseWriter, r *http.Request) {
// Create a buffer to store the encoded value
var buf bytes.Buffer
// Encode the original object into the buffer
encoder := gob.NewEncoder(&buf)
err := encoder.Encode(*EmailSender)
if err != nil {
utils.SendErrorResponse(w, "Internal encode error")
return
}
// Decode the buffer into a new object
var copied email.Sender
decoder := gob.NewDecoder(&buf)
err = decoder.Decode(&copied)
if err != nil {
utils.SendErrorResponse(w, "Internal decode error")
return
}
copied.Password = ""
js, _ := json.Marshal(copied)
utils.SendJSONResponse(w, string(js))
}
func HandleAdminEmailGet(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(loadSMTPAdminAddr())
utils.SendJSONResponse(w, string(js))
}
func HandleTestEmailSend(w http.ResponseWriter, r *http.Request) {
adminEmailAccount := loadSMTPAdminAddr()
if adminEmailAccount == "" {
utils.SendErrorResponse(w, "Management account not set")
return
}
err := EmailSender.SendEmail(adminEmailAccount,
"SMTP Testing Email | Zoraxy", "This is a test email sent by Zoraxy. Please do not reply to this email.<br>Zoraxy からのテストメールです。このメールには返信しないでください。<br>這是由 Zoraxy 發送的測試電子郵件。請勿回覆此郵件。<br>Ceci est un email de test envoyé par Zoraxy. Merci de ne pas répondre à cet email.<br>Dies ist eine Test-E-Mail, die von Zoraxy gesendet wurde. Bitte antworten Sie nicht auf diese E-Mail.")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/*
SMTP config
The following handle SMTP configs
*/
func setSMTPConfig(config *email.Sender) error {
return sysdb.Write("smtp", "config", config)
}
func loadSMTPConfig() *email.Sender {
if sysdb.KeyExists("smtp", "config") {
thisEmailSender := email.Sender{
Port: 587,
}
err := sysdb.Read("smtp", "config", &thisEmailSender)
if err != nil {
return &email.Sender{
Port: 587,
}
}
return &thisEmailSender
} else {
//Not set. Return an empty one
return &email.Sender{
Port: 587,
}
}
}
func setSMTPAdminAddress(adminAddr string) error {
return sysdb.Write("smtp", "admin", adminAddr)
}
// Load SMTP admin address. Return empty string if not set
func loadSMTPAdminAddr() string {
adminAddr := ""
if sysdb.KeyExists("smtp", "admin") {
err := sysdb.Read("smtp", "admin", &adminAddr)
if err != nil {
return ""
}
return adminAddr
} else {
return ""
}
}
/*
Admin Account Reset
*/
var (
accountResetEmailDelay int64 = 30 //Delay between each account reset email, default 30s
tokenValidDuration int64 = 15 * 60 //Duration of the token, default 15 minutes
lastAccountResetEmail int64 = 0 //Timestamp for last sent account reset email
passwordResetAccessToken string = "" //Access token for resetting password
)
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
if EmailSender.Username == "" || EmailSender.Domain == "" {
//Reset account not setup
utils.SendErrorResponse(w, "Reset account not setup.")
return
}
if loadSMTPAdminAddr() == "" {
utils.SendErrorResponse(w, "Reset account not setup.")
}
//Check if the delay expired
if lastAccountResetEmail+accountResetEmailDelay > time.Now().Unix() {
//Too frequent
utils.SendErrorResponse(w, "You cannot send another account reset email in cooldown time")
return
}
passwordResetAccessToken = uuid.New().String()
//SMTP info exists. Send reset account email
lastAccountResetEmail = time.Now().Unix()
EmailSender.SendEmail(loadSMTPAdminAddr(), "Management Account Reset | Zoraxy",
"Enter the following reset token to reset your password on your Zoraxy router.<br>"+passwordResetAccessToken+"<br><br> This is an automated generated email. DO NOT REPLY TO THIS EMAIL.")
utils.SendOK(w)
}
func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) {
if passwordResetAccessToken == "" {
//Not initiated
utils.SendErrorResponse(w, "Invalid usage")
return
}
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "Invalid username given")
return
}
token, err := utils.PostPara(r, "token")
if err != nil {
utils.SendErrorResponse(w, "Invalid token given")
return
}
newPassword, err := utils.PostPara(r, "newpw")
if err != nil {
utils.SendErrorResponse(w, "Invalid new password given")
return
}
token = strings.TrimSpace(token)
username = strings.TrimSpace(username)
//Validate the token
if token != passwordResetAccessToken {
utils.SendErrorResponse(w, "Invalid Token")
return
}
//Check if time expired
if lastAccountResetEmail+tokenValidDuration < time.Now().Unix() {
//Expired
utils.SendErrorResponse(w, "Token expired")
return
}
//Check if user exists
if !authAgent.UserExists(username) {
//Invalid admin account name
utils.SendErrorResponse(w, "Invalid Username")
return
}
//Delete the user account
authAgent.UnregisterUser(username)
//Ok. Set the new password
err = authAgent.CreateUserAccount(username, newPassword, "")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

34
src/go.mod Normal file
View File

@ -0,0 +1,34 @@
module imuslab.com/zoraxy
go 1.21
toolchain go1.22.2
require (
github.com/boltdb/bolt v1.3.1
github.com/go-acme/lego/v4 v4.16.1
github.com/go-ping/ping v1.1.0
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
)
require (
github.com/aymerick/douceur v0.2.0 // 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/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
)

85
src/go.sum Normal file
View File

@ -0,0 +1,85 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

178
src/main.go Normal file
View File

@ -0,0 +1,178 @@
package main
import (
"embed"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/google/uuid"
"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"
"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/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.1"
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, also handle black list and whitelist features
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.
func SetupCloseHandler() {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
ShutdownSeq()
os.Exit(0)
}()
}
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()
if *showver {
fmt.Println(name + " - Version " + version)
os.Exit(0)
}
SetupCloseHandler()
//Read or create the system uuid
uuidRecord := "./sys.uuid"
if !utils.FileExists(uuidRecord) {
newSystemUUID := uuid.New().String()
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
}
uuidBytes, err := os.ReadFile(uuidRecord)
if err != nil {
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
panic(err)
}
nodeUUID = string(uuidBytes)
//Startup all modules
startupSequence()
//Initiate management interface APIs
requireAuth = !(*noauth)
initAPIs()
//Start the reverse proxy server in go routine
go func() {
ReverseProxtInit()
}()
time.Sleep(500 * time.Millisecond)
//Start the finalize sequences
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(*webUIPort, nil)
if err != nil {
log.Fatal(err)
}
}

377
src/mod/acme/acme.go Normal file
View File

@ -0,0 +1,377 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"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/utils"
)
type CertificateInfoJSON struct {
AcmeName string `json:"acme_name"`
AcmeUrl string `json:"acme_url"`
SkipTLS bool `json:"skip_tls"`
}
// ACMEUser represents a user in the ACME system.
type ACMEUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
// GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string {
return u.Email
}
// GetRegistration returns the registration resource of the ACMEUser.
func (u ACMEUser) GetRegistration() *registration.Resource {
return u.Registration
}
// GetPrivateKey returns the private key of the ACMEUser.
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// ACMEHandler handles ACME-related operations.
type ACMEHandler struct {
DefaultAcmeServer string
Port string
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
Port: port,
}
}
// 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...")
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
return false, err
}
// create a admin user for our new generation
adminUser := ACMEUser{
Email: email,
key: privateKey,
}
// create config
config := lego.NewConfig(&adminUser)
// 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")
config.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
// setup the custom ACME url endpoint.
if caUrl != "" {
config.CADirURL = caUrl
}
// if not custom ACME url, load it from ca.json
if caName == "custom" {
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
} else {
caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
} else {
// (caName == "" || caUrl == "") will use default acme
config.CADirURL = a.DefaultAcmeServer
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
}
}
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
return false, err
}
// setup how to receive challenge
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
return false, err
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
adminUser.Registration = reg
// obtain the certificate
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
return false, err
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
return false, err
}
// Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{
AcmeName: caName,
AcmeUrl: caUrl,
SkipTLS: skipTLS,
}
certInfoBytes, err := json.Marshal(certInfo)
if err != nil {
log.Println(err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
if err != nil {
log.Println(err)
return false, err
}
return true, nil
}
// CheckCertificate returns a list of domains that are in expired certificates.
// It will return all domains that is in expired certificates
// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
// it will said expired as well!
func (a *ACMEHandler) CheckCertificate() []string {
// read from dir
filenames, err := os.ReadDir("./conf/certs/")
expiredCerts := []string{}
if err != nil {
log.Println(err)
return []string{}
}
for _, filename := range filenames {
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBytes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
for _, dnsName := range cert.DNSNames {
if !contains(expiredCerts, dnsName) {
expiredCerts = append(expiredCerts, dnsName)
}
}
if !contains(expiredCerts, cert.Subject.CommonName) {
expiredCerts = append(expiredCerts, cert.Subject.CommonName)
}
}
}
}
}
}
return expiredCerts
}
// return the current port number
func (a *ACMEHandler) Getport() string {
return a.Port
}
// contains checks if a string is present in a slice.
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
// containing the list of expired domains.
func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
type ExpiredDomains struct {
Domain []string `json:"domain"`
}
info := ExpiredDomains{
Domain: a.CheckCertificate(),
}
js, _ := json.MarshalIndent(info, "", " ")
utils.SendJSONResponse(w, string(js))
}
// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
filename, err := utils.PostPara(r, "filename")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
email, err := utils.PostPara(r, "email")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
var caUrl string
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("[INFO] CA not set. Using default")
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")
ca, caUrl = "", ""
}
}
if ca == "" {
//default. Use Let's Encrypt
ca = "Let's Encrypt"
}
var skipTLS bool
if skipTLSString, err := utils.PostPara(r, "skipTLS"); err != nil {
skipTLS = false
} else if skipTLSString != "true" {
skipTLS = false
} else {
skipTLS = true
}
domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
utils.SendJSONResponse(w, strconv.FormatBool(result))
}
// Escape JSON string
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)
return s[1 : len(s)-1]
}
// Helper function to check if a port is in use
func IsPortInUse(port int) bool {
address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address)
if err != nil {
return true // Port is in use
}
defer listener.Close()
return false // Port is not in use
}
// Load cert information from json file
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
certInfoBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
certInfo := &CertificateInfoJSON{}
if err = json.Unmarshal(certInfoBytes, certInfo); err != nil {
return nil, err
}
return certInfo, nil
}

24
src/mod/acme/acme_test.go Normal file
View File

@ -0,0 +1,24 @@
package acme_test
import (
"fmt"
"testing"
"imuslab.com/zoraxy/mod/acme"
)
// Test if the issuer extraction is working
func TestExtractIssuerNameFromPEM(t *testing.T) {
pemFilePath := "test/stackoverflow.pem"
expectedIssuer := "Let's Encrypt"
issuerName, err := acme.ExtractIssuerNameFromPEM(pemFilePath)
fmt.Println(issuerName)
if err != nil {
t.Errorf("Error extracting issuer name: %v", err)
}
if issuerName != expectedIssuer {
t.Errorf("Unexpected issuer name. Expected: %s, Got: %s", expectedIssuer, issuerName)
}
}

View File

@ -0,0 +1,163 @@
package acmewizard
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
/*
ACME Wizard
This wizard help validate the acme settings and configurations
*/
func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
stepNoStr, err := utils.GetPara(r, "step")
if err != nil {
utils.SendErrorResponse(w, "invalid step number given")
return
}
stepNo, err := strconv.Atoi(stepNoStr)
if err != nil {
utils.SendErrorResponse(w, "invalid step number given")
return
}
if stepNo == 1 {
isListening, err := isLocalhostListening()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(isListening)
utils.SendJSONResponse(w, string(js))
} else if stepNo == 2 {
publicIp, err := getPublicIPAddress()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
publicIp = strings.TrimSpace(publicIp)
httpServerReachable := isHTTPServerAvailable(publicIp)
js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js))
} else if stepNo == 3 {
domain, err := utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain cannot be empty")
return
}
domain = strings.TrimSpace(domain)
//Check if the domain is reachable
reachable := isDomainReachable(domain)
if !reachable {
utils.SendErrorResponse(w, "domain is not reachable")
return
}
//Check http is setup correctly
httpServerReachable := isHTTPServerAvailable(domain)
js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "invalid step number")
}
}
// Step 1
func isLocalhostListening() (isListening bool, err error) {
timeout := 2 * time.Second
isListening = false
// Check if localhost is listening on port 80 (HTTP)
conn, err := net.DialTimeout("tcp", "localhost:80", timeout)
if err == nil {
isListening = true
conn.Close()
}
// Check if localhost is listening on port 443 (HTTPS)
conn, err = net.DialTimeout("tcp", "localhost:443", timeout)
if err == nil {
isListening = true
conn.Close()
}
if isListening {
return true, nil
}
return isListening, err
}
// Step 2
func getPublicIPAddress() (string, error) {
resp, err := http.Get("http://checkip.amazonaws.com/")
if err != nil {
return "", err
}
defer resp.Body.Close()
ip, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(ip), nil
}
func isHTTPServerAvailable(ipAddress string) bool {
client := http.Client{
Timeout: 5 * time.Second, // Timeout for the HTTP request
}
urls := []string{
"http://" + ipAddress + ":80",
"https://" + ipAddress + ":443",
}
for _, url := range urls {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println(err, url)
continue // Ignore invalid URLs
}
// Disable TLS verification to handle invalid certificates
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
resp, err := client.Do(req)
if err == nil {
resp.Body.Close()
return true // HTTP server is available
}
}
return false // HTTP server is not available
}
// Step 3
func isDomainReachable(domain string) bool {
_, err := net.LookupHost(domain)
if err != nil {
return false // Domain is not reachable
}
return true // Domain is reachable
}

375
src/mod/acme/autorenew.go Normal file
View File

@ -0,0 +1,375 @@
package acme
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/mail"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
/*
autorenew.go
This script handle auto renew
*/
type AutoRenewConfig struct {
Enabled bool //Automatic renew is enabled
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
}
type AutoRenewer struct {
ConfigFilePath string
CertFolder string
AcmeHandler *ACMEHandler
RenewerConfig *AutoRenewConfig
RenewTickInterval int64
TickerstopChan chan bool
}
type ExpiredCerts struct {
Domains []string
Filepath string
}
// 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) {
if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day
}
//Load the config file. If not found, create one
if !utils.FileExists(config) {
//Create one
os.MkdirAll(filepath.Dir(config), 0775)
newConfig := AutoRenewConfig{
RenewAll: true,
FilesToRenew: []string{},
}
js, _ := json.MarshalIndent(newConfig, "", " ")
err := os.WriteFile(config, js, 0775)
if err != nil {
return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
}
}
renewerConfig := AutoRenewConfig{}
content, err := os.ReadFile(config)
if err != nil {
return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
}
err = json.Unmarshal(content, &renewerConfig)
if err != nil {
return nil, errors.New("Malformed acme config file: " + err.Error())
}
//Create an Auto renew object
thisRenewer := AutoRenewer{
ConfigFilePath: config,
CertFolder: certFolder,
AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval,
}
if thisRenewer.RenewerConfig.Enabled {
//Start the renew ticker
thisRenewer.StartAutoRenewTicker()
//Check and renew certificate on startup
go thisRenewer.CheckAndRenewCertificates()
}
return &thisRenewer, nil
}
func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
time.Sleep(1 * time.Second)
ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
done := make(chan bool)
//Start the ticker to check and renew every x seconds
go func(a *AutoRenewer) {
for {
select {
case <-done:
return
case <-ticker.C:
log.Println("Check and renew certificates in progress")
a.CheckAndRenewCertificates()
}
}
}(a)
a.TickerstopChan = done
}
func (a *AutoRenewer) StopAutoRenewTicker() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
a.TickerstopChan = nil
}
// Handle update auto renew domains
// Set opr for different mode of operations
// 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")
if err != nil {
utils.SendErrorResponse(w, "Operation not set")
return
}
if opr == "setSelected" {
files, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, "Domains is not defined")
return
}
//Parse it int array of string
matchingRuleFiles := []string{}
err = json.Unmarshal([]byte(files), &matchingRuleFiles)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Update the configs
a.RenewerConfig.RenewAll = false
a.RenewerConfig.FilesToRenew = matchingRuleFiles
a.saveRenewConfigToFile()
utils.SendOK(w)
} else if opr == "setAuto" {
a.RenewerConfig.RenewAll = true
a.saveRenewConfigToFile()
utils.SendOK(w)
}
}
// if auto renew all is true (aka auto scan), it will return []string{"*"}
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
results := []string{}
if a.RenewerConfig.RenewAll {
//Auto pick which cert to renew.
results = append(results, "*")
} else {
//Manually set the files to renew
results = a.RenewerConfig.FilesToRenew
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
//Load the current value
js, _ := json.Marshal(a.RenewerConfig.RenewAll)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
renewedDomains, err := a.CheckAndRenewCertificates()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
message := "Domains renewed"
if len(renewedDomains) == 0 {
message = ("All certificates are up-to-date!")
} else {
message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
}
js, _ := json.Marshal(message)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
val, err := utils.PostPara(r, "enable")
if err != nil {
js, _ := json.Marshal(a.RenewerConfig.Enabled)
utils.SendJSONResponse(w, string(js))
} else {
if val == "true" {
//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.StartAutoRenewTicker()
} else {
a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled")
a.StopAutoRenewTicker()
}
}
}
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
email, err := utils.PostPara(r, "set")
if err != nil {
//Return the current email to user
js, _ := json.Marshal(a.RenewerConfig.Email)
utils.SendJSONResponse(w, string(js))
} else {
//Check if the email is valid
_, err := mail.ParseAddress(email)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Set the new config
a.RenewerConfig.Email = email
a.saveRenewConfigToFile()
}
}
// Check and renew certificates. This check all the certificates in the
// certificate folder and return a list of certs that is renewed in this call
// Return string array with length 0 when no cert is expired
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())
return []string{}, err
}
expiredCertList := []*ExpiredCerts{}
if a.RenewerConfig.RenewAll {
//Scan and renew all
for _, file := range files {
if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
//This is a public key file
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || 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())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
Domains: DNSName,
})
}
}
}
} else {
//Only renew those in the list
for _, file := range files {
fileName := file.Name()
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
if contains(a.RenewerConfig.FilesToRenew, certName) {
//This is the one to auto renew
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || 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())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
Domains: DNSName,
})
}
}
}
}
return a.renewExpiredDomains(expiredCertList)
}
func (a *AutoRenewer) Close() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
}
// Renew the certificate by filename extract all DNS name from the
// certificate and renew them one by one by calling to the acmeHandler
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{}
for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
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)
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)
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
certInfo = &CertificateInfoJSON{}
} else {
certInfo = &CertificateInfoJSON{AcmeName: CAName}
}
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
} else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
}
}
return renewedCertFiles, nil
}
// Write the current renewer config to file
func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775)
}

56
src/mod/acme/ca.go Normal file
View File

@ -0,0 +1,56 @@
package acme
/*
CA.go
This script load CA defination from embedded ca.json
*/
import (
_ "embed"
"encoding/json"
"errors"
"log"
"strings"
)
// CA Defination, load from embeded json when startup
type CaDef struct {
Production map[string]string
Test map[string]string
}
//go:embed ca.json
var caJson []byte
var caDef CaDef = CaDef{}
func init() {
runtimeCaDef := CaDef{}
err := json.Unmarshal(caJson, &runtimeCaDef)
if err != nil {
log.Println("[ERR] Unable to unmarshal CA def from embedded file. You sure your ca.json is valid?")
return
}
caDef = runtimeCaDef
}
// Get the CA ACME server endpoint and error if not found
func loadCAApiServerFromName(caName string) (string, error) {
// handle BuyPass cert org section (Buypass AS-983163327)
if strings.HasPrefix(caName, "Buypass AS") {
caName = "Buypass"
}
val, ok := caDef.Production[caName]
if !ok {
return "", errors.New("This CA is not supported")
}
return val, nil
}
func IsSupportedCA(caName string) bool {
_, err := loadCAApiServerFromName(caName)
return err == nil
}

15
src/mod/acme/ca.json Normal file
View File

@ -0,0 +1,15 @@
{
"production": {
"Let's Encrypt": "https://acme-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.buypass.com/acme/directory",
"ZeroSSL": "https://acme.zerossl.com/v2/DV90",
"Google": "https://dv.acme-v02.api.pki.goog/directory"
},
"test":{
"Let's Encrypt": "https://acme-staging-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.test4.buypass.no/acme/directory",
"Google": "https://dv.acme-v02.test-api.pki.goog/directory"
}
}

99
src/mod/acme/utils.go Normal file
View File

@ -0,0 +1,99 @@
package acme
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"time"
)
// Get the issuer name from pem file
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
// Read the PEM file
pemData, err := ioutil.ReadFile(pemFilePath)
if err != nil {
return "", err
}
return ExtractIssuerName(pemData)
}
// Get the DNSName in the cert
func ExtractDomains(certBytes []byte) ([]string, error) {
domains := []string{}
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return []string{}, err
}
for _, dnsName := range cert.DNSNames {
if !contains(domains, dnsName) {
domains = append(domains, dnsName)
}
}
return domains, nil
}
return []string{}, errors.New("decode cert bytes failed")
}
func ExtractIssuerName(certBytes []byte) (string, error) {
// Parse the PEM block
block, _ := pem.Decode(certBytes)
if block == nil || block.Type != "CERTIFICATE" {
return "", fmt.Errorf("failed to decode PEM block containing certificate")
}
// Parse the certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %v", err)
}
// Check if exist incase some acme server didn't have org section
if len(cert.Issuer.Organization) == 0 {
return "", fmt.Errorf("cert didn't have org section exist")
}
// Extract the issuer name
issuer := cert.Issuer.Organization[0]
return issuer, nil
}
// Check if a cert is expired by public key
func CertIsExpired(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
return true
}
}
}
return false
}
func CertExpireSoon(certBytes []byte) 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
timeRemaining := time.Until(expirationDate)
if timeRemaining <= threshold {
return true
}
}
}
return false
}

76
src/mod/aroz/aroz.go Normal file
View File

@ -0,0 +1,76 @@
package aroz
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
)
//To be used with arozos system
type ArozHandler struct {
Port string
restfulEndpoint string
}
//Information required for registering this subservice to arozos
type ServiceInfo struct {
Name string //Name of this module. e.g. "Audio"
Desc string //Description for this module
Group string //Group of the module, e.g. "system" / "media" etc
IconPath string //Module icon image path e.g. "Audio/img/function_icon.png"
Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
StartDir string //Default starting dir, e.g. "Audio/index.html"
SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded
LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode
SupportEmb bool //Support embedded mode
LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module
InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height
InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height
SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
}
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
func HandleFlagParse(info ServiceInfo) *ArozHandler {
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
var port = flag.String("port", ":8000", "Management web interface listening port")
var restful = flag.String("rpt", "", "Reserved")
//Parse the flags
flag.Parse()
if *infoRequestMode {
//Information request mode
jsonString, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(jsonString))
os.Exit(0)
}
return &ArozHandler{
Port: *port,
restfulEndpoint: *restful,
}
}
//Get the username and resources access token from the request, return username, token
func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) {
username := r.Header.Get("aouser")
token := r.Header.Get("aotoken")
return username, token
}
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
return !(a.restfulEndpoint == "")
}
//Request gateway interface for advance permission sandbox control
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
resp, err := http.PostForm(a.restfulEndpoint,
url.Values{"token": {token}, "script": {script}})
if err != nil {
// handle error
return nil, err
}
return resp, nil
}

BIN
src/mod/aroz/doc.txt Normal file

Binary file not shown.

478
src/mod/auth/auth.go Normal file
View File

@ -0,0 +1,478 @@
package auth
/*
author: tobychui
*/
import (
"crypto/rand"
"crypto/sha512"
"errors"
"net/http"
"net/mail"
"strings"
"encoding/hex"
"log"
"github.com/gorilla/sessions"
db "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils"
)
type AuthAgent struct {
//Session related
SessionName string
SessionStore *sessions.CookieStore
Database *db.Database
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
}
type AuthEndpoints struct {
Login string
Logout string
Register string
CheckLoggedIn string
Autologin string
}
//Constructor
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, 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.")
panic(err)
}
//Create a new AuthAgent object
newAuthAgent := AuthAgent{
SessionName: sessionName,
SessionStore: store,
Database: sysdb,
LoginRedirectionHandler: loginRedirectionHandler,
}
//Return the authAgent
return &newAuthAgent
}
func GetSessionKey(sysdb *db.Database) (string, error) {
sysdb.NewTable("auth")
sessionKey := ""
if !sysdb.KeyExists("auth", "sessionkey") {
key := make([]byte, 32)
rand.Read(key)
sessionKey = string(key)
sysdb.Write("auth", "sessionkey", sessionKey)
log.Println("[Auth] New authentication session key generated")
} else {
log.Println("[Auth] Authentication session key loaded from database")
err := sysdb.Read("auth", "sessionkey", &sessionKey)
if err != nil {
return "", errors.New("database read error. Is the database file corrupted?")
}
}
return sessionKey, nil
}
//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
handler(w, r)
} else {
//User not logged in
a.LoginRedirectionHandler(w, r)
}
}
//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)
utils.SendErrorResponse(w, "Username not defined or empty.")
return
}
//Get password from request using POST mode
password, err := utils.PostPara(r, "password")
if err != nil {
//Password not defined
utils.SendErrorResponse(w, "Password not defined or empty.")
return
}
//Get rememberme settings
rememberme := false
rmbme, _ := utils.PostPara(r, "rmbme")
if rmbme == "true" {
rememberme = true
}
//Check the database and see if this user is in the database
passwordCorrect, rejectionReason := a.ValidateUsernameAndPasswordWithReason(username, password)
//The database contain this user information. Check its password if it is correct
if passwordCorrect {
//Password correct
// Set user as authenticated
a.LoginUserByRequest(w, r, username, rememberme)
//Print the login message to console
log.Println(username + " logged in.")
utils.SendOK(w)
} else {
//Password incorrect
log.Println(username + " login request rejected: " + rejectionReason)
utils.SendErrorResponse(w, rejectionReason)
return
}
}
func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool {
succ, _ := a.ValidateUsernameAndPasswordWithReason(username, password)
return succ
}
//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")
return false, "Invalid username or password"
}
if passwordInDB == hashedPassword {
return true, ""
} else {
return false, "Invalid username or password"
}
}
//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)
session.Values["authenticated"] = true
session.Values["username"] = username
session.Values["rememberMe"] = rememberme
//Check if remember me is clicked. If yes, set the maxage to 1 week.
if rememberme {
session.Options = &sessions.Options{
MaxAge: 3600 * 24 * 7, //One week
Path: "/",
}
} else {
session.Options = &sessions.Options{
MaxAge: 3600 * 1, //One hour
Path: "/",
}
}
session.Save(r, w)
}
//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 username != "" {
log.Println(username + " logged out.")
}
// Revoke users authentication
err = a.Logout(w, r)
if err != nil {
utils.SendErrorResponse(w, "Logout failed")
return
}
w.Write([]byte("OK"))
}
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
session, err := a.SessionStore.Get(r, a.SessionName)
if err != nil {
return err
}
session.Values["authenticated"] = false
session.Values["username"] = nil
session.Save(r, w)
return nil
}
//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.
session, _ := a.SessionStore.Get(r, a.SessionName)
return session.Values["username"].(string), nil
} else {
//This user has not logged in.
return "", errors.New("user not logged in")
}
}
//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.
session, _ := a.SessionStore.Get(r, a.SessionName)
username := session.Values["username"].(string)
userEmail := ""
err := a.Database.Read("auth", "email/"+username, &userEmail)
if err != nil {
return "", err
}
return userEmail, nil
} else {
//This user has not logged in.
return "", errors.New("user not logged in")
}
}
//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")
} else {
utils.SendJSONResponse(w, "false")
}
}
//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")
if err != nil {
utils.SendErrorResponse(w, "Missing 'username' paramter")
return
}
//Get password from request
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "Missing 'password' paramter")
return
}
//Get email from request
email, err := utils.PostPara(r, "email")
if err != nil {
utils.SendErrorResponse(w, "Missing 'email' paramter")
return
}
_, err = mail.ParseAddress(email)
if err != nil {
utils.SendErrorResponse(w, "Invalid or malformed email")
return
}
//Ok to proceed create this user
err = a.CreateUserAccount(newusername, password, email)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Do callback if exists
if callback != nil {
callback(newusername, email)
}
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] New user " + newusername + " added to system.")
}
//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")
if err != nil {
utils.SendErrorResponse(w, "Missing 'username' paramter")
return
}
//Get password from request
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "Missing 'password' paramter")
return
}
//Ok to proceed create this user
err = a.CreateUserAccount(newusername, password, "")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Do callback if exists
if callback != nil {
callback(newusername, "")
}
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] Admin account created: " + newusername)
}
//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
}
return true
}
//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) {
//This user has not logged in
utils.SendErrorResponse(w, "Login required to remove user from the system.")
return
}
//Get username from request
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "Missing 'username' paramter")
return
}
err = a.UnregisterUser(username)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] User " + username + " has been removed from the system.")
}
func (a *AuthAgent) UnregisterUser(username string) error {
//Check if the user exists in the system database.
if !a.Database.KeyExists("auth", "passhash/"+username) {
//This user do not exists.
return errors.New("this user does not exists")
}
//OK! Remove the user from the database
a.Database.Delete("auth", "passhash/"+username)
a.Database.Delete("auth", "email/"+username)
return nil
}
//Get the number of users in the system
func (a *AuthAgent) GetUserCounts() int {
entries, _ := a.Database.ListTable("auth")
usercount := 0
for _, keypairs := range entries {
if strings.Contains(string(keypairs[0]), "passhash/") {
//This is a user registry
usercount++
}
}
if usercount == 0 {
log.Println("There are no user in the database.")
}
return usercount
}
//List all username within the system
func (a *AuthAgent) ListUsers() []string {
entries, _ := a.Database.ListTable("auth")
results := []string{}
for _, keypairs := range entries {
if strings.Contains(string(keypairs[0]), "passhash/") {
username := strings.Split(string(keypairs[0]), "/")[1]
results = append(results, username)
}
}
return results
}
//Check if the given username exists
func (a *AuthAgent) UserExists(username string) bool {
userpasswordhash := ""
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
if err != nil || userpasswordhash == "" {
return false
}
return true
}
//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) {
//User authenticated. Extend its expire time
rememberme := session.Values["rememberMe"].(bool)
//Extend the session expire time
if rememberme {
session.Options = &sessions.Options{
MaxAge: 3600 * 24 * 7, //One week
Path: "/",
}
} else {
session.Options = &sessions.Options{
MaxAge: 3600 * 1, //One hour
Path: "/",
}
}
session.Save(r, w)
return true
} else {
return false
}
}
//Create user account
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
//Check user already exists
if a.UserExists(newusername) {
return errors.New("user with same name already exists")
}
key := newusername
hashedPassword := Hash(password)
err := a.Database.Write("auth", "passhash/"+key, hashedPassword)
if err != nil {
return err
}
if email != "" {
err = a.Database.Write("auth", "email/"+key, email)
if err != nil {
return err
}
}
return nil
}
//Hash the given raw string into sha512 hash
func Hash(raw string) string {
h := sha512.New()
h.Write([]byte(raw))
return hex.EncodeToString(h.Sum(nil))
}

53
src/mod/auth/router.go Normal file
View File

@ -0,0 +1,53 @@
package auth
import (
"errors"
"log"
"net/http"
)
type RouterOption struct {
AuthAgent *AuthAgent
RequireAuth bool //This router require authentication
DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected
}
type RouterDef struct {
option RouterOption
endpoints map[string]func(http.ResponseWriter, *http.Request)
}
func NewManagedHTTPRouter(option RouterOption) *RouterDef {
return &RouterDef{
option: option,
endpoints: map[string]func(http.ResponseWriter, *http.Request){},
}
}
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)
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) {
handler(w, r)
})
} else {
handler(w, r)
}
})
router.endpoints[endpoint] = handler
return nil
}

View File

@ -0,0 +1,120 @@
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 (
"sync"
)
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
}
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return newDatabase(dbfile, readOnlyMode)
}
/*
Create / Drop a table
Usage:
err := sysdb.NewTable("MyTable")
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
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)
}
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)
}
func (d *Database) Close() {
d.close()
}

View File

@ -0,0 +1,186 @@
//go:build !mipsle && !riscv64
// +build !mipsle,!riscv64
package database
import (
"encoding/json"
"errors"
"log"
"sync"
"github.com/boltdb/bolt"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
}
tableMap := sync.Map{}
//Build the table list from database
err = db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
tableMap.Store(string(name), "")
return nil
})
})
return &Database{
Db: db,
Tables: tableMap,
ReadOnly: readOnlyMode,
}, err
}
//Dump the whole db into a log file
func (d *Database) dump(filename string) ([]string, error) {
results := []string{}
d.Tables.Range(func(tableName, v interface{}) bool {
entries, err := d.ListTable(tableName.(string))
if err != nil {
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
return false
}
for _, keypairs := range entries {
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
}
return true
})
return results, nil
}
//Create a new table
func (d *Database) newTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
d.Tables.Store(tableName, "")
return err
}
//Check is table exists
func (d *Database) tableExists(tableName string) bool {
if _, ok := d.Tables.Load(tableName); ok {
return true
}
return false
}
//Drop the given table
func (d *Database) dropTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
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 {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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 {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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()
}

View File

@ -0,0 +1,208 @@
//go:build mipsle || riscv64
// +build mipsle riscv64
package database
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"strings"
"sync"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
dbRootPath = "fsdb/" + dbRootPath
err := os.MkdirAll(dbRootPath, 0755)
if err != nil {
return nil, err
}
tableMap := sync.Map{}
//build the table list from file system
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
if err != nil {
return nil, err
}
for _, file := range files {
if isDirectory(file) {
tableMap.Store(filepath.Base(file), "")
}
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{
Db: dbRootPath,
Tables: tableMap,
ReadOnly: readOnlyMode,
}, 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 {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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 {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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 {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
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
}

View File

@ -0,0 +1,240 @@
package dynamicproxy
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
)
/*
Server.go
Main server for dynamic proxy core
Routing Handler Priority (High to Low)
- Blacklist
- Whitelist
- Redirectable
- Subdomain Routing
- Vitrual Directory Routing
*/
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
*/
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r)
return
}
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/*
Redirection Routing
*/
//Check if this is a redirection url
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
h.logRequest(r, statusCode != 500, statusCode, "redirect", "")
return
}
//Extract request host to see if it is virtual directory or subdomain
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
/*
Host Routing
*/
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
return
}
}
//Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
return
}
}
//Fallback to handle by the host proxy forwarder
h.hostRequest(w, r, sep)
return
}
/*
Root Router Handling
*/
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
} else {
//Passthrough the request to root
h.handleRootRouting(w, r)
}
} else {
//No routing rules found.
h.handleRootRouting(w, r)
}
}
/*
handleRootRouting
This function handle root routing situations where there are no subdomain
, vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
//Get the proxy root config
proot := h.Parent.Root
switch proot.DefaultSiteOption {
case DefaultSite_InternalStaticWebServer:
fallthrough
case DefaultSite_ReverseProxy:
//They both share the same behavior
//Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
return
}
}
//No vdir match. Route via root router
h.hostRequest(w, r, h.Parent.Root)
case DefaultSite_Redirect:
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
if redirectTarget == "" {
redirectTarget = "about:blank"
}
//Check if it is an infinite loopback redirect
parsedURL, err := url.Parse(proot.DefaultSiteValue)
if err != nil {
//Error when parsing target. Send to root
h.hostRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if hostname == domainOnly {
h.logRequest(r, false, 500, "root-redirect", domainOnly)
http.Error(w, "Loopback redirects due to invalid settings", 500)
return
}
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
//Serve the not found page, use template if exists
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
if err != nil {
w.Write(page_hosterror)
} else {
w.Write(template)
}
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}

View File

@ -0,0 +1,54 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
break
}
}
if !matchingFound {
h.logRequest(r, false, 401, "host", pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -0,0 +1,23 @@
package domainsniff
import (
"net"
"time"
)
//Check if the domain is reachable and return err if not reachable
func DomainReachableWithError(domain string) error {
timeout := 1 * time.Second
conn, err := net.DialTimeout("tcp", domain, timeout)
if err != nil {
return err
}
conn.Close()
return nil
}
//Check if domain reachable
func DomainReachable(domain string) bool {
return DomainReachableWithError(domain) == nil
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-present tobychui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,542 @@
package dpcore
import (
"context"
"errors"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the
// client, support http, also support https tunnel using http.hijacker
type ReverseProxy struct {
// Set the timeout of the proxy server, default is 5 minutes
Timeout time.Duration
// Director must be a function which modifies
// the request into a new request to be sent
// using Transport. Its response is then copied
// back to the original client unmodified.
// Director must not access the provided Request
// after returning.
Director func(*http.Request)
// The transport used to perform proxy requests.
// default is http.DefaultTransport.
Transport http.RoundTripper
// FlushInterval specifies the flush interval
// to flush to the client while copying the
// response body. If zero, no periodic flushing is done.
FlushInterval time.Duration
// ErrorLog specifies an optional logger for errors
// that occur when attempting to proxy the request.
// If nil, logging goes to os.Stderr via the log package's
// standard logger.
ErrorLog *log.Logger
// ModifyResponse is an optional function that
// modifies the Response from the backend.
// If it returns an error, the proxy returns a StatusBadGateway error.
ModifyResponse func(*http.Response) error
//Prepender is an optional prepend text for URL rewrite
//
Prepender string
Verbal bool
}
type ResponseRewriteRuleSet struct {
ProxyDomain string
OriginalHost string
UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this
}
type requestCanceler interface {
CancelRequest(req *http.Request)
}
type DpcoreOptions struct {
IgnoreTLSVerification bool
FlushInterval time.Duration
}
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
targetQuery := target.RawQuery
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
}
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
optimalConcurrentConnection := 32
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
}
return &ReverseProxy{
Director: director,
Prepender: prepender,
FlushInterval: dpcOptions.FlushInterval,
Verbal: false,
Transport: thisTransporter,
}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
func joinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return singleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()
aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")
switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
// Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
var hopHeaders = []string{
//"Connection",
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te", // canonicalized version of "TE"
"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
"Transfer-Encoding",
//"Upgrade",
}
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
var w io.Writer = dst
if flushInterval != 0 {
mlw := &maxLatencyWriter{
dst: dst,
flush: http.NewResponseController(dst).Flush,
latency: flushInterval,
}
defer mlw.stop()
// set up initial timer so headers get flushed even if body writes are delayed
mlw.flushPending = true
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
w = mlw
}
var buf []byte
_, err := p.copyBuffer(w, src, buf)
return err
}
// Copy with given buffer size. Default to 64k
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
if len(buf) == 0 {
buf = make([]byte, 64*1024)
}
var written int64
for {
nr, rerr := src.Read(buf)
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
p.logf("dpcore read error during body copy: %v", rerr)
}
if nr > 0 {
nw, werr := dst.Write(buf[:nr])
if nw > 0 {
written += int64(nw)
}
if werr != nil {
return written, werr
}
if nr != nw {
return written, io.ErrShortWrite
}
}
if rerr != nil {
if rerr == io.EOF {
rerr = nil
}
return written, rerr
}
}
}
func (p *ReverseProxy) logf(format string, args ...interface{}) {
if p.ErrorLog != nil {
p.ErrorLog.Printf(format, args...)
} else {
log.Printf(format, args...)
}
}
func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") {
if f = strings.TrimSpace(f); f != "" {
header.Del(f)
}
}
}
// Remove hop-by-hop headers
for _, h := range hopHeaders {
if header.Get(h) != "" {
header.Del(h)
}
}
//Restore the Upgrade header if any
if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
//Hide Go-HTTP-Client UA if the client didnt sent us one
if _, ok := header["User-Agent"]; !ok {
// If the outbound request doesn't have a User-Agent header set,
// don't send the default Go HTTP client User-Agent.
header.Set("User-Agent", "")
}
}
func addXForwardedForHeader(req *http.Request) {
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := req.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
if req.Header.Get("X-Real-Ip") == "" {
//Check if CF-Connecting-IP header exists
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
} else {
// Not exists. Fill it in with first entry in X-Forwarded-For
ips := strings.Split(clientIP, ",")
if len(ips) > 0 {
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
}
}
}
}
}
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
transport := p.Transport
outreq := new(http.Request)
// Shallow copies of maps, like header
*outreq = *req
if cn, ok := rw.(http.CloseNotifier); ok {
if requestCanceler, ok := transport.(requestCanceler); ok {
// After the Handler has returned, there is no guarantee
// that the channel receives a value, so to make sure
reqDone := make(chan struct{})
defer close(reqDone)
clientGone := cn.CloseNotify()
go func() {
select {
case <-clientGone:
requestCanceler.CancelRequest(outreq)
case <-reqDone:
}
}()
}
}
p.Director(outreq)
outreq.Close = false
if !rrr.UseTLS {
//This seems to be routing to external sites
//Do not keep the original host
outreq.Host = rrr.OriginalHost
}
// We may modify the header (shallow copied above), so we only copy it.
outreq.Header = make(http.Header)
copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header.
addXForwardedForHeader(outreq)
res, err := transport.RoundTrip(outreq)
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
//rw.WriteHeader(http.StatusBadGateway)
return err
}
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header, rrr.NoCache)
//Remove the User-Agent header if exists
if _, ok := res.Header["User-Agent"]; ok {
//Server to client request should not contains a User-Agent header
res.Header.Del("User-Agent")
}
if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
//rw.WriteHeader(http.StatusBadGateway)
return err
}
}
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Custom header rewriter functions
if res.Header.Get("Location") != "" {
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation)
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path
//Replace the forwarded target with expected Host
lr, err := replaceLocationHost(locationRewrite, rrr, req.TLS != nil)
if err == nil {
locationRewrite = lr
}
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
//Back to the root of this proxy object
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation
} else {
//Relative path. Do not modifiy location header
}
//Custom redirection to this rproxy relative path
res.Header.Set("Location", locationRewrite)
}
// Copy header from response to client.
copyHeader(rw.Header(), res.Header)
// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
if len(res.Trailer) > 0 {
trailerKeys := make([]string, 0, len(res.Trailer))
for k := range res.Trailer {
trailerKeys = append(trailerKeys, k)
}
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
}
rw.WriteHeader(res.StatusCode)
if len(res.Trailer) > 0 {
// Force chunking if we saw a response trailer.
// This prevents net/http from calculating the length for short
// bodies and adding a Content-Length.
if fl, ok := rw.(http.Flusher); ok {
fl.Flush()
}
}
//Get flush interval in real time and start copying the request
flushInterval := p.getFlushInterval(req, res)
p.copyResponse(rw, res.Body, flushInterval)
// close now, instead of defer, to populate res.Trailer
res.Body.Close()
copyHeader(rw.Header(), res.Trailer)
return nil
}
func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error {
hij, ok := rw.(http.Hijacker)
if !ok {
p.logf("http server does not support hijacker")
return errors.New("http server does not support hijacker")
}
clientConn, _, err := hij.Hijack()
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
}
proxyConn, err := net.Dial("tcp", req.URL.Host)
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
}
// The returned net.Conn may have read or write deadlines
// already set, depending on the configuration of the
// Server, to set or clear those deadlines as needed
// we set timeout to 5 minutes
deadline := time.Now()
if p.Timeout == 0 {
deadline = deadline.Add(time.Minute * 5)
} else {
deadline = deadline.Add(p.Timeout)
}
err = clientConn.SetDeadline(deadline)
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
}
err = proxyConn.SetDeadline(deadline)
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
}
_, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
if err != nil {
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
}
go func() {
io.Copy(clientConn, proxyConn)
clientConn.Close()
proxyConn.Close()
}()
io.Copy(proxyConn, clientConn)
proxyConn.Close()
clientConn.Close()
return nil
}
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
if req.Method == "CONNECT" {
err := p.ProxyHTTPS(rw, req)
return err
} else {
err := p.ProxyHTTP(rw, req, rrr)
return err
}
}

View File

@ -0,0 +1,49 @@
package dpcore_test
import (
"testing"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
func TestReplaceLocationHost(t *testing.T) {
urlString := "http://private.com/test/newtarget/"
rrr := &dpcore.ResponseRewriteRuleSet{
OriginalHost: "test.example.com",
ProxyDomain: "private.com/test",
UseTLS: true,
}
useTLS := true
expectedResult := "https://test.example.com/newtarget/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if result != expectedResult {
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
}
}
func TestReplaceLocationHostRelative(t *testing.T) {
urlString := "api/"
rrr := &dpcore.ResponseRewriteRuleSet{
OriginalHost: "test.example.com",
ProxyDomain: "private.com/test",
UseTLS: true,
}
useTLS := true
expectedResult := "https://test.example.com/api/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if result != expectedResult {
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
}
}

View File

@ -0,0 +1,38 @@
package dpcore
import (
"mime"
"net/http"
"time"
)
// Auto sniff of flush interval from header
func (p *ReverseProxy) getFlushInterval(req *http.Request, res *http.Response) time.Duration {
contentType := req.Header.Get("Content-Type")
if actualContentType, _, _ := mime.ParseMediaType(contentType); actualContentType == "text/event-stream" {
return -1
}
if req.ContentLength == -1 || p.isBidirectionalStream(req, res) {
return -1
}
//Cannot sniff anything. Use default value
return p.FlushInterval
}
// Check for bidirectional stream, copy from Caddy :D
func (p *ReverseProxy) isBidirectionalStream(req *http.Request, res *http.Response) bool {
// We have to check the encoding here; only flush headers with identity encoding.
// Non-identity encoding might combine with "encode" directive, and in that case,
// if body size larger than enc.MinLength, upper level encode handle might have
// Content-Encoding header to write.
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
ae := req.Header.Get("Accept-Encoding")
return req.ProtoMajor == 2 &&
res.ProtoMajor == 2 &&
res.ContentLength == -1 &&
(ae == "identity" || ae == "")
}

View File

@ -0,0 +1,73 @@
package dpcore
/*
Max Latency Writer
This script implements a io writer with periodic flushing base on a ticker
Mostly based on httputil.ReverseProxy
*/
import (
"io"
"sync"
"time"
)
type maxLatencyWriter struct {
dst io.Writer
flush func() error
latency time.Duration // non-zero; negative means to flush immediately
mu sync.Mutex // protects t, flushPending, and dst.Flush
t *time.Timer
flushPending bool
}
func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
n, err = m.dst.Write(p)
if m.latency < 0 {
//Flush immediately
m.flush()
return
}
if m.flushPending {
//Flush in next tick cycle
return
}
if m.t == nil {
m.t = time.AfterFunc(m.latency, m.delayedFlush)
} else {
m.t.Reset(m.latency)
}
m.flushPending = true
return
}
func (m *maxLatencyWriter) delayedFlush() {
m.mu.Lock()
defer m.mu.Unlock()
if !m.flushPending {
// if stop was called but AfterFunc already started this goroutine
return
}
m.flush()
m.flushPending = false
}
func (m *maxLatencyWriter) stop() {
m.mu.Lock()
defer m.mu.Unlock()
m.flushPending = false
if m.t != nil {
m.t.Stop()
}
}

View File

@ -0,0 +1,62 @@
package dpcore
import (
"net/url"
"strings"
)
// replaceLocationHost rewrite the backend server's location header to a new URL based on the given proxy rules
// If you have issues with tailing slash, you can try to fix them here (and remember to PR :D )
func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
u, err := url.Parse(urlString)
if err != nil {
return "", err
}
//Update the schemetic if the proxying target is http
//but exposed as https to the internet via Zoraxy
if useTLS {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
//Issue #39: Check if it is location target match the proxying domain
//E.g. Proxy config: blog.example.com -> example.com/blog
//Check if it is actually redirecting to example.com instead of a new domain
//like news.example.com.
// The later check bypass apache screw up method of redirection header
// e.g. https://imuslab.com -> http://imuslab.com:443
if rrr.ProxyDomain != u.Host && !strings.Contains(u.Host, rrr.OriginalHost+":") {
//New location domain not matching proxy target domain.
//Do not modify location header
return urlString, nil
}
u.Host = rrr.OriginalHost
if strings.Contains(rrr.ProxyDomain, "/") {
//The proxy domain itself seems contain subpath.
//Trim it off from Location header to prevent URL segment duplicate
//E.g. Proxy config: blog.example.com -> example.com/blog
//Location Header: /blog/post?id=1
//Expected Location Header send to client:
// blog.example.com/post?id=1 instead of blog.example.com/blog/post?id=1
ProxyDomainURL := "http://" + rrr.ProxyDomain
if rrr.UseTLS {
ProxyDomainURL = "https://" + rrr.ProxyDomain
}
ru, err := url.Parse(ProxyDomainURL)
if err == nil {
//Trim off the subpath
u.Path = strings.TrimPrefix(u.Path, ru.Path)
}
}
return u.String(), nil
}
// Debug functions
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
return replaceLocationHost(urlString, rrr, useTLS)
}

View File

@ -0,0 +1,316 @@
package dynamicproxy
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
Zoraxy Dynamic Proxy
*/
func NewDynamicProxy(option RouterOption) (*Router, error) {
proxyMap := sync.Map{}
thisRouter := Router{
Option: &option,
ProxyEndpoints: &proxyMap,
Running: false,
server: nil,
routingRules: []*RoutingRule{},
tldMap: map[string]int{},
}
thisRouter.mux = &ProxyHandler{
Parent: &thisRouter,
}
return &thisRouter, nil
}
// Update TLS setting in runtime. Will restart the proxy server
// if it is already running in the background
func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
router.Option.UseTls = tlsEnabled
router.Restart()
}
// Update TLS Version in runtime. Will restart proxy server if running.
// Set this to true to force TLS 1.2 or above
func (router *Router) UpdateTLSVersion(requireLatest bool) {
router.Option.ForceTLSLatest = requireLatest
router.Restart()
}
// Update port 80 listener state
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
router.Option.ListenOnPort80 = useRedirect
router.Restart()
}
// Update https redirect, which will require updates
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
router.Option.ForceHttpsRedirect = useRedirect
router.Restart()
}
// Start the dynamic routing
func (router *Router) StartProxyService() error {
//Create a new server object
if router.server != nil {
return errors.New("reverse proxy server already running")
}
//Check if root route is set
if router.Root == nil {
return errors.New("reverse proxy router root not set")
}
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
}
config := &tls.Config{
GetCertificate: router.Option.TlsManager.GetCert,
MinVersion: uint16(minVersion),
}
if router.Option.UseTls {
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
TLSConfig: config,
}
router.Running = true
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
//Add a 80 to 443 redirector
httpServer := &http.Server{
Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Check if the domain requesting allow non TLS mode
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := router.getProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS,
PathPrefix: "",
})
return
}
if router.Option.ForceHttpsRedirect {
//Redirect to https is enabled
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
} else {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
}
} else {
//Do not do redirection
if sep != nil {
//Sub-domain exists but not allow non-TLS access
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("400 - Bad Request"))
} else {
//No defined sub-domain
http.NotFound(w, r)
}
}
}),
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Println("Starting HTTP-to-HTTPS redirector (port 80)")
//Create a redirection stop channel
stopChan := make(chan bool)
//Start a blocking wait for shutting down the http to https redirection server
go func() {
<-stopChan
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
httpServer.Shutdown(ctx)
log.Println("HTTP to HTTPS redirection listener stopped")
}()
//Start the http server that listens to port 80 and redirect to 443
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
//Unable to startup port 80 listener. Handle shutdown process gracefully
stopChan <- true
log.Fatalf("Could not start redirection server: %v\n", err)
}
}()
router.tlsRedirectStop = stopChan
}
//Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)")
go func() {
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start proxy server: %v\n", err)
}
}()
} else {
//Serve with non TLS mode
router.tlsListener = nil
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true
log.Println("Reverse proxy service started in the background (Plain HTTP mode)")
go func() {
router.server.ListenAndServe()
//log.Println("[DynamicProxy] " + err.Error())
}()
}
return nil
}
func (router *Router) StopProxyService() error {
if router.server == nil {
return errors.New("reverse proxy server already stopped")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := router.server.Shutdown(ctx)
if err != nil {
return err
}
if router.tlsListener != nil {
router.tlsListener.Close()
}
if router.tlsRedirectStop != nil {
router.tlsRedirectStop <- true
}
//Discard the server object
router.tlsListener = nil
router.server = nil
router.Running = false
router.tlsRedirectStop = nil
return nil
}
// Restart the current router if it is running.
func (router *Router) Restart() error {
//Stop the router if it is already running
if router.Running {
err := router.StopProxyService()
if err != nil {
return err
}
time.Sleep(300 * time.Millisecond)
// Start the server
err = router.StartProxyService()
if err != nil {
return err
}
}
return nil
}
/*
Check if a given request is accessed via a proxied subdomain
*/
func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
hostname := r.Header.Get("X-Forwarded-Host")
if hostname == "" {
hostname = r.Host
}
hostname = strings.Split(hostname, ":")[0]
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
return subdEndpoint != nil
}
/*
Load routing from RP
*/
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
var targetProxyEndpoint *ProxyEndpoint
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
key, ok := key.(string)
if !ok {
return true
}
v, ok := value.(*ProxyEndpoint)
if !ok {
return true
}
if key == matchingDomain {
targetProxyEndpoint = v
}
return true
})
if targetProxyEndpoint == nil {
return nil, errors.New("target routing rule not found")
}
return targetProxyEndpoint, nil
}
// Deep copy a proxy endpoint, excluding runtime paramters
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
js, _ := json.Marshal(endpoint)
newProxyEndpoint := ProxyEndpoint{}
err := json.Unmarshal(js, &newProxyEndpoint)
if err != nil {
return nil
}
return &newProxyEndpoint
}
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint)
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
k, ok := key.(string)
if !ok {
return true
}
v, ok := value.(*ProxyEndpoint)
if !ok {
return true
}
m[k] = v
return true
})
return m
}

View File

@ -0,0 +1,158 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
/*
endpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
/*
User Defined Header Functions
*/
// Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) {
return true
}
}
return false
}
// Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header)
}
}
ep.UserDefinedHeaders = newHeaderList
return nil
}
// Add a user defined header to the list, duplicates will be automatically removed
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
if ep.UserDefinedHeaderExists(key) {
ep.RemoveUserDefinedHeader(key)
}
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
Value: value,
})
return nil
}
/*
Virtual Directory Functions
*/
// Get virtual directory handler from given URI
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories {
if strings.HasPrefix(requestURI, vdir.MatchingPath) {
return vdir
}
}
return nil
}
// Get virtual directory handler by matching path (exact match required)
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories {
if vdir.MatchingPath == matchingPath {
return vdir
}
}
return nil
}
// Delete a vdir rule by its matching path
func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath string) error {
entryFound := false
newVirtualDirectoryList := []*VirtualDirectoryEndpoint{}
for _, vdir := range ep.VirtualDirectories {
if vdir.MatchingPath == matchingPath {
entryFound = true
} else {
newVirtualDirectoryList = append(newVirtualDirectoryList, vdir)
}
}
if entryFound {
//Update the list of vdirs
ep.VirtualDirectories = newVirtualDirectoryList
return nil
}
return errors.New("target virtual directory routing rule not found")
}
// Delete a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
return nil, errors.New("rule with same matching path already exists")
}
//Append it to the list of virtual directory
ep.VirtualDirectories = append(ep.VirtualDirectories, vdir)
//Prepare to replace the current routing rule
parentRouter := ep.parent
readyRoutingRule, err := parentRouter.PrepareProxyRoute(ep)
if err != nil {
return nil, err
}
if ep.ProxyType == ProxyType_Root {
parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host {
ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else {
return nil, errors.New("unsupported proxy type")
}
return readyRoutingRule, nil
}
// Create a deep clone object of the proxy endpoint
// Note the returned object is not activated. Call to prepare function before pushing into runtime
func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
clonedProxyEndpoint := ProxyEndpoint{}
js, _ := json.Marshal(ep)
json.Unmarshal(js, &clonedProxyEndpoint)
return &clonedProxyEndpoint
}
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
}

View File

@ -0,0 +1,240 @@
package dynamicproxy
import (
"errors"
"log"
"net"
"net/http"
"net/url"
"path/filepath"
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy"
)
func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
var targetProxyEndpoint *ProxyEndpoint = nil
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
rootname := key.(string)
if strings.HasPrefix(requestURI, rootname) {
thisProxyEndpoint := value.(*ProxyEndpoint)
targetProxyEndpoint = thisProxyEndpoint
}
return true
})
return targetProxyEndpoint
}
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.ProxyEndpoints.Load(hostname)
if ok {
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
}
//No hit. Try with wildcard
matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
if err != nil {
//Continue
return true
}
if match {
//targetSubdomainEndpoint = ep
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
return true
})
if len(matchProxyEndpoints) == 1 {
//Only 1 match
return matchProxyEndpoints[0]
} else if len(matchProxyEndpoints) > 1 {
//More than one match. Get the best match one
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
})
return matchProxyEndpoints[0]
}
return targetSubdomainEndpoint
}
// Clearn URL Path (without the http:// part) replaces // in a URL to /
func (router *Router) clearnURL(targetUrlOPath string) string {
return strings.ReplaceAll(targetUrlOPath, "//", "/")
}
// Rewrite URL rewrite the prefix part of a virtual directory URL with /
func (router *Router) rewriteURL(rooturl string, requestURL string) string {
rewrittenURL := requestURL
rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/"))
if strings.Contains(rewrittenURL, "//") {
rewrittenURL = router.clearnURL(rewrittenURL)
}
return rewrittenURL
}
// Handle host request
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.UserDefinedHeaders) > 0 {
for _, customHeader := range target.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
}
if len(requestURL) > 0 && requestURL[:1] == "/" {
//Remove starting / from request URL if exists
requestURL = requestURL[1:]
}
u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
}
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r)
return
}
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.logRequest(r, false, 404, "subdomain-http", target.Domain)
} else {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
h.logRequest(r, false, 521, "subdomain-http", target.Domain)
}
}
h.logRequest(r, true, 200, "subdomain-http", target.Domain)
}
// Handle vdir type request
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.parent.UserDefinedHeaders) > 0 {
for _, customHeader := range target.parent.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
}
u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String())
if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
}
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r)
return
}
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath,
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.logRequest(r, false, 404, "vdir-http", target.Domain)
} else {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
h.logRequest(r, false, 521, "vdir-http", target.Domain)
}
}
h.logRequest(r, true, 200, "vdir-http", target.Domain)
}
func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
if h.Parent.Option.StatisticCollector != nil {
go func() {
requestInfo := statistic.RequestInfo{
IpAddr: geodb.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ,
StatusCode: statusCode,
ForwardType: forwardType,
Referer: r.Referer(),
UserAgent: r.UserAgent(),
RequestURL: r.Host + r.RequestURI,
Target: target,
}
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
}()
}
}

View File

@ -0,0 +1,58 @@
package redirection
import (
"log"
"net/http"
"strings"
)
/*
handler.go
This script store the handlers use for handling
redirection request
*/
// Check if a request URL is a redirectable URI
func (t *RuleTable) IsRedirectable(r *http.Request) bool {
requestPath := r.Host + r.URL.Path
rr := t.MatchRedirectRule(requestPath)
return rr != nil
}
// Handle the redirect request, return after calling this function to prevent
// multiple write to the response writer
// Return the status code of the redirection handling
func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
requestPath := r.Host + r.URL.Path
rr := t.MatchRedirectRule(requestPath)
if rr != nil {
redirectTarget := rr.TargetURL
if rr.ForwardChildpath {
//Remove the first / in the path if the redirect target already have tailing slash
if strings.HasSuffix(redirectTarget, "/") {
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
} else {
redirectTarget += r.URL.Path
}
if r.URL.RawQuery != "" {
redirectTarget += "?" + r.URL.RawQuery
}
}
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
redirectTarget = "http://" + redirectTarget
}
http.Redirect(w, r, redirectTarget, rr.StatusCode)
return rr.StatusCode
} else {
//Invalid usage
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
log.Println("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!")
return 500
}
}

View File

@ -0,0 +1,199 @@
package redirection
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type RuleTable struct {
AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity
Logger *logger.Logger
configPath string //The location where the redirection rules is stored
rules sync.Map //Store the redirection rules for this reverse proxy instance
}
type RedirectRules struct {
RedirectURL string //The matching URL to redirect
TargetURL string //The destination redirection url
ForwardChildpath bool //Also redirect the pathname
StatusCode int //Status Code for redirection
}
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
thisRuleTable := RuleTable{
rules: sync.Map{},
configPath: configPath,
AllowRegex: allowRegex,
}
//Load all the rules from the config path
if !utils.FileExists(configPath) {
os.MkdirAll(configPath, 0775)
}
// Load all the *.json from the configPath
files, err := filepath.Glob(filepath.Join(configPath, "*.json"))
if err != nil {
return nil, err
}
// Parse the json content into RedirectRules
var rules []*RedirectRules
for _, file := range files {
b, err := os.ReadFile(file)
if err != nil {
continue
}
thisRule := RedirectRules{}
err = json.Unmarshal(b, &thisRule)
if err != nil {
continue
}
rules = append(rules, &thisRule)
}
//Map the rules into the sync map
for _, rule := range rules {
log.Println("Redirection rule added: " + rule.RedirectURL + " -> " + rule.TargetURL)
thisRuleTable.rules.Store(rule.RedirectURL, rule)
}
return &thisRuleTable, nil
}
func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int) error {
// Create a new RedirectRules object with the given parameters
newRule := &RedirectRules{
RedirectURL: redirectURL,
TargetURL: destURL,
ForwardChildpath: forwardPathname,
StatusCode: statusCode,
}
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename)
// Create a new file for writing the JSON data
file, err := os.Create(filepath)
if err != nil {
log.Printf("Error creating file %s: %s", filepath, err)
return err
}
defer file.Close()
// Encode the RedirectRules object to JSON and write it to the file
err = json.NewEncoder(file).Encode(newRule)
if err != nil {
log.Printf("Error encoding JSON to file %s: %s", filepath, err)
return err
}
// Store the RedirectRules object in the sync.Map
t.rules.Store(redirectURL, newRule)
return nil
}
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename)
fmt.Println(redirectURL, filename, filepath)
// Check if the file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to delete
}
// Delete the file
if err := os.Remove(filepath); err != nil {
log.Printf("Error deleting file %s: %s", filepath, err)
return err
}
// Delete the key-value pair from the sync.Map
t.rules.Delete(redirectURL)
return nil
}
// Get a list of all the redirection rules
func (t *RuleTable) GetAllRedirectRules() []*RedirectRules {
rules := []*RedirectRules{}
t.rules.Range(func(key, value interface{}) bool {
r, ok := value.(*RedirectRules)
if ok {
rules = append(rules, r)
}
return true
})
return rules
}
// Check if a given request URL matched any of the redirection rule
func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
// Iterate through all the keys in the rules map
var targetRedirectionRule *RedirectRules = nil
var maxMatch int = 0
t.rules.Range(func(key interface{}, value interface{}) bool {
// Check if the requested URL starts with the key as a prefix
if t.AllowRegex {
//Regexp matching rule
matched, err := regexp.MatchString(key.(string), requestedURL)
if err != nil {
//Something wrong with the regex?
t.log("Unable to match regex", err)
return true
}
if matched {
maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules)
}
} else {
//Default: prefix matching redirect
if strings.HasPrefix(requestedURL, key.(string)) {
// This request URL matched the domain
if len(key.(string)) > maxMatch {
maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules)
}
}
}
return true
})
return targetRedirectionRule
}
// Log the message to log file, use STDOUT if logger not set
func (t *RuleTable) log(message string, err error) {
if t.Logger == nil {
if err == nil {
log.Println("[Redirect] " + message)
} else {
log.Println("[Redirect] " + message + ": " + err.Error())
}
} else {
t.Logger.PrintAndLog("Redirect", message, err)
}
}

View File

@ -0,0 +1,114 @@
package dynamicproxy
import (
"errors"
"net/url"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
Dynamic Proxy Router Functions
This script handle the proxy rules router spawning
and preparation
*/
// Prepare proxy route generate a proxy handler service object for your endpoint
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
//Filter the tailing slash if any
domain := endpoint.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
endpoint.Domain = domain
//Parse the web proxy endpoint
webProxyEndpoint := domain
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if endpoint.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return nil, err
}
//Create the proxy routing handler
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
IgnoreTLSVerification: endpoint.SkipCertValidations,
})
endpoint.proxy = proxy
endpoint.parent = router
//Prepare proxy routing hjandler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
//Parse the web proxy endpoint
webProxyEndpoint = domain
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if vdir.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
}
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return nil, err
}
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, &dpcore.DpcoreOptions{
IgnoreTLSVerification: vdir.SkipCertValidations,
})
vdir.proxy = proxy
vdir.parent = endpoint
}
return endpoint, nil
}
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
// Push record into running subdomain endpoints
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
return nil
}
// Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
// Push record into running root endpoints
router.Root = endpoint
return nil
}
// ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -0,0 +1,86 @@
package dynamicproxy
import (
"errors"
"net/http"
)
/*
Special.go
This script handle special routing rules
by external modules
*/
type RoutingRule struct {
ID string //ID of the routing rule
Enabled bool //If the routing rule enabled
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
MatchRule func(r *http.Request) bool
RoutingHandler func(http.ResponseWriter, *http.Request)
}
// Router functions
// Check if a routing rule exists given its id
func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) {
for _, rr := range router.routingRules {
if rr.ID == rrid {
return rr, nil
}
}
return nil, errors.New("routing rule with given id not found")
}
// Add a routing rule to the router
func (router *Router) AddRoutingRules(rr *RoutingRule) error {
_, err := router.GetRoutingRuleById(rr.ID)
if err == nil {
//routing rule with given id already exists
return errors.New("routing rule with same id already exists")
}
router.routingRules = append(router.routingRules, rr)
return nil
}
// Remove a routing rule from the router
func (router *Router) RemoveRoutingRule(rrid string) {
newRoutingRules := []*RoutingRule{}
for _, rr := range router.routingRules {
if rr.ID != rrid {
newRoutingRules = append(newRoutingRules, rr)
}
}
router.routingRules = newRoutingRules
}
// Get all routing rules
func (router *Router) GetAllRoutingRules() []*RoutingRule {
return router.routingRules
}
// Get the matching routing rule that describe this request.
// Return nil if no routing rule is match
func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule {
for _, thisRr := range router.routingRules {
if thisRr.IsMatch(r) {
return thisRr
}
}
return nil
}
// Routing Rule functions
// Check if a request object match the
func (e *RoutingRule) IsMatch(r *http.Request) bool {
if !e.Enabled {
return false
}
return e.MatchRule(r)
}
func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) {
e.RoutingHandler(w, r)
}

View File

@ -0,0 +1,55 @@
<html>
<head>
<!-- Zoraxy Forbidden Template -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
<title>Forbidden</title>
<style>
#msg{
position: absolute;
top: calc(50% - 150px);
left: calc(50% - 250px);
width: 500px;
height: 300px;
text-align: center;
}
#footer{
position: fixed;
padding: 2em;
padding-left: 5em;
padding-right: 5em;
bottom: 0px;
left: 0px;
width: 100%;
}
small{
word-break: break-word;
}
</style>
</head>
<body>
<div id="msg">
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
<div>
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
<div class="ui divider"></div>
<p>You do not have permission to view this directory or page. <br>
This might cause by the region limit setting of this site.</p>
<div class="ui divider"></div>
<div style="text-align: left;">
<small>Request time: <span id="reqtime"></span></small><br>
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
</div>
</div>
</div>
<script>
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
$("#requrl").text(window.location.href);
</script>
</body>
</html>

View File

@ -0,0 +1,157 @@
<!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{
font-family: 'Noto Sans TC', sans-serif;
font-weight: 300;
color: rgb(88, 88, 88)
}
.diagram{
background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
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>

View File

@ -0,0 +1,148 @@
package dynamicproxy
import (
_ "embed"
"net"
"net/http"
"sync"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/tlscert"
)
const (
ProxyType_Root = 0
ProxyType_Host = 1
ProxyType_Vdir = 2
)
type ProxyHandler struct {
Parent *Router
}
type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json
}
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Key string
Value string
}
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
proxy *dpcore.ReverseProxy `json:"-"`
parent *ProxyEndpoint `json:"-"`
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
Domain string //Domain or IP to proxy to
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Fallback routing logic
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
Disabled bool //If the rule is disabled
//Internal Logic Elements
parent *Router
proxy *dpcore.ReverseProxy `json:"-"`
}
/*
Routing type specific interface
These are options that only avaible for a specific interface
when running, these are converted into "ProxyEndpoint" objects
for more generic routing logic
*/
// Root options are those that are required for reverse proxy handler to work
const (
DefaultSite_InternalStaticWebServer = 0
DefaultSite_ReverseProxy = 1
DefaultSite_Redirect = 2
DefaultSite_NotFoundPage = 3
)
/*
Web Templates
*/
var (
//go:embed templates/forbidden.html
page_forbidden []byte
//go:embed templates/hosterror.html
page_hosterror []byte
)

60
src/mod/email/email.go Normal file
View File

@ -0,0 +1,60 @@
package email
import (
"net/smtp"
"strconv"
)
/*
Email.go
This script handle mailing services using SMTP protocol
*/
type Sender struct {
Hostname string //E.g. mail.gandi.net
Domain string //E.g. arozos.com
Port int //E.g. 587
Username string //Username of the email account
Password string //Password of the email account
SenderAddr string //e.g. admin@arozos.com
}
//Create a new email sender object
func NewEmailSender(hostname string, domain string, port int, username string, password string, senderAddr string) *Sender {
return &Sender{
Hostname: hostname,
Domain: domain,
Port: port,
Username: username,
Password: password,
SenderAddr: senderAddr,
}
}
/*
Send a email to a reciving addr
Example Usage:
SendEmail(
test@example.com,
"Free donuts",
"Come get your free donuts on this Sunday!"
)
*/
func (s *Sender) SendEmail(to string, subject string, content string) error {
//Parse the email content
msg := []byte("To: " + to + "\n" +
"From: Zoraxy <" + s.SenderAddr + ">\n" +
"Subject: " + subject + "\n" +
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
content + "\n\n")
//Login to the SMTP server
auth := smtp.PlainAuth("", s.Username+"@"+s.Domain, s.Password, s.Hostname)
err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
if err != nil {
return err
}
return nil
}

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