98 Commits
2.6.3 ... v2

Author SHA1 Message Date
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
118 changed files with 25451 additions and 870 deletions

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

@ -0,0 +1,45 @@
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
#echo "${{ secrets.GHCR_PASSWORD }}" | docker login ghcr.io -u "${{ secrets.GHCR_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 \
# Since this is still undetermined, I will leave it commented
#--tag ghcr.io/zoraxydocker/zoraxy:${{ steps.get_latest_release_tag.outputs.latest_tag }} \
#--tag ghcr.io/zoraxydocker/zoraxy:latest \
.

3
.gitignore vendored
View File

@ -29,3 +29,6 @@ src/Zoraxy_*_*
src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
src/mod/acme/test/stackoverflow.pem

View File

@ -1,9 +1,72 @@
# 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
+ 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
+ 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
@ -21,6 +84,6 @@
+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referer and file type analysis
+ Added referrer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore

View File

@ -8,9 +8,7 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- Simple to use interface with detail in-system instructions
- Reverse Proxy
- Subdomain Reverse Proxy
- Virtual Directory Reverse Proxy
- Redirection Rules
- TLS / SSL setup and deploy
@ -19,7 +17,6 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- Integrated Up-time Monitor
- Web-SSH Terminal
- Utilities
- CIDR IP converters
- mDNS Scanner
- IP Scanner
@ -29,9 +26,9 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- SMTP config for password reset
## Build from Source
Require Go 1.20 or above
Requires Go 1.20 or higher
```
```bash
git clone https://github.com/tobychui/zoraxy
cd ./zoraxy/src/
go mod tidy
@ -42,11 +39,11 @@ 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
@ -60,22 +57,51 @@ 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
Thanks for cyb3rdoc and PassiveLemon for providing support over the Docker installation. You can check out their repo over here.
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
[https://github.com/cyb3rdoc/zoraxy-docker](https://github.com/cyb3rdoc/zoraxy-docker)
[https://github.com/PassiveLemon/zoraxy-docker](https://github.com/PassiveLemon/zoraxy-docker)
### 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")
-rpt string
Reserved
-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
@ -85,32 +111,32 @@ If you already have a up-stream reverse proxy server in place with permission ma
#### 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
The [ArozOS](https://arozos.com) subservice is a built-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 into your ArozOS subservice folder. Sometimes it is under ~/arozos/src/subservice.
cd ~/arozos/subservices
mkdir zoraxy
cd ./zoraxy
# Download the release binary from Github release
# Download the release binary from Github release.
wget {binary executable link from release page}
# Set permission. Change this if required
# 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
# After unzipping, press Ctrl + C to kill it.
# Rename it to validate the ArozOS subservice binary format.
mv ./zoraxy zoraxy_linux_amd64
# If you are using SBCs with different CPU arch, use the following names
# If you are using SBCs with a different CPU arch, use the following names:
# mv ./zoraxy zoraxy_linux_arm
# mv ./zoraxy zoraxy_linux_arm64
# Restart arozos
# Restart ArozOS
sudo systemctl restart arozos
```
@ -132,22 +158,21 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
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 weird installation profile, you can also pass in the ZeroTier auth token using the following flags
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
```
```bash
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
```
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 currently only support Linux based OS. The following platforms are supported
## Web SSH
Web SSH currently only supports Linux based OSes. The following platforms are supported:
- linux/amd64
- linux/arm64
- linux/armv6 (experimental)
@ -155,13 +180,19 @@ Web SSH currently only support Linux based OS. The following platforms are suppo
### 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 project is open source under AGPL. I open source this project so everyone can check for security issues and benefit all users. **If your plans to use this project in commercial environment which violate the AGPL terms, please contact toby@imuslab.com for an alternative commercial license.**
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative commercial license.**

40
docker/Dockerfile Normal file
View File

@ -0,0 +1,40 @@
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
# Alternatives for security
RUN apk add --no-cache openssl=3.1.4-r1
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}

View File

@ -148,7 +148,7 @@
Reverse Proxy
</div>
</h3>
<p>Simple to use, noobs friendly reverse proxy server that can be easily set-up using a web form and a few toggle switches.</p>
<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">
@ -158,7 +158,7 @@
Redirection
</div>
</h3>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most of the simple use cases.</p>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
</div>
<div class="four wide column featureItem">
@ -168,7 +168,7 @@
Geo-IP & Blacklist
</div>
</h3>
<p>Blacklist with GeoIP support. Allow easy setup for regional services.</p>
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
</div>
<div class="four wide column featureItem">
@ -189,7 +189,7 @@
Web SSH
</div>
</h3>
<p>Integrated with Gotty Web SSH terminal, allow one-stop management of your nodes inside private LAN via gateway nodes.</p>
<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">
@ -199,7 +199,7 @@
Real Time Statistics
</div>
</h3>
<p>Traffic data collection and real time analytic tools, provide you the best insights of visitors data without cookies.</p>
<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">
@ -209,7 +209,7 @@
Scanner & Utilities
</div>
</h3>
<p>Build in IP scanner and mDNS discovering service, enable automatic service discovery within LAN.</p>
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
</div>
<div class="four wide column featureItem">
@ -219,7 +219,7 @@
Open Source
</div>
</h3>
<p>Project is open source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
</div>
</div>
</div>

View File

@ -19,7 +19,8 @@ clean:
$(PLATFORMS):
@echo "Building $(os)/$(arch)"
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) 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:

136
src/acme.go Normal file
View File

@ -0,0 +1,136 @@
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)")
}
// 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)
}
}

View File

@ -3,9 +3,12 @@ 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"
)
@ -51,12 +54,22 @@ func initAPIs() {
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)
//Reverse proxy root related APIs
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
//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)
@ -81,6 +94,11 @@ func initAPIs() {
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)
@ -126,6 +144,9 @@ func initAPIs() {
//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)
@ -138,8 +159,44 @@ func initAPIs() {
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", staticWebServer.HandlePortChange)
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
}

View File

@ -6,10 +6,11 @@ import (
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
@ -44,6 +45,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
Domain string
LastModifiedDate string
ExpireDate string
RemainingDays int
}
results := []*CertInfo{}
@ -60,6 +62,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
expiredIn := 0
if err != nil {
//Unable to load this file
continue
@ -70,6 +73,11 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
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)
}
}
}
@ -78,6 +86,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
}
results = append(results, &thisCertInfo)
@ -99,6 +108,64 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
}
// 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
@ -114,11 +181,11 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
} else {
if newState == "true" {
sysdb.Write("settings", "usetls", true)
log.Println("Enabling TLS mode on reverse proxy")
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(true)
} else if newState == "false" {
sysdb.Write("settings", "usetls", false)
log.Println("Disabling TLS mode on reverse proxy")
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(false)
} else {
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
@ -130,6 +197,33 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
}
}
// 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
@ -178,8 +272,8 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
defer file.Close()
// create file in upload directory
os.MkdirAll("./certs", 0775)
f, err := os.Create(filepath.Join("./certs", overWriteFilename))
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

View File

@ -1,12 +1,16 @@
package main
import (
"archive/zip"
"encoding/json"
"io/ioutil"
"log"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
@ -20,18 +24,21 @@ import (
*/
type Record struct {
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
BypassGlobalTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
}
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
// Save a reverse proxy config record to file
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
//TODO: Make this accept new def types
os.MkdirAll("conf", 0775)
os.MkdirAll("./conf/proxy/", 0775)
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
//Generate record
@ -39,17 +46,26 @@ func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775)
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
}
func RemoveReverseProxyConfig(rootname string) error {
// Save a running reverse proxy endpoint to file (with automatic endpoint to record conversion)
func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) error {
recordToSave, err := ConvertProxyEndpointToRecord(proxyEndpoint)
if err != nil {
return err
}
return SaveReverseProxyConfigToFile(recordToSave)
}
func RemoveReverseProxyConfigFile(rootname string) error {
filename := getFilenameFromRootName(rootname)
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
log.Println("Config Removed: ", removePendingFile)
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
SystemWideLogger.Println("Config Removed: ", removePendingFile)
if utils.FileExists(removePendingFile) {
err := os.Remove(removePendingFile)
if err != nil {
log.Println(err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
return err
}
}
@ -60,8 +76,19 @@ func RemoveReverseProxyConfig(rootname string) error {
// Return ptype, rootname and proxyTarget, error if any
func LoadReverseProxyConfig(filename string) (*Record, error) {
thisRecord := Record{}
configContent, err := ioutil.ReadFile(filename)
thisRecord := Record{
ProxyType: "",
Rootname: "",
ProxyTarget: "",
UseTLS: false,
BypassGlobalTLS: false,
SkipTlsValidation: false,
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
configContent, err := os.ReadFile(filename)
if err != nil {
return &thisRecord, err
}
@ -76,6 +103,23 @@ func LoadReverseProxyConfig(filename string) (*Record, error) {
return &thisRecord, nil
}
// Convert a running proxy endpoint object into a save-able record struct
func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoint) (*Record, error) {
thisProxyConfigRecord := Record{
ProxyType: targetProxyEndpoint.GetProxyTypeString(),
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
ProxyTarget: targetProxyEndpoint.Domain,
UseTLS: targetProxyEndpoint.RequireTLS,
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
BasicAuthExceptionRules: targetProxyEndpoint.BasicAuthExceptionRules,
}
return &thisProxyConfigRecord, nil
}
func getFilenameFromRootName(rootname string) string {
//Generate a filename for this rootname
filename := strings.ReplaceAll(rootname, ".", "_")
@ -83,3 +127,202 @@ func getFilenameFromRootName(rootname string) string {
filename = filename + ".config"
return filename
}
/*
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)
}()
}
}

View File

@ -9,7 +9,7 @@ import (
"strings"
"time"
uuid "github.com/satori/go.uuid"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/utils"
)
@ -180,7 +180,7 @@ func setSMTPAdminAddress(adminAddr string) error {
return sysdb.Write("smtp", "admin", adminAddr)
}
//Load SMTP admin address. Return empty string if not set
// Load SMTP admin address. Return empty string if not set
func loadSMTPAdminAddr() string {
adminAddr := ""
if sysdb.KeyExists("smtp", "admin") {
@ -223,7 +223,7 @@ func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
return
}
passwordResetAccessToken = uuid.NewV4().String()
passwordResetAccessToken = uuid.New().String()
//SMTP info exists. Send reset account email
lastAccountResetEmail = time.Now().Unix()

View File

@ -1,39 +0,0 @@
package main
import (
"net"
"net/http"
"strings"
"github.com/oschwald/geoip2-golang"
)
func getCountryCodeFromRequest(r *http.Request) string {
countryCode := ""
// Get the IP address of the user from the request headers
ipAddress := r.Header.Get("X-Forwarded-For")
if ipAddress == "" {
ipAddress = strings.Split(r.RemoteAddr, ":")[0]
}
// Open the GeoIP database
db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb")
if err != nil {
// Handle the error
return countryCode
}
defer db.Close()
// Look up the country code for the IP address
record, err := db.Country(net.ParseIP(ipAddress))
if err != nil {
// Handle the error
return countryCode
}
// Get the ISO country code from the record
countryCode = record.Country.IsoCode
return countryCode
}

View File

@ -4,13 +4,15 @@ go 1.16
require (
github.com/boltdb/bolt v1.3.1
github.com/go-acme/lego/v4 v4.14.0
github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/grandcat/zeroconf v1.0.0
github.com/microcosm-cc/bluemonday v1.0.24
github.com/oschwald/geoip2-golang v1.8.0
github.com/satori/go.uuid v1.2.0
golang.org/x/sys v0.8.0
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0
golang.org/x/tools v0.12.0 // indirect
)

1756
src/go.sum

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/aroz"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
@ -19,8 +20,10 @@ import (
"imuslab.com/zoraxy/mod/email"
"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"
@ -28,17 +31,25 @@ import (
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
)
// General flags
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 = "2.6.3"
version = "2.6.8"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -57,6 +68,7 @@ var (
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
@ -65,10 +77,14 @@ var (
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
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
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.
@ -77,29 +93,40 @@ func SetupCloseHandler() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
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()
fmt.Println("- Stopping mDNS Discoverer")
//Stop the mdns service
mdnsTickerStop <- true
mdnsScanner.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database, final
fmt.Println("- Stopping system database")
sysdb.Close()
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() {
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
@ -130,7 +157,7 @@ func main() {
}
uuidBytes, err := os.ReadFile(uuidRecord)
if err != nil {
log.Println("Unable to read system uuid from file system")
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
panic(err)
}
nodeUUID = string(uuidBytes)
@ -149,7 +176,10 @@ func main() {
time.Sleep(500 * time.Millisecond)
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
//Start the finalize sequences
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
err = http.ListenAndServe(handler.Port, nil)
if err != nil {

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+".crt", 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
}

View File

@ -1,8 +1,13 @@
package dynamicproxy
import (
_ "embed"
"errors"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
@ -21,37 +26,39 @@ import (
- Vitrual Directory Routing
*/
var (
//go:embed tld.json
rawTldMap []byte
)
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
}
/*
General Access Check
*/
//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("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return
}
//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("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
@ -65,15 +72,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//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
matchedRoutingRule.Route(w, r)
return
}
//Extract request host to see if it is virtual directory or subdomain
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
@ -115,16 +113,150 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxyRequest(w, r, targetProxyEndpoint)
} else 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.proxyRequest(w, r, h.Parent.Root)
h.handleRootRouting(w, r)
}
} else {
//No routing rules found. Route to root.
//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]
}
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
//Route to custom domain
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
//Not set. Redirect to first level of domain redirectable
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
if err != nil {
//Redirect to proxy root
h.proxyRequest(w, r, h.Parent.Root)
} else {
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
}
return
} else if h.isTopLevelRedirectableDomain(domainOnly) {
//This is requesting a top level private domain that should be serving root
h.proxyRequest(w, r, h.Parent.Root)
} else {
//Validate the redirection target URL
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
if err != nil {
//Error when parsing target. Send to root
h.proxyRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if domainOnly != hostname {
//Redirect to target
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
return
} else {
//Loopback request due to bad settings (Shd leave it empty)
//Forward it to root proxy
h.proxyRequest(w, r, h.Parent.Root)
}
}
} else {
//Route to root
h.proxyRequest(w, r, h.Parent.Root)
}
}
// 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
}
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@ -3,6 +3,7 @@ package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
@ -15,6 +16,16 @@ import (
*/
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
}
}
}
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"

View File

@ -14,10 +14,6 @@ import (
var onExitFlushLoop func()
const (
defaultTimeout = time.Minute * 5
)
// 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
@ -91,11 +87,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
thisTransporter.(*http.Transport).MaxIdleConns = 3000
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
//thisTransporter.(*http.Transport).DisableCompression = true
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 ignoreTLSVerification {
//Ignore TLS certificate validation error
@ -357,11 +354,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom header rewriter functions
if res.Header.Get("Location") != "" {
/*
fmt.Println(">>> REQ", req)
fmt.Println(">>> OUTR", outreq)
fmt.Println(">>> RESP", res)
*/
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation)
@ -369,12 +361,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path
//Replace the forwarded target with expected Host
lr, err := replaceLocationHost(locationRewrite, rrr.OriginalHost, req.TLS != nil)
lr, err := replaceLocationHost(locationRewrite, rrr, req.TLS != nil)
if err == nil {
locationRewrite = lr
}
//locationRewrite = strings.ReplaceAll(locationRewrite, rrr.ProxyDomain, rrr.OriginalHost)
//locationRewrite = strings.ReplaceAll(locationRewrite, domainWithoutPort, rrr.OriginalHost)
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
//Back to the root of this proxy object
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
@ -387,6 +377,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom redirection to this rproxy relative path
res.Header.Set("Location", locationRewrite)
}
// Copy header from response to client.
copyHeader(rw.Header(), res.Header)

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

@ -2,20 +2,61 @@ package dpcore
import (
"net/url"
"strings"
)
func replaceLocationHost(urlString string, newHost string, useTLS bool) (string, error) {
// 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"
}
u.Host = newHost
//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

@ -3,6 +3,7 @@ package dynamicproxy
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log"
"net/http"
@ -29,12 +30,19 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
tldMap: map[string]int{},
}
thisRouter.mux = &ProxyHandler{
Parent: &thisRouter,
}
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil
}
@ -45,6 +53,19 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
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
@ -58,36 +79,95 @@ func (router *Router) StartProxyService() error {
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")
}
//Load root options from file
loadedRootOption, err := loadRootRoutingOptionsFromFile()
if err != nil {
return err
}
router.RootRoutingOptions = loadedRootOption
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 {
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
/*
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
}
router.tlsListener = ln
*/
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
TLSConfig: config,
}
router.tlsListener = ln
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
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) {
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
//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.getSubdomainProxyEndpointFromHostname(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 {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
//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)
}
}
}),
@ -115,7 +195,7 @@ func (router *Router) StartProxyService() error {
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 server: %v\n", err)
log.Fatalf("Could not start redirection server: %v\n", err)
}
}()
router.tlsRedirectStop = stopChan
@ -124,8 +204,8 @@ func (router *Router) StartProxyService() error {
//Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)")
go func() {
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start server: %v\n", err)
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start proxy server: %v\n", err)
}
}()
} else {
@ -171,18 +251,22 @@ func (router *Router) StopProxyService() error {
}
// Restart the current router if it is running.
// Startup the server if it is not running initially
func (router *Router) Restart() error {
//Stop the router if it is already running
var err error = nil
if router.Running {
err := router.StopProxyService()
if err != nil {
return err
}
// Start the server
err = router.StartProxyService()
if err != nil {
return err
}
}
//Start the server
err := router.StartProxyService()
return err
}
@ -230,14 +314,15 @@ func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) erro
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
endpointObject := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
@ -255,46 +340,24 @@ func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
} else if ptype == "subd" {
proxy, ok := router.SubdomainEndpoint.Load(key)
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
}
return nil, errors.New("unsupported ptype")
}
/*
Save routing from RP
*/
func (router *Router) SaveProxy(ptype string, key string, newConfig *ProxyEndpoint) {
if ptype == "vdir" {
router.ProxyEndpoints.Store(key, newConfig)
} else if ptype == "subd" {
router.SubdomainEndpoint.Store(key, newConfig)
}
}
/*
Remove routing from RP
*/
func (router *Router) RemoveProxy(ptype string, key string) error {
//fmt.Println(ptype, key)
if ptype == "vdir" {
router.ProxyEndpoints.Delete(key)
return nil
} else if ptype == "subd" {
router.SubdomainEndpoint.Delete(key)
return nil
}
return errors.New("invalid ptype")
}
/*
Add an default router for the proxy server
*/
@ -319,14 +382,15 @@ func (router *Router) SetRootProxy(options *RootOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint

View File

@ -0,0 +1,68 @@
package dynamicproxy
import "errors"
/*
ProxyEndpoint.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
*/
//Get the string version of proxy type
func (ep *ProxyEndpoint) GetProxyTypeString() string {
if ep.ProxyType == ProxyType_Subdomain {
return "subd"
} else if ep.ProxyType == ProxyType_Vdir {
return "vdir"
}
return "unknown"
}
//Update change in the current running proxy endpoint config
func (ep *ProxyEndpoint) UpdateToRuntime() {
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Store(ep.RootOrMatchingDomain, ep)
}
}
//Return true if the endpoint type is virtual directory
func (ep *ProxyEndpoint) IsVdir() bool {
return ep.ProxyType == ProxyType_Vdir
}
//Return true if the endpoint type is subdomain
func (ep *ProxyEndpoint) IsSubDomain() bool {
return ep.ProxyType == ProxyType_Subdomain
}
//Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
//fmt.Println(ptype, key)
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Delete(ep.RootOrMatchingDomain)
return nil
}
return errors.New("invalid or unsupported type")
}
//ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(proxyType string, rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(proxyType, rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -95,6 +95,7 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
UseTLS: target.RequireTLS,
PathPrefix: "",
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
@ -182,6 +183,5 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
}
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
}()
}
}

View File

