65 Commits
2.6.7 ... 3.0.2

Author SHA1 Message Date
4d6c79f51b Update README.md
Added Alias support in Features
2024-04-24 16:17:47 +08:00
2c045f4f40 Merge pull request #124 from tobychui/v3.0.2
V3.0.2 Updates

Pre-checks on git.hkwtc is working and approved

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

View File

@ -9,18 +9,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.release.tag_name }} ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Dockerhub - name: Login to Docker & GHCR
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Setup building file structure - name: Setup building file structure
run: | run: |
@ -36,11 +37,5 @@ jobs:
--provenance=false \ --provenance=false \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \ --tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
.
docker buildx build --push \
--build-arg VERSION=${{ github.event.release.tag_name }} \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:latest \ --tag zoraxydocker/zoraxy:latest \
. .

View File

@ -1,3 +1,62 @@
# v3.0.1 Apr 04 2024
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
+ Added regex support for redirect (slow, don't use it unless you really needs it) [#42](https://github.com/tobychui/zoraxy/issues/42)
+ Added new dpcore implementations for faster proxy speed
+ Added support for CF-Connecting-IP to X-Real-IP auto rewrite [#114](https://github.com/tobychui/zoraxy/issues/114)
+ Added enable / disable of HTTP proxy rules in runtime via slider [#108](https://github.com/tobychui/zoraxy/issues/108)
+ Added better 404 page
+ Added option to bypass websocket origin check [#107](https://github.com/tobychui/zoraxy/issues/107)
+ Updated project homepage design
+ Fixed recursive port detection logic
+ Fixed UserAgent in resp bug
+ Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped) [#112](https://github.com/tobychui/zoraxy/issues/112)
# v3.0.0 Feb 18 2024
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!
+ Added comments for whitelist [#97](https://github.com/tobychui/zoraxy/issues/97)
+ Added force-renew for certificates [#92](https://github.com/tobychui/zoraxy/issues/92)
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header / Added X-real-Ip header [#93](https://github.com/tobychui/zoraxy/issues/93)
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
+ Added "Add controller as member" feature to Global Area Network editor
+ Added custom header
+ Deprecated aroz subservice support
+ Updated visuals, improving logical structure, less depressing colors [#95](https://github.com/tobychui/zoraxy/issues/95)
+ Added virtual directory into host routing object (each host now got its own sets of virtual directories)
+ Added support for wildcard host names (e.g. *.example.com)
+ Added best-fit selection for wildcard matching rules (e.g. *.a.example.com > *.example.com in routing)
+ Generalized root and hosts routing struct (no more conversion between runtime & save record object
+ Added "Default Site" to replace "Proxy Root" interface
+ Added Redirect & 404 page for "Default Site"
# v2.6.8 Nov 25 2023
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)
+ Optimized subdomain / vdir editing interface
+ Added system-wide logger (Work in progress)
+ Fixed issue for uptime monitor bug [#77](https://github.com/tobychui/zoraxy/issues/77)
+ Changed default static web port to 5487 (prevent already in use)
+ Added automatic HTTP/2 to TLS mode
+ Bug fix for webserver autostart [67](https://github.com/tobychui/zoraxy/issues/67)
# v2.6.7 Sep 26 2023
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
+ Web Directory Manager (see static webserver tab)
+ Added static web server and black / whitelist template [#38](https://github.com/tobychui/zoraxy/issues/38)
+ Added default / preferred CA features for ACME [#47](https://github.com/tobychui/zoraxy/issues/47)
+ Optimized TLS/SSL page and added dedicated section for ACME related features
+ Bugfixes [#61](https://github.com/tobychui/zoraxy/issues/61) [#58](https://github.com/tobychui/zoraxy/issues/58)
# v2.6.6 Aug 30 2023 # v2.6.6 Aug 30 2023
+ Added basic auth editor custom exception rules + Added basic auth editor custom exception rules
@ -5,20 +64,20 @@
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52) + 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) + 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) + Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
+ Added custom acme feature in back-end, thx [@daluntw](https://github.com/daluntw) + 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) + Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
+ Introduce new startparameter `-fastgeoip=true`, see [Releases](https://github.com/tobychui/zoraxy/releases/tag/2.6.6) + 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 # 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) + 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 + 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 + 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 # v2.6.5 Jul 19 2023
+ Added Import / Export-Feature + Added Import / Export-Feature
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26) + 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) + Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18) + Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
+ Added Whois + Added Whois
@ -28,7 +87,7 @@
+ Added force TLS v1.2 above toggle + Added force TLS v1.2 above toggle
+ Added trace route + Added trace route
+ Added ICMP ping + Added ICMP ping
+ Added special routing rules module for up-coming acme integration + Added special routing rules module for up-coming ACME integration
+ Fixed IPv6 check bug in black/whitelist + Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy + Optimized UI for TCP Proxy
@ -38,7 +97,7 @@
+ Split blacklist and whitelist from geodb script file + Split blacklist and whitelist from geodb script file
+ Optimized compile binary size + Optimized compile binary size
+ Added access control to TCP proxy + Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7) + 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 + Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials + Reduced file size of embedded materials
@ -65,6 +124,6 @@
+ Basic auth + Basic auth
+ Support TLS verification skip (for self signed certs) + Support TLS verification skip (for self signed certs)
+ Added trend analysis + Added trend analysis
+ Added referer and file type analysis + Added referrer and file type analysis
+ Added cert expire day display + Added cert expire day display
+ Moved subdomain proxy logic to dpcore + Moved subdomain proxy logic to dpcore

131
README.md
View File

@ -2,24 +2,29 @@
# Zoraxy # Zoraxy
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go! General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
### Features ### Features
- Simple to use interface with detail in-system instructions - Simple to use interface with detail in-system instructions
- Reverse Proxy - Reverse Proxy (HTTP/2)
- Virtual Directory
- Subdomain Reverse Proxy - WebSocket Proxy (automatic, no set-up needed)
- Basic Auth
- Virtual Directory Reverse Proxy - Alias Hostnames
- Custom Headers
- Redirection Rules - Redirection Rules
- TLS / SSL setup and deploy - TLS / SSL setup and deploy
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners) - ACME features like auto-renew to serve your sites in http**s**
- SNI support (one certificate contains multiple host names)
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
- Global Area Network Controller Web UI (ZeroTier not included) - Global Area Network Controller Web UI (ZeroTier not included)
- TCP Tunneling / Proxy
- Integrated Up-time Monitor - Integrated Up-time Monitor
- Web-SSH Terminal - Web-SSH Terminal
- Utilities - Utilities
- CIDR IP converters - CIDR IP converters
- mDNS Scanner - mDNS Scanner
- IP Scanner - IP Scanner
@ -28,10 +33,17 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- External permission management system for easy system integration - External permission management system for easy system integration
- SMTP config for password reset - SMTP config for password reset
## Build from Source ## Downloads
Require Go 1.20 or above [Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
``` For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
## Build from Source
Requires Go 1.22 or higher
```bash
git clone https://github.com/tobychui/zoraxy git clone https://github.com/tobychui/zoraxy
cd ./zoraxy/src/ cd ./zoraxy/src/
go mod tidy go mod tidy
@ -42,11 +54,11 @@ sudo ./zoraxy -port=:8000
## Usage ## 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
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 #### Linux
@ -60,18 +72,49 @@ Download the binary executable and double click the binary file to start it.
#### Raspberry Pi #### 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 #### 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. 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 #### Docker
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
### Start Paramters
```
Usage of zoraxy:
-autorenew int
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-info
Show information about this program in JSON
-log
Log terminal output to file (default true)
-mdns
Enable mDNS scanner and transponder (default true)
-noauth
Disable authentication for management interface
-port string
Management web interface listening port (default ":8000")
-sshlb
Allow loopback web ssh connection (DANGER)
-version
Show version of this server
-webfm
Enable web file manager for static web server root folder (default true)
-webroot string
Static web server root folder. Only allow chnage in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
ZeroTier controller API port (default 9993)
```
### External Permission Management Mode ### 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 ```bash
./zoraxy -noauth=true ./zoraxy -noauth=true
@ -79,45 +122,12 @@ If you already have a up-stream reverse proxy server in place with permission ma
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.* *Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
#### Use with ArozOS
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
```bash
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
cd ~/arozos/subservices
mkdir zoraxy
cd ./zoraxy
# Download the release binary from Github release
wget {binary executable link from release page}
# Set permission. Change this if required
sudo chmod 775 -R ./
# Start zoraxy to see if the downloaded arch is correct.
./zoraxy
# After the unzip done, press Ctrl + C to kill it
# Rename it to valid arozos subservice binary format
mv ./zoraxy zoraxy_linux_amd64
# If you are using SBCs with different CPU arch, use the following names
# mv ./zoraxy zoraxy_linux_arm
# mv ./zoraxy zoraxy_linux_arm64
# Restart arozos
sudo systemctl restart arozos
```
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
## Screenshots ## Screenshots
![](img/screenshots/0_1.png)
![](img/screenshots/1.png) ![](img/screenshots/1.png)
![](img/screenshots/2.png)
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)! More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
## FAQ ## FAQ
@ -128,22 +138,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. 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 ./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```. The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
This allows you to have infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/). This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
## Web.SSH ## Web SSH
Web SSH currently only support Linux based OS. The following platforms are supported
Web SSH currently only supports Linux based OSes. The following platforms are supported:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
- linux/armv6 (experimental) - linux/armv6 (experimental)
@ -151,9 +160,9 @@ Web SSH currently only support Linux based OS. The following platforms are suppo
### Loopback Connection ### 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 ./zoraxy -sshlb=true
``` ```
@ -165,5 +174,5 @@ If you like the project and want to support us, please consider a donation. You
## License ## 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 license.**

View File

@ -39,7 +39,7 @@ services:
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. | | `-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. | | `-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. | | `-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 I have published. | | `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
## Examples: </br> ## Examples: </br>
### Docker Run </br> ### Docker Run </br>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

View File

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

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 377 B

View File

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

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 448 B

View File

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

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 227 B

View File

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

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 332 B

View File

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

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 242 B

View File

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

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 688 B

View File

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

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 249 B

View File

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

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 195 B

View File

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

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 667 B

View File

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

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 355 B

View File

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

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View File

@ -8,7 +8,7 @@
<meta name="author" content="tobychui"> <meta name="author" content="tobychui">
<!-- HTML Meta Tags --> <!-- HTML Meta Tags -->
<title>Cluster Proxy Gateway | Zoraxy</title> <title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs"> <meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<!-- Facebook Meta Tags --> <!-- Facebook Meta Tags -->
@ -74,21 +74,16 @@
</div> </div>
<div class="right-content"> <div class="right-content">
<!-- Hero Banner Section --> <!-- Hero Banner Section -->
<div class="dot-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="headbanner"></div> <div class="headbanner"></div>
<div id="home" class="herotext"> <div id="home" class="herotext">
<div class="ui basic segment"> <div class="ui basic segment">
<div class="bannerHeaderWrapper"> <div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1> <h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">All in one homelab network routing solution</p> <p class="bannerSubheader">All in one homelab network routing solution</p>
</div> </div>
<br><br> <br><br>
<a class="ui black big button" href="#features">Learn More</a> <a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br> <br><br>
<table class="ui very basic collapsing unstackable celled table"> <table class="ui very basic collapsing unstackable celled table">
<thead> <thead>
@ -126,6 +121,22 @@
</tr> </tr>
</table> </table>
</div> </div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
</div> </div>
<!-- Features --> <!-- Features -->
@ -148,7 +159,7 @@
Reverse Proxy Reverse Proxy
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -158,7 +169,7 @@
Redirection Redirection
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -168,7 +179,7 @@
Geo-IP & Blacklist Geo-IP & Blacklist
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -189,7 +200,7 @@
Web SSH Web SSH
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -199,7 +210,7 @@
Real Time Statistics Real Time Statistics
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -209,7 +220,7 @@
Scanner & Utilities Scanner & Utilities
</div> </div>
</h3> </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>
<div class="four wide column featureItem"> <div class="four wide column featureItem">
@ -219,7 +230,7 @@
Open Source Open Source
</div> </div>
</h3> </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> </div>
</div> </div>
@ -240,34 +251,34 @@
<div class="ui three column stackable grid"> <div class="ui three column stackable grid">
<div class="column"> <div class="column">
<a href="img/screenshots/1.webp" target="_blank"><img src="img/screenshots/1.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/2.webp" target="_blank"><img src="img/screenshots/2.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/3.webp" target="_blank"><img src="img/screenshots/3.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/4.webp" target="_blank"><img src="img/screenshots/4.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/5.webp" target="_blank"><img src="img/screenshots/5.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/6.webp" target="_blank"><img src="img/screenshots/6.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/7.webp" target="_blank"><img src="img/screenshots/7.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/8.webp" target="_blank"><img src="img/screenshots/8.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/9.webp" target="_blank"><img src="img/screenshots/9.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div> </div>
<div class="column"> <div class="column">
<a href="img/screenshots/10.webp" target="_blank"><img src="img/screenshots/10.webp" class="ui fluid image screenshot"></a> <a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
body{ body{
background: #f6f6f6 !important; background: #ffffff !important;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-y: hidden; overflow-y: hidden;
@ -18,7 +18,7 @@ body{
.left-menu { .left-menu {
width: 80px; width: 80px;
min-width: 80px; min-width: 80px;
background-color: #ffffff; background-color: #fcfcfc;
min-height: 100vh; min-height: 100vh;
padding-top: 1.5em; padding-top: 1.5em;
} }
@ -48,17 +48,19 @@ body{
text-align: center; text-align: center;
border-bottom: 1px solid #f6f6f6; border-bottom: 1px solid #f6f6f6;
width: 100%; width: 100%;
border-right: 0.4em solid var(--themeTextColor);
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s; transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
} }
.menu-item.active{ .menu-item.active{
border-right: 0.4em solid var(--themeSkyblueColorDecondary); background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
background-color: #f0f8ff; }
.menu-item .item-icon{
fill: #fcfcfc;
} }
.menu-item:hover{ .menu-item:hover{
border-right: 0.4em solid var(--themeSkyblueColorDecondary); background: rgba(35,35,35,0.1);
} }
.menu-item img{ .menu-item img{
@ -69,18 +71,6 @@ body{
/* Head banner */ /* Head banner */
.headbanner{
background-image: url('img/bg.png');
background-repeat: no-repeat;
background-position: right center;
background-size: auto 100%;
position:absolute;
right: 0;
top: 0;
height: 100vh;
width: 100%;
z-index: -100;
}
.herotext{ .herotext{
padding-top: 15em; padding-top: 15em;
@ -91,11 +81,13 @@ body{
.bannerHeader{ .bannerHeader{
font-size: 8em; font-size: 8em;
font-weight: 600; font-weight: 600;
color: white;
} }
.bannerSubheader{ .bannerSubheader{
font-weight: 400; font-weight: 400;
font-size: 1.2em; font-size: 1.2em;
color: #ebebeb;
margin-top: -20px; margin-top: -20px;
} }
@ -104,6 +96,21 @@ body{
display: inline-block; display: inline-block;
} }
#home{
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
}
#home .table th, #home .table h4{
color: white;
}
#home .table h4 .content, #home .table h4 .sub.header{
color: white;
}
#home .table td a{
color: #d6ddff;
}
/* features */ /* features */
#features{ #features{
padding-top: 4em; padding-top: 4em;
@ -173,56 +180,58 @@ body{
} }
} }
/* Decorative Animation */ /*
.dot-container { Waves CSS
display: flex; */
justify-content: center;
align-items: center; #wavesWrapper{
height: 40px; position: absolute;
position: absolute; bottom: 0;
top: 2em; width: 100%;
left: 2em; left: 0;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #d9d9d9;
margin-right: 6px;
animation-name: dot-animation;
animation-duration: 4s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
} }
.dot:nth-child(1) { .waves {
animation-delay: 0s; position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
} }
.dot:nth-child(2) {
animation-delay: 1s;
}
.dot:nth-child(3) { .parallax > use {
animation-delay: 2s; animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
} }
.parallax > use:nth-child(1) {
.dot:nth-child(4) { animation-delay: -8s;
animation-delay: 3s; animation-duration: 28s;
} }
.parallax > use:nth-child(2) {
@keyframes dot-animation { animation-delay: -12s;
0% { animation-duration: 40s;
background-color: #d9d9d9;
transform: scale(1);
}
50% {
background-color: #a9d1f3;
transform: scale(1.5);
}
100% {
background-color: #d9d9d9;
transform: scale(1);
}
} }
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

View File

@ -3,7 +3,12 @@ package main
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"github.com/google/uuid"
"github.com/microcosm-cc/bluemonday"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -15,6 +20,157 @@ import (
banning / whitelist a specific IP address or country code banning / whitelist a specific IP address or country code
*/ */
/*
General Function
*/
func handleListAccessRules(w http.ResponseWriter, r *http.Request) {
allAccessRules := accessController.ListAllAccessRules()
js, _ := json.Marshal(allAccessRules)
utils.SendJSONResponse(w, string(js))
}
func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) {
ruleid, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
host, err := utils.PostPara(r, "host")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
//Check if access rule and proxy rule exists
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host)
if err != nil {
utils.SendErrorResponse(w, "invalid host given")
return
}
if !accessController.AccessRuleExists(ruleid) {
utils.SendErrorResponse(w, "access rule not exists")
return
}
//Update the proxy host acess rule id
targetProxyEndpoint.AccessFilterUUID = ruleid
targetProxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Create a new access rule, require name and desc only
func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter out injection if any
p := bluemonday.StripTagsPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
ruleUUID := uuid.New().String()
newAccessRule := access.AccessRule{
ID: ruleUUID,
Name: ruleName,
Desc: ruleDesc,
BlacklistEnabled: false,
WhitelistEnabled: false,
}
//Add it to runtime
err = accessController.AddNewAccessRule(&newAccessRule)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle removing an access rule. All proxy endpoint using this rule will be
// set to use the default rule
func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id given")
return
}
if ruleID == "default" {
utils.SendErrorResponse(w, "default access rule cannot be removed")
return
}
ruleID = strings.TrimSpace(ruleID)
//Set all proxy hosts that use this access rule back to using "default"
allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap()
for _, proxyEndpoint := range allProxyEndpoints {
if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) {
//This proxy endpoint is using the current access filter.
//set it to default
proxyEndpoint.AccessFilterUUID = "default"
proxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(proxyEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err)
} else {
SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil)
}
}
}
//Remove the access rule by ID
err = accessController.RemoveAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil)
utils.SendOK(w)
}
// Only the name and desc, for other properties use blacklist / whitelist api
func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id")
return
}
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter anything weird
p := bluemonday.StrictPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/* /*
Blacklist Related Blacklist Related
*/ */
@ -26,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
ruleID, err := utils.GetPara(r, "id")
if err != nil {
//Use default if not set
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []string{} resulst := []string{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllBlacklistedCountryCode() resulst = rule.GetAllBlacklistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
resulst = geodbStore.GetAllBlacklistedIp() resulst = rule.GetAllBlacklistedIp()
} }
js, _ := json.Marshal(resulst) js, _ := json.Marshal(resulst)
@ -45,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddCountryCodeToBlackList(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.AddCountryCodeToBlackList(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -57,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveCountryCodeFromBlackList(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromBlackList(countryCode)
utils.SendOK(w) utils.SendOK(w)
} }
@ -69,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddIPToBlackList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.GetPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
rule.AddIPToBlackList(ipAddr, comment)
utils.SendOK(w)
} }
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) { func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
@ -79,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveIPFromBlackList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromBlackList(ipAddr)
utils.SendOK(w) utils.SendOK(w)
} }
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) { func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable") enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil { if err != nil {
//Return the current enabled state ruleID = "default"
currentEnabled := geodbStore.BlacklistEnabled }
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//enable paramter not set
currentEnabled := rule.BlacklistEnabled
js, _ := json.Marshal(currentEnabled) js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else { } else {
if enable == "true" { if enable == "true" {
geodbStore.ToggleBlacklist(true) rule.ToggleBlacklist(true)
} else if enable == "false" { } else if enable == "false" {
geodbStore.ToggleBlacklist(false) rule.ToggleBlacklist(false)
} else { } else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return return
@ -115,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
resulst := []string{} ruleID, err := utils.GetPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []*access.WhitelistEntry{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode() resulst = rule.GetAllWhitelistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
resulst = geodbStore.GetAllWhitelistedIp() resulst = rule.GetAllWhitelistedIp()
} }
js, _ := json.Marshal(resulst) js, _ := json.Marshal(resulst)
@ -134,7 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddCountryCodeToWhitelist(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -146,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveCountryCodeFromWhitelist(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromWhitelist(countryCode)
utils.SendOK(w) utils.SendOK(w)
} }
@ -158,7 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddIPToWhiteList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddIPToWhiteList(ipAddr, comment)
utils.SendOK(w)
} }
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
@ -168,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveIPFromWhiteList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromWhiteList(ipAddr)
utils.SendOK(w) utils.SendOK(w)
} }
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) { func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable") enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil { if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//Return the current enabled state //Return the current enabled state
currentEnabled := geodbStore.WhitelistEnabled currentEnabled := rule.WhitelistEnabled
js, _ := json.Marshal(currentEnabled) js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else { } else {
if enable == "true" { if enable == "true" {
geodbStore.ToggleWhitelist(true) rule.ToggleWhitelist(true)
} else if enable == "false" { } else if enable == "false" {
geodbStore.ToggleWhitelist(false) rule.ToggleWhitelist(false)
} else { } else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return return

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"regexp" "regexp"
@ -29,7 +28,7 @@ func getRandomPort(minPort int) int {
// init the new ACME instance // init the new ACME instance
func initACME() *acme.ACMEHandler { func initACME() *acme.ACMEHandler {
log.Println("Starting ACME handler") SystemWideLogger.Println("Starting ACME handler")
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
// Generate a random port above 30000 // Generate a random port above 30000
port := getRandomPort(30000) port := getRandomPort(30000)
@ -39,12 +38,12 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000) port = getRandomPort(30000)
} }
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port)) return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
} }
// create the special routing rule for ACME // create the special routing rule for ACME
func acmeRegisterSpecialRoutingRule() { func acmeRegisterSpecialRoutingRule() {
log.Println("Assigned temporary port:" + acmeHandler.Getport()) SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{ err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
ID: "acme-autorenew", ID: "acme-autorenew",
@ -79,7 +78,7 @@ func acmeRegisterSpecialRoutingRule() {
}) })
if err != nil { if err != nil {
log.Println("[Err] " + err.Error()) SystemWideLogger.PrintAndLog("ACME", "Unable register temp port for DNS resolver", err)
} }
} }
@ -89,7 +88,7 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
if dynamicProxyRouter.Option.Port == 443 { if dynamicProxyRouter.Option.Port == 443 {
//Enable port 80 to 443 redirect //Enable port 80 to 443 redirect
if !dynamicProxyRouter.Option.ForceHttpsRedirect { if !dynamicProxyRouter.Option.ForceHttpsRedirect {
log.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests") SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true) dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
} else { } else {
//Set this to true, so after renew, do not turn it off //Set this to true, so after renew, do not turn it off
@ -104,13 +103,16 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)") utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
} }
//Add a 3 second delay to make sure everything is settle down
time.Sleep(3 * time.Second)
// Pass over to the acmeHandler to deal with the communication // Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r) acmeHandler.HandleRenewCertificate(w, r)
if dynamicProxyRouter.Option.Port == 443 { if dynamicProxyRouter.Option.Port == 443 {
if !isForceHttpsRedirectEnabledOriginally { if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off //Default is off. Turn the redirection off
log.Println("Restoring HTTP to HTTPS redirect settings") SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false) dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
} }
} }
@ -130,7 +132,7 @@ func HandleACMEPreferredCA(w http.ResponseWriter, r *http.Request) {
acme.IsSupportedCA(ca) acme.IsSupportedCA(ca)
//Set the new config //Set the new config
sysdb.Write("acmepref", "prefca", ca) sysdb.Write("acmepref", "prefca", ca)
log.Println("Updating prefered ACME CA to " + ca) SystemWideLogger.Println("Updating prefered ACME CA to " + ca)
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -47,17 +47,28 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff) authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint) authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
//Reverse proxy root related APIs authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList) //Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate) authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
//Reverse proxy auth related APIs //Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
@ -76,7 +87,14 @@ func initAPIs() {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
//Access Rules API
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs //Blacklist APIs
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
@ -84,7 +102,6 @@ func initAPIs() {
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs //Whitelist APIs
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
@ -114,6 +131,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming) authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails) //authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges) authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList) authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP) authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming) authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
@ -153,6 +172,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet) authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet) authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset //Account Reset
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail) http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
@ -165,6 +186,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA) authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail) authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains) authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains) authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy) authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow) authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
@ -174,7 +196,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer) authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer) authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange) authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager { if *allowWebFileManager {
//Web Directory Manager file operation functions //Web Directory Manager file operation functions

View File

@ -6,7 +6,6 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -52,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
results := []*CertInfo{} results := []*CertInfo{}
for _, filename := range filenames { for _, filename := range filenames {
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt") certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key") //keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath) fileInfo, err := os.Stat(certFilepath)
if err != nil { if err != nil {
@ -128,7 +127,7 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
certBtyes, err := os.ReadFile(certFilepath) certBtyes, err := os.ReadFile(certFilepath)
if err != nil { if err != nil {
// Unable to load this file // Unable to load this file
log.Println("Unable to load certificate: " + certFilepath) SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
continue continue
} else { } else {
// Cert loaded. Check its expiry time // Cert loaded. Check its expiry time
@ -182,11 +181,11 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
} else { } else {
if newState == "true" { if newState == "true" {
sysdb.Write("settings", "usetls", 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) dynamicProxyRouter.UpdateTLSSetting(true)
} else if newState == "false" { } else if newState == "false" {
sysdb.Write("settings", "usetls", 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) dynamicProxyRouter.UpdateTLSSetting(false)
} else { } else {
utils.SendErrorResponse(w, "invalid state given. Only support true or false") utils.SendErrorResponse(w, "invalid state given. Only support true or false")
@ -213,11 +212,11 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
} else { } else {
if newState == "true" { if newState == "true" {
sysdb.Write("settings", "forceLatestTLS", true) sysdb.Write("settings", "forceLatestTLS", true)
log.Println("Updating minimum TLS version to v1.2 or above") SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true) dynamicProxyRouter.UpdateTLSVersion(true)
} else if newState == "false" { } else if newState == "false" {
sysdb.Write("settings", "forceLatestTLS", false) sysdb.Write("settings", "forceLatestTLS", false)
log.Println("Updating minimum TLS version to v1.0 or above") SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false) dynamicProxyRouter.UpdateTLSVersion(false)
} else { } else {
utils.SendErrorResponse(w, "invalid state given") utils.SendErrorResponse(w, "invalid state given")
@ -249,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
} }
if keytype == "pub" { if keytype == "pub" {
overWriteFilename = domain + ".crt" overWriteFilename = domain + ".pem"
} else if keytype == "pri" { } else if keytype == "pri" {
overWriteFilename = domain + ".key" overWriteFilename = domain + ".key"
} else { } else {
@ -288,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
//Update cert list
tlsCertManager.UpdateLoadedCertList()
// send response // send response
fmt.Fprintln(w, "File upload successful!") fmt.Fprintln(w, "File upload successful!")
} }

View File

@ -3,9 +3,9 @@ package main
import ( import (
"archive/zip" "archive/zip"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -29,101 +29,125 @@ type Record struct {
Rootname string Rootname string
ProxyTarget string ProxyTarget string
UseTLS bool UseTLS bool
BypassGlobalTLS bool
SkipTlsValidation bool SkipTlsValidation bool
RequireBasicAuth bool RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
} }
// Save a reverse proxy config record to file /*
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error { Load Reverse Proxy Config from file and append it to current runtime proxy router
//TODO: Make this accept new def types */
os.MkdirAll("./conf/proxy/", 0775) func LoadReverseProxyConfig(configFilepath string) error {
filename := getFilenameFromRootName(proxyConfigRecord.Rootname) //Load the config file from disk
endpointConfig, err := os.ReadFile(configFilepath)
//Generate record
thisRecord := proxyConfigRecord
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
}
// 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 { if err != nil {
return err return err
} }
return SaveReverseProxyConfigToFile(recordToSave)
}
func RemoveReverseProxyConfigFile(rootname string) error { //Parse it into dynamic proxy endpoint
filename := getFilenameFromRootName(rootname) thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/") err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
log.Println("Config Removed: ", removePendingFile) if err != nil {
if utils.FileExists(removePendingFile) { return err
err := os.Remove(removePendingFile)
if err != nil {
log.Println(err.Error())
return err
}
} }
//File already gone //Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
} else {
return errors.New("not supported proxy type")
}
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
return nil return nil
} }
// Return ptype, rootname and proxyTarget, error if any func filterProxyConfigFilename(filename string) string {
func LoadReverseProxyConfig(filename string) (*Record, error) { //Filter out wildcard characters
thisRecord := Record{ filename = strings.ReplaceAll(filename, "*", "(ST)")
ProxyType: "", filename = strings.ReplaceAll(filename, "?", "(QM)")
Rootname: "", filename = strings.ReplaceAll(filename, "[", "(OB)")
ProxyTarget: "", filename = strings.ReplaceAll(filename, "]", "(CB)")
UseTLS: false, filename = strings.ReplaceAll(filename, "#", "(HT)")
SkipTlsValidation: false, return filepath.ToSlash(filename)
}
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
//Save config to file
js, err := json.MarshalIndent(endpoint, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, js, 0775)
}
func RemoveReverseProxyConfig(endpoint string) error {
filename := filepath.Join("./conf/proxy/", endpoint+".config")
if endpoint == "/" {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
if !utils.FileExists(filename) {
return errors.New("target endpoint not exists")
}
return os.Remove(filename)
}
// Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
BypassGlobalTLS: false,
SkipCertValidations: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false, RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{}, BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
} DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
configContent, err := os.ReadFile(filename) })
if err != nil { if err != nil {
return &thisRecord, err return nil, err
} }
//Unmarshal the content into config return rootProxyEndpoint, nil
err = json.Unmarshal(configContent, &thisRecord)
if err != nil {
return &thisRecord, err
}
//Return it
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,
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, ".", "_")
filename = strings.ReplaceAll(filename, "/", "-")
filename = filename + ".config"
return filename
} }
/* /*
@ -191,14 +215,14 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
//Also zip in the sysdb //Also zip in the sysdb
zipFile, err := zipWriter.Create("sys.db") zipFile, err := zipWriter.Create("sys.db")
if err != nil { if err != nil {
log.Println("[Backup] Unable to zip sysdb: " + err.Error()) SystemWideLogger.PrintAndLog("Backup", "Unable to zip sysdb", err)
return return
} }
// Open the file on disk // Open the file on disk
file, err := os.Open("sys.db") file, err := os.Open("sys.db")
if err != nil { if err != nil {
log.Println("[Backup] Unable to open sysdb: " + err.Error()) SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return return
} }
defer file.Close() defer file.Close()
@ -206,7 +230,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
// Copy the file contents to the zip file // Copy the file contents to the zip file
_, err = io.Copy(zipFile, file) _, err = io.Copy(zipFile, file)
if err != nil { if err != nil {
log.Println(err) SystemWideLogger.Println(err)
return return
} }
@ -311,12 +335,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
// Send a success response // Send a success response
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
log.Println("Configuration restored") SystemWideLogger.Println("Configuration restored")
fmt.Fprintln(w, "Configuration restored") fmt.Fprintln(w, "Configuration restored")
if restoreDatabase { if restoreDatabase {
go func() { go func() {
log.Println("Database altered. Restarting in 3 seconds...") SystemWideLogger.Println("Database altered. Restarting in 3 seconds...")
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
os.Exit(0) os.Exit(0)
}() }()

View File

@ -1,18 +1,34 @@
module imuslab.com/zoraxy module imuslab.com/zoraxy
go 1.16 go 1.21
toolchain go1.22.2
require ( require (
github.com/boltdb/bolt v1.3.1 github.com/boltdb/bolt v1.3.1
github.com/go-acme/lego/v4 v4.14.0 github.com/go-acme/lego/v4 v4.16.1
github.com/go-ping/ping v1.1.0 github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.3.1 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.1
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1 github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25 github.com/microcosm-cc/bluemonday v1.0.26
golang.org/x/net v0.14.0 golang.org/x/net v0.23.0
golang.org/x/sys v0.11.0 golang.org/x/sys v0.18.0
golang.org/x/tools v0.12.0 // indirect golang.org/x/text v0.14.0
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/miekg/dns v1.1.58 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/tools v0.19.0 // indirect
) )

1805
src/go.sum

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,16 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/aroz"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email" "imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv" "imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/mdns" "imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule" "imuslab.com/zoraxy/mod/pathrule"
@ -34,6 +36,7 @@ import (
) )
// General flags // General flags
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface") var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server") var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
@ -44,10 +47,11 @@ var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL cert
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)") 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 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 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 ( var (
name = "Zoraxy" name = "Zoraxy"
version = "2.6.7" version = "3.0.2"
nodeUUID = "generic" nodeUUID = "generic"
development = false //Set this to false to use embedded web fs development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix() bootTime = time.Now().Unix()
@ -61,13 +65,13 @@ var (
/* /*
Handler Modules Handler Modules
*/ */
handler *aroz.ArozHandler //Handle arozos managed permission system
sysdb *database.Database //System database sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets redirectTable *redirection.RuleTable //Handle special redirection rule sets
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker uptimeMonitor *uptime.Monitor //Uptime monitor service worker
@ -78,10 +82,12 @@ var (
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
//Helper modules //Helper modules
EmailSender *email.Sender //Email sender that handle email sending EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic 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. // Kill signal handler. Do something before the system the core terminate.
@ -116,26 +122,17 @@ func ShutdownSeq() {
fmt.Println("- Cleaning up tmp files") fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp") os.RemoveAll("./tmp")
fmt.Println("- Closing system wide logger")
SystemWideLogger.Close()
//Close database, final //Close database, final
fmt.Println("- Stopping system database") fmt.Println("- Stopping system database")
sysdb.Close() sysdb.Close()
} }
func main() { func main() {
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information //Parse startup flags
handler = aroz.HandleFlagParse(aroz.ServiceInfo{ flag.Parse()
Name: name,
Desc: "Dynamic Reverse Proxy Server",
Group: "Network",
IconPath: "zoraxy/img/small_icon.png",
Version: version,
StartDir: "zoraxy/index.html",
SupportFW: true,
LaunchFWDir: "zoraxy/index.html",
SupportEmb: false,
InitFWSize: []int{1080, 580},
})
if *showver { if *showver {
fmt.Println(name + " - Version " + version) fmt.Println(name + " - Version " + version)
os.Exit(0) os.Exit(0)
@ -151,7 +148,7 @@ func main() {
} }
uuidBytes, err := os.ReadFile(uuidRecord) uuidBytes, err := os.ReadFile(uuidRecord)
if err != nil { 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) panic(err)
} }
nodeUUID = string(uuidBytes) nodeUUID = string(uuidBytes)
@ -160,7 +157,7 @@ func main() {
startupSequence() startupSequence()
//Initiate management interface APIs //Initiate management interface APIs
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager()) requireAuth = !(*noauth)
initAPIs() initAPIs()
//Start the reverse proxy server in go routine //Start the reverse proxy server in go routine
@ -173,8 +170,8 @@ func main() {
//Start the finalize sequences //Start the finalize sequences
finalSequence() finalSequence()
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port) SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(handler.Port, nil) err = http.ListenAndServe(*webUIPort, nil)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

217
src/mod/access/access.go Normal file
View File

@ -0,0 +1,217 @@
package access
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/utils"
)
/*
Access.go
This module is the new version of access control system
where now the blacklist / whitelist are seperated from
geodb module
*/
// Create a new access controller to handle blacklist / whitelist
func NewAccessController(options *Options) (*Controller, error) {
sysdb := options.Database
if sysdb == nil {
return nil, errors.New("missing database access")
}
//Create the config folder if not exists
confFolder := options.ConfigFolder
if !utils.FileExists(confFolder) {
err := os.MkdirAll(confFolder, 0775)
if err != nil {
return nil, err
}
}
// Create the global access rule if not exists
var defaultAccessRule = AccessRule{
ID: "default",
Name: "Default",
Desc: "Default access rule for all HTTP proxy hosts",
BlacklistEnabled: false,
WhitelistEnabled: false,
WhiteListCountryCode: &map[string]string{},
WhiteListIP: &map[string]string{},
BlackListContryCode: &map[string]string{},
BlackListIP: &map[string]string{},
}
defaultRuleSettingFile := filepath.Join(confFolder, "default.json")
if utils.FileExists(defaultRuleSettingFile) {
//Load from file
defaultRuleBytes, err := os.ReadFile(defaultRuleSettingFile)
if err == nil {
err = json.Unmarshal(defaultRuleBytes, &defaultAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse default routing rule config file. Using default", err)
}
}
} else {
//Create one
js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
os.WriteFile(defaultRuleSettingFile, js, 0775)
}
//Generate a controller object
thisController := Controller{
DefaultAccessRule: &defaultAccessRule,
ProxyAccessRule: &sync.Map{},
Options: options,
}
//Load all acccess rules from file
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
if err != nil {
return nil, err
}
ProxyAccessRules := sync.Map{}
for _, configFile := range configFiles {
if filepath.Base(configFile) == "default.json" {
//Skip this, as this was already loaded as default
continue
}
configContent, err := os.ReadFile(configFile)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to load config "+filepath.Base(configFile), err)
continue
}
//Parse the config file into AccessRule
thisAccessRule := AccessRule{}
err = json.Unmarshal(configContent, &thisAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err)
continue
}
thisAccessRule.parent = &thisController
ProxyAccessRules.Store(thisAccessRule.ID, &thisAccessRule)
}
thisController.ProxyAccessRule = &ProxyAccessRules
return &thisController, nil
}
// Get the global access rule
func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
if c.DefaultAccessRule == nil {
return nil, errors.New("global access rule is not set")
}
return c.DefaultAccessRule, nil
}
// Load access rules to runtime, require rule ID
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
if accessRuleID == "default" || accessRuleID == "" {
return c.DefaultAccessRule, nil
}
//Load from sync.Map, should be O(1)
targetRule, ok := c.ProxyAccessRule.Load(accessRuleID)
if !ok {
return nil, errors.New("target access rule not exists")
}
ar, ok := targetRule.(*AccessRule)
if !ok {
return nil, errors.New("assertion of access rule failed, version too old?")
}
return ar, nil
}
// Return all the access rules currently in runtime, including default
func (c *Controller) ListAllAccessRules() []*AccessRule {
results := []*AccessRule{c.DefaultAccessRule}
c.ProxyAccessRule.Range(func(key, value interface{}) bool {
results = append(results, value.(*AccessRule))
return true
})
return results
}
// Check if an access rule exists given the rule id
func (c *Controller) AccessRuleExists(ruleID string) bool {
r, _ := c.GetAccessRuleByID(ruleID)
if r != nil {
//An access rule with identical ID exists
return true
}
return false
}
// Add a new access rule to runtime and save it to file
func (c *Controller) AddNewAccessRule(newRule *AccessRule) error {
r, _ := c.GetAccessRuleByID(newRule.ID)
if r != nil {
//An access rule with identical ID exists
return errors.New("access rule already exists")
}
//Check if the blacklist and whitelist are populated with empty map
if newRule.BlackListContryCode == nil {
newRule.BlackListContryCode = &map[string]string{}
}
if newRule.BlackListIP == nil {
newRule.BlackListIP = &map[string]string{}
}
if newRule.WhiteListCountryCode == nil {
newRule.WhiteListCountryCode = &map[string]string{}
}
if newRule.WhiteListIP == nil {
newRule.WhiteListIP = &map[string]string{}
}
//Add access rule to runtime
newRule.parent = c
c.ProxyAccessRule.Store(newRule.ID, newRule)
//Save rule to file
newRule.SaveChanges()
return nil
}
// Update the access rule meta info.
func (c *Controller) UpdateAccessRule(ruleID string, name string, desc string) error {
targetAccessRule, err := c.GetAccessRuleByID(ruleID)
if err != nil {
return err
}
///Update the name and desc
targetAccessRule.Name = name
targetAccessRule.Desc = desc
//Overwrite the rule currently in sync map
if ruleID == "default" {
c.DefaultAccessRule = targetAccessRule
} else {
c.ProxyAccessRule.Store(ruleID, targetAccessRule)
}
return targetAccessRule.SaveChanges()
}
// Remove the access rule by its id
func (c *Controller) RemoveAccessRuleByID(ruleID string) error {
if !c.AccessRuleExists(ruleID) {
return errors.New("access rule not exists")
}
//Default cannot be removed
if ruleID == "default" {
return errors.New("default access rule cannot be removed")
}
//Remove it
return c.DeleteAccessRuleByID(ruleID)
}

View File

@ -0,0 +1,153 @@
package access
import (
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
)
// Check both blacklist and whitelist for access for both geoIP and ip / CIDR ranges
func (s *AccessRule) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
// Check both blacklist and whitelist for access using net.Conn
func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
// Toggle black list
func (s *AccessRule) ToggleBlacklist(enabled bool) {
s.BlacklistEnabled = enabled
s.SaveChanges()
}
// Toggel white list
func (s *AccessRule) ToggleWhitelist(enabled bool) {
s.WhitelistEnabled = enabled
s.SaveChanges()
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *AccessRule) IsBlacklisted(ipAddr string) bool {
if !s.BlacklistEnabled {
//Blacklist not enabled. Always return false
return false
}
if ipAddr == "" {
//Unable to get the target IP address
return false
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return false
}
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPBlacklisted(ipAddr) {
return true
}
return false
}
/*
IsWhitelisted check if a given IP address is in the current
server's white list.
Note that the Whitelist default result is true even
when encountered error
*/
func (s *AccessRule) IsWhitelisted(ipAddr string) bool {
if !s.WhitelistEnabled {
//Whitelist not enabled. Always return true (allow access)
return true
}
if ipAddr == "" {
//Unable to get the target IP address, assume ok
return true
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return true
}
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPWhitelisted(ipAddr) {
return true
}
return false
}
/* Utilities function */
// Update the current access rule to json file
func (s *AccessRule) SaveChanges() error {
if s.parent == nil {
return errors.New("save failed: access rule detached from controller")
}
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
js, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
err = os.WriteFile(saveTarget, js, 0775)
return err
}
// Delete this access rule, this will only delete the config file.
// for runtime delete, use DeleteAccessRuleByID from parent Controller
func (s *AccessRule) DeleteConfigFile() error {
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
return os.Remove(saveTarget)
}
// Delete the access rule by given ID
func (c *Controller) DeleteAccessRuleByID(accessRuleID string) error {
targetAccessRule, err := c.GetAccessRuleByID(accessRuleID)
if err != nil {
return err
}
//Delete config file associated with this access rule
err = targetAccessRule.DeleteConfigFile()
if err != nil {
return err
}
//Delete the access rule in runtime
c.ProxyAccessRule.Delete(accessRuleID)
return nil
}
// Create a deep copy object of the access rule list
func deepCopy(valueList map[string]string) map[string]string {
result := map[string]string{}
js, _ := json.Marshal(valueList)
json.Unmarshal(js, &result)
return result
}

View File

@ -0,0 +1,75 @@
package access
import (
"strings"
)
/*
Blacklist.go
This script store the blacklist related functions
*/
// Geo Blacklist
func (s *AccessRule) AddCountryCodeToBlackList(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
newBlacklistCountryCode[countryCode] = comment
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
delete(newBlacklistCountryCode, countryCode)
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
blacklistMap := *s.BlackListContryCode
_, ok := blacklistMap[countryCode]
return ok
}
func (s *AccessRule) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
blacklistMap := *s.BlackListContryCode
for cc, _ := range blacklistMap {
bannedCountryCodes = append(bannedCountryCodes, cc)
}
return bannedCountryCodes
}
// IP Blacklsits
func (s *AccessRule) AddIPToBlackList(ipAddr string, comment string) {
newBlackListIP := deepCopy(*s.BlackListIP)
newBlackListIP[ipAddr] = comment
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromBlackList(ipAddr string) {
newBlackListIP := deepCopy(*s.BlackListIP)
delete(newBlackListIP, ipAddr)
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) GetAllBlacklistedIp() []string {
bannedIps := []string{}
blacklistMap := *s.BlackListIP
for ip, _ := range blacklistMap {
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
IPBlacklist := *s.BlackListIP
_, ok := IPBlacklist[ipAddr]
return ok
}

38
src/mod/access/typedef.go Normal file
View File

@ -0,0 +1,38 @@
package access
import (
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
)
type Options struct {
Logger logger.Logger
ConfigFolder string //Path for storing config files
GeoDB *geodb.Store //For resolving country code
Database *database.Database //System key-value database
}
type AccessRule struct {
ID string
Name string
Desc string
BlacklistEnabled bool
WhitelistEnabled bool
/* Whitelist Blacklist Table, value is comment if supported */
WhiteListCountryCode *map[string]string
WhiteListIP *map[string]string
BlackListContryCode *map[string]string
BlackListIP *map[string]string
parent *Controller
}
type Controller struct {
DefaultAccessRule *AccessRule
ProxyAccessRule *sync.Map
Options *Options
}

112
src/mod/access/whitelist.go Normal file
View File

@ -0,0 +1,112 @@
package access
import (
"strings"
"imuslab.com/zoraxy/mod/netutils"
)
/*
Whitelist.go
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *AccessRule) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
newWhitelistCC[countryCode] = comment
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
delete(newWhitelistCC, countryCode)
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
whitelistCC := *s.WhiteListCountryCode
_, ok := whitelistCC[countryCode]
return ok
}
func (s *AccessRule) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
whitelistCC := *s.WhiteListCountryCode
for cc, comment := range whitelistCC {
whitelistedCountryCode = append(whitelistedCountryCode, &WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: cc,
Comment: comment,
})
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *AccessRule) AddIPToWhiteList(ipAddr string, comment string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
newWhitelistIP[ipAddr] = comment
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromWhiteList(ipAddr string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
delete(newWhitelistIP, ipAddr)
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool {
//Check for IP wildcard and CIRD rules
WhitelistedIP := *s.WhiteListIP
for ipOrCIDR, _ := range WhitelistedIP {
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
if wildcardMatch {
return true
}
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
if cidrMatch {
return true
}
}
return false
}
func (s *AccessRule) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
currentWhitelistedIP := *s.WhiteListIP
for ipOrCIDR, comment := range currentWhitelistedIP {
thisEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipOrCIDR,
Comment: comment,
}
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}

View File

@ -9,6 +9,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"log" "log"
"net" "net"
@ -24,6 +25,7 @@ import (
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -40,6 +42,11 @@ type ACMEUser struct {
key crypto.PrivateKey key crypto.PrivateKey
} }
type EABConfig struct {
Kid string `json:"kid"`
HmacKey string `json:"HmacKey"`
}
// GetEmail returns the email of the ACMEUser. // GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string { func (u *ACMEUser) GetEmail() string {
return u.Email return u.Email
@ -59,13 +66,15 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
type ACMEHandler struct { type ACMEHandler struct {
DefaultAcmeServer string DefaultAcmeServer string
Port string Port string
Database *database.Database
} }
// NewACME creates a new ACMEHandler instance. // NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler { func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
return &ACMEHandler{ return &ACMEHandler{
DefaultAcmeServer: acmeServer, DefaultAcmeServer: acmeServer,
Port: port, Port: port,
Database: database,
} }
} }
@ -143,10 +152,63 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
} }
// New users will need to register // New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) /*
if err != nil { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
log.Println(err) if err != nil {
return false, err log.Println(err)
return false, err
}
*/
var reg *registration.Resource
// New users will need to register
if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.")
// IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") {
a.Database.NewTable("acme")
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
}
if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
}
var kid string
var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil {
log.Println(err)
return false, err
}
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil {
log.Println(err)
return false, err
}
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
if err != nil {
log.Println(err)
return false, err
}
//return false, errors.New("External Account Required for this ACME Provider.")
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
} }
adminUser.Registration = reg adminUser.Registration = reg
@ -163,7 +225,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Each certificate comes back with the cert bytes, the bytes of the client's // Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. // private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return false, err return false, err

View File

@ -373,3 +373,34 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ") js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775) return os.WriteFile(a.ConfigFilePath, js, 0775)
} }
// Handle update auto renew EAD configuration
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
kid, err := utils.GetPara(r, "kid")
if err != nil {
utils.SendErrorResponse(w, "kid not set")
return
}
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
if err != nil {
utils.SendErrorResponse(w, "hmacEncoded not set")
return
}
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
if err != nil {
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
return
}
if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme")
}
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
utils.SendOK(w)
}

View File

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

Binary file not shown.

View File

@ -1,16 +1,11 @@
package dynamicproxy package dynamicproxy
import ( import (
_ "embed"
"errors"
"log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"imuslab.com/zoraxy/mod/geodb"
) )
/* /*
@ -26,41 +21,21 @@ import (
- Vitrual Directory Routing - Vitrual Directory Routing
*/ */
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
Special Routing Rules, bypass most of the limitations Special Routing Rules, bypass most of the limitations
*/ */
//Check if there are external routing rule matches. //Check if there are external routing rule matches.
//If yes, route them via external rr //If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r) matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil { if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it //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) matchedRoutingRule.Route(w, r)
return return
} }
/* //Inject headers
General Access Check w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/* /*
Redirection Routing Redirection Routing
@ -72,46 +47,64 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
//Extract request host to see if it is virtual directory or subdomain /*
Host Routing
*/
//Extract request host to see if any proxy rule is matched
domainOnly := r.Host domainOnly := r.Host
if strings.Contains(r.Host, ":") { if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
/* if sep != nil && !sep.Disabled {
Subdomain Routing //Matching proxy rule found
*/ //Access Check (blacklist / whitelist)
if strings.Contains(r.Host, ".") { ruleID := sep.AccessFilterUUID
//This might be a subdomain. See if there are any subdomain proxy router for this if sep.AccessFilterUUID == "" {
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly) //Use default rule
if sep != nil { ruleID = "default"
if sep.RequireBasicAuth { }
err := h.handleBasicAuthRouting(w, r, sep) if h.handleAccessRouting(ruleID, w, r) {
if err != nil { //Request handled by subroute
return
}
}
h.subdomainRequest(w, r, sep)
return return
} }
}
/* //Validate basic auth
Virtual Directory Routing if sep.RequireBasicAuth {
*/ err := h.handleBasicAuthRouting(w, r, sep)
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil {
if targetProxyEndpoint.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
if err != nil { if err != nil {
return return
} }
} }
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") { //Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
return
}
}
//Fallback to handle by the host proxy forwarder
h.hostRequest(w, r, sep)
return
}
/*
Root Router Handling
*/
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil { if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -142,121 +135,63 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules { //Get the proxy root config
//Route to custom domain proot := h.Parent.Root
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" { switch proot.DefaultSiteOption {
//Not set. Redirect to first level of domain redirectable case DefaultSite_InternalStaticWebServer:
fld, err := h.getTopLevelRedirectableDomain(domainOnly) fallthrough
if err != nil { case DefaultSite_ReverseProxy:
//Redirect to proxy root //They both share the same behavior
h.proxyRequest(w, r, h.Parent.Root)
} else { //Check if any virtual directory rules matches
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld) proxyingPath := strings.TrimSpace(r.RequestURI)
h.logRequest(r, false, 307, "root-redirect", domainOnly) targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect) if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
} //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if h.isTopLevelRedirectableDomain(domainOnly) { } else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
//This is requesting a top level private domain that should be serving root potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
h.proxyRequest(w, r, h.Parent.Root) if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
} else { //Missing tailing slash. Redirect to target proxy endpoint
//Validate the redirection target URL http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
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 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 //No vdir match. Route via root router
// if the return value is false, you can continue process the response writer h.hostRequest(w, r, h.Parent.Root)
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool { case DefaultSite_Redirect:
//Check if this ip is in blacklist redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
clientIpAddr := geodb.GetRequesterIP(r) if redirectTarget == "" {
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) { redirectTarget = "about:blank"
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")) //Check if it is an infinite loopback redirect
parsedURL, err := url.Parse(proot.DefaultSiteValue)
if err != nil { if err != nil {
w.Write(page_forbidden) //Error when parsing target. Send to root
h.hostRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if hostname == domainOnly {
h.logRequest(r, false, 500, "root-redirect", domainOnly)
http.Error(w, "Loopback redirects due to invalid settings", 500)
return
}
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
//Serve the not found page, use template if exists
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
if err != nil {
w.Write(page_hosterror)
} else { } else {
w.Write(template) 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

@ -0,0 +1,64 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/netutils"
)
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool {
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true
}
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked {
h.logRequest(r, false, 403, blockedReason, "")
}
return isBlocked
}
// Return boolean, return true if access is blocked
// For string, it will return the blocked reason (if any)
func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) {
//Check if this ip is in blacklist
clientIpAddr := netutils.GetRequesterIP(r)
if accessRule.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "blacklist"
}
//Check if this ip is in whitelist
if !accessRule.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "whitelist"
}
//Not blocked.
return false, ""
}

View File

@ -16,6 +16,16 @@ import (
*/ */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.logRequest(r, false, 401, "host", pe.Domain)
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 { if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules //Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules { for _, exceptionRule := range pe.BasicAuthExceptionRules {
@ -26,10 +36,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
} }
} }
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"
}
u, p, ok := r.BasicAuth() u, p, ok := r.BasicAuth()
if !ok { if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
@ -48,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
} }
if !matchingFound { if !matchingFound {
h.logRequest(r, false, 401, proxyType, pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401) w.WriteHeader(401)
return errors.New("unauthorized") return errors.New("unauthorized")

View File

@ -1,6 +1,7 @@
package dpcore package dpcore
import ( import (
"context"
"errors" "errors"
"io" "io"
"log" "log"
@ -8,12 +9,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"sync"
"time" "time"
) )
var onExitFlushLoop func()
// ReverseProxy is an HTTP Handler that takes an incoming request and // ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the // sends it to another server, proxying the response back to the
// client, support http, also support https tunnel using http.hijacker // client, support http, also support https tunnel using http.hijacker
@ -60,6 +58,7 @@ type ResponseRewriteRuleSet struct {
ProxyDomain string ProxyDomain string
OriginalHost string OriginalHost string
UseTLS bool UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this PathPrefix string //Vdir prefix for root, / will be rewrite to this
} }
@ -67,7 +66,12 @@ type requestCanceler interface {
CancelRequest(req *http.Request) CancelRequest(req *http.Request)
} }
func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerification bool) *ReverseProxy { type DpcoreOptions struct {
IgnoreTLSVerification bool
FlushInterval time.Duration
}
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
targetQuery := target.RawQuery targetQuery := target.RawQuery
director := func(req *http.Request) { director := func(req *http.Request) {
req.URL.Scheme = target.Scheme req.URL.Scheme = target.Scheme
@ -79,10 +83,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
} }
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
} }
//Hack the default transporter to handle more connections //Hack the default transporter to handle more connections
@ -94,16 +94,17 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2 thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification { if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error //Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
} }
return &ReverseProxy{ return &ReverseProxy{
Director: director, Director: director,
Prepender: prepender, Prepender: prepender,
Verbal: false, FlushInterval: dpcOptions.FlushInterval,
Transport: thisTransporter, Verbal: false,
Transport: thisTransporter,
} }
} }
@ -177,64 +178,66 @@ var hopHeaders = []string{
//"Upgrade", //"Upgrade",
} }
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { // Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
if p.FlushInterval != 0 { func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
if wf, ok := dst.(writeFlusher); ok { var w io.Writer = dst
mlw := &maxLatencyWriter{ if flushInterval != 0 {
dst: wf, mlw := &maxLatencyWriter{
latency: p.FlushInterval, dst: dst,
done: make(chan bool), flush: http.NewResponseController(dst).Flush,
} latency: flushInterval,
go mlw.flushLoop()
defer mlw.stop()
dst = mlw
} }
defer mlw.stop()
// set up initial timer so headers get flushed even if body writes are delayed
mlw.flushPending = true
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
w = mlw
} }
io.Copy(dst, src) var buf []byte
_, err := p.copyBuffer(w, src, buf)
return err
} }
type writeFlusher interface { // Copy with given buffer size. Default to 64k
io.Writer func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
http.Flusher if len(buf) == 0 {
} buf = make([]byte, 64*1024)
}
type maxLatencyWriter struct { var written int64
dst writeFlusher
latency time.Duration
mu sync.Mutex
done chan bool
}
func (m *maxLatencyWriter) Write(b []byte) (int, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.dst.Write(b)
}
func (m *maxLatencyWriter) flushLoop() {
t := time.NewTicker(m.latency)
defer t.Stop()
for { for {
select { nr, rerr := src.Read(buf)
case <-m.done: if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
if onExitFlushLoop != nil { p.logf("dpcore read error during body copy: %v", rerr)
onExitFlushLoop() }
if nr > 0 {
nw, werr := dst.Write(buf[:nr])
if nw > 0 {
written += int64(nw)
} }
return
case <-t.C: if werr != nil {
m.mu.Lock() return written, werr
m.dst.Flush() }
m.mu.Unlock()
if nr != nw {
return written, io.ErrShortWrite
}
}
if rerr != nil {
if rerr == io.EOF {
rerr = nil
}
return written, rerr
} }
} }
} }
func (m *maxLatencyWriter) stop() {
m.done <- true
}
func (p *ReverseProxy) logf(format string, args ...interface{}) { func (p *ReverseProxy) logf(format string, args ...interface{}) {
if p.ErrorLog != nil { if p.ErrorLog != nil {
p.ErrorLog.Printf(format, args...) p.ErrorLog.Printf(format, args...)
@ -243,7 +246,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
} }
} }
func removeHeaders(header http.Header) { func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header. // Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" { if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") { for _, f := range strings.Split(c, ",") {
@ -260,10 +263,25 @@ func removeHeaders(header http.Header) {
} }
} }
if header.Get("A-Upgrade") != "" { //Restore the Upgrade header if any
header.Set("Upgrade", header.Get("A-Upgrade")) if header.Get("Zr-Origin-Upgrade") != "" {
header.Del("A-Upgrade") header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
} }
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
//Hide Go-HTTP-Client UA if the client didnt sent us one
if _, ok := header["User-Agent"]; !ok {
// If the outbound request doesn't have a User-Agent header set,
// don't send the default Go HTTP client User-Agent.
header.Set("User-Agent", "")
}
} }
func addXForwardedForHeader(req *http.Request) { func addXForwardedForHeader(req *http.Request) {
@ -281,6 +299,22 @@ func addXForwardedForHeader(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http") req.Header.Set("X-Forwarded-Proto", "http")
} }
if req.Header.Get("X-Real-Ip") == "" {
//Check if CF-Connecting-IP header exists
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
} else {
// Not exists. Fill it in with first entry in X-Forwarded-For
ips := strings.Split(clientIP, ",")
if len(ips) > 0 {
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
}
}
}
} }
} }
@ -323,7 +357,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header) copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
removeHeaders(outreq.Header) removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header. // Add X-Forwarded-For Header.
addXForwardedForHeader(outreq) addXForwardedForHeader(outreq)
@ -339,7 +373,13 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header) removeHeaders(res.Header, rrr.NoCache)
//Remove the User-Agent header if exists
if _, ok := res.Header["User-Agent"]; ok {
//Server to client request should not contains a User-Agent header
res.Header.Del("User-Agent")
}
if p.ModifyResponse != nil { if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil { if err := p.ModifyResponse(res); err != nil {
@ -352,6 +392,12 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
} }
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Custom header rewriter functions //Custom header rewriter functions
if res.Header.Get("Location") != "" { if res.Header.Get("Location") != "" {
locationRewrite := res.Header.Get("Location") locationRewrite := res.Header.Get("Location")
@ -400,7 +446,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
} }
p.copyResponse(rw, res.Body) //Get flush interval in real time and start copying the request
flushInterval := p.getFlushInterval(req, res)
p.copyResponse(rw, res.Body, flushInterval)
// close now, instead of defer, to populate res.Trailer // close now, instead of defer, to populate res.Trailer
res.Body.Close() res.Body.Close()
copyHeader(rw.Header(), res.Trailer) copyHeader(rw.Header(), res.Trailer)

View File

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

View File

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

View File

@ -22,27 +22,19 @@ import (
func NewDynamicProxy(option RouterOption) (*Router, error) { func NewDynamicProxy(option RouterOption) (*Router, error) {
proxyMap := sync.Map{} proxyMap := sync.Map{}
domainMap := sync.Map{}
thisRouter := Router{ thisRouter := Router{
Option: &option, Option: &option,
ProxyEndpoints: &proxyMap, ProxyEndpoints: &proxyMap,
SubdomainEndpoint: &domainMap, Running: false,
Running: false, server: nil,
server: nil, routingRules: []*RoutingRule{},
routingRules: []*RoutingRule{}, tldMap: map[string]int{},
tldMap: map[string]int{},
} }
thisRouter.mux = &ProxyHandler{ thisRouter.mux = &ProxyHandler{
Parent: &thisRouter, 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 return &thisRouter, nil
} }
@ -60,6 +52,12 @@ func (router *Router) UpdateTLSVersion(requireLatest bool) {
router.Restart() 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 // Update https redirect, which will require updates
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) { func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
router.Option.ForceHttpsRedirect = useRedirect router.Option.ForceHttpsRedirect = useRedirect
@ -70,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
func (router *Router) StartProxyService() error { func (router *Router) StartProxyService() error {
//Create a new server object //Create a new server object
if router.server != nil { if router.server != nil {
return errors.New("Reverse proxy server already running") return errors.New("reverse proxy server already running")
} }
//Check if root route is set //Check if root route is set
if router.Root == nil { if router.Root == nil {
return errors.New("Reverse proxy router root not set") 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 minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest { if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12 minVersion = tls.VersionTLS12
@ -95,27 +86,85 @@ func (router *Router) StartProxyService() error {
} }
if router.Option.UseTls { if router.Option.UseTls {
//Serve with TLS mode router.server = &http.Server{
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config) Addr: ":" + strconv.Itoa(router.Option.Port),
if err != nil { Handler: router.mux,
log.Println(err) TLSConfig: config,
router.Running = false
return err
} }
router.tlsListener = ln
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true 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 //Add a 80 to 443 redirector
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":80", Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
protocol := "https://" //Check if the domain requesting allow non TLS mode
if router.Option.Port == 443 { domainOnly := r.Host
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect) if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := router.getProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
if err == nil {
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
if isBlocked {
return
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
}
}
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 { } 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)
}
} }
}), }),
@ -143,7 +192,7 @@ func (router *Router) StartProxyService() error {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
//Unable to startup port 80 listener. Handle shutdown process gracefully //Unable to startup port 80 listener. Handle shutdown process gracefully
stopChan <- true stopChan <- true
log.Fatalf("Could not start server: %v\n", err) log.Fatalf("Could not start redirection server: %v\n", err)
} }
}() }()
router.tlsRedirectStop = stopChan router.tlsRedirectStop = stopChan
@ -152,8 +201,8 @@ func (router *Router) StartProxyService() error {
//Start the TLS server //Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)") log.Println("Reverse proxy service started in the background (TLS mode)")
go func() { go func() {
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed { if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start server: %v\n", err) log.Fatalf("Could not start proxy server: %v\n", err)
} }
}() }()
} else { } else {
@ -173,7 +222,7 @@ func (router *Router) StartProxyService() error {
func (router *Router) StopProxyService() error { func (router *Router) StopProxyService() error {
if router.server == nil { if router.server == nil {
return errors.New("Reverse proxy server already stopped") return errors.New("reverse proxy server already stopped")
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -201,13 +250,13 @@ func (router *Router) StopProxyService() error {
// Restart the current router if it is running. // Restart the current router if it is running.
func (router *Router) Restart() error { func (router *Router) Restart() error {
//Stop the router if it is already running //Stop the router if it is already running
var err error = nil
if router.Running { if router.Running {
err := router.StopProxyService() err := router.StopProxyService()
if err != nil { if err != nil {
return err return err
} }
time.Sleep(300 * time.Millisecond)
// Start the server // Start the server
err = router.StartProxyService() err = router.StartProxyService()
if err != nil { if err != nil {
@ -215,7 +264,7 @@ func (router *Router) Restart() error {
} }
} }
return err return nil
} }
/* /*
@ -228,128 +277,17 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
hostname = r.Host hostname = r.Host
} }
hostname = strings.Split(hostname, ":")[0] hostname = strings.Split(hostname, ":")[0]
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname) subdEndpoint := router.getProxyEndpointFromHostname(hostname)
return subdEndpoint != nil return subdEndpoint != nil
} }
/*
Add an URL into a custom proxy services
*/
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
/*
if rootname[len(rootname)-1:] == "/" {
rootname = rootname[:len(rootname)-1]
}
*/
webProxyEndpoint := domain
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
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,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
return nil
}
/* /*
Load routing from RP Load routing from RP
*/ */
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) { func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
if ptype == "vdir" { var targetProxyEndpoint *ProxyEndpoint
proxy, ok := router.ProxyEndpoints.Load(key) router.ProxyEndpoints.Range(func(key, value interface{}) bool {
if !ok { key, ok := key.(string)
return nil, errors.New("target proxy not found")
}
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")
}
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
}
return nil, errors.New("unsupported ptype")
}
/*
Add an default router for the proxy server
*/
func (router *Router) SetRootProxy(options *RootOptions) error {
proxyLocation := options.ProxyLocation
if proxyLocation[len(proxyLocation)-1:] == "/" {
proxyLocation = proxyLocation[:len(proxyLocation)-1]
}
webProxyEndpoint := proxyLocation
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
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,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint
return nil
}
// Helpers to export the syncmap for easier processing
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint)
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
k, ok := key.(string)
if !ok { if !ok {
return true return true
} }
@ -357,13 +295,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
if !ok { if !ok {
return true return true
} }
m[k] = v
if key == matchingDomain {
targetProxyEndpoint = v
}
return true return true
}) })
return m
if targetProxyEndpoint == nil {
return nil, errors.New("target routing rule not found")
}
return targetProxyEndpoint, nil
} }
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint { // Deep copy a proxy endpoint, excluding runtime paramters
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
js, _ := json.Marshal(endpoint)
newProxyEndpoint := ProxyEndpoint{}
err := json.Unmarshal(js, &newProxyEndpoint)
if err != nil {
return nil
}
return &newProxyEndpoint
}
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint) m := make(map[string]*ProxyEndpoint)
r.ProxyEndpoints.Range(func(key, value interface{}) bool { r.ProxyEndpoints.Range(func(key, value interface{}) bool {
k, ok := key.(string) k, ok := key.(string)

View File

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

View File

@ -1,68 +0,0 @@
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

@ -6,10 +6,12 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
"sort"
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy" "imuslab.com/zoraxy/mod/websocketproxy"
) )
@ -28,11 +30,61 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
return targetProxyEndpoint return targetProxyEndpoint
} }
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint { func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.SubdomainEndpoint.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
if ok { if ok {
//Exact hit
targetSubdomainEndpoint = ep.(*ProxyEndpoint) targetSubdomainEndpoint = ep.(*ProxyEndpoint)
if !targetSubdomainEndpoint.Disabled {
return targetSubdomainEndpoint
}
}
//No hit. Try with wildcard and alias
matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
if err != nil {
//Bad pattern. Skip this rule
return true
}
if match {
//Wildcard matches. Skip checking alias
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
//Wildcard not match. Check for alias
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
for _, aliasDomain := range ep.MatchingDomainAlias {
match, err := filepath.Match(aliasDomain, hostname)
if err != nil {
//Bad pattern. Skip this alias
continue
}
if match {
//This alias match
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
}
}
return true
})
if len(matchProxyEndpoints) == 1 {
//Only 1 match
return matchProxyEndpoints[0]
} else if len(matchProxyEndpoints) > 1 {
//More than one match. Get the best match one
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
})
return matchProxyEndpoints[0]
} }
return targetSubdomainEndpoint return targetSubdomainEndpoint
@ -54,14 +106,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
return rewrittenURL return rewrittenURL
} }
// Handle subdomain request // Handle host request
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.UserDefinedHeaders) > 0 {
for _, customHeader := range target.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists //Append / to the end of the redirection endpoint if not exists
@ -76,7 +136,10 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
} }
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain) h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations) wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
} }
@ -89,10 +152,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "", PathPrefix: "",
}) })
@ -113,15 +177,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
} }
// Handle vdir type request // Handle vdir type request
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI) rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
r.URL, _ = url.Parse(rewriteURL) r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.parent.UserDefinedHeaders) > 0 {
for _, customHeader := range target.parent.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/" wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
@ -131,7 +203,10 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String()) u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
} }
h.logRequest(r, true, 101, "vdir-websocket", target.Domain) h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations) wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
} }
@ -144,11 +219,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
PathPrefix: target.RootOrMatchingDomain, PathPrefix: target.MatchingPath,
}) })
var dnsError *net.DNSError var dnsError *net.DNSError
@ -171,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
if h.Parent.Option.StatisticCollector != nil { if h.Parent.Option.StatisticCollector != nil {
go func() { go func() {
requestInfo := statistic.RequestInfo{ requestInfo := statistic.RequestInfo{
IpAddr: geodb.GetRequesterIP(r), IpAddr: netutils.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r), RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ, Succ: succ,
StatusCode: statusCode, StatusCode: statusCode,

View File

@ -2,19 +2,25 @@ package redirection
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"sync" "sync"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
type RuleTable struct { type RuleTable struct {
AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity
Logger *logger.Logger
configPath string //The location where the redirection rules is stored configPath string //The location where the redirection rules is stored
rules sync.Map //Store the redirection rules for this reverse proxy instance rules sync.Map //Store the redirection rules for this reverse proxy instance
} }
type RedirectRules struct { type RedirectRules struct {
@ -24,10 +30,11 @@ type RedirectRules struct {
StatusCode int //Status Code for redirection StatusCode int //Status Code for redirection
} }
func NewRuleTable(configPath string) (*RuleTable, error) { func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
thisRuleTable := RuleTable{ thisRuleTable := RuleTable{
rules: sync.Map{}, rules: sync.Map{},
configPath: configPath, configPath: configPath,
AllowRegex: allowRegex,
} }
//Load all the rules from the config path //Load all the rules from the config path
if !utils.FileExists(configPath) { if !utils.FileExists(configPath) {
@ -77,7 +84,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
} }
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename // Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename) filepath := path.Join(t.configPath, filename)
@ -105,11 +112,12 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error { func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename // Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename) filepath := path.Join(t.configPath, filename)
fmt.Println(redirectURL, filename, filepath)
// Check if the file exists // Check if the file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) { if _, err := os.Stat(filepath); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to delete return nil // File doesn't exist, nothing to delete
@ -145,18 +153,47 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
// Iterate through all the keys in the rules map // Iterate through all the keys in the rules map
var targetRedirectionRule *RedirectRules = nil var targetRedirectionRule *RedirectRules = nil
var maxMatch int = 0 var maxMatch int = 0
t.rules.Range(func(key interface{}, value interface{}) bool { t.rules.Range(func(key interface{}, value interface{}) bool {
// Check if the requested URL starts with the key as a prefix // Check if the requested URL starts with the key as a prefix
if strings.HasPrefix(requestedURL, key.(string)) { if t.AllowRegex {
// This request URL matched the domain //Regexp matching rule
if len(key.(string)) > maxMatch { matched, err := regexp.MatchString(key.(string), requestedURL)
if err != nil {
//Something wrong with the regex?
t.log("Unable to match regex", err)
return true
}
if matched {
maxMatch = len(key.(string)) maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules) targetRedirectionRule = value.(*RedirectRules)
} }
} else {
//Default: prefix matching redirect
if strings.HasPrefix(requestedURL, key.(string)) {
// This request URL matched the domain
if len(key.(string)) > maxMatch {
maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules)
}
}
} }
return true return true
}) })
return targetRedirectionRule return targetRedirectionRule
} }
// Log the message to log file, use STDOUT if logger not set
func (t *RuleTable) log(message string, err error) {
if t.Logger == nil {
if err == nil {
log.Println("[Redirect] " + message)
} else {
log.Println("[Redirect] " + message + ": " + err.Error())
}
} else {
t.Logger.PrintAndLog("Redirect", message, err)
}
}

View File

@ -1,51 +0,0 @@
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

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

View File

@ -1,49 +0,0 @@
package dynamicproxy
import (
"log"
"net/url"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
Add an URL intoa custom subdomain service
*/
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
webProxyEndpoint := domain
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
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,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
})
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
return nil
}

View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
<meta charset="UTF-8">
<meta name="theme-color" content="#4b75ff">
<link rel="icon" type="image/png" href="img/small_icon.png"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
<title>404 - Host Not Found</title>
<style>
h1, h2, h3, h4, h5, p, a, span{
font-family: 'Noto Sans TC', sans-serif;
font-weight: 300;
color: rgb(88, 88, 88)
}
.diagram{
background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
padding-bottom: 2em;
}
.diagramHeader{
margin-top: 0.2em;
}
@media (max-width:512px) {
.widescreenOnly{
display: none !important;
}
.four.wide.column:not(.widescreenOnly){
width: 50% !important;
}
.ui.grid{
justify-content: center !important;
}
}
</style>
</head>
<body>
<div>
<br><br>
<div class="ui container">
<h1 style="font-size: 4rem;">Error 404</h1>
<p style="font-size: 2rem; margin-bottom: 0.4em;">Target Host Not Found</p>
<small id="timestamp"></small>
</div>
<br><br>
</div>
<div class="diagram">
<div class="ui text container">
<div class="ui grid">
<div class="four wide column widescreenOnly" align="center">
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
82.989,143.204 "/>
</svg>
<small>You</small>
<h2 class="diagramHeader">Browser</h2>
<p style="font-weight: 500; color: #9bca3e;">Working</p>
</div>
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
</div>
<div class="four wide column widescreenOnly" align="center">
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
83.971,148.123 "/>
</svg>
<small>Gateway Node</small>
<h2 class="diagramHeader">Reverse Proxy</h2>
<p style="font-weight: 500; color: #9bca3e;">Working</p>
</div>
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
</div>
<div class="four wide column" align="center">
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
</svg>
<small id="host"></small>
<h2 class="diagramHeader">Host</h2>
<p style="font-weight: 500; color: #bd2426;">Not Found</p>
</div>
</div>
</div>
</div>
<div>
<br>
<div class="ui container">
<div class="ui stackable grid">
<div class="eight wide column">
<h1>What happend?</h1>
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
</div>
<div class="eight wide column">
<h1>What can I do?</h1>
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
<p>Please try again in a few minutes</p>
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
<div class="ui bulleted list">
<div class="item">Check if the proxy rules that match this hostname exists</div>
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
</div>
</div>
</div>
</div>
<br>
</div>
<div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%">
<p>Powered by Zoraxy</p>
</div>
<br><br>
<script>
$("#timestamp").text(new Date());
$("#host").text(location.href);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"sync" "sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
@ -14,8 +15,9 @@ import (
) )
const ( const (
ProxyType_Subdomain = 0 ProxyType_Root = 0
ProxyType_Vdir = 1 ProxyType_Host = 1
ProxyType_Vdir = 2
) )
type ProxyHandler struct { type ProxyHandler struct {
@ -24,28 +26,30 @@ type ProxyHandler struct {
type RouterOption struct { type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder WebDirectory string //The static web server directory containing the templates folder
} }
type Router struct { type Router struct {
Option *RouterOption Option *RouterOption
ProxyEndpoints *sync.Map ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map Running bool
Running bool Root *ProxyEndpoint
Root *ProxyEndpoint mux http.Handler
RootRoutingOptions *RootRoutingOptions server *http.Server
mux http.Handler tlsListener net.Listener
server *http.Server routingRules []*RoutingRule
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool //Stop channel for tls redirection server tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json tldMap map[string]int //Top level domain map, see tld.json
@ -68,63 +72,76 @@ type BasicAuthExceptionRule struct {
PathPrefix string PathPrefix string
} }
// A proxy endpoint record // User defined headers to add into a proxy endpoint
type ProxyEndpoint struct { type UserDefinedHeader struct {
ProxyType int //The type of this proxy, see const def Key string
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key Value string
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
} }
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
proxy *dpcore.ReverseProxy `json:"-"`
parent *ProxyEndpoint `json:"-"`
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
Domain string //Domain or IP to proxy to
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Access Control
AccessFilterUUID string //Access filter ID
Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements
parent *Router `json:"-"`
proxy *dpcore.ReverseProxy `json:"-"`
}
/*
Routing type specific interface
These are options that only avaible for a specific interface
when running, these are converted into "ProxyEndpoint" objects
for more generic routing logic
*/
// Root options are those that are required for reverse proxy handler to work // Root options are those that are required for reverse proxy handler to work
type RootOptions struct { const (
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here DefaultSite_InternalStaticWebServer = 0
RequireTLS bool //Proxy root target require TLS connection (not recommended) DefaultSite_ReverseProxy = 1
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended) DefaultSite_Redirect = 2
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED DefaultSite_NotFoundPage = 3
)
//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
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
/* /*
Web Templates Web Templates
@ -132,4 +149,6 @@ Web Templates
var ( var (
//go:embed templates/forbidden.html //go:embed templates/forbidden.html
page_forbidden []byte page_forbidden []byte
//go:embed templates/hosterror.html
page_hosterror []byte
) )

View File

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

View File

@ -0,0 +1,109 @@
package cproxy
import (
"net/http"
"time"
)
func New(options ...option) http.Handler {
var this configuration
Options.apply(options...)(&this)
return newHandler(this.Filter, this.ClientConnector, this.ServerConnector, this.Monitor)
}
var Options singleton
type singleton struct{}
type option func(*configuration)
type configuration struct {
DialTimeout time.Duration
Filter Filter
DialAddress string
Dialer Dialer
LogConnections bool
ProxyProtocol bool
Initializer initializer
ClientConnector clientConnector
ServerConnector serverConnector
Monitor monitor
Logger logger
}
func (singleton) DialTimeout(value time.Duration) option {
return func(this *configuration) { this.DialTimeout = value }
}
func (singleton) Filter(value Filter) option {
return func(this *configuration) { this.Filter = value }
}
func (singleton) ClientConnector(value clientConnector) option {
return func(this *configuration) { this.ClientConnector = value }
}
func (singleton) DialAddress(value string) option {
return func(this *configuration) { this.DialAddress = value }
}
func (singleton) Dialer(value Dialer) option {
return func(this *configuration) { this.Dialer = value }
}
func (singleton) LogConnections(value bool) option {
return func(this *configuration) { this.LogConnections = value }
}
func (singleton) ProxyProtocol(value bool) option {
return func(this *configuration) { this.ProxyProtocol = value }
}
func (singleton) Initializer(value initializer) option {
return func(this *configuration) { this.Initializer = value }
}
func (singleton) ServerConnector(value serverConnector) option {
return func(this *configuration) { this.ServerConnector = value }
}
func (singleton) Monitor(value monitor) option {
return func(this *configuration) { this.Monitor = value }
}
func (singleton) Logger(value logger) option {
return func(this *configuration) { this.Logger = value }
}
func (singleton) apply(options ...option) option {
return func(this *configuration) {
for _, item := range Options.defaults(options...) {
item(this)
}
if this.Dialer == nil {
this.Dialer = newDialer(this)
}
this.Dialer = newRoutingDialer(this)
if this.ProxyProtocol {
this.Initializer = newProxyProtocolInitializer()
}
if this.Initializer == nil {
this.Initializer = nop{}
}
this.Initializer = newLoggingInitializer(this)
if this.ServerConnector == nil {
this.ServerConnector = newServerConnector(this.Dialer, this.Initializer)
}
}
}
func (singleton) defaults(options ...option) []option {
return append([]option{
Options.DialTimeout(time.Second * 10),
Options.Filter(newFilter()),
Options.ClientConnector(newClientConnector()),
Options.Initializer(nop{}),
Options.Monitor(nop{}),
Options.Logger(nop{}),
}, options...)
}
type nop struct{}
func (nop) Measure(int) {}
func (nop) Printf(string, ...interface{}) {}
func (nop) Initialize(Socket, Socket) bool { return true }

View File

@ -0,0 +1,19 @@
package cproxy
import "net/http"
type defaultClientConnector struct{}
func newClientConnector() *defaultClientConnector {
return &defaultClientConnector{}
}
func (this *defaultClientConnector) Connect(response http.ResponseWriter) Socket {
if hijacker, ok := response.(http.Hijacker); !ok {
return nil
} else if socket, _, _ := hijacker.Hijack(); socket == nil {
return nil // this 'else if' exists to avoid the pointer nil != interface nil issue
} else {
return socket
}
}

View File

@ -0,0 +1,25 @@
package cproxy
import (
"net"
"time"
)
type defaultDialer struct {
timeout time.Duration
logger logger
}
func newDialer(config *configuration) *defaultDialer {
return &defaultDialer{timeout: config.DialTimeout, logger: config.Logger}
}
func (this *defaultDialer) Dial(address string) Socket {
if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil {
return socket
} else {
this.logger.Printf("[INFO] Unable to establish connection to [%s]: %s", address, err)
}
return nil
}

View File

@ -0,0 +1,9 @@
package cproxy
import "net/http"
type defaultFilter struct{}
func newFilter() *defaultFilter { return &defaultFilter{} }
func (this *defaultFilter) IsAuthorized(http.ResponseWriter, *http.Request) bool { return true }

View File

@ -0,0 +1,56 @@
package cproxy
import "net/http"
type defaultHandler struct {
filter Filter
clientConnector clientConnector
serverConnector serverConnector
meter monitor
}
func newHandler(filter Filter, clientConnector clientConnector, serverConnector serverConnector, meter monitor) *defaultHandler {
return &defaultHandler{
filter: filter,
clientConnector: clientConnector,
serverConnector: serverConnector,
meter: meter,
}
}
func (this *defaultHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
this.meter.Measure(MeasurementHTTPRequest)
if request.Method != "CONNECT" {
this.meter.Measure(MeasurementBadMethod)
writeResponseStatus(response, http.StatusMethodNotAllowed)
} else if !this.filter.IsAuthorized(response, request) {
this.meter.Measure(MeasurementUnauthorizedRequest)
//writeResponseStatus(response, http.StatusUnauthorized)
} else if client := this.clientConnector.Connect(response); client == nil {
this.meter.Measure(MeasurementClientConnectionFailed)
writeResponseStatus(response, http.StatusNotImplemented)
} else if connection := this.serverConnector.Connect(client, request.URL.Host); connection == nil {
this.meter.Measure(MeasurementServerConnectionFailed)
_, _ = client.Write(statusBadGateway)
_ = client.Close()
} else {
this.meter.Measure(MeasurementProxyReady)
_, _ = client.Write(statusReady)
connection.Proxy()
this.meter.Measure(MeasurementProxyComplete)
}
}
func writeResponseStatus(response http.ResponseWriter, statusCode int) {
http.Error(response, http.StatusText(statusCode), statusCode)
}
var (
statusBadGateway = []byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")
statusReady = []byte("HTTP/1.1 200 OK\r\n\r\n")
)

View File

@ -0,0 +1,54 @@
package cproxy
import (
"io"
"sync"
)
type defaultProxy struct {
client Socket
server Socket
waiter *sync.WaitGroup
}
func newProxy(client, server Socket) *defaultProxy {
waiter := &sync.WaitGroup{}
waiter.Add(2) // wait on both client->server and server->client streams
return &defaultProxy{
waiter: waiter,
client: client,
server: server,
}
}
func (this *defaultProxy) Proxy() {
go this.streamAndClose(this.client, this.server)
go this.streamAndClose(this.server, this.client)
this.closeSockets()
}
func (this *defaultProxy) streamAndClose(reader, writer Socket) {
_, _ = io.Copy(writer, reader)
tryCloseRead(reader)
tryCloseWrite(writer)
this.waiter.Done()
}
func tryCloseRead(socket Socket) {
if tcp, ok := socket.(tcpSocket); ok {
_ = tcp.CloseRead()
}
}
func tryCloseWrite(socket Socket) {
if tcp, ok := socket.(tcpSocket); ok {
_ = tcp.CloseWrite()
}
}
func (this *defaultProxy) closeSockets() {
this.waiter.Wait()
_ = this.client.Close()
_ = this.server.Close()
}

View File

@ -0,0 +1,24 @@
package cproxy
type defaultServerConnector struct {
dialer Dialer
initializer initializer
}
func newServerConnector(dialer Dialer, initializer initializer) *defaultServerConnector {
return &defaultServerConnector{dialer: dialer, initializer: initializer}
}
func (this *defaultServerConnector) Connect(client Socket, serverAddress string) proxy {
server := this.dialer.Dial(serverAddress)
if server == nil {
return nil
}
if !this.initializer.Initialize(client, server) {
_ = server.Close()
return nil
}
return newProxy(client, server)
}

View File

@ -0,0 +1,32 @@
package cproxy
import "net/http"
type hostnameFilter struct {
authorized []string
}
func NewHostnameFilter(authorized []string) Filter {
return &hostnameFilter{authorized: authorized}
}
func (this hostnameFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool {
if len(this.authorized) == 0 {
return true
}
host := request.URL.Host
hostLength := len(host)
for _, authorized := range this.authorized {
if authorized[:2] == "*." {
have, want := hostLength, len(authorized)-1
if have > want && authorized[1:] == host[hostLength-want:] {
return true
}
} else if authorized == host {
return true
}
}
return false
}

View File

@ -0,0 +1,26 @@
package cproxy
import (
"net/http"
"strings"
)
type hostnameSuffixFilter struct {
authorized []string
}
func NewHostnameSuffixFilter(authorized []string) Filter {
return &hostnameSuffixFilter{authorized: authorized}
}
func (this hostnameSuffixFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool {
host := request.URL.Host
for _, authorized := range this.authorized {
if strings.HasSuffix(host, authorized) {
return true
}
}
return false
}

View File

@ -0,0 +1,67 @@
package cproxy
import (
"io"
"net"
"net/http"
)
type (
Filter interface {
IsAuthorized(http.ResponseWriter, *http.Request) bool
}
clientConnector interface {
Connect(http.ResponseWriter) Socket
}
)
type (
Dialer interface {
Dial(string) Socket
}
serverConnector interface {
Connect(Socket, string) proxy
}
initializer interface {
Initialize(Socket, Socket) bool
}
proxy interface {
Proxy()
}
)
type (
Socket interface {
io.ReadWriteCloser
RemoteAddr() net.Addr
}
tcpSocket interface {
Socket
CloseRead() error
CloseWrite() error
}
)
type (
monitor interface {
Measure(int)
}
logger interface {
Printf(string, ...interface{})
}
)
const (
MeasurementHTTPRequest int = iota
MeasurementBadMethod
MeasurementUnauthorizedRequest
MeasurementClientConnectionFailed
MeasurementServerConnectionFailed
MeasurementProxyReady
MeasurementProxyComplete
)

View File

@ -0,0 +1,24 @@
package cproxy
type loggingInitializer struct {
logger logger
inner initializer
}
func newLoggingInitializer(config *configuration) initializer {
if !config.LogConnections {
return config.Initializer
}
return &loggingInitializer{inner: config.Initializer, logger: config.Logger}
}
func (this *loggingInitializer) Initialize(client, server Socket) bool {
result := this.inner.Initialize(client, server)
if !result {
this.logger.Printf("[INFO] Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr())
}
return result
}

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