@ -28,13 +28,15 @@ func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
rr := t.MatchRedirectRule(requestPath)
if rr != nil {
redirectTarget := rr.TargetURL
//Always pad a / at the back of the target URL
if redirectTarget[len(redirectTarget)-1:] != "/" {
redirectTarget += "/"
}
if rr.ForwardChildpath {
//Remove the first / in the path
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
//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
}

View File

@ -0,0 +1,51 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"log"
"os"
"imuslab.com/zoraxy/mod/utils"
)
/*
rootRoute.go
This script handle special case in routing where the root proxy
entity is involved. This also include its setting object
RootRoutingOptions
*/
var rootConfigFilepath string = "conf/root_config.json"
func loadRootRoutingOptionsFromFile() (*RootRoutingOptions, error) {
if !utils.FileExists(rootConfigFilepath) {
//Not found. Create a root option
js, _ := json.MarshalIndent(RootRoutingOptions{}, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
if err != nil {
return nil, errors.New("Unable to write root config to file: " + err.Error())
}
}
newRootOption := RootRoutingOptions{}
rootOptionsBytes, err := os.ReadFile(rootConfigFilepath)
if err != nil {
log.Println("[Error] Unable to read root config file at " + rootConfigFilepath + ": " + err.Error())
return nil, err
}
err = json.Unmarshal(rootOptionsBytes, &newRootOption)
if err != nil {
log.Println("[Error] Unable to parse root config file: " + err.Error())
return nil, err
}
return &newRootOption, nil
}
// Save the new config to file. Note that this will not overwrite the runtime one
func (opt *RootRoutingOptions) SaveToFile() error {
js, _ := json.MarshalIndent(opt, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
return err
}

View File

@ -13,14 +13,15 @@ import (
*/
type RoutingRule struct {
ID string
MatchRule func(r *http.Request) bool
RoutingHandler http.Handler
Enabled bool
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
// 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 {
@ -31,19 +32,19 @@ func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) {
return nil, errors.New("routing rule with given id not found")
}
//Add a routing rule to the router
// Add a routing rule to the router
func (router *Router) AddRoutingRules(rr *RoutingRule) error {
_, err := router.GetRoutingRuleById(rr.ID)
if err != nil {
if err == nil {
//routing rule with given id already exists
return err
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
// Remove a routing rule from the router
func (router *Router) RemoveRoutingRule(rrid string) {
newRoutingRules := []*RoutingRule{}
for _, rr := range router.routingRules {
@ -55,13 +56,13 @@ func (router *Router) RemoveRoutingRule(rrid string) {
router.routingRules = newRoutingRules
}
//Get all routing rules
// 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
// 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) {
@ -71,8 +72,8 @@ func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule {
return nil
}
//Routing Rule functions
//Check if a request object match the
// Routing Rule functions
// Check if a request object match the
func (e *RoutingRule) IsMatch(r *http.Request) bool {
if !e.Enabled {
return false
@ -81,5 +82,5 @@ func (e *RoutingRule) IsMatch(r *http.Request) bool {
}
func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) {
e.RoutingHandler.ServeHTTP(w, r)
e.RoutingHandler(w, r)
}

View File

@ -34,13 +34,15 @@ func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
BypassGlobalTLS: options.BypassGlobalTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
})
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)

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>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package dynamicproxy
import (
_ "embed"
"net"
"net/http"
"sync"
@ -22,28 +23,33 @@ type ProxyHandler struct {
}
type RouterOption struct {
HostUUID string
Port int
UseTls bool
ForceHttpsRedirect bool
HostUUID string //The UUID 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
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
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
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
RootRoutingOptions *RootRoutingOptions
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
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
@ -58,56 +64,73 @@ type BasicAuthUnhashedCredentials struct {
Password string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
Proxy *dpcore.ReverseProxy `json:"-"`
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
Domain string //Domain or IP to proxy to
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
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
Proxy *dpcore.ReverseProxy `json:"-"`
parent *Router
}
// Root options are those that are required for reverse proxy handler to work
type RootOptions struct {
ProxyLocation string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
RequireTLS bool //Proxy root target require TLS connection (not recommended)
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
//Basic Auth Related
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
// Additional options are here for letting router knows how to route exception cases for root
type RootRoutingOptions struct {
//Root only configs
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
RootName string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
/*
type ProxyEndpoint struct {
Root string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
type SubdomainEndpoint struct {
MatchingDomain string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
Web Templates
*/
var (
//go:embed templates/forbidden.html
page_forbidden []byte
)

16
src/mod/expose/expose.go Normal file
View File

@ -0,0 +1,16 @@
package expose
/*
Service Expose Proxy
A tunnel for getting your local server online in one line
(No, this is not ngrok)
*/
type Router struct {
}
//Create a new service expose router
func NewServiceExposeRouter() {
}

111
src/mod/expose/security.go Normal file
View File

@ -0,0 +1,111 @@
package expose
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"errors"
"log"
)
// GenerateKeyPair generates a new key pair
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
privkey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return privkey, &privkey.PublicKey, nil
}
// PrivateKeyToBytes private key to bytes
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
privBytes := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
},
)
return privBytes
}
// PublicKeyToBytes public key to bytes
func PublicKeyToBytes(pub *rsa.PublicKey) ([]byte, error) {
pubASN1, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return []byte(""), err
}
pubBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pubASN1,
})
return pubBytes, nil
}
// BytesToPrivateKey bytes to private key
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(priv)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
log.Println("is encrypted pem block")
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key, nil
}
// BytesToPublicKey bytes to public key
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pub)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
log.Println("is encrypted pem block")
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
ifc, err := x509.ParsePKIXPublicKey(b)
if err != nil {
return nil, err
}
key, ok := ifc.(*rsa.PublicKey)
if !ok {
return nil, errors.New("key not valid")
}
return key, nil
}
// EncryptWithPublicKey encrypts data with public key
func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) ([]byte, error) {
hash := sha512.New()
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
if err != nil {
return []byte(""), err
}
return ciphertext, nil
}
// DecryptWithPrivateKey decrypts data with private key
func DecryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) {
hash := sha512.New()
plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil)
if err != nil {
return []byte(""), err
}
return plaintext, nil
}

View File

@ -13,8 +13,8 @@ import (
)
func readAuthTokenAsAdmin() (string, error) {
if utils.FileExists("./authtoken.secret") {
authKey, err := os.ReadFile("./authtoken.secret")
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}

View File

@ -19,8 +19,8 @@ import (
// Use admin permission to read auth token on Windows
func readAuthTokenAsAdmin() (string, error) {
//Check if the previous startup already extracted the authkey
if utils.FileExists("./authtoken.secret") {
authKey, err := os.ReadFile("./authtoken.secret")
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
@ -30,7 +30,7 @@ func readAuthTokenAsAdmin() (string, error) {
exe := "cmd.exe"
cwd, _ := os.Getwd()
output, _ := filepath.Abs(filepath.Join("./", "authtoken.secret"))
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
os.WriteFile(output, []byte(""), 0775)
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
@ -49,13 +49,13 @@ func readAuthTokenAsAdmin() (string, error) {
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
retry := 0
time.Sleep(3 * time.Second)
for !utils.FileExists("./authtoken.secret") && retry < 10 {
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
time.Sleep(3 * time.Second)
log.Println("Waiting for ZeroTier authtoken extraction...")
retry++
}
authKey, err := os.ReadFile("./authtoken.secret")
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err != nil {
return "", err
}

View File

@ -20,13 +20,16 @@ type Store struct {
WhitelistEnabled bool
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
geotrie *trie
geotrieIpv6 *trie
//geoipCache sync.Map
sysdb *database.Database
option *StoreOptions
}
sysdb *database.Database
type StoreOptions struct {
AllowSlowIpv4LookUp bool
AllowSloeIpv6Lookup bool
}
type CountryInfo struct {
@ -34,7 +37,7 @@ type CountryInfo struct {
ContinetCode string
}
func NewGeoDb(sysdb *database.Database) (*Store, error) {
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
parsedGeoData, err := parseCSV(geoipv4)
if err != nil {
return nil, err
@ -79,14 +82,25 @@ func NewGeoDb(sysdb *database.Database) (*Store, error) {
log.Println("Database pointer set to nil: Entering debug mode")
}
var ipv4Trie *trie
if !option.AllowSlowIpv4LookUp {
ipv4Trie = constrctTrieTree(parsedGeoData)
}
var ipv6Trie *trie
if !option.AllowSloeIpv6Lookup {
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
}
return &Store{
BlacklistEnabled: blacklistEnabled,
WhitelistEnabled: whitelistEnabled,
geodb: parsedGeoData,
geotrie: constrctTrieTree(parsedGeoData),
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: constrctTrieTree(parsedGeoDataIpv6),
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
option: option,
}, nil
}
@ -106,6 +120,7 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
CountryIsoCode: cc,
ContinetCode: "",
}, nil
}
func (s *Store) Close() {

View File

@ -41,7 +41,10 @@ func TestTrieConstruct(t *testing.T) {
func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store
store, err := geodb.NewGeoDb(nil)
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
false,
})
if err != nil {
t.Errorf("error creating store: %v", err)
return

View File

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/csv"
"io"
"net"
"strings"
)
@ -26,9 +25,17 @@ func (s *Store) search(ip string) string {
//Search in geotrie tree
cc := ""
if IsIPv6(ip) {
cc = s.geotrieIpv6.search(ip)
if s.geotrieIpv6 == nil {
cc = s.slowSearchIpv6(ip)
} else {
cc = s.geotrieIpv6.search(ip)
}
} else {
cc = s.geotrie.search(ip)
if s.geotrie == nil {
cc = s.slowSearchIpv4(ip)
} else {
cc = s.geotrie.search(ip)
}
}
/*
@ -69,27 +76,3 @@ func parseCSV(content []byte) ([][]string, error) {
}
return records, nil
}
// Check if a ip string is within the range of two others
func isIPInRange(ip, start, end string) bool {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return false
}
startAddr := net.ParseIP(start)
if startAddr == nil {
return false
}
endAddr := net.ParseIP(end)
if endAddr == nil {
return false
}
if ipAddr.To4() == nil || startAddr.To4() == nil || endAddr.To4() == nil {
return false
}
return bytes.Compare(ipAddr.To4(), startAddr.To4()) >= 0 && bytes.Compare(ipAddr.To4(), endAddr.To4()) <= 0
}

View File

@ -0,0 +1,81 @@
package geodb
import (
"errors"
"math/big"
"net"
)
/*
slowSearch.go
This script implement the slow search method for ip to country code
lookup. If you have the memory allocation for near O(1) lookup,
you should not be using slow search mode.
*/
func ipv4ToUInt32(ip net.IP) uint32 {
ip = ip.To4()
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}
func isIPv4InRange(startIP, endIP, testIP string) (bool, error) {
start := net.ParseIP(startIP)
end := net.ParseIP(endIP)
test := net.ParseIP(testIP)
if start == nil || end == nil || test == nil {
return false, errors.New("invalid IP address format")
}
startUint := ipv4ToUInt32(start)
endUint := ipv4ToUInt32(end)
testUint := ipv4ToUInt32(test)
return testUint >= startUint && testUint <= endUint, nil
}
func isIPv6InRange(startIP, endIP, testIP string) (bool, error) {
start := net.ParseIP(startIP)
end := net.ParseIP(endIP)
test := net.ParseIP(testIP)
if start == nil || end == nil || test == nil {
return false, errors.New("invalid IP address format")
}
startInt := new(big.Int).SetBytes(start.To16())
endInt := new(big.Int).SetBytes(end.To16())
testInt := new(big.Int).SetBytes(test.To16())
return testInt.Cmp(startInt) >= 0 && testInt.Cmp(endInt) <= 0, nil
}
// Slow country code lookup for
func (s *Store) slowSearchIpv4(ipAddr string) string {
for _, ipRange := range s.geodb {
startIp := ipRange[0]
endIp := ipRange[1]
cc := ipRange[2]
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
if inRange {
return cc
}
}
return ""
}
func (s *Store) slowSearchIpv6(ipAddr string) string {
for _, ipRange := range s.geodbIpv6 {
startIp := ipRange[0]
endIp := ipRange[1]
cc := ipRange[2]
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
if inRange {
return cc
}
}
return ""
}

View File

@ -1,15 +1,12 @@
package geodb
import (
"fmt"
"math"
"net"
"strconv"
"strings"
)
type trie_Node struct {
childrens [2]*trie_Node
ends bool
cc string
}
@ -18,7 +15,7 @@ type trie struct {
root *trie_Node
}
func ipToBitString(ip string) string {
func ipToBytes(ip string) []byte {
// Parse the IP address string into a net.IP object
parsedIP := net.ParseIP(ip)
@ -29,49 +26,7 @@ func ipToBitString(ip string) string {
ipBytes = parsedIP.To16()
}
// Convert each byte in the IP address to its 8-bit binary representation
var result []string
for _, b := range ipBytes {
result = append(result, fmt.Sprintf("%08b", b))
}
// Join the binary representation of each byte with dots to form the final bit string
return strings.Join(result, "")
}
func bitStringToIp(bitString string) string {
// Check if the bit string represents an IPv4 or IPv6 address
isIPv4 := len(bitString) == 32
// Split the bit string into 8-bit segments
segments := make([]string, 0)
if isIPv4 {
for i := 0; i < 4; i++ {
segments = append(segments, bitString[i*8:(i+1)*8])
}
} else {
for i := 0; i < 16; i++ {
segments = append(segments, bitString[i*8:(i+1)*8])
}
}
// Convert each segment to its decimal equivalent
decimalSegments := make([]int, len(segments))
for i, s := range segments {
val, _ := strconv.ParseInt(s, 2, 64)
decimalSegments[i] = int(val)
}
// Construct the IP address string based on the type (IPv4 or IPv6)
if isIPv4 {
return fmt.Sprintf("%d.%d.%d.%d", decimalSegments[0], decimalSegments[1], decimalSegments[2], decimalSegments[3])
} else {
ip := make(net.IP, net.IPv6len)
for i := 0; i < net.IPv6len; i++ {
ip[i] = byte(decimalSegments[i])
}
return ip.String()
}
return ipBytes
}
// inititlaizing a new trie
@ -83,20 +38,39 @@ func newTrie() *trie {
// Passing words to trie
func (t *trie) insert(ipAddr string, cc string) {
word := ipToBitString(ipAddr)
ipBytes := ipToBytes(ipAddr)
current := t.root
for _, wr := range word {
index := wr - '0'
if current.childrens[index] == nil {
current.childrens[index] = &trie_Node{
childrens: [2]*trie_Node{},
ends: false,
cc: cc,
for _, b := range ipBytes {
//For each byte in the ip address
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
cc: cc,
}
}
current = current.childrens[bit]
}
current = current.childrens[index]
}
current.ends = true
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
cc: cc,
}
}
current = current.childrens[bit]
}
*/
}
func isReservedIP(ip string) bool {
@ -126,16 +100,34 @@ func (t *trie) search(ipAddr string) string {
if isReservedIP(ipAddr) {
return ""
}
word := ipToBitString(ipAddr)
ipBytes := ipToBytes(ipAddr)
current := t.root
for _, wr := range word {
index := wr - '0'
if current.childrens[index] == nil {
return current.cc
for _, b := range ipBytes {
//For each byte in the ip address
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
current = current.childrens[index]
}
if current.ends {
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
*/
if len(current.childrens) == 0 {
return current.cc
}

View File

@ -0,0 +1,103 @@
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"time"
)
/*
Zoraxy Logger
This script is designed to make a managed log for the Zoraxy
and replace the ton of log.Println in the system core
*/
type Logger struct {
LogToFile bool //Set enable write to file
Prefix string //Prefix for log files
LogFolder string //Folder to store the log file
CurrentLogFile string //Current writing filename
file *os.File
}
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
err := os.MkdirAll(logFolder, 0775)
if err != nil {
return nil, err
}
thisLogger := Logger{
LogToFile: logToFile,
Prefix: logFilePrefix,
LogFolder: logFolder,
}
logFilePath := thisLogger.getLogFilepath()
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
return nil, err
}
thisLogger.CurrentLogFile = logFilePath
thisLogger.file = f
return &thisLogger, nil
}
func (l *Logger) getLogFilepath() string {
year, month, _ := time.Now().Date()
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
}
// PrintAndLog will log the message to file and print the log to STDOUT
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
go func() {
l.Log(title, message, originalError)
}()
log.Println("[" + title + "] " + message)
}
// Println is a fast snap-in replacement for log.Println
func (l *Logger) Println(v ...interface{}) {
//Convert the array of interfaces into string
message := fmt.Sprint(v...)
go func() {
l.Log("info", string(message), nil)
}()
log.Println("[INFO] " + string(message))
}
func (l *Logger) Log(title string, errorMessage string, originalError error) {
l.ValidateAndUpdateLogFilepath()
if l.LogToFile {
if originalError == nil {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
} else {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
}
}
}
// Validate if the logging target is still valid (detect any months change)
func (l *Logger) ValidateAndUpdateLogFilepath() {
expectedCurrentLogFilepath := l.getLogFilepath()
if l.CurrentLogFile != expectedCurrentLogFilepath {
//Change of month. Update to a new log file
l.file.Close()
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
l.LogToFile = false
return
}
l.CurrentLogFile = expectedCurrentLogFilepath
l.file = f
}
}
func (l *Logger) Close() {
l.file.Close()
}

View File

@ -0,0 +1,122 @@
package logviewer
import (
"encoding/json"
"errors"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
type ViewerOption struct {
RootFolder string //The root folder to scan for log
Extension string //The extension the root files use, include the . in your ext (e.g. .log)
}
type Viewer struct {
option *ViewerOption
}
type LogFile struct {
Title string
Filename string
Fullpath string
Filesize int64
}
func NewLogViewer(option *ViewerOption) *Viewer {
return &Viewer{option: option}
}
/*
Log Request Handlers
*/
//List all the log files in the log folder. Return in map[string]LogFile format
func (v *Viewer) HandleListLog(w http.ResponseWriter, r *http.Request) {
logFiles := v.ListLogFiles(false)
js, _ := json.Marshal(logFiles)
utils.SendJSONResponse(w, string(js))
}
// Read log of a given catergory and filename
// Require GET varaible: file and catergory
func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filename given")
return
}
catergory, err := utils.GetPara(r, "catergory")
if err != nil {
utils.SendErrorResponse(w, "invalid catergory given")
return
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendTextResponse(w, content)
}
/*
Log Access Functions
*/
func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
result := map[string][]*LogFile{}
filepath.WalkDir(v.option.RootFolder, func(path string, di fs.DirEntry, err error) error {
if filepath.Ext(path) == v.option.Extension {
catergory := filepath.Base(filepath.Dir(path))
logList, ok := result[catergory]
if !ok {
//this catergory hasn't been scanned before.
logList = []*LogFile{}
}
fullpath := filepath.ToSlash(path)
if !showFullpath {
fullpath = ""
}
st, err := os.Stat(path)
if err != nil {
return nil
}
logList = append(logList, &LogFile{
Title: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
Filename: filepath.Base(path),
Fullpath: fullpath,
Filesize: st.Size(),
})
result[catergory] = logList
}
return nil
})
return result
}
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
if utils.FileExists(logFilepath) {
//Load it
content, err := os.ReadFile(logFilepath)
if err != nil {
return "", err
}
return string(content), nil
} else {
return "", errors.New("log file not found")
}
}

View File

@ -226,7 +226,7 @@ func (m *MDNSHost) Scan(timeout int, domainFilter string) []*NetworkHost {
return discoveredHost
}
//Get all mac address of all interfaces
// Get all mac address of all interfaces
func getMacAddr() ([]string, error) {
ifas, err := net.Interfaces()
if err != nil {

View File

@ -213,6 +213,7 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
out, err := cmd.Output()
if err != nil {
callbackChan <- wmicResult{0, 0, err}
return
}
//Filter out the first line
@ -251,18 +252,16 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
go func() {
//Spawn a timer to terminate the cmd process if timeout
var timer *time.Timer
timer = time.AfterFunc(3*time.Second, func() {
timer.Stop()
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
}
time.Sleep(3 * time.Second)
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
callbackChan <- wmicResult{0, 0, errors.New("wmic execution timeout")}
})
}
}()
result := wmicResult{}
result = <-callbackChan
cmd = nil
if result.Err != nil {
log.Println("Unable to extract NIC info from wmic: " + result.Err.Error())
}

View File

@ -0,0 +1,159 @@
package netutils
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/likexian/whois"
"imuslab.com/zoraxy/mod/utils"
)
/*
This script handles basic network utilities like
- traceroute
- ping
*/
func HandleTraceRoute(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
maxhopsString, err := utils.GetPara(r, "maxhops")
if err != nil {
maxhopsString = "64"
}
maxHops, err := strconv.Atoi(maxhopsString)
if err != nil {
maxHops = 64
}
results, err := TraceRoute(targetIpOrDomain, maxHops)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) {
return traceroute(targetIpOrDomain, maxHops)
}
func HandleWhois(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
raw, _ := utils.GetPara(r, "raw")
result, err := whois.Whois(targetIpOrDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if raw == "true" {
utils.SendTextResponse(w, result)
} else {
if isDomainName(targetIpOrDomain) {
//Is Domain
parsedOutput, err := ParseWHOISResponse(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
} else {
//Is IP
parsedOutput, err := ParseWhoisIpData(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
}
}
}
func HandlePing(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
type MixedPingResults struct {
ICMP []string
TCP []string
UDP []string
}
results := MixedPingResults{
ICMP: []string{},
TCP: []string{},
UDP: []string{},
}
//Ping ICMP
for i := 0; i < 4; i++ {
realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
if err != nil {
results.ICMP = append(results.ICMP, "Reply from "+realIP+": "+err.Error())
} else {
results.ICMP = append(results.ICMP, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
}
}
//Ping TCP
for i := 0; i < 4; i++ {
pingTime, err := TCPPing(targetIpOrDomain)
if err != nil {
results.TCP = append(results.TCP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.TCP = append(results.TCP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
//Ping UDP
for i := 0; i < 4; i++ {
pingTime, err := UDPPing(targetIpOrDomain)
if err != nil {
results.UDP = append(results.UDP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.UDP = append(results.UDP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func resolveIpFromDomain(targetIpOrDomain string) string {
//Resolve target ip address
targetIpAddrString := ""
ipAddr, err := net.ResolveIPAddr("ip", targetIpOrDomain)
if err != nil {
targetIpAddrString = targetIpOrDomain
} else {
targetIpAddrString = ipAddr.IP.String()
}
return targetIpAddrString
}

View File

@ -0,0 +1,28 @@
package netutils_test
import (
"testing"
"imuslab.com/zoraxy/mod/netutils"
)
func TestHandleTraceRoute(t *testing.T) {
results, err := netutils.TraceRoute("imuslab.com", 64)
if err != nil {
t.Fatal(err)
}
t.Log(results)
}
func TestHandlePing(t *testing.T) {
ipOrDomain := "example.com"
realIP, pingTime, ttl, err := netutils.PingIP(ipOrDomain)
if err != nil {
t.Fatal("Error:", err)
return
}
t.Log(realIP, pingTime, ttl)
}

View File

@ -0,0 +1,81 @@
package netutils
import (
"fmt"
"net"
"time"
)
// TCP ping
func TCPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("tcp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish TCP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// UDP Ping
func UDPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("udp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish UDP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// Traditional ICMP ping
func PingIP(ipOrDomain string) (string, time.Duration, int, error) {
ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
if err != nil {
return "", 0, 0, fmt.Errorf("failed to resolve IP address: %v", err)
}
ip := ipAddr.IP.String()
start := time.Now()
conn, err := net.Dial("ip:icmp", ip)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to establish ICMP connection: %v", err)
}
defer conn.Close()
icmpMsg := []byte{8, 0, 0, 0, 0, 1, 0, 0}
_, err = conn.Write(icmpMsg)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to send ICMP message: %v", err)
}
reply := make([]byte, 1500)
err = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to set read deadline: %v", err)
}
_, err = conn.Read(reply)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to read ICMP reply: %v", err)
}
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
ttl := int(reply[8])
return ip, pingTime, ttl, nil
}

View File

@ -0,0 +1,212 @@
package netutils
import (
"fmt"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
protocolICMP = 1
)
// liveTraceRoute return realtime tracing information to live response handler
func liveTraceRoute(dst string, maxHops int, liveRespHandler func(string)) error {
timeout := time.Second * 3
// resolve the host name to an IP address
ipAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
return fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
}
// create a socket to listen for incoming ICMP packets
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return fmt.Errorf("failed to create ICMP listener: %v", err)
}
defer conn.Close()
id := os.Getpid() & 0xffff
seq := 0
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
// set the TTL on the socket
if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
return fmt.Errorf("failed to set TTL: %v", err)
}
seq++
// create an ICMP message
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: []byte("zoraxy_trace"),
},
}
// serialize the ICMP message
msgBytes, err := msg.Marshal(nil)
if err != nil {
return fmt.Errorf("failed to serialize ICMP message: %v", err)
}
// send the ICMP message
start := time.Now()
if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
//log.Printf("%d: %v", ttl, err)
liveRespHandler(fmt.Sprintf("%d: %v", ttl, err))
continue loop_ttl
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
n, peer, err := conn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
//fmt.Printf("%d: *\n", ttl)
liveRespHandler(fmt.Sprintf("%d: *\n", ttl))
continue loop_ttl
} else {
liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
}
continue
}
// parse the ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
continue
}
// check if the reply is an echo reply
if replyMsg.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, peer, time.Since(start)))
break loop_ttl
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
var raddr = peer.String()
names, _ := net.LookupAddr(raddr)
if len(names) > 0 {
raddr = names[0] + " (" + raddr + ")"
} else {
raddr = raddr + " (" + raddr + ")"
}
liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, raddr, time.Since(start)))
continue loop_ttl
}
}
}
return nil
}
// Standard traceroute, return results after complete
func traceroute(dst string, maxHops int) ([]string, error) {
results := []string{}
timeout := time.Second * 3
// resolve the host name to an IP address
ipAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
return results, fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
}
// create a socket to listen for incoming ICMP packets
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return results, fmt.Errorf("failed to create ICMP listener: %v", err)
}
defer conn.Close()
id := os.Getpid() & 0xffff
seq := 0
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
// set the TTL on the socket
if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
return results, fmt.Errorf("failed to set TTL: %v", err)
}
seq++
// create an ICMP message
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: []byte("zoraxy_trace"),
},
}
// serialize the ICMP message
msgBytes, err := msg.Marshal(nil)
if err != nil {
return results, fmt.Errorf("failed to serialize ICMP message: %v", err)
}
// send the ICMP message
start := time.Now()
if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
//log.Printf("%d: %v", ttl, err)
results = append(results, fmt.Sprintf("%d: %v", ttl, err))
continue loop_ttl
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return results, fmt.Errorf("failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
n, peer, err := conn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
//fmt.Printf("%d: *\n", ttl)
results = append(results, fmt.Sprintf("%d: *", ttl))
continue loop_ttl
} else {
results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
}
continue
}
// parse the ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
continue
}
// check if the reply is an echo reply
if replyMsg.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
results = append(results, fmt.Sprintf("%d: %v %v", ttl, peer, time.Since(start)))
break loop_ttl
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
var raddr = peer.String()
names, _ := net.LookupAddr(raddr)
if len(names) > 0 {
raddr = names[0] + " (" + raddr + ")"
} else {
raddr = raddr + " (" + raddr + ")"
}
results = append(results, fmt.Sprintf("%d: %v %v", ttl, raddr, time.Since(start)))
continue loop_ttl
}
}
}
return results, nil
}

199
src/mod/netutils/whois.go Normal file
View File

@ -0,0 +1,199 @@
package netutils
import (
"net"
"strings"
"time"
)
type WHOISResult struct {
DomainName string `json:"domainName"`
RegistryDomainID string `json:"registryDomainID"`
Registrar string `json:"registrar"`
UpdatedDate time.Time `json:"updatedDate"`
CreationDate time.Time `json:"creationDate"`
ExpiryDate time.Time `json:"expiryDate"`
RegistrantID string `json:"registrantID"`
RegistrantName string `json:"registrantName"`
RegistrantEmail string `json:"registrantEmail"`
AdminID string `json:"adminID"`
AdminName string `json:"adminName"`
AdminEmail string `json:"adminEmail"`
TechID string `json:"techID"`
TechName string `json:"techName"`
TechEmail string `json:"techEmail"`
NameServers []string `json:"nameServers"`
DNSSEC string `json:"dnssec"`
}
func ParseWHOISResponse(response string) (WHOISResult, error) {
result := WHOISResult{}
lines := strings.Split(response, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Domain Name:") {
result.DomainName = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:"))
} else if strings.HasPrefix(line, "Registry Domain ID:") {
result.RegistryDomainID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Domain ID:"))
} else if strings.HasPrefix(line, "Registrar:") {
result.Registrar = strings.TrimSpace(strings.TrimPrefix(line, "Registrar:"))
} else if strings.HasPrefix(line, "Updated Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Updated Date:"))
updatedDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.UpdatedDate = updatedDate
}
} else if strings.HasPrefix(line, "Creation Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Creation Date:"))
creationDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.CreationDate = creationDate
}
} else if strings.HasPrefix(line, "Registry Expiry Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Registry Expiry Date:"))
expiryDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.ExpiryDate = expiryDate
}
} else if strings.HasPrefix(line, "Registry Registrant ID:") {
result.RegistrantID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Registrant ID:"))
} else if strings.HasPrefix(line, "Registrant Name:") {
result.RegistrantName = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Name:"))
} else if strings.HasPrefix(line, "Registrant Email:") {
result.RegistrantEmail = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Email:"))
} else if strings.HasPrefix(line, "Registry Admin ID:") {
result.AdminID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Admin ID:"))
} else if strings.HasPrefix(line, "Admin Name:") {
result.AdminName = strings.TrimSpace(strings.TrimPrefix(line, "Admin Name:"))
} else if strings.HasPrefix(line, "Admin Email:") {
result.AdminEmail = strings.TrimSpace(strings.TrimPrefix(line, "Admin Email:"))
} else if strings.HasPrefix(line, "Registry Tech ID:") {
result.TechID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Tech ID:"))
} else if strings.HasPrefix(line, "Tech Name:") {
result.TechName = strings.TrimSpace(strings.TrimPrefix(line, "Tech Name:"))
} else if strings.HasPrefix(line, "Tech Email:") {
result.TechEmail = strings.TrimSpace(strings.TrimPrefix(line, "Tech Email:"))
} else if strings.HasPrefix(line, "Name Server:") {
ns := strings.TrimSpace(strings.TrimPrefix(line, "Name Server:"))
result.NameServers = append(result.NameServers, ns)
} else if strings.HasPrefix(line, "DNSSEC:") {
result.DNSSEC = strings.TrimSpace(strings.TrimPrefix(line, "DNSSEC:"))
}
}
return result, nil
}
type WhoisIpLookupEntry struct {
NetRange string
CIDR string
NetName string
NetHandle string
Parent string
NetType string
OriginAS string
Organization Organization
RegDate time.Time
Updated time.Time
Ref string
}
type Organization struct {
OrgName string
OrgId string
Address string
City string
StateProv string
PostalCode string
Country string
/*
RegDate time.Time
Updated time.Time
OrgTechHandle string
OrgTechName string
OrgTechPhone string
OrgTechEmail string
OrgAbuseHandle string
OrgAbuseName string
OrgAbusePhone string
OrgAbuseEmail string
OrgRoutingHandle string
OrgRoutingName string
OrgRoutingPhone string
OrgRoutingEmail string
*/
}
func ParseWhoisIpData(data string) (WhoisIpLookupEntry, error) {
var entry WhoisIpLookupEntry = WhoisIpLookupEntry{}
var org Organization = Organization{}
lines := strings.Split(data, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "NetRange:") {
entry.NetRange = strings.TrimSpace(strings.TrimPrefix(line, "NetRange:"))
} else if strings.HasPrefix(line, "CIDR:") {
entry.CIDR = strings.TrimSpace(strings.TrimPrefix(line, "CIDR:"))
} else if strings.HasPrefix(line, "NetName:") {
entry.NetName = strings.TrimSpace(strings.TrimPrefix(line, "NetName:"))
} else if strings.HasPrefix(line, "NetHandle:") {
entry.NetHandle = strings.TrimSpace(strings.TrimPrefix(line, "NetHandle:"))
} else if strings.HasPrefix(line, "Parent:") {
entry.Parent = strings.TrimSpace(strings.TrimPrefix(line, "Parent:"))
} else if strings.HasPrefix(line, "NetType:") {
entry.NetType = strings.TrimSpace(strings.TrimPrefix(line, "NetType:"))
} else if strings.HasPrefix(line, "OriginAS:") {
entry.OriginAS = strings.TrimSpace(strings.TrimPrefix(line, "OriginAS:"))
} else if strings.HasPrefix(line, "Organization:") {
org.OrgName = strings.TrimSpace(strings.TrimPrefix(line, "Organization:"))
} else if strings.HasPrefix(line, "OrgId:") {
org.OrgId = strings.TrimSpace(strings.TrimPrefix(line, "OrgId:"))
} else if strings.HasPrefix(line, "Address:") {
org.Address = strings.TrimSpace(strings.TrimPrefix(line, "Address:"))
} else if strings.HasPrefix(line, "City:") {
org.City = strings.TrimSpace(strings.TrimPrefix(line, "City:"))
} else if strings.HasPrefix(line, "StateProv:") {
org.StateProv = strings.TrimSpace(strings.TrimPrefix(line, "StateProv:"))
} else if strings.HasPrefix(line, "PostalCode:") {
org.PostalCode = strings.TrimSpace(strings.TrimPrefix(line, "PostalCode:"))
} else if strings.HasPrefix(line, "Country:") {
org.Country = strings.TrimSpace(strings.TrimPrefix(line, "Country:"))
} else if strings.HasPrefix(line, "RegDate:") {
entry.RegDate, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "RegDate:")))
} else if strings.HasPrefix(line, "Updated:") {
entry.Updated, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Updated:")))
} else if strings.HasPrefix(line, "Ref:") {
entry.Ref = strings.TrimSpace(strings.TrimPrefix(line, "Ref:"))
}
}
entry.Organization = org
return entry, nil
}
func parseDate(dateStr string) (time.Time, error) {
dateLayout := "2006-01-02"
date, err := time.Parse(dateLayout, strings.TrimSpace(dateStr))
if err != nil {
return time.Time{}, err
}
return date, nil
}
func isDomainName(input string) bool {
ip := net.ParseIP(input)
if ip != nil {
// Check if it's IPv4 or IPv6
if ip.To4() != nil {
return false
} else if ip.To16() != nil {
return false
}
}
_, err := net.LookupHost(input)
return err == nil
}

100
src/mod/pathrule/handler.go Normal file
View File

@ -0,0 +1,100 @@
package pathrule
import (
"encoding/json"
"net/http"
"strconv"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/utils"
)
/*
handler.go
This script handles pathblock api
*/
func (h *Handler) HandleListBlockingPath(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(h.BlockingPaths)
utils.SendJSONResponse(w, string(js))
}
func (h *Handler) HandleAddBlockingPath(w http.ResponseWriter, r *http.Request) {
matchingPath, err := utils.PostPara(r, "matchingPath")
if err != nil {
utils.SendErrorResponse(w, "invalid matching path given")
return
}
exactMatch, err := utils.PostPara(r, "exactMatch")
if err != nil {
utils.SendErrorResponse(w, "invalid exact match value given")
return
}
statusCodeString, err := utils.PostPara(r, "statusCode")
if err != nil {
utils.SendErrorResponse(w, "invalid status code given")
return
}
statusCode, err := strconv.Atoi(statusCodeString)
if err != nil {
utils.SendErrorResponse(w, "invalid status code given")
return
}
enabled, err := utils.PostPara(r, "enabled")
if err != nil {
utils.SendErrorResponse(w, "invalid enabled value given")
return
}
caseSensitive, err := utils.PostPara(r, "caseSensitive")
if err != nil {
utils.SendErrorResponse(w, "invalid case sensitive value given")
return
}
targetBlockingPath := BlockingPath{
UUID: uuid.New().String(),
MatchingPath: matchingPath,
ExactMatch: exactMatch == "true",
StatusCode: statusCode,
CustomHeaders: http.Header{},
CustomHTML: []byte(""),
Enabled: enabled == "true",
CaseSenitive: caseSensitive == "true",
}
err = h.AddBlockingPath(&targetBlockingPath)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
func (h *Handler) HandleRemoveBlockingPath(w http.ResponseWriter, r *http.Request) {
blockerUUID, err := utils.PostPara(r, "uuid")
if err != nil {
utils.SendErrorResponse(w, "invalid uuid given")
return
}
targetRule := h.GetPathBlockerFromUUID(blockerUUID)
if targetRule == nil {
//Not found
utils.SendErrorResponse(w, "target path blocker not found")
return
}
err = h.RemoveBlockingPathByUUID(blockerUUID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -0,0 +1,174 @@
package pathrule
import (
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
/*
Pathrules.go
This script handle advance path settings and rules on particular
paths of the incoming requests
*/
type Options struct {
Enabled bool //If the pathrule is enabled.
ConfigFolder string //The folder to store the path blocking config files
}
type BlockingPath struct {
UUID string
MatchingPath string
ExactMatch bool
StatusCode int
CustomHeaders http.Header
CustomHTML []byte
Enabled bool
CaseSenitive bool
}
type Handler struct {
Options *Options
BlockingPaths []*BlockingPath
}
// Create a new path blocker handler
func NewPathRuleHandler(options *Options) *Handler {
//Create folder if not exists
if !utils.FileExists(options.ConfigFolder) {
os.Mkdir(options.ConfigFolder, 0775)
}
//Load the configs from file
//TODO
return &Handler{
Options: options,
BlockingPaths: []*BlockingPath{},
}
}
func (h *Handler) ListBlockingPath() []*BlockingPath {
return h.BlockingPaths
}
// Get the blocker from matching path (path match, ignore tailing slash)
func (h *Handler) GetPathBlockerFromMatchingPath(matchingPath string) *BlockingPath {
for _, blocker := range h.BlockingPaths {
if blocker.MatchingPath == matchingPath {
return blocker
} else if strings.TrimSuffix(blocker.MatchingPath, "/") == strings.TrimSuffix(matchingPath, "/") {
return blocker
}
}
return nil
}
func (h *Handler) GetPathBlockerFromUUID(UUID string) *BlockingPath {
for _, blocker := range h.BlockingPaths {
if blocker.UUID == UUID {
return blocker
}
}
return nil
}
func (h *Handler) AddBlockingPath(pathBlocker *BlockingPath) error {
//Check if the blocker exists
blockerPath := pathBlocker.MatchingPath
targetBlocker := h.GetPathBlockerFromMatchingPath(blockerPath)
if targetBlocker != nil {
//Blocker with the same matching path already exists
return errors.New("path blocker with the same path already exists")
}
h.BlockingPaths = append(h.BlockingPaths, pathBlocker)
//Write the new config to file
return h.SaveBlockerToFile(pathBlocker)
}
func (h *Handler) RemoveBlockingPathByUUID(uuid string) error {
newBlockingList := []*BlockingPath{}
for _, thisBlocker := range h.BlockingPaths {
if thisBlocker.UUID != uuid {
newBlockingList = append(newBlockingList, thisBlocker)
}
}
if len(h.BlockingPaths) == len(newBlockingList) {
//Nothing is removed
return errors.New("given matching path blocker not exists")
}
h.BlockingPaths = newBlockingList
return h.RemoveBlockerFromFile(uuid)
}
func (h *Handler) SaveBlockerToFile(pathBlocker *BlockingPath) error {
saveFilename := filepath.Join(h.Options.ConfigFolder, pathBlocker.UUID)
js, _ := json.MarshalIndent(pathBlocker, "", " ")
return os.WriteFile(saveFilename, js, 0775)
}
func (h *Handler) RemoveBlockerFromFile(uuid string) error {
expectedConfigFile := filepath.Join(h.Options.ConfigFolder, uuid)
if !utils.FileExists(expectedConfigFile) {
return errors.New("config file not found on disk")
}
return os.Remove(expectedConfigFile)
}
// Get all the matching blockers for the given URL path
// return all the path blockers and the max length matching rule
func (h *Handler) GetMatchingBlockers(urlPath string) ([]*BlockingPath, *BlockingPath) {
urlPath = strings.TrimSuffix(urlPath, "/")
matchingBlockers := []*BlockingPath{}
var longestMatchingPrefix *BlockingPath = nil
for _, thisBlocker := range h.BlockingPaths {
if thisBlocker.Enabled == false {
//This blocker is not enabled. Ignore this
continue
}
incomingURLPath := urlPath
matchingPath := strings.TrimSuffix(thisBlocker.MatchingPath, "/")
if !thisBlocker.CaseSenitive {
//This is not case sensitive
incomingURLPath = strings.ToLower(incomingURLPath)
matchingPath = strings.ToLower(matchingPath)
}
if matchingPath == incomingURLPath {
//This blocker have exact url path match
matchingBlockers = append(matchingBlockers, thisBlocker)
if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) {
longestMatchingPrefix = thisBlocker
}
continue
}
if !thisBlocker.ExactMatch && strings.HasPrefix(incomingURLPath, matchingPath) {
//This blocker have prefix url match
matchingBlockers = append(matchingBlockers, thisBlocker)
if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) {
longestMatchingPrefix = thisBlocker
}
continue
}
}
return matchingBlockers, longestMatchingPrefix
}

View File

@ -4,7 +4,7 @@ import (
"errors"
"net"
uuid "github.com/satori/go.uuid"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/database"
)
@ -95,7 +95,7 @@ func NewTCProxy(options *Options) *Manager {
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate a new config from options
configUUID := uuid.NewV4().String()
configUUID := uuid.New().String()
thisConfig := ProxyRelayConfig{
UUID: configUUID,
Name: config.Name,

View File

@ -93,8 +93,6 @@ func (m *Monitor) ExecuteUptimeCheck() {
Latency: laterncy,
}
//fmt.Println(thisRecord)
} else {
log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
continue
@ -238,9 +236,11 @@ func getWebsiteStatus(url string) (int, error) {
}
return 0, err
}
defer resp.Body.Close()
status_code := resp.StatusCode
return status_code, nil
}
defer resp.Body.Close()
status_code := resp.StatusCode
resp.Body.Close()
return status_code, nil
}

View File

@ -1,13 +1,11 @@
package utils
import (
"bufio"
"encoding/base64"
"errors"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
@ -40,46 +38,6 @@ func SendOK(w http.ResponseWriter) {
w.Write([]byte("\"OK\""))
}
/*
The paramter move function (mv)
You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
r (HTTP Request Object)
getParamter (string, aka $_GET['This string])
Will return
Paramter string (if any)
Error (if error)
*/
/*
func Mv(r *http.Request, getParamter string, postMode bool) (string, error) {
if postMode == false {
//Access the paramter via GET
keys, ok := r.URL.Query()[getParamter]
if !ok || len(keys[0]) < 1 {
//log.Println("Url Param " + getParamter +" is missing")
return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
}
// Query()["key"] will return an array of items,
// we only want the single item.
key := keys[0]
return string(key), nil
} else {
//Access the parameter via POST
r.ParseForm()
x := r.Form.Get(getParamter)
if len(x) == 0 || x == "" {
return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
}
return string(x), nil
}
}
*/
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
keys, ok := r.URL.Query()[key]
@ -101,6 +59,40 @@ func PostPara(r *http.Request, key string) (string, error) {
}
}
// Get POST paramter as boolean, accept 1 or true
func PostBool(r *http.Request, key string) (bool, error) {
x, err := PostPara(r, key)
if err != nil {
return false, err
}
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" {
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" {
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST paramter as int
func PostInt(r *http.Request, key string) (int, error) {
x, err := PostPara(r, key)
if err != nil {
return 0, err
}
x = strings.TrimSpace(x)
rx, err := strconv.Atoi(x)
if err != nil {
return 0, err
}
return rx, nil
}
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
@ -131,30 +123,6 @@ func TimeToString(targetTime time.Time) string {
return targetTime.Format("2006-01-02 15:04:05")
}
func LoadImageAsBase64(filepath string) (string, error) {
if !FileExists(filepath) {
return "", errors.New("File not exists")
}
f, _ := os.Open(filepath)
reader := bufio.NewReader(f)
content, _ := io.ReadAll(reader)
encoded := base64.StdEncoding.EncodeToString(content)
return string(encoded), nil
}
// Use for redirections
func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string {
if strings.Count(requestURI, "/") == 1 {
//Already root level
return redirectionLocation
}
for i := 0; i < strings.Count(requestURI, "/")-1; i++ {
redirectionLocation = "../" + redirectionLocation
}
return redirectionLocation
}
// Check if given string in a given slice
func StringInArray(arr []string, str string) bool {
for _, a := range arr {

View File

@ -0,0 +1,406 @@
package filemanager
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
/*
File Manager
This is a simple package that handles file management
under the web server directory
*/
type FileManager struct {
Directory string
}
// Create a new file manager with directory as root
func NewFileManager(directory string) *FileManager {
return &FileManager{
Directory: directory,
}
}
// Handle listing of a given directory
func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
directory, err := utils.GetPara(r, "dir")
if err != nil {
utils.SendErrorResponse(w, "invalid directory given")
return
}
// Construct the absolute path to the target directory
targetDir := filepath.Join(fm.Directory, directory)
// Open the target directory
dirEntries, err := os.ReadDir(targetDir)
if err != nil {
utils.SendErrorResponse(w, "unable to open directory")
return
}
// Create a slice to hold the file information
var files []map[string]interface{} = []map[string]interface{}{}
// Iterate through the directory entries
for _, dirEntry := range dirEntries {
fileInfo := make(map[string]interface{})
fileInfo["filename"] = dirEntry.Name()
fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name())
fileInfo["isDir"] = dirEntry.IsDir()
// Get file size and last modified time
finfo, err := dirEntry.Info()
if err != nil {
//unable to load its info. Skip this file
continue
}
fileInfo["lastModified"] = finfo.ModTime().Unix()
if !dirEntry.IsDir() {
// If it's a file, get its size
fileInfo["size"] = finfo.Size()
} else {
// If it's a directory, set size to 0
fileInfo["size"] = 0
}
// Append file info to the list
files = append(files, fileInfo)
}
// Serialize the file info slice to JSON
jsonData, err := json.Marshal(files)
if err != nil {
utils.SendErrorResponse(w, "unable to marshal JSON")
return
}
// Set response headers and send the JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}
// Handle upload of a file (multi-part), 25MB max
func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
dir, err := utils.PostPara(r, "dir")
if err != nil {
log.Println("no dir given")
utils.SendErrorResponse(w, "invalid dir given")
return
}
// Parse the multi-part form data
err = r.ParseMultipartForm(25 << 20)
if err != nil {
utils.SendErrorResponse(w, "unable to parse form data")
return
}
// Get the uploaded file
file, fheader, err := r.FormFile("file")
if err != nil {
log.Println(err.Error())
utils.SendErrorResponse(w, "unable to get uploaded file")
return
}
defer file.Close()
// Specify the directory where you want to save the uploaded file
uploadDir := filepath.Join(fm.Directory, dir)
if !utils.FileExists(uploadDir) {
utils.SendErrorResponse(w, "upload target directory not exists")
return
}
filename := sanitizeFilename(fheader.Filename)
if !isValidFilename(filename) {
utils.SendErrorResponse(w, "filename contain invalid or reserved characters")
return
}
// Create the file on the server
filePath := filepath.Join(uploadDir, filepath.Base(filename))
out, err := os.Create(filePath)
if err != nil {
utils.SendErrorResponse(w, "unable to create file on the server")
return
}
defer out.Close()
// Copy the uploaded file to the server
_, err = io.Copy(out, file)
if err != nil {
utils.SendErrorResponse(w, "unable to copy file to server")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
}
// Handle download of a selected file, serve with content dispose header
func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filepath given")
return
}
previewMode, _ := utils.GetPara(r, "preview")
if previewMode == "true" {
// Serve the file using http.ServeFile
filePath := filepath.Join(fm.Directory, filename)
http.ServeFile(w, r, filePath)
} else {
// Trigger a download with content disposition headers
filePath := filepath.Join(fm.Directory, filename)
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
http.ServeFile(w, r, filePath)
}
}
// HandleNewFolder creates a new folder in the specified directory
func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
// Parse the directory name from the request
dirName, err := utils.GetPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "invalid directory name")
return
}
//Prevent path escape
dirName = strings.ReplaceAll(dirName, "\\", "/")
dirName = strings.ReplaceAll(dirName, "../", "")
// Specify the directory where you want to create the new folder
newFolderPath := filepath.Join(fm.Directory, dirName)
// Check if the folder already exists
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
// Create the new folder
err := os.Mkdir(newFolderPath, os.ModePerm)
if err != nil {
utils.SendErrorResponse(w, "unable to create the new folder")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
} else {
// If the folder already exists, respond with an error
utils.SendErrorResponse(w, "folder already exists")
}
}
// HandleFileCopy copies a file or directory from the source path to the destination path
func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
// Parse the source and destination paths from the request
srcPath, err := utils.PostPara(r, "srcpath")
if err != nil {
utils.SendErrorResponse(w, "invalid source path")
return
}
destPath, err := utils.PostPara(r, "destpath")
if err != nil {
utils.SendErrorResponse(w, "invalid destination path")
return
}
// Validate and sanitize the source and destination paths
srcPath = filepath.Clean(srcPath)
destPath = filepath.Clean(destPath)
// Construct the absolute paths
absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath)
// Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist")
return
}
// Check if the destination path exists
if _, err := os.Stat(absDestPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "destination path does not exist")
return
}
//Join the name to create final paste filename
absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath))
//Reject opr if already exists
if utils.FileExists(absDestPath) {
utils.SendErrorResponse(w, "target already exists")
return
}
// Perform the copy operation based on whether the source is a file or directory
if isDir(absSrcPath) {
// Recursive copy for directories
err := copyDirectory(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err))
return
}
} else {
// Copy a single file
err := copyFile(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err))
return
}
}
utils.SendOK(w)
}
func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
// Parse the source and destination paths from the request
srcPath, err := utils.GetPara(r, "srcpath")
if err != nil {
utils.SendErrorResponse(w, "invalid source path")
return
}
destPath, err := utils.GetPara(r, "destpath")
if err != nil {
utils.SendErrorResponse(w, "invalid destination path")
return
}
// Validate and sanitize the source and destination paths
srcPath = filepath.Clean(srcPath)
destPath = filepath.Clean(destPath)
// Construct the absolute paths
absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath)
// Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist")
return
}
// Check if the destination path exists
if _, err := os.Stat(absDestPath); !os.IsNotExist(err) {
utils.SendErrorResponse(w, "destination path already exists")
return
}
// Rename the source to the destination
err = os.Rename(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err))
return
}
utils.SendOK(w)
}
func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) {
// Parse the target file or directory path from the request
filePath, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid file path")
return
}
// Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath)
// Check if the target path exists
_, err = os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "file or directory does not exist")
return
}
// Initialize a map to hold file properties
fileProps := make(map[string]interface{})
fileProps["filename"] = filepath.Base(absPath)
fileProps["filepath"] = filePath
fileProps["isDir"] = isDir(absPath)
// Get file size and last modified time
finfo, err := os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to retrieve file properties")
return
}
fileProps["lastModified"] = finfo.ModTime().Unix()
if !isDir(absPath) {
// If it's a file, get its size
fileProps["size"] = finfo.Size()
} else {
// If it's a directory, calculate its total size containing all child files and folders
totalSize, err := calculateDirectorySize(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to calculate directory size")
return
}
fileProps["size"] = totalSize
}
// Count the number of sub-files and sub-folders
numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to count sub-files and sub-folders")
return
}
fileProps["fileCounts"] = numSubFiles
fileProps["folderCounts"] = numSubFolders
// Serialize the file properties to JSON
jsonData, err := json.Marshal(fileProps)
if err != nil {
utils.SendErrorResponse(w, "unable to marshal JSON")
return
}
// Set response headers and send the JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}
// HandleFileDelete deletes a file or directory
func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) {
// Parse the target file or directory path from the request
filePath, err := utils.PostPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid file path")
return
}
// Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath)
// Check if the target path exists
_, err = os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "file or directory does not exist")
return
}
// Delete the file or directory
err = os.RemoveAll(absPath)
if err != nil {
utils.SendErrorResponse(w, "error deleting file or directory")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
}

View File

@ -0,0 +1,156 @@
package filemanager
import (
"io"
"os"
"path/filepath"
"strings"
)
// isValidFilename checks if a given filename is safe and valid.
func isValidFilename(filename string) bool {
// Define a list of disallowed characters and reserved names
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} // Add more if needed
// Check for disallowed characters
for _, char := range disallowedChars {
if strings.Contains(filename, char) {
return false
}
}
// Check for reserved names (case-insensitive)
lowerFilename := strings.ToUpper(filename)
for _, reserved := range reservedNames {
if lowerFilename == reserved {
return false
}
}
// Check for empty filename
if filename == "" {
return false
}
// The filename is considered valid
return true
}
// sanitizeFilename sanitizes a given filename by removing disallowed characters.
func sanitizeFilename(filename string) string {
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
// Replace disallowed characters with underscores
for _, char := range disallowedChars {
filename = strings.ReplaceAll(filename, char, "_")
}
return filename
}
// copyFile copies a single file from source to destination
func copyFile(srcPath, destPath string) error {
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(destPath)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
return nil
}
// copyDirectory recursively copies a directory and its contents from source to destination
func copyDirectory(srcPath, destPath string) error {
// Create the destination directory
err := os.MkdirAll(destPath, os.ModePerm)
if err != nil {
return err
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
srcEntryPath := filepath.Join(srcPath, entry.Name())
destEntryPath := filepath.Join(destPath, entry.Name())
if entry.IsDir() {
err := copyDirectory(srcEntryPath, destEntryPath)
if err != nil {
return err
}
} else {
err := copyFile(srcEntryPath, destEntryPath)
if err != nil {
return err
}
}
}
return nil
}
// isDir checks if the given path is a directory
func isDir(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}
// calculateDirectorySize calculates the total size of a directory and its contents
func calculateDirectorySize(dirPath string) (int64, error) {
var totalSize int64
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
totalSize += info.Size()
return nil
})
if err != nil {
return 0, err
}
return totalSize, nil
}
// countSubFilesAndFolders counts the number of sub-files and sub-folders within a directory
func countSubFilesAndFolders(dirPath string) (int, int, error) {
var numSubFiles, numSubFolders int
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
numSubFolders++
} else {
numSubFiles++
}
return nil
})
if err != nil {
return 0, 0, err
}
// Subtract 1 from numSubFolders to exclude the root directory itself
return numSubFiles, numSubFolders - 1, nil
}

View File

@ -0,0 +1,88 @@
package webserv
import (
"encoding/json"
"net/http"
"strconv"
"imuslab.com/zoraxy/mod/utils"
)
/*
Handler.go
Handler for web server options change
web server is directly listening to the TCP port
handlers in this script are for setting change only
*/
type StaticWebServerStatus struct {
ListeningPort int
EnableDirectoryListing bool
WebRoot string
Running bool
EnableWebDirManager bool
}
// Handle getting current static web server status
func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
listeningPortInt, _ := strconv.Atoi(ws.option.Port)
currentStatus := StaticWebServerStatus{
ListeningPort: listeningPortInt,
EnableDirectoryListing: ws.option.EnableDirectoryListing,
WebRoot: ws.option.WebRoot,
Running: ws.isRunning,
EnableWebDirManager: ws.option.EnableWebDirManager,
}
js, _ := json.Marshal(currentStatus)
utils.SendJSONResponse(w, string(js))
}
// Handle request for starting the static web server
func (ws *WebServer) HandleStartServer(w http.ResponseWriter, r *http.Request) {
err := ws.Start()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle request for stopping the static web server
func (ws *WebServer) HandleStopServer(w http.ResponseWriter, r *http.Request) {
err := ws.Stop()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle change server listening port request
func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
newPort, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port number given")
return
}
err = ws.ChangePort(strconv.Itoa(newPort))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Change enable directory listing settings
func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Request) {
enableList, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid setting given")
return
}
ws.option.EnableDirectoryListing = enableList
utils.SendOK(w)
}

View File

@ -0,0 +1,41 @@
package webserv
import (
"net/http"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
// Convert a request path (e.g. /index.html) into physical path on disk
func (ws *WebServer) resolveFileDiskPath(requestPath string) string {
fileDiskpath := filepath.Join(ws.option.WebRoot, "html", requestPath)
//Force convert it to slash even if the host OS is on Windows
fileDiskpath = filepath.Clean(fileDiskpath)
fileDiskpath = strings.ReplaceAll(fileDiskpath, "\\", "/")
return fileDiskpath
}
// File server middleware to handle directory listing (and future expansion)
func (ws *WebServer) fsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !ws.option.EnableDirectoryListing {
if strings.HasSuffix(r.URL.Path, "/") {
//This is a folder. Let check if index exists
if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.html")) {
} else if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.htm")) {
} else {
http.NotFound(w, r)
return
}
}
}
h.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,61 @@
<html>
<head>
<title>Hello Zoraxy</title>
<style>
body {
font-family: Tahoma, sans-serif;
background-color: #f6f6f6;
color: #2d2e30;
}
.sectionHeader{
background-color: #c4d0d9;
padding: 0.1em;
}
.sectionHeader h3{
text-align: center;
}
.container{
margin: 4em;
margin-left: 10em;
margin-right: 10em;
background-color: #fefefe;
}
@media (max-width:960px) {
.container{
margin-left: 1em;
margin-right: 1em;
}
.sectionHeader{
padding-left: 1em;
padding-right: 1em;
}
}
.textcontainer{
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<div class="sectionHeader">
<h3>Welcome to Zoraxy Static Web Server!</h3>
</div>
<div class="textcontainer">
<p>If you see this page, that means your static web server is running.<br>
By default, all the html files are stored under <code>./web/html/</code>
relative to the zoraxy runtime directory.<br>
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
</p>
<p>
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
Thank you for using Zoraxy!
</p>
</div>
</div>
</body>
</html>

18
src/mod/webserv/utils.go Normal file
View File

@ -0,0 +1,18 @@
package webserv
import (
"net"
)
// IsPortInUse checks if a port is in use.
func IsPortInUse(port string) bool {
listener, err := net.Listen("tcp", "localhost:"+port)
if err != nil {
// If there was an error, the port is in use.
return true
}
defer listener.Close()
// No error means the port is available.
return false
}

195
src/mod/webserv/webserv.go Normal file
View File

@ -0,0 +1,195 @@
package webserv
import (
"embed"
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv/filemanager"
)
/*
Static Web Server package
This module host a static web server
*/
//go:embed templates/*
var templates embed.FS
type WebServerOptions struct {
Port string //Port for listening
EnableDirectoryListing bool //Enable listing of directory
WebRoot string //Folder for stroing the static web folders
EnableWebDirManager bool //Enable web file manager to handle files in web directory
Sysdb *database.Database //Database for storing configs
}
type WebServer struct {
FileManager *filemanager.FileManager
mux *http.ServeMux
server *http.Server
option *WebServerOptions
isRunning bool
mu sync.Mutex
}
// NewWebServer creates a new WebServer instance. One instance only
func NewWebServer(options *WebServerOptions) *WebServer {
if !utils.FileExists(options.WebRoot) {
//Web root folder not exists. Create one with default templates
os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
indexTemplate, err := templates.ReadFile("templates/index.html")
if err != nil {
log.Println("Failed to read static wev server template file: ", err.Error())
} else {
os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
}
}
//Create a new file manager if it is enabled
var newDirManager *filemanager.FileManager
if options.EnableWebDirManager {
fm := filemanager.NewFileManager(filepath.Join(options.WebRoot, "/html"))
newDirManager = fm
}
//Create new table to store the config
options.Sysdb.NewTable("webserv")
return &WebServer{
mux: http.NewServeMux(),
FileManager: newDirManager,
option: options,
isRunning: false,
mu: sync.Mutex{},
}
}
// Restore the configuration to previous config
func (ws *WebServer) RestorePreviousState() {
//Set the port
port := ws.option.Port
ws.option.Sysdb.Read("webserv", "port", &port)
ws.option.Port = port
//Set the enable directory list
enableDirList := ws.option.EnableDirectoryListing
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
ws.option.EnableDirectoryListing = enableDirList
//Check the running state
webservRunning := false
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
if webservRunning {
ws.Start()
} else {
ws.Stop()
}
}
// ChangePort changes the server's port.
func (ws *WebServer) ChangePort(port string) error {
if IsPortInUse(port) {
return errors.New("Selected port is used by another process")
}
if ws.isRunning {
if err := ws.Stop(); err != nil {
return err
}
}
ws.option.Port = port
ws.server.Addr = ":" + port
err := ws.Start()
if err != nil {
return err
}
ws.option.Sysdb.Write("webserv", "port", port)
return nil
}
// Start starts the web server.
func (ws *WebServer) Start() error {
ws.mu.Lock()
defer ws.mu.Unlock()
//Check if server already running
if ws.isRunning {
return fmt.Errorf("web server is already running")
}
//Check if the port is usable
if IsPortInUse(ws.option.Port) {
return errors.New("Port already in use or access denied by host OS")
}
//Dispose the old mux and create a new one
ws.mux = http.NewServeMux()
//Create a static web server
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
ws.mux.Handle("/", ws.fsMiddleware(fs))
ws.server = &http.Server{
Addr: ":" + ws.option.Port,
Handler: ws.mux,
}
go func() {
if err := ws.server.ListenAndServe(); err != nil {
if err != http.ErrServerClosed {
fmt.Printf("Web server error: %v\n", err)
}
}
}()
log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
ws.isRunning = true
ws.option.Sysdb.Write("webserv", "enabled", true)
return nil
}
// Stop stops the web server.
func (ws *WebServer) Stop() error {
ws.mu.Lock()
defer ws.mu.Unlock()
if !ws.isRunning {
return fmt.Errorf("web server is not running")
}
if err := ws.server.Close(); err != nil {
return err
}
ws.isRunning = false
ws.option.Sysdb.Write("webserv", "enabled", false)
return nil
}
// UpdateDirectoryListing enables or disables directory listing.
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
ws.option.EnableDirectoryListing = enable
ws.option.Sysdb.Write("webserv", "dirlist", enable)
}
// Close stops the web server without returning an error.
func (ws *WebServer) Close() {
ws.Stop()
}

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"log"
"net/http"
"path/filepath"
"sort"
@ -25,50 +24,71 @@ func ReverseProxtInit() {
inboundPort := 80
if sysdb.KeyExists("settings", "inbound") {
sysdb.Read("settings", "inbound", &inboundPort)
log.Println("Serving inbound port ", inboundPort)
SystemWideLogger.Println("Serving inbound port ", inboundPort)
} else {
log.Println("Inbound port not set. Using default (80)")
SystemWideLogger.Println("Inbound port not set. Using default (80)")
}
useTls := false
sysdb.Read("settings", "usetls", &useTls)
if useTls {
log.Println("TLS mode enabled. Serving proxxy request with TLS")
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
} else {
log.Println("TLS mode disabled. Serving proxy request with plain http")
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
}
forceLatestTLSVersion := false
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
if forceLatestTLSVersion {
SystemWideLogger.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
} else {
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
}
listenOnPort80 := false
sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 {
SystemWideLogger.Println("Port 80 listener enabled")
} else {
SystemWideLogger.Println("Port 80 listener disabled")
}
forceHttpsRedirect := false
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
if forceHttpsRedirect {
log.Println("Force HTTPS mode enabled")
SystemWideLogger.Println("Force HTTPS mode enabled")
//Port 80 listener must be enabled to perform http -> https redirect
listenOnPort80 = true
} else {
log.Println("Force HTTPS mode disabled")
SystemWideLogger.Println("Force HTTPS mode disabled")
}
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager,
RedirectRuleTable: redirectTable,
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
})
if err != nil {
log.Println(err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
return
}
dynamicProxyRouter = dprouter
//Load all conf from files
confs, _ := filepath.Glob("./conf/*.config")
confs, _ := filepath.Glob("./conf/proxy/*.config")
for _, conf := range confs {
record, err := LoadReverseProxyConfig(conf)
if err != nil {
log.Println("Failed to load "+filepath.Base(conf), err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
return
}
@ -79,24 +99,28 @@ func ReverseProxtInit() {
})
} else if record.ProxyType == "subd" {
dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
BypassGlobalTLS: record.BypassGlobalTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
})
} else if record.ProxyType == "vdir" {
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
RootName: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
RootName: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
BypassGlobalTLS: record.BypassGlobalTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
})
} else {
log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
SystemWideLogger.PrintAndLog("Proxy", "Unsupported endpoint type: "+record.ProxyType+". Skipping "+filepath.Base(conf), nil)
}
}
@ -105,7 +129,7 @@ func ReverseProxtInit() {
//reverse proxy server in front of this service
time.Sleep(300 * time.Millisecond)
dynamicProxyRouter.StartProxyService()
log.Println("Dynamic Reverse Proxy service started")
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
//Add all proxy services to uptime monitor
//Create a uptime monitor service
@ -116,7 +140,7 @@ func ReverseProxtInit() {
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
})
log.Println("Uptime Monitor background service started")
SystemWideLogger.Println("Uptime Monitor background service started")
}()
}
@ -168,6 +192,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useTLS := (tls == "true")
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
if bypassGlobalTLS == "" {
bypassGlobalTLS = "false"
}
useBypassGlobalTLS := bypassGlobalTLS == "true"
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
@ -228,6 +259,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
RootName: vdir,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
@ -245,6 +277,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
MatchingDomain: subdomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
@ -269,11 +302,12 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
Rootname: rootname,
ProxyTarget: endpoint,
UseTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipTlsValidation: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update utm if exists
if uptimeMonitor != nil {
@ -320,9 +354,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
if stv == "" {
stv = "false"
}
skipTlsValidation := (stv == "true")
//Load bypass TLS option
bpgtls, _ := utils.PostPara(r, "bpgtls")
if bpgtls == "" {
bpgtls = "false"
}
bypassGlobalTLS := (bpgtls == "true")
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
@ -342,10 +382,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RootName: targetProxyEntry.RootOrMatchingDomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: false,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" {
@ -353,10 +395,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: bypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
}
@ -370,14 +414,10 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update the current running config
targetProxyEntry.Domain = endpoint
targetProxyEntry.RequireTLS = useTLS
targetProxyEntry.SkipCertValidations = skipTlsValidation
targetProxyEntry.RequireBasicAuth = requireBasicAuth
dynamicProxyRouter.SaveProxy(eptype, targetProxyEntry.RootOrMatchingDomain, targetProxyEntry)
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@ -395,13 +435,15 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
err = dynamicProxyRouter.RemoveProxy(ptype, ep)
//Remove the config from runtime
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
RemoveReverseProxyConfig(ep)
//Remove the config from file
RemoveReverseProxyConfigFile(ep)
//Update utm if exists
if uptimeMonitor != nil {
@ -409,6 +451,9 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
uptimeMonitor.CleanRecords()
}
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@ -525,19 +570,10 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
targetProxy.BasicAuthCredentials = mergedCredentials
//Save it to file
thisProxyConfigRecord := Record{
ProxyType: ptype,
Rootname: targetProxy.RootOrMatchingDomain,
ProxyTarget: targetProxy.Domain,
UseTLS: targetProxy.RequireTLS,
SkipTlsValidation: targetProxy.SkipCertValidations,
RequireBasicAuth: targetProxy.RequireBasicAuth,
BasicAuthCredentials: targetProxy.BasicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyEndpointToFile(targetProxy)
//Replace runtime configuration
dynamicProxyRouter.SaveProxy(ptype, ep, targetProxy)
targetProxy.UpdateToRuntime()
utils.SendOK(w)
} else {
http.Error(w, "invalid usage", http.StatusMethodNotAllowed)
@ -545,6 +581,147 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
}
// List, Update or Remove the exception paths for basic auth.
func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
ep, err := utils.GetPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.GetPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
//Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//List all the exception paths for this proxy
results := targetProxy.BasicAuthExceptionRules
if results == nil {
//It is a config from a really old version of zoraxy. Overwrite it with empty array
results = []*dynamicproxy.BasicAuthExceptionRule{}
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
return
}
func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
//Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Check if the prefix starts with /. If not, prepend it
if !strings.HasPrefix(matchingPrefix, "/") {
matchingPrefix = "/" + matchingPrefix
}
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true
break
}
}
if alreadyExists {
utils.SendErrorResponse(w, "This matching path already exists")
return
}
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix),
})
//Save configs to runtime and file
targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy)
utils.SendOK(w)
}
func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
// Delete a rule
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
// Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
matchingExists = true
}
}
if !matchingExists {
utils.SendErrorResponse(w, "target matching rule not exists")
return
}
targetProxy.BasicAuthExceptionRules = newExceptionRuleList
// Save configs to runtime and file
targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy)
utils.SendOK(w)
}
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(dynamicProxyRouter)
utils.SendJSONResponse(w, string(js))
@ -591,6 +768,35 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
}
}
// Handle port 80 incoming traffics
func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.GetPara(r, "enable")
if err != nil {
//Load the current status
currentEnabled := false
err = sysdb.Read("settings", "listenP80", &currentEnabled)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enabled == "true" {
sysdb.Write("settings", "listenP80", true)
SystemWideLogger.Println("Enabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true)
} else if enabled == "false" {
sysdb.Write("settings", "listenP80", false)
SystemWideLogger.Println("Disabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true)
} else {
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
}
utils.SendOK(w)
}
}
// Handle https redirect
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
useRedirect, err := utils.GetPara(r, "set")
@ -611,11 +817,11 @@ func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
}
if useRedirect == "true" {
sysdb.Write("settings", "redirect", true)
log.Println("Updating force HTTPS redirection to true")
SystemWideLogger.Println("Updating force HTTPS redirection to true")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
} else if useRedirect == "false" {
sysdb.Write("settings", "redirect", false)
log.Println("Updating force HTTPS redirection to false")
SystemWideLogger.Println("Updating force HTTPS redirection to false")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
@ -641,11 +847,18 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
newIncomingPortInt, err := strconv.Atoi(newIncomingPort)
if err != nil {
utils.SendErrorResponse(w, "invalid incoming port given")
utils.SendErrorResponse(w, "Invalid incoming port given")
return
}
//Check if it is identical as proxy root (recursion!)
if dynamicProxyRouter.Root == nil || dynamicProxyRouter.Root.Domain == "" {
//Check if proxy root is set before checking recursive listen
//Fixing issue #43
utils.SendErrorResponse(w, "Set Proxy Root before changing inbound port")
return
}
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
//Listening port is same as proxy root
@ -668,3 +881,34 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
// Handle list of root route options
func HandleRootRouteOptionList(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(dynamicProxyRouter.RootRoutingOptions)
utils.SendJSONResponse(w, string(js))
}
// Handle update of the root route edge case options. See dynamicproxy/rootRoute.go
func HandleRootRouteOptionsUpdate(w http.ResponseWriter, r *http.Request) {
enableUnsetSubdomainRedirect, err := utils.PostBool(r, "unsetRedirect")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
unsetRedirectTarget, _ := utils.PostPara(r, "unsetRedirectTarget")
newRootOption := dynamicproxy.RootRoutingOptions{
EnableRedirectForUnsetRules: enableUnsetSubdomainRedirect,
UnsetRuleRedirectTarget: unsetRedirectTarget,
}
dynamicProxyRouter.RootRoutingOptions = &newRootOption
err = newRootOption.SaveToFile()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -8,18 +8,22 @@ import (
"strings"
"time"
"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/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/webserv"
)
/*
@ -47,8 +51,9 @@ func startupSequence() {
//Create tables for the database
sysdb.NewTable("settings")
//Create tmp folder
//Create tmp folder and conf folder
os.MkdirAll("./tmp", 0775)
os.MkdirAll("./conf/proxy/", 0775)
//Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb)
@ -61,19 +66,22 @@ func startupSequence() {
})
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./certs", development)
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
if err != nil {
panic(err)
}
//Create a redirection rule table
redirectTable, err = redirection.NewRuleTable("./rules")
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
if err != nil {
panic(err)
}
//Create a geodb store
geodbStore, err = geodb.NewGeoDb(sysdb)
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
})
if err != nil {
panic(err)
}
@ -86,59 +94,81 @@ func startupSequence() {
panic(err)
}
//Create a system wide logger
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
//Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300)
if err != nil {
log.Println("Failed to load network statistic info")
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
panic(err)
}
/*
Path Rules
This section of starutp script start the path rules where
user can define their own routing logics
*/
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
Enabled: false,
ConfigFolder: "./conf/rules/pathrules",
})
/*
MDNS Discovery Service
This discover nearby ArozOS Nodes or other services
that provide mDNS discovery with domain (e.g. Synology NAS)
*/
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
if err != nil {
portInt = 8000
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID,
Port: portInt,
Domain: "zoraxy.imuslab.com",
Model: "Network Gateway",
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
}, "")
if err != nil {
panic(err)
}
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
log.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
if *allowMdnsScanning {
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
if err != nil {
portInt = 8000
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID,
Port: portInt,
Domain: "zoraxy.arozos.com",
Model: "Network Gateway",
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
}, "")
if err != nil {
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
} else {
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
log.Println("mDNS scan result updated")
}
SystemWideLogger.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS scan result updated")
}
}
}()
mdnsTickerStop = stopChan
}
}()
mdnsTickerStop = stopChan
}
/*
Global Area Network
@ -149,7 +179,7 @@ func startupSequence() {
if usingZtAuthToken == "" {
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
if err != nil {
log.Println("Failed to load ZeroTier controller API authtoken")
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
}
}
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
@ -176,4 +206,39 @@ func startupSequence() {
//Create an analytic loader
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
/*
ACME API
Obtaining certificates from ACME Server
*/
//Create a table just to store acme related preferences
sysdb.NewTable("acmepref")
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
if err != nil {
log.Fatal(err)
}
/*
Static Web Server
Start the static web server
*/
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb,
Port: "5487", //Default Port
WebRoot: *staticWebServerRoot,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
})
//Restore the web server to previous shutdown state
staticWebServer.RestorePreviousState()
}
// This sequence start after everything is initialized
func finalSequence() {
//Start ACME renew agent
acmeRegisterSpecialRoutingRule()
}

View File

@ -308,7 +308,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
@ -625,7 +625,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
@ -1082,24 +1082,51 @@
//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string
function isValidIpFilter(input) {
// Check if input is a valid IP address
const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
// Check if input is a valid IPv4 address
const isValidIPv4 = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
if (isValidIp) {
if (isValidIPv4) {
return true;
}
// Check if input is a wildcard IP address
const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
// Check if input is a valid IPv4 wildcard address
const isValidIPv4Wildcard = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
if (isValidWildcardIp) {
if (isValidIPv4Wildcard) {
return true;
}
// Check if input is a valid CIDR address string
const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
// Check if input is a valid IPv4 CIDR address
const isValidIPv4CIDR = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
if (isValidCidr) {
if (isValidIPv4CIDR) {
return true;
}
// Check if input is loopback ipv6
if (input == "::1"){
return true;
}
// Check if input is a valid IPv6 address
const isValidIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(input);
if (isValidIPv6) {
return true;
}
//Pure magic, I have no idea how this works
//src: https://stackoverflow.com/questions/70348674/alternate-solution-validate-ipv4-and-ipv6-with-wildcard-characters-using-r
function evalIp6(t){var e=t.split(":"),n=t.split("::").length-1;if(8<e.length&&(9!=t.split(":").length||""!=e[e.length-1]||1!=n))return!1;if(1<n)return!1;if(-1!=t.indexOf("::*")||-1!=t.indexOf("*::"))return!1;var r=!1;for(let t=0;t<e.length;t++){if(!isIPV6Group(e[t]))return!1;"*"==e[t]&&(r=!0)}return!(!r&&0==n&&8!=e.length)}function isIPV6Group(t){var e="^(([0-9A-Fa-f]{1,4})|\\*|)$";return(e=new RegExp(e)).test(t)}
if (evalIp6(input)){
return true;
}
// Check if input is a valid IPv6 CIDR address
const isValidIPv6CIDR = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/.test(input);
if (isValidIPv6CIDR) {
return true;
}

View File

@ -1,3 +1,13 @@
<style>
.expired.certdate{
font-weight: bolder;
color: #bd001c;
}
.valid.certdate{
color: #31c071;
}
</style>
<div class="standardContainer">
<div class="ui basic segment">
<h2>TLS / SSL Certificates</h2>
@ -55,17 +65,20 @@
</div>
<br>
<div>
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
</div>
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui message">
@ -74,11 +87,49 @@
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
</div>
<div class="ui divider"></div>
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
<p>Management features regarding CA and ACME</p>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
</div>
</div>
</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="red circle icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">Auto-Renewer Status</div>
</div>
</h4>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<script>
var uploadPendingPublicKey = undefined;
var uploadPendingPrivateKey = undefined;
$("#defaultCA").dropdown();
//Delete the certificate by its domain
function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){
@ -99,6 +150,62 @@
}
function initAcmeStatus(){
//Initialize the current default CA options
$.get("/api/acme/autoRenew/email", function(data){
$("#prefACMEEmail").val(data);
});
$.get("/api/acme/autoRenew/ca", function(data){
$("#defaultCA").dropdown("set value", data);
});
$.get("/api/acme/autoRenew/enable", function(data){
setACMEEnableStates(data);
})
}
//Set the status of the acme enable icon
function setACMEEnableStates(enabled){
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
}
initAcmeStatus();
function saveDefaultCA(){
let newDefaultEmail = $("#prefACMEEmail").val().trim();
let newDefaultCA = $("#defaultCA").dropdown("get value");
if (newDefaultEmail == ""){
msgbox("Invalid acme email given", false);
return;
}
$.ajax({
url: "/api/acme/autoRenew/email",
method: "POST",
data: {"set": newDefaultEmail},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}
}
});
$.ajax({
url: "/api/acme/autoRenew/ca",
data: {"set": newDefaultCA},
method: "POST",
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}
}
});
msgbox("Settings updated");
}
//List the stored certificates
function initManagedDomainCertificateList(){
$.get("/api/cert/list?date=true", function(data){
@ -106,11 +213,16 @@
msgbox(data.error, false, 5000);
}else{
$("#certifiedDomainList").html("");
data.sort((a,b) => {
return a.Domain > b.Domain
});
data.forEach(entry => {
let isExpired = entry.RemainingDays <= 0;
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td>
<td>${entry.ExpireDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
});
@ -125,6 +237,10 @@
}
initManagedDomainCertificateList();
function openACMEManager(){
showSideWrapper('snippet/acme.html');
}
function handleDomainUploadByKeypress(){
handleDomainKeysUpload(function(){
$("#certUploadingDomain").text($("#certdomain").val().trim());

View File

@ -12,13 +12,50 @@
</div>
<div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
<!-- MDNS Scanner-->
<h2>Multicast DNS (mDNS) Scanner</h2>
<p>Discover mDNS enabled service in this gateway forwarded network</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
<div class="ui divider"></div>
<!-- IP Scanner-->
<h2>IP Scanner</h2>
<p>Discover local area network devices by pinging them one by one</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
<div class="ui divider"></div>
<!-- Traceroute-->
<h2>Traceroute / Ping</h2>
<p>Trace the network nodes that your packets hops through</p>
<div class="ui form">
<div class="two fields">
<div class="field">
<label>Target domain or IP</label>
<input type="text" id="traceroute_domain" placeholder="1.1.1.1">
</div>
<div class="field">
<label>Max Hops</label>
<input type="number" min="1" step="1" id="traceroute_maxhops" placeholder="64" value="64">
</div>
</div>
<button class="ui basic button" onclick="traceroute();"><i class="ui blue location arrow icon"></i> Start Tracing</button>
<button class="ui basic button" onclick="ping();"><i class="ui teal circle outline icon"></i> Ping</button>
<br><br>
<div class="field">
<label>Results</label>
<textarea id="traceroute_results" rows="10" style=""></textarea>
</div>
</div>
<div class="ui divider"></div>
<!-- Whois-->
<h2>Whois</h2>
<p>Check the owner and registration information of a given domain</p>
<div class="ui icon input">
<input id="whoisdomain" type="text" onkeypress="if(event.keyCode === 13) { performWhoisLookup(); }" placeholder="Domain or IP">
<i onclick="performWhoisLookup();" class="circular search link icon"></i>
</div><br>
<small>Lookup might take a few minutes to complete</small>
<br>
<div id="whois_table"></div>
</div>
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
@ -435,6 +472,91 @@ function updateMDNSListForWoL(){
}
updateMDNSListForWoL();
function traceroute(){
let domain = $("#traceroute_domain").val().trim();
let maxhops = $("#traceroute_maxhops").val().trim();
$("#traceroute_results").val("Loading...");
$.get("/api/tools/traceroute?target=" + domain + "&maxhops=" + maxhops, function(data){
if (data.error != undefined){
$("#traceroute_results").val("");
msgbox(data.error, false, 6000);
}else{
$("#traceroute_results").val(data.join("\n"));
}
});
}
function ping(){
let domain = $("#traceroute_domain").val().trim();
$("#traceroute_results").val("Loading...");
$.get("/api/tools/ping?target=" + domain, function(data){
if (data.error != undefined){
$("#traceroute_results").val("");
msgbox(data.error, false, 6000);
}else{
$("#traceroute_results").val(`--------- ICMP Ping -------------
${data.ICMP.join("\n")}\n
---------- TCP Ping -------------
${data.TCP.join("\n")}\n
---------- UDP Ping -------------
${data.UDP.join("\n")}`);
}
});
}
function performWhoisLookup(){
let whoisDomain = $("#whoisdomain").val().trim();
$("#whoisdomain").parent().addClass("disabled");
$("#whoisdomain").parent().css({
"cursor": "wait"
});
$.get("/api/tools/whois?target=" + whoisDomain, function(data){
$("#whoisdomain").parent().removeClass("disabled");
$("#whoisdomain").parent().css({
"cursor": "auto"
});
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
renderWhoisDomainTable(data);
}
})
}
function renderWhoisDomainTable(jsonData) {
function formatDate(dateString) {
var date = new Date(dateString);
return date.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
var table = $('<table>').addClass('ui definition table');
// Create table body
var body = $('<tbody>');
for (var key in jsonData) {
var value = jsonData[key];
var row = $('<tr>');
row.append($('<td>').text(key));
if (key.endsWith('Date')) {
row.append($('<td>').text(formatDate(value)));
} else if (Array.isArray(value)) {
row.append($('<td>').text(value.join(', ')));
}else if (typeof(value) == "object"){
row.append($('<td>').text(JSON.stringify(value)));
} else {
row.append($('<td>').text(value));
}
body.append(row);
}
// Append the table body to the table
table.append(body);
// Append the table to the target element
$('#whois_table').empty().append(table);
}
</script>

View File

@ -1,78 +1,100 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
<label>Forward Pathname</label>
<div class="ui basic segment">
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="ui message">
<p>Append the current pathname after the redirect destination</p>
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
<i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, <b>sometime you might need to add tailing slash (/) to your URL depending on your use cases</b></small>
</div>
</div>
<div class="grouped fields">
<label>Redirection Status Code</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="307" checked>
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
<label>Forward Pathname</label>
</div>
<div class="ui message">
<p>Append the current pathname after the redirect destination</p>
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
<i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
</div>
</div>
<div class="grouped fields">
<label>Redirection Status Code</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="307" checked>
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="301">
<label>Moved Permanently <br><small>Status Code: 301</small></label>
</div>
</div>
</div>
<button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
<div class="ui green message" id="ruleAddSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rules Added
</div>
<br><br>
<div class="advancezone ui basic segment">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Options
</div>
<div class="content">
<p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
<button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="301">
<label>Moved Permanently <br><small>Status Code: 301</small></label>
</div>
</div>
</div>
<button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
<div class="ui green message" id="ruleAddSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rules Added
</div>
</div>
</div>
</div>
</div>
<script>
$(".advanceSettings").accordion();
/*
Redirection functions
*/
$(".checkbox").checkbox();
function resetForm() {
@ -129,6 +151,10 @@
}
}
function createAdvanceRules(){
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
}
function initRedirectionRuleList(){
$("#redirectionRuleList").html("");
$.get("/api/redirect/list", function(data){

View File

@ -1,7 +1,7 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Set Proxy Root</h2>
<p>For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
<p>The default routing point for all incoming traffics. For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
<div class="ui form">
<div class="field">
<label>Proxy Root</label>
@ -10,17 +10,86 @@
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="rootReqTLS" >
<label>Root require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
<input type="checkbox" id="rootReqTLS">
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
</div>
</div>
<div class="ui horizontal divider">OR</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
</div>
</div>
<br>
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
<div class="ui divider"></div>
<div class="field">
<h4>Root Routing Options</h4>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="unsetRedirect">
<label>Enable redirect for unset subdomains <br><small>Redirect subdomain that is not found to custom domain</small></label>
</div>
</div>
<div class="ui basic segment" id="unsetRedirectDomainWrapper" style="background-color: #f7f7f7; border-radius: 1em; margin-left: 2em; padding-left: 2em; display:none;">
<div style="
position: absolute;
top:0;
left: 1em;
width: 0px;
height: 0px;
margin-top: -10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #f7f7f7;">
</div>
<div class="field">
<label>Redirect target domain</label>
<div class="ui input">
<input id="unsetRedirectDomain" type="text" placeholder="http://example.com">
</div>
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)<br>
Leave empty for redirecting to upper level domain (e.g. notfound.example.com <i class="right arrow icon"></i> example.com)</small>
</div>
</div>
<br>
<button class="ui basic button" onclick="updateRootOptions()"><i class="blue save icon" ></i> Save Root Options</button>
</div>
<br>
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
</div>
</div>
<script>
function initRootInfo(){
$("#advanceRootSettings").accordion();
function handleUseStaticWebServerAsRoot(){
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
if (useStaticWebServer){
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
$("#proxyRoot").val(staticWebServerURL);
$("#proxyRoot").parent().addClass("disabled");
$("#rootReqTLS").parent().checkbox("set unchecked");
$("#rootReqTLS").parent().addClass("disabled");
//Check if web server is enabled. If not, ask if the user want to enable it
/*if (!$("#webserv_enable").parent().checkbox("is checked")){
confirmBox("Enable static web server now?", function(choice){
if (choice == true){
$("#webserv_enable").parent().checkbox("set checked");
}
});
}*/
}else{
$("#rootReqTLS").parent().removeClass("disabled");
$("#proxyRoot").parent().removeClass("disabled");
initRootInfo();
}
}
function initRootInfo(callback=undefined){
$.get("/api/proxy/list?type=root", function(data){
if (data == null){
@ -28,11 +97,91 @@
$("#proxyRoot").val(data.Domain);
checkRootRequireTLS(data.Domain);
}
if (callback != undefined){
callback();
}
});
}
initRootInfo();
initRootInfo(function(){
updateWebServerLinkSettings();
});
//Update the current web server port settings
function updateWebServerLinkSettings(){
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
if (isUsingWebServ){
$(".webservRootDisabled").addClass("disabled");
$("#useStaticWebServer").parent().checkbox("set checked");
}else{
$(".webservRootDisabled").removeClass("disabled");
$("#useStaticWebServer").parent().checkbox("set unchecked");
}
})
}
function isUsingStaticWebServerAsRoot(callback){
let currentProxyRoot = $("#proxyRoot").val().trim();
$.get("/api/webserv/status", function(webservStatus){
if (currentProxyRoot == "127.0.0.1:" + webservStatus.ListeningPort || currentProxyRoot == "localhost:" + webservStatus.ListeningPort){
return callback(true);
}
return callback(false);
});
}
function updateRootSettingStates(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#disableRootTLS").parent().removeClass('disabled').attr("title", "");
}else{
$("#disableRootTLS").parent().addClass('disabled').attr("title", "TLS listener is not enabled");
}
});
}
//Bind event to tab switch
tabSwitchEventBind["setroot"] = function(){
//On switch over to this page, update root info
updateRootSettingStates();
}
//Toggle the display status of the input box for domain setting
function updateRedirectionDomainSettingInputBox(useRedirect){
if(useRedirect){
$("#unsetRedirectDomainWrapper").stop().finish().slideDown("fast");
}else{
$("#unsetRedirectDomainWrapper").stop().finish().slideUp("fast");
}
}
function checkCustomRedirectForUnsetSubd(){
$.get("/api/proxy/root/listOptions", function(data){
$("#unsetRedirect")[0].checked = data.EnableRedirectForUnsetRules || false;
$("#unsetRedirectDomain").val(data.UnsetRuleRedirectTarget);
updateRedirectionDomainSettingInputBox(data.EnableRedirectForUnsetRules);
//Bind event to the checkbox
$("#unsetRedirect").off("change").on("change", function(){
let useRedirect = $("#unsetRedirect")[0].checked;
updateRedirectionDomainSettingInputBox(useRedirect);
});
});
}
checkCustomRedirectForUnsetSubd();
//Check if the given domain will redirect to https
function checkRootRequireTLS(targetDomain){
//Trim off the http or https from the origin
if (targetDomain.startsWith("http://")){
targetDomain = targetDomain.substring(7);
$("#proxyRoot").val(targetDomain);
}else if (targetDomain.startsWith("https://")){
targetDomain = targetDomain.substring(8);
$("#proxyRoot").val(targetDomain);
}
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
@ -44,11 +193,13 @@
}else if (data == "http"){
$("#rootReqTLS").parent().checkbox("set unchecked");
}
}
})
}
//Set the new proxy root option
function setProxyRoot(){
var newpr = $("#proxyRoot").val();
if (newpr.trim() == ""){
@ -66,14 +217,51 @@
data: {"type": "root", tls: rootReqTls, ep: newpr},
success: function(data){
if (data.error != undefined){
alert(data.error);
msgbox(data.error, false, 5000);
}else{
//OK
initRootInfo();
msgbox("Proxy Root Updated")
initRootInfo(function(){
//Check if WebServ is enabled
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
if (isUsingWebServ){
//Force enable static web server
//See webserv.html for details
setWebServerRunningState(true);
}
setTimeout(function(){
//Update the checkbox
updateWebServerLinkSettings();
msgbox("Proxy Root Updated");
}, 1000);
})
});
}
}
});
}
function updateRootOptions(){
$.ajax({
type: "POST",
url: "/api/proxy/root/updateOptions",
data: {
unsetRedirect: $("#unsetRedirect")[0].checked,
unsetRedirectTarget: $("#unsetRedirectDomain").val().trim(),
},
success: function(data) {
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Root Routing Options updated");
}
},
error: function(error) {
console.log("Error:", error);
}
});
}
</script>

View File

@ -8,7 +8,7 @@
<div class="field">
<label>Proxy Type</label>
<div class="ui selection dropdown">
<input type="hidden" id="ptype" value="subd">
<input type="hidden" id="ptype" value="subd" onchange="handleProxyTypeOptionChange(this.value)">
<i class="dropdown icon"></i>
<div class="default text">Proxy Type</div>
<div class="menu">
@ -22,7 +22,7 @@
<input type="text" id="rootname" placeholder="s1.mydomain.com">
</div>
<div class="field">
<label>IP Address or Domain Name with port</label>
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
<small>E.g. 192.168.0.101:8000 or example.com</small>
</div>
@ -44,7 +44,13 @@
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="bypassGlobalTLS">
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
</div>
</div>
<div class="field">
@ -123,6 +129,7 @@
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
if (type === "vdir") {
@ -162,6 +169,7 @@
tls: useTLS,
ep: proxyDomain,
tlsval: skipTLSValidation,
bypassGlobalTLS: bypassGlobalTLS,
bauth: requireBasicAuth,
cred: JSON.stringify(credentials),
},
@ -172,19 +180,48 @@
//OK
listVdirs();
listSubd();
msgbox("Proxy Endpoint Added");
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
credentials = [];
updateTable();
//Check if it is a new subdomain and TLS enabled
if (type == "subd" && $("#tls").checkbox("is checked")){
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
if (choice == true){
//Load the prefer CA from TLS page
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA.trim() == ""){
defaultCA = "Let's Encrypt";
}
//Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"...");
console.log("Trying to get a new certificate via ACME");
obtainCertificate(rootname, defaultCA.trim());
}else{
msgbox("Proxy Endpoint Added");
}
});
}else{
msgbox("Proxy Endpoint Added");
}
}
}
});
}
function handleProxyTypeOptionChange(newType){
if (newType == "subd"){
$("#bypassGlobalTLS").parent().removeClass("disabled");
}else if (newType == "vdir"){
$("#bypassGlobalTLS").parent().addClass("disabled");
}
}
//Generic functions for delete rp endpoints
function deleteEndpoint(ptype, epoint){
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
@ -310,7 +347,7 @@
var columns = row.find('td[data-label]');
var payload = $(row).attr("payload");
payload = JSON.parse(decodeURIComponent(payload));
console.log(payload);
//console.log(payload);
columns.each(function(index) {
var column = $(this);
@ -326,34 +363,37 @@
var datatype = $(this).attr("datatype");
if (datatype == "domain"){
let domain = payload.Domain;
//Target require TLS for proxying
let tls = payload.RequireTLS;
if (tls){
tls = "checked";
}else{
tls = "";
}
//Require TLS validation
let skipTLSValidation = payload.SkipCertValidations;
let checkstate = "";
if (skipTLSValidation){
checkstate = "checked";
}
input = `
<div class="ui mini fluid input">
<input type="text" class="Domain" value="${domain}">
</div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireTLS" ${tls}>
<label>Require TLS</label>
<label>Require TLS<br>
<small>Proxy target require HTTPS connection</small></label>
</div><br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
<label>Skip Verification<br>
<small>Check this if proxy target is using self signed certificates</small></label>
</div>
`;
column.empty().append(input);
}else if (datatype == "skipver"){
let skipTLSValidation = payload.SkipCertValidations;
let checkstate = "";
if (skipTLSValidation){
checkstate = "checked";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
<label>Skip Verification</label>
<small>Check this if you are using self signed certificates</small>
</div>`);
}else if (datatype == "basicauth"){
let requireBasicAuth = payload.RequireBasicAuth;
let checkstate = "";
@ -363,13 +403,24 @@
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label>
</div> <button class="ui basic tiny button" style="margin-left: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Credentials</button>`);
</div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Settings</button>`);
}else if (datatype == 'action'){
column.empty().append(`
<button title="Cancel" onclick="exitProxyInlineEdit('${endpointType}');" class="ui basic small circular icon button"><i class="ui remove icon"></i></button>
<button title="Save" onclick="saveProxyInlineEdit('${uuid}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
`);
}else if (datatype == "inbound" && payload.ProxyType == 0){
let originalContent = $(column).html();
column.empty().append(`${originalContent}
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
`);
}else{
//Unknown field. Leave it untouched
}
@ -401,6 +452,7 @@
let requireTLS = $(row).find(".RequireTLS")[0].checked;
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
@ -411,6 +463,7 @@
"type": epttype,
"rootname": uuid,
"ep":newDomain,
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bauth" :requireBasicAuth,
@ -437,4 +490,67 @@
}));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
/*
Obtain Certificate via ACME
*/
//Load the ACME email from server side
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
});
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
let rootDomain = domains.split(".").pop();
email = "admin@" + rootDomain;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
return;
}
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
// Renew the parent certificate list
initManagedDomainCertificateList();
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
}
</script>

View File

@ -72,11 +72,35 @@
<label>Use TLS to serve proxy request</label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly checkbox" style="margin-top: 0.6em;">
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS<br>
<small>(Only apply when listening port is not 80)</small></label>
<label>Enable HTTP server on port 80<br>
<small>(Only apply when TLS enabled and not using port 80)</small></label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p>If you have no idea what are these, you can leave them as default :)</p>
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
</div>
<br>
</div>
</div>
</div>
<br><br>
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
@ -128,6 +152,8 @@
</div>
<script>
let loopbackProxiedInterface = false;
$(".advanceSettings").accordion();
//Initial the start stop button if this is reverse proxied
$.get("/api/proxy/requestIsProxied", function(data){
if (data == true){
@ -160,6 +186,7 @@
$("#serverstatus").removeClass("green");
}
$("#incomingPort").val(data.Option.Port);
});
}
@ -284,6 +311,27 @@
});
}
function handleP80ListenerStateChange(enabled){
$.ajax({
url: "/api/proxy/listenPort80",
data: {"enable": enabled},
success: function(data){
if (data.error != undefined){
console.log(data.error);
return;
}
if (enabled){
$("#redirect").show();
msgbox("Port 80 listener enabled");
}else{
$("#redirect").hide();
msgbox("Port 80 listener disabled");
}
}
});
}
function handlePortChange(){
var newPortValue = $("#incomingPort").val();
@ -302,6 +350,25 @@
});
}
function initPort80ListenerSetting(){
$.get("/api/proxy/listenPort80", function(data){
if (data){
$("#listenP80").checkbox("set checked");
$("#redirect").show();
}else{
$("#listenP80").checkbox("set unchecked");
$("#redirect").hide();
}
$("#listenP80").find("input").on("change", function(){
let enabled = $(this)[0].checked;
handleP80ListenerStateChange(enabled);
})
});
}
initPort80ListenerSetting();
function initHTTPtoHTTPSRedirectSetting(){
$.get("/api/proxy/useHttpsRedirect", function(data){
if (data == true){
@ -335,26 +402,53 @@
})
});
});
}
initHTTPtoHTTPSRedirectSetting();
function initTlsVersionSetting(){
$.get("/api/cert/tlsRequireLatest", function(data){
if (data == true){
$("#tlsMinVer").checkbox("set checked");
}else{
$("#tlsMinVer").checkbox("set unchecked");
}
//Bind events to the checkbox
$("#tlsMinVer").find("input").on("change", function(){
let thisValue = $("#tlsMinVer").checkbox("is checked");
$.ajax({
url: "/api/cert/tlsRequireLatest",
data: {"set": thisValue},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("TLS Version Setting Updated");
}
}
})
});
});
}
initTlsVersionSetting();
function initTlsSetting(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#tls").checkbox("set checked");
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
//Initiate the input listener on the checkbox
$("#tls").find("input").on("change", function(){
let thisValue = $("#tls").checkbox("is checked");
if (thisValue){
$("#redirect").removeClass('disabled');
$(".tlsEnabledOnly").removeClass('disabled');
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
$.ajax({
url: "/api/cert/tls",
@ -364,7 +458,27 @@
alert(data.error);
}else{
//Updated
msgbox("Setting Updated");
//Check for case if the port is invalid default ports
if ($("#incomingPort").val() == "80" && thisValue == true){
confirmBox("Change listen port to :443?", function(choice){
if (choice == true){
$("#incomingPort").val("443");
handlePortChange();
}
});
}else if ($("#incomingPort").val() == "443" && thisValue == false){
confirmBox("Change listen port to :80?", function(choice){
if (choice == true){
$("#incomingPort").val("80");
handlePortChange();
}
});
}else{
msgbox("Setting Updated");
}
initRPStaste();
}
}

View File

@ -9,7 +9,6 @@
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -41,12 +40,14 @@
let subdData = encodeURIComponent(JSON.stringify(subd));
if (subd.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="false"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

View File

@ -42,11 +42,75 @@
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode Instructions</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment">
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
@ -67,72 +131,7 @@
</table>
</div>
</div>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID

View File

@ -86,7 +86,7 @@
let id = value[0].ID;
let name = value[0].Name;
let url = value[0].URL;
let url = value[value.length - 1].URL;
let protocol = value[0].Protocol;
//Generate the status dot
@ -112,6 +112,9 @@
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
//Special type of error, cause by downstream reverse proxy
dotType = "error";
}else if (thisStatus.StatusCode == 401){
//Unauthorized error
dotType = "error";
}else{
dotType = "offline";
}
@ -141,6 +144,28 @@
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
}else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){
switch(value[value.length - 1].StatusCode){
case 400:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`;
break;
case 401:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Unauthorized`;
break;
case 403:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Forbidden`;
break;
case 404:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Not Found`;
break;
case 405:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`;
break;
}
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;

View File

@ -116,7 +116,11 @@
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div>
<!-- Config Tools -->
<div class="ui divider"></div>
<h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<!-- System Information -->
<div class="ui divider"></div>
<div id="zoraxyinfo">

View File

@ -9,7 +9,6 @@
<tr>
<th>Virtual Directory</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -43,11 +42,21 @@
let vdirData = encodeURIComponent(JSON.stringify(vdir));
if (vdir.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (vdir.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
let tlsVerificationField = "";
if (vdir.RequireTLS){
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
}else{
tlsVerificationField = "N/A"
}
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

View File

@ -0,0 +1,229 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Static Web Server</h2>
<p>A simple static web server that serve html css and js files</p>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h4 class="ui header" id="webservRunningState">
<i class="green circle icon"></i>
<div class="content">
<span class="webserv_status">Running</span>
<div class="sub header">Listen port :<span class="webserv_port">8081</span></div>
</div>
</h4>
</div>
<h3>Web Server Settings</h3>
<div class="ui form">
<div class="inline field">
<div class="ui toggle checkbox webservRootDisabled">
<input id="webserv_enable" type="checkbox" class="hidden">
<label>Enable Static Web Server</label>
</div>
</div>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableDirList" type="checkbox" class="hidden">
<label>Enable Directory Listing</label>
<small>If this folder do not contains any index files, list the directory of this folder.</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
<small>
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
See the -webserv flag for more details.
</small>
</div>
<div class="field webservRootDisabled">
<label>Port Number</label>
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
</div>
</div>
<small><i class="ui blue save icon"></i> Changes are saved automatically</small>
<br>
<div class="ui message">
<div class="ui accordion webservhelp">
<div class="title">
<i class="dropdown icon"></i>
How to access the static web server?
</div>
<div class="content">
There are three ways to access the static web server. <br>
<div class="ui ordered list">
<div class="item">
If you are using Zoraxy as your gateway reverse proxy server,
you can add a new subdomain proxy rule that points to
<a>http://127.0.0.1:<span class="webserv_port">8081</span></a>
</div>
<div class="item">
If you are using Zoraxy under another reverse proxy server,
add <a>http://127.0.0.1:<span class="webserv_port">8081</span></a> to the config of your upper layer reverse proxy server's config file.
</div>
<div class="item">
Directly access the web server via <a>http://{zoraxy_host_ip}:<span class="webserv_port">8081</span></a> (Not recommended)
</div>
<br>
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h2>Web Directory Manager</h2>
<p>Manage your files inside your web directory</p>
</div>
<div class="ui basic segment" style="display:none;" id="webdirManDisabledNotice">
<h4 class="ui header">
<i class="ui red times icon"></i>
<div class="content">
Web Directory Manager Disabled
<div class="sub header">Web Directory Manager has been disabled by the system administrator</div>
</div>
</h4>
</div>
<iframe id="webserv_dirManager" src="tools/fs.html" style="width: 100%; height: 800px; border: 0px; overflow-y: hidden;">
</iframe>
<small>If you do not want to enable web access to your web directory, you can disable this feature with <code>-webfm=false</code> startup paramter</small>
<script>
$(".webservhelp").accordion();
$(".ui.checkbox").checkbox();
function setWebServerRunningState(running){
if (running){
$("#webserv_enable").parent().checkbox("set checked");
$("#webservRunningState").find("i").attr("class", "green circle icon");
$("#webservRunningState").find(".webserv_status").text("Running");
}else{
$("#webserv_enable").parent().checkbox("set unchecked");
$("#webservRunningState").find("i").attr("class", "red circle icon");
$("#webservRunningState").find(".webserv_status").text("Stopped");
}
}
function updateWebServState(){
$.get("/api/webserv/status", function(data){
//Clear all event listeners
$("#webserv_enableDirList").off("change");
$("#webserv_enable").off("change");
$("#webserv_listenPort").off("change");
setWebServerRunningState(data.Running);
if (data.EnableDirectoryListing){
$("#webserv_enableDirList").parent().checkbox("set checked");
}else{
$("#webserv_enableDirList").parent().checkbox("set unchecked");
}
$("#webserv_docRoot").val(data.WebRoot + "/html/");
if (!data.EnableWebDirManager){
$("#webdirManDisabledNotice").show();
$("#webserv_dirManager").remove();
}
$("#webserv_listenPort").val(data.ListeningPort);
updateWebServLinkExample(data.ListeningPort);
//Bind checkbox events
$("#webserv_enable").off("change").on("change", function(){
let enable = $(this)[0].checked;
if (enable){
$.get("/api/webserv/start", function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Static web server started");
setWebServerRunningState(true);
}
});
}else{
$.get("/api/webserv/stop", function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Static web server stopped");
setWebServerRunningState(false);
}
});
}
});
$("#webserv_enableDirList").off("change").on("change", function(){
let enable = $(this)[0].checked;
$.ajax({
url: "/api/webserv/setDirList",
method: "POST",
data: {"enable": enable},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Directory listing setting updated");
}
}
})
});
$("#webserv_listenPort").off("change").on("change", function(){
let newPort = $(this).val();
//Check if the new value is same as listening port
let rpListeningPort = $("#incomingPort").val();
if (rpListeningPort == newPort){
confirmBox("This setting might cause port conflict. Continue Anyway?", function(choice){
if (choice == true){
//Continue anyway
$.ajax({
url: "/api/webserv/setPort",
method: "POST",
data: {"port": newPort},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Listening port updated");
}
updateWebServState();
}
});
}else{
//Cancel. Restore to previous value
updateWebServState();
msgbox("Setting restored");
}
});
}else{
$.ajax({
url: "/api/webserv/setPort",
method: "POST",
data: {"port": newPort},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Listening port updated");
}
}
})
}
});
})
}
updateWebServState();
function updateWebServLinkExample(newport){
$(".webserv_port").text(newport);
}
</script>
</div>

View File

@ -0,0 +1,10 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Service Expose Proxy</h2>
<p>Expose your local test-site on the internet with single command</p>
</div>
<div class="ui message">
<h4>Work In Progress</h4>
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
</div>
</div>

View File

@ -39,7 +39,7 @@
<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>
This might be caused 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>
@ -52,4 +52,4 @@
$("#requrl").text(window.location.href);
</script>
</body>
</html>
</html>

View File

@ -154,4 +154,4 @@
$("#host").text(location.href);
</script>
</body>
</html>
</html>

View File

@ -50,7 +50,7 @@
</a>
<div class="ui divider menudivider">Access & Connections</div>
<a class="item" tag="cert">
<i class="simplistic lock icon"></i> TLS / SSL certificate
<i class="simplistic lock icon"></i> TLS / SSL certificates
</a>
<a class="item" tag="redirectset">
<i class="simplistic level up alternate icon"></i> Redirection
@ -62,13 +62,16 @@
<a class="item" tag="gan">
<i class="simplistic globe icon"></i> Global Area Network
</a>
<a class="item" tag="">
<a class="item" tag="zgrok">
<i class="simplistic podcast icon"></i> Service Expose Proxy
</a>
<a class="item" tag="tcpprox">
<i class="simplistic exchange icon"></i> TCP Proxy
</a>
<div class="ui divider menudivider">Others</div>
<a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server
</a>
<a class="item" tag="utm">
<i class="simplistic time icon"></i> Uptime Monitor
</a>
@ -114,9 +117,15 @@
<!-- Global Area Networking -->
<div id="gan" class="functiontab" target="gan.html"></div>
<!-- Service Expose Proxy -->
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
<!-- TCP Proxy -->
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
<!-- Web Server -->
<div id="webserv" class="functiontab" target="webserv.html"></div>
<!-- Up Time Monitor -->
<div id="utm" class="functiontab" target="uptime.html"></div>
@ -141,13 +150,23 @@
<br><br>
<div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%">
<p>CopyRight Zoraxy project and its author, 2022 - <span class="year"></span></p>
<p>CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
</div>
<div id="messageBox" class="ui green floating big compact message">
<p><i class="green check circle icon"></i> There are no message</p>
<p><i class="green check circle icon"></i> There are no messages</p>
</div>
<div id="confirmBox" style="display:none;">
<div class="ui top attached progress">
<div class="bar" style="width: 100%; min-width: 0px;"></div>
</div>
<div class="confirmBoxBody">
<button class="ui red basic mini circular icon right floated button" style="margin-left: 0.4em;"><i class="ui times icon"></i></button>
<button class="ui green basic mini circular icon right floated button"><i class="ui check icon"></i></button>
<div class="questionToConfirm">Confirm Exit?</div>
</div>
</div>
<br><br>
<script>
$(".year").text(new Date().getFullYear());
@ -280,6 +299,64 @@
$("#messageBox").stop().finish().fadeIn("fast").delay(delayDuration).fadeOut("fast");
}
function confirmBox(question_to_confirm, callback, delaytime=15) {
var progressBar = $("#confirmBox .bar");
var questionElement = $("#confirmBox .questionToConfirm");
//Just to make sure there are no animation runnings
progressBar.stop();
// Update the question to confirm
questionElement.text(question_to_confirm);
// Start the progress bar animation
progressBar.css("width", "100%");
progressBar.animate({ width: "0%", easing: "linear" }, delaytime * 1000, function() {
// Animation complete, invoke the callback with undefined
callback(undefined);
//Unset the event listener
$("#confirmBox .ui.green.button").off("click");
// Hide the confirm box
$("#confirmBox").hide();
});
// Bind click event to "Yes" button
$("#confirmBox .ui.green.button").on("click", function() {
// Stop the progress bar animation
progressBar.stop();
// Invoke the callback with true
callback(true);
// Hide the confirm box
$("#confirmBox").hide();
//Unset the event listener
$("#confirmBox .ui.green.button").off("click");
});
// Bind click event to "No" button
$("#confirmBox .ui.red.button").on("click", function() {
// Stop the progress bar animation
progressBar.stop();
// Invoke the callback with false
callback(false);
// Hide the confirm box
$("#confirmBox").hide();
//Unset the event listener
$("#confirmBox .ui.red.button").off("click");
});
// Show the confirm box
$("#confirmBox").show().transition('jiggle');
}
/*
Toggles for side wrapper
*/
@ -295,6 +372,7 @@
$(".sideWrapper").show();
$(".sideWrapper .fadingBackground").fadeIn("fast");
$(".sideWrapper .content").transition('slide left in', 300);
$("body").css("overflow", "hidden");
}
function hideSideWrapper(discardFrameContent = false){
@ -309,7 +387,8 @@
$(".sideWrapper").hide();
});
});
$("body").css("overflow", "auto");
}
</script>
</body>
</html>
</html>

View File

@ -6,6 +6,7 @@
--theme_lgrey: #f6f6f6;
--theme_green: #3c9c63;
--theme_fcolor: #979797;
--theme_advance: #f8f8f9;
}
body{
background-color:#f6f6f6;
@ -16,6 +17,15 @@ body{
display:none;
}
.advance{
background: var(--theme_advance) !important;
}
.advancezone{
background: var(--theme_advance) !important;
border-radius: 1em !important;
}
.menubar{
width: 100%;
padding: 0.4em;
@ -89,6 +99,34 @@ body{
z-index: 999;
}
/* Confirm Box */
#confirmBox{
position: fixed;
z-index: 999;
bottom: 1em;
right: 1em;
min-width: 300px;
background-color: #ffffff;
color: rgb(65, 65, 65);
box-shadow: 10px 10px 5px -2px rgba(0,0,0,0.13);
}
#confirmBox .confirmBoxBody{
padding: 1em;
}
#confirmBox .ui.progress .bar{
background: #a9d1f3 !important;
}
#confirmBox .confirmBoxBody .button{
margin-top: -0.4em;
}
#confirmBox .questionToConfirm{
margin-top: -0.2em;
}
/* Standard containers */
.standardContainer{
position: relative;
@ -141,7 +179,7 @@ body{
}
.sideWrapper iframe{
height: 100%;
height: calc(100% - 55px);
width: 100%;
border: 0px solid transparent;
}
@ -552,4 +590,18 @@ body{
.GANetMember.unauthorized{
border-left: 6px solid #9c3c3c !important;
}
}
/*
Network Utilities
*/
#traceroute_results{
resize: none;
background-color: #202020;
color: white;
}
#traceroute_results::selection {
background: #a9d1f3;
}

View File

@ -119,7 +119,7 @@
<h2 class="diagramHeader">Host</h2>
<p style="font-weight: 500; color: #bd2426;">Error</p>
</div>
</div>
</div>
</div>
</div>
<div>
@ -140,7 +140,7 @@
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
</div>
</div>
</div>
</div>
</div>
<br>
</div>
@ -155,4 +155,4 @@
$("#host").text(location.href);
</script>
</body>
</html>
</html>

515
src/web/snippet/acme.html Normal file
View File

@ -0,0 +1,515 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<style>
.disabled.table{
opacity: 0.5;
pointer-events: none;
}
.expiredDomain{
color: rgb(238, 31, 31);
}
.validDomain{
color: rgb(49, 192, 113);
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Certificates Auto Renew Settings
<div class="sub header">Fetch and renew your certificates with Automated Certificate Management Environment (ACME) protocol</div>
</div>
</div>
<div class="ui basic segment">
<p style="float: right; color: #21ba45; display:none;" id="enableToggleSucc"><i class="green checkmark icon"></i> Setting Updated</p>
<div class="ui toggle checkbox">
<input type="checkbox" id="enableCertAutoRenew">
<label>Enable Certificate Auto Renew</label>
</div>
<br>
<h3>ACME Email</h3>
<p>Email is required by many CAs for renewing via ACME protocol</p>
<div class="ui fluid action input">
<input id="caRegisterEmail" type="text" placeholder="webmaster@example.com">
<button class="ui icon basic button" onclick="saveEmailToConfig(this);">
<i class="blue save icon"></i>
</button>
</div>
<small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Renew Policy
</div>
<div class="content">
<p>Renew all certificates with ACME supported CAs</p>
<div class="ui toggle checkbox">
<input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);">
<label>Renew All Certs</label>
</div><br>
<button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
<div class="ui horizontal divider"> OR </div>
<p>Select the certificates to automatic renew in the list below</p>
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
<thead>
<tr>
<th>Domain Name</th>
<th>Match Rule</th>
<th>Auto-Renew</th>
</tr>
</thead>
<tbody id="domainTableBody"></tbody>
</table>
<small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
<div class="ui yellow message">
Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
</div>
<button class="ui basic right floated button" onclick="saveAutoRenewPolicy();"><i class="blue save icon"></i> Save Changes</button>
<button id="renewSelectedButton" onclick="renewNow();" class="ui basic right floated disabled button"><i class="yellow refresh icon"></i> Renew Selected</button>
<br><br>
</div>
</div>
</div>
<div class="ui divider"></div>
<h3>Generate New Certificate</h3>
<p>Enter a new / existing domain(s) to request new certificate(s)</p>
<div class="ui form">
<div class="field">
<label>Domain(s)</label>
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<label>Matching Rule</label>
<input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
<small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
</div>
<div class="field">
<label>Certificate Authority (CA)</label>
<div class="ui selection dropdown" id="ca">
<input type="hidden" name="ca">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div>
</div>
</div>
<div class="field" id="caInput" style="display:none;">
<label>ACME Server URL</label>
<input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
</div>
<div class="field" id="skipTLS" style="display:none;">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSCheckbox">
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Get Certificate</button>
</div>
<div class="ui divider"></div>
<small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
<br><br><br><br>
</div>
<script>
let expiredDomains = [];
let enableTrigerOnChangeEvent = true;
$(".accordion").accordion();
$(".dropdown").dropdown();
$(".checkbox").checkbox();
function setAutoRenewIfCASupportMode(useAutoMode = true){
if (useAutoMode){
$("#domainCertFileTable").addClass("disabled");
$("#renewNowBtn").removeClass("disabled");
$("#renewSelectedButton").addClass("disabled");
}else{
$("#domainCertFileTable").removeClass("disabled");
$("#renewNowBtn").addClass("disabled");
$("#renewSelectedButton").removeClass("disabled");
}
}
function initRenewerConfigFromFile(){
//Set the renew switch state
$.get("/api/acme/autoRenew/enable", function(data){
if (data == true){
$("#enableCertAutoRenew").parent().checkbox("set checked");
}
$("#enableCertAutoRenew").on("change", function(){
if (!enableTrigerOnChangeEvent){
return;
}
toggleAutoRenew();
})
});
//Load the email from server side
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
$("#caRegisterEmail").val(data);
}
});
//Load the domain selection options
$.get("/api/acme/autoRenew/renewPolicy", function(data){
if (data == true){
$("#renewAllSupported").parent().checkbox("set checked");
}else{
$("#renewAllSupported").parent().checkbox("set unchecked");
}
});
}
initRenewerConfigFromFile();
function saveEmailToConfig(btn){
$.ajax({
url: "/api/acme/autoRenew/email",
data: {set: $("#caRegisterEmail").val()},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
parent.msgbox("Email updated");
$(btn).html(`<i class="green check icon"></i>`);
$(btn).addClass("disabled");
setTimeout(function(){
$(btn).html(`<i class="blue save icon"></i>`);
$(btn).removeClass("disabled");
}, 3000);
}
}
});
}
function toggleAutoRenew(){
var enabled = $("#enableCertAutoRenew").parent().checkbox("is checked");
$.post("/api/acme/autoRenew/enable?enable=" + enabled, function(data){
if (data.error){
parent.msgbox(data.error, false, 5000);
if (enabled){
enableTrigerOnChangeEvent = false;
$("#enableCertAutoRenew").parent().checkbox("set unchecked");
enableTrigerOnChangeEvent = true;
}
}else{
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
}
});
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
}
//Render the domains table that exists in this zoraxy host
function renderDomainTable(domainFileList) {
// Get the table body element
var tableBody = $('#domainTableBody');
// Clear the table body
tableBody.empty();
// Iterate over the domain names
var counter = 0;
for (const [srcfile, domains] of Object.entries(domainFileList)) {
// Create a table row
var row = $('<tr>');
// Create the domain name cell
var domainClass = "validDomain";
for (var i = 0; i < domains.length; i++){
let thisDomain = domains[i];
if (expiredDomains.includes(thisDomain)){
domainClass = "expiredDomain";
}
}
var domainCell = $('<td class="' + domainClass +'">').html(domains.join("<br>"));
row.append(domainCell);
var srcFileCell = $('<td>').text(srcfile);
row.append(srcFileCell);
// Create the auto-renew checkbox cell
let domainsEncoded = encodeURIComponent(JSON.stringify(domains));
var checkboxCell = $(`<td domain="${domainsEncoded}" srcfile="${srcfile}">`);
var checkbox = $(`<input name="${srcfile}">`).attr('type', 'checkbox');
checkboxCell.append(checkbox);
row.append(checkboxCell);
// Add the row to the table body
tableBody.append(row);
counter++;
}
if (Object.keys(domainFileList).length == 0){
//No certificate in this system
tableBody.append(`<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No certificate in use</td>
</tr>`);
}
}
//Initiate domain table. If you needs to update the expired domain as well
//call from initDomainFileList() instead
function initDomainTable(){
$.get("/api/cert/listdomains?compact=true", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
renderDomainTable(data);
}
initAutoRenewPolicy();
})
}
function initDomainFileList() {
$.ajax({
url: "/api/acme/listExpiredDomains",
method: "GET",
success: function(response) {
// Render domain table
expiredDomains = response.domain;
initDomainTable();
//renderDomainTable(response.domain);
},
error: function(error) {
console.log("Failed to fetch expired domains:", error);
}
});
}
initDomainFileList();
// Button click event handler for obtaining certificate
$("#obtainButton").click(function() {
$("#obtainButton").addClass("loading").addClass("disabled");
obtainCertificate();
});
$("input[name=ca]").on('change', function() {
if(this.value == "Custom ACME Server") {
$("#caInput").show();
$("#skipTLS").show();
} else {
$("#caInput").hide();
$("#skipTLS").hide();
}
})
// Obtain certificate from API
function obtainCertificate() {
var domains = $("#domainsInput").val();
var filename = $("#filenameInput").val();
var email = $("#caRegisterEmail").val();
if (email == ""){
parent.msgbox("ACME renew email is not set", false)
$("#obtainButton").removeClass("loading").removeClass("disabled");
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else if (filename == "" && domains.includes(",")){
parent.msgbox("Filename cannot be empty for certs containing multiple domains.", false, 5000);
$("#obtainButton").removeClass("loading").removeClass("disabled");
return;
}
var ca = $("#ca").dropdown("get value");
var caURL = "";
if (ca == "Custom ACME Server") {
ca = "custom";
caURL = $("#caURL").val();
}
var skipTLSValue = $("#skipTLSCheckbox")[0].checked;
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: ca,
caURL: caURL,
skipTLS: skipTLSValue,
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
if (response.error) {
console.log("Error:", response.error);
// Show error message
parent.msgbox(response.error, false, 12000);
} else {
console.log("Certificate renewed successfully");
// Show success message
parent.msgbox("Certificate renewed successfully");
// Renew the parent certificate list
parent.initManagedDomainCertificateList();
}
},
error: function(error) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
console.log("Failed to renewed certificate:", error);
}
});
}
function checkIfInputDomainIsMultiple(){
var inputDomains = $("#domainsInput").val();
if (inputDomains.includes(",")){
$(".multiDomainOnly").show();
}else{
$(".multiDomainOnly").hide();
}
}
//Grab the longest common suffix of all domains
//not that smart technically
function autoDetectMatchingRules(){
var domainsString = $("#domainsInput").val();
if (!domainsString.includes(",")){
return domainsString;
}
let domains = domainsString.split(",");
//Clean out any spacing between commas
for (var i = 0; i < domains.length; i++){
domains[i] = domains[i].trim();
}
function getLongestCommonSuffix(strings) {
if (strings.length === 0) {
return ''; // Return an empty string if the array is empty
}
var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
var firstString = sortedStrings[0];
var lastString = sortedStrings[sortedStrings.length - 1];
var suffix = '';
var minLength = Math.min(firstString.length, lastString.length);
for (var i = 0; i < minLength; i++) {
if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
break; // Stop iterating if characters don't match
}
suffix = firstString[firstString.length - 1 - i] + suffix;
}
return suffix;
}
let longestSuffix = getLongestCommonSuffix(domains);
//Check if the suffix is a valid domain
if (longestSuffix.substr(0,1) == "."){
//Trim off the first dot
longestSuffix = longestSuffix.substr(1);
}
if (!longestSuffix.includes(".")){
parent.msgbox("Auto Detect failed: Multiple Domains", false, 5000);
return;
}
$("#filenameInput").val(longestSuffix);
}
//Handle the renew now btn click
function renewNow(){
$.get("/api/acme/autoRenew/renewNow", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 6000);
}else{
parent.msgbox(data)
}
})
}
function initAutoRenewPolicy(){
$.get("/api/acme/autoRenew/listDomains", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false)
}else{
if (data[0] == "*"){
//Auto select and renew is enabled
$("#renewAllSupported").parent().checkbox("set checked");
}else{
//This is a list of domain files
data.forEach(function(name) {
$('#domainTableBody input[type="checkbox"][name="' + name + '"]').prop('checked', true);
});
$("#domainCertFileTable").removeClass("disabled");
$("#renewNowBtn").addClass("disabled");
$("#renewSelectedButton").removeClass("disabled");
}
}
})
}
function saveAutoRenewPolicy(){
let autoRenewAll = $("#renewAllSupported").parent().checkbox("is checked");
if (autoRenewAll == true){
$.ajax({
url: "/api/acme/autoRenew/setDomains",
data: {opr: "setAuto"},
success: function(data){
parent.msgbox("Renew policy rule updated")
}
});
}else{
let checkedNames = [];
$('#domainTableBody input[type="checkbox"]:checked').each(function() {
checkedNames.push($(this).attr('name'));
});
$.ajax({
url: "/api/acme/autoRenew/setDomains",
data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
success: function(data){
parent.msgbox("Renew policy rule updated")
}
});
}
}
//Clear up the input field when page load
$("#filenameInput").val("");
</script>
</body>
</html>

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<!-- Path Rules -->
<div class="ui header">
<div class="content">
Special Path Rules
<div class="sub header">Advanced customization for response on particular matching path or URL</div>
</div>
</div>
<h4>Current list of special path rules.</h4>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Matching Path</th>
<th>Status Code</th>
<th class="no-sort">Exact Match</th>
<th class="no-sort">Case Sensitive</th>
<th class="no-sort">Enabled</th>
<th class="no-sort">Actions</th>
</tr>
</thead>
<tbody id="specialPathRules">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui divider"></div>
<h4>Add Special Path Rule</h4>
<div class="ui form">
<div class="field">
<label>Matching URI</label>
<input type="text" name="matchingPath" placeholder="Matching URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="exactMatch" tabindex="0" class="hidden">
<label>Require Exact Match</label>
</div>
<div class="ui message">
<p>Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of <b>example.com/secret/</b> and set it to return 401</p>
<i class="check square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> (content of image.png)<br>
<i class="square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> HTTP 401
</div>
</div>
<div class="field">
<label>Response Status Code</label>
<input type="text"name="statusCode" placeholder="500">
<small><i class="ui circle info icon"></i> HTTP Status Code to be served by this rule</small>
</div>
</div>
<br><br>
<button class="ui basic button iframeOnly" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
</div>
<script>
</script>
</body>
</html>

View File

@ -11,10 +11,12 @@
<div class="ui container">
<div class="ui header">
<div class="content">
Basic Auth Credential
Basic Auth Settings
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<h3 class="ui header">Basic Auth Credential</h3>
<div class="scrolling content ui form">
<div id="inlineEditBasicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
@ -40,15 +42,54 @@
</div>
<div class="field" >
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="blue add icon"></i> Add Credential</button>
<button class="ui basic button" style="float: right;" onclick="saveCredentials();"><i class="green save icon"></i> Save Credential</button>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="saveCredentials();"><i class="green save icon"></i> Save</button>
<button class="ui basic button" style="float: right;" onclick="cancelCredentialEdit();"><i class="remove icon"></i> Cancel</button>
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<h3 class="ui header">Authentication Exclusion Paths</h3>
<div class="scrolling content ui form">
<p>Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.</p>
<table class="ui very basic compacted unstackable celled table">
<thead>
<tr>
<th>Path Prefix</th>
<th>Remove</th>
</tr></thead>
<tbody id="exclusionPaths">
<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
</tr>
</tbody>
</table>
<div class="field">
<input id="newExclusionPath" type="text" placeholder="/public/api/" autocomplete="off">
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
</div>
<div class="field" >
<button class="ui basic button" onclick="addExceptionPath();"><i class="blue add icon"></i> Add Exception</button>
</div>
<div class="field">
<div class="ui basic message">
<h4>How to use set excluded paths?</h4>
<p>All request URI that contains the given prefix will be allowed to bypass authentication and <b>the prefix must start with a slash.</b> For example, given the following prefix.<br>
<code>/public/res/</code><br>
<br>
Zoraxy will allow authentication bypass of any subdirectories or resources under the /public/res/ directory. For example, the following paths access will be able to bypass basic auth mechanism under this setting.<br>
<code>/public/res/photo.png</code><br>
<code>/public/res/far/boo/</code></p>
</div>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
</div>
<script>
let editingCredentials = [];
@ -124,6 +165,80 @@
updateEditingCredentialList();
}
function addExceptionPath(){
// Retrieve the username and password input values
var newExclusionPathMatchingPrefix = $('#newExclusionPath').val().trim();
if (newExclusionPathMatchingPrefix == ""){
parent.msgbox("Matching prefix cannot be empty!", false, 5000);
return;
}
$.ajax({
url: "/api/proxy/auth/exceptions/add",
data:{
ptype: editingEndpoint.ept,
ep: editingEndpoint.ep,
prefix: newExclusionPathMatchingPrefix
},
method: "POST",
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
initExceptionPaths();
parent.msgbox("New exception path added", true);
$('#newExclusionPath').val("");
}
}
});
}
function removeExceptionPath(object){
let matchingPrefix = $(object).attr("prefix");
$.ajax({
url: "/api/proxy/auth/exceptions/delete",
data:{
ptype: editingEndpoint.ept,
ep: editingEndpoint.ep,
prefix: matchingPrefix
},
method: "POST",
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
initExceptionPaths();
parent.msgbox("Exception path removed", true);
}
}
});
}
//Load exception paths from server
function initExceptionPaths(){
$.get(`/api/proxy/auth/exceptions/list?ptype=${editingEndpoint.ept}&ep=${editingEndpoint.ep}`, function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
if (data.length == 0){
$("#exclusionPaths").html(` <tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
</tr>`);
}else{
$("#exclusionPaths").html("");
data.forEach(function(rule){
$("#exclusionPaths").append(` <tr>
<td>${rule.PathPrefix}</td>
<td><button class="ui red basic mini icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
</tr>`);
})
}
}
});
}
initExceptionPaths();
function updateEditingCredentialList() {
var tableBody = $('#inlineEditBasicAuthCredentialTable');
tableBody.empty();
@ -168,7 +283,7 @@
return isExists;
}
function cancelCredentialEdit(){
function closeThisWrapper(){
parent.hideSideWrapper(true);
}
@ -186,7 +301,7 @@
parent.msgbox(data.error, false, 6000);
}else{
parent.msgbox("Credentials Updated");
parent.hideSideWrapper(true);
//parent.hideSideWrapper(true);
}
}
})

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Config Export and Import Tool
<div class="sub header">Painless migration with one click</div>
</div>
</div>
<h3>Backup Current Configs</h3>
<p>This will download all your configuration on zoraxy in a zip file. This includes all the proxy configs and certificates. Please keep it somewhere safe and after migration, delete this if possible.</p>
<div class="ui form">
<div class="grouped fields">
<label>Backup Mode</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="rules" name="backupmode" checked="checked">
<label>Proxy Settings, Redirect Rules and Certificates Only</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="full" name="backupmode">
<label>Full System Snapshot</label>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="downloadConfig();"><i class="ui blue download icon"></i> Download</button>
<div class="ui divider"></div>
<h3>Restore from Config</h3>
<p>You can restore your previous settings and database from a zip file config backup.
<br><b style="color: rgba(255, 0, 0, 0.644);">RESTORE FULL SYSTEM SNAPSHOT WILL CAUSE THE SYSTEM TO SHUTDOWN AFTER COMPLETED. Make sure your Zoraxy is configured to work with systemd to automatic restart Zoraxy after system restore completed.<br>
</b></p>
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" accept=".zip">
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button>
</form>
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
<br><br>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
</div>
<script>
$(".checkbox").checkbox();
function getCheckedRadioValue() {
var checkedValue = $("input[name='backupmode']:checked").val();
return checkedValue;
}
function downloadConfig(){
let backupMode = getCheckedRadioValue();
if (backupMode == "full"){
window.open("/api/conf/export?includeDB=true");
}else{
window.open("/api/conf/export");
}
}
document.getElementById("uploadForm").addEventListener("submit", function(event) {
event.preventDefault(); // Prevent the form from submitting normally
var fileInput = document.getElementById("fileInput");
var file = fileInput.files[0];
if (!file) {
alert("Missing file.");
return;
}
var formData = new FormData();
formData.append("file", file);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/conf/import", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
parent.msgbox("Config restore succeed. Restart Zoraxy to apply changes.")
} else {
parent.msgbox("Restore failed: " + xhr.responseText, false, 5000);
}
}
};
xhr.send(formData);
});
</script>
</body>
</html>

1388
src/web/tools/fs.css Normal file

File diff suppressed because it is too large Load Diff

1057
src/web/tools/fs.html Normal file

File diff suppressed because it is too large Load Diff

327
src/web/tools/https.html Normal file
View File

@ -0,0 +1,327 @@
<!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="./favicon.png" />
<title>HTTPS Setup Wizard | Zoraxy</title>
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../../script/ao_module.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/tablesort.js"></script>
<link rel="stylesheet" href="shepherd.js/dist/css/shepherd.css"/>
<script src="shepherd.js/dist/js/shepherd.min.js"></script>
<link rel="stylesheet" href="../main.css">
</head>
<body>
<br>
<div class="ui container">
<div class="ui yellow message">
This Wizard require both client and server connected to the internet.
<br><b>
As different deployment methods might involve different network environment,
this wizard is only provided for assistant and the correctness of the setup is not guaranteed.
If you need to verify your TLS/SSL certificate installation is valid, please seek help
from IT professionals.</b>
</div>
<div class="ui segment">
<h3 class="ui header">
HTTPS (TLS/SSL Certificate) Setup Wizard
<div class="sub header">This tool help you setup https with your domain / subdomain on your Zoraxy host. <br>
Follow the steps below to get started</div>
</h3>
</div>
<div class="ui segment stepContainer" step="1">
<h4 class="ui header">
1. Setup Zoraxy to listen to port 80 or 443 and start listening
<div class="sub header">ACME can only works on port 80 (or 80 redirected 443). Please make sure Zoarxy is listening to either one of the ports.</div>
</h4>
<button class="ui basic green button" onclick="checkStep(1, step1Callback, this);">Check Port Setup</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="2">
<h4 class="ui header">
2. If you are under NAT, setup Port Forward and forward external port 80 (<b style="color: rgb(206, 29, 29); font-weight:bolder;">and</b> 443, if you are using 443) to your Zoraxy's LAN IP address port 80 (and 443)
<div class="sub header">If your Zoraxy server IP address starts with 192.168., you are mostly under a NAT router.</div>
</h4>
<small>The check function below will use public ip to check if port is opened. Make sure your host is reachable from the internet!<br>
<b style="color: rgb(206, 29, 29); font-weight:bolder;">If you are using 443, you still need to forward port 80 for performing 80 to 443 redirect.</b></small><br>
<button style="margin-top: 0.6em;" class="ui basic green button" onclick="checkStep(2, step2Callback, this);">Check Internet Reachable</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="3">
<h4 class="ui header">
3. Point your domain (or sub-domain) to your Zoraxy server public IP address
<div class="sub header">DNS records might takes 5 - 10 minutes to take effect. If checking did not poss the first time, wait for a few minutes and retry.</div>
</h4>
<div class="ui fluid input">
<input type="text" name="domain" placeholder="Your Domain / DNS name (e.g. dev.example.com)">
</div>
<br>
<button class="ui basic green button" onclick="checkStep(3, step3Callback, this);">Check Domain Reachable</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="4">
<h4 class="ui header">
4. Request a public CA to assign you a certificate
<div class="sub header">This process might take a few minutes and usually fully automated. If there are any error, you can see Zoraxy STDOUT / log for more information.</div>
</h4>
<div class="ui form">
<div class="field">
<label>Renewer Email</label>
<div class="ui fluid input">
<input id="caRegisterEmail" type="text" placeholder="webmaster@example.com">
</div>
<small>Your CA might send expire notification to you via this email.</small>
</div>
<div class="field">
<label>Domain(s)</label>
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<label>Matching Rule</label>
<input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
<small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
</div>
<div class="field">
<label>Certificate Authority (CA)</label>
<div class="ui selection dropdown" id="ca">
<input type="hidden" name="ca">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div>
</div>
</div>
<button id="obtainButton" class="ui green basic button" type="submit"><i class="green download icon"></i> Get Certificate</button>
</div>
<div class="ui green message" id="installSucc" style="display:none;">
<i class="ui check icon"></i> Certificate for this domain has been installed. Visit the TLS/SSL tab for advance operations.
</div>
</div>
<script>
$(".dropdown").dropdown();
function checkIfInputDomainIsMultiple(){
var inputDomains = $("#domainsInput").val();
if (inputDomains.includes(",")){
$(".multiDomainOnly").show();
}else{
$(".multiDomainOnly").hide();
}
}
//Grab the longest common suffix of all domains
//not that smart technically
function autoDetectMatchingRules(){
var domainsString = $("#domainsInput").val();
if (!domainsString.includes(",")){
return domainsString;
}
let domains = domainsString.split(",");
//Clean out any spacing between commas
for (var i = 0; i < domains.length; i++){
domains[i] = domains[i].trim();
}
function getLongestCommonSuffix(strings) {
if (strings.length === 0) {
return ''; // Return an empty string if the array is empty
}
var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
var firstString = sortedStrings[0];
var lastString = sortedStrings[sortedStrings.length - 1];
var suffix = '';
var minLength = Math.min(firstString.length, lastString.length);
for (var i = 0; i < minLength; i++) {
if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
break; // Stop iterating if characters don't match
}
suffix = firstString[firstString.length - 1 - i] + suffix;
}
return suffix;
}
let longestSuffix = getLongestCommonSuffix(domains);
//Check if the suffix is a valid domain
if (longestSuffix.substr(0,1) == "."){
//Trim off the first dot
longestSuffix = longestSuffix.substr(1);
}
if (!longestSuffix.includes(".")){
alert("Auto Detect failed: Multiple Domains");
return;
}
$("#filenameInput").val(longestSuffix);
}
$("#obtainButton").click(function() {
$("#obtainButton").addClass("loading").addClass("disabled");
obtainCertificate();
});
// Obtain certificate from API
function obtainCertificate() {
var domains = $("#domainsInput").val();
var filename = $("#filenameInput").val();
var email = $("#caRegisterEmail").val();
if (email == ""){
alert("ACME renew email is not set")
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
alert("Filename cannot be empty for certs containing multiple domains.")
return;
}
var ca = $("#ca").dropdown("get value");
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: ca,
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
if (response.error) {
console.log("Error:", response.error);
// Show error message
alert(response.error);
$("#installSucc").hide();
} else {
console.log("Certificate installed successfully");
// Show success message
//alert("Certificate installed successfully");
$("#installSucc").show();
}
},
error: function(error) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
console.log("Failed to renewed certificate:", error);
}
});
}
function step3Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> Domain is reachable and seems there is a HTTP server listening. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Domain is reachable but there are no HTTP server listening<br>
Make sure you have point to the correct IP address and there are not another proxy server above Zoraxy.
</div>`);
}
}
function step2Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> HTTP Server reachable from public IP address. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Server unreachable from public IP address<br>
Check if you have correct NAT port forward setup in your home router, firewall and make sure network is reachable from the public internet.
</div>`);
}
}
function step1Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> Supported listening port. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Invalid listening port.<br>
Go to Status tab and change the listening port to 80 or 443
</div>`);
}
}
function getStepContainerByNo(stepNo){
let targetStepContainer = undefined;
$(".stepContainer").each(function(){
if ($(this).attr("step") == stepNo){
let thisContainer = $(this);
targetStepContainer = thisContainer;
}
});
return targetStepContainer;
}
function checkStep(stepNo, callback, btn){
let targetContainer = getStepContainerByNo(stepNo);
$(btn).addClass("loading");
//Load all the inputs
data = {};
$(targetContainer).find("input").each(function(){
let key = $(this).attr("name")
if (key != undefined){
data[key] = $(this).val();
}
});
$.ajax({
url: "/api/acme/wizard?step=" + stepNo,
data: data,
success: function(data){
$(btn).removeClass("loading");
if (data.error != undefined){
$(targetContainer).find(".checkResult").html(`
<div class="ui red message">${data.error}</div>`);
}else{
callback($(targetContainer).find(".checkResult"), data);
}
},
error: function(){
$(btn).removeClass("loading");
$(targetContainer).find(".checkResult").html(`
<div class="ui red message">Server return an Unknown Error</div>`);
}
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="105.518,113.641 20.981,64.833
105.518,16.027 "/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="20.981,16.026 105.518,64.833
20.981,113.64 "/>
</svg>

After

Width:  |  Height:  |  Size: 616 B

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