Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
05297d854b | |||
0d7bce4d30 | |||
8db95dddc6 | |||
05daeded37 | |||
8ce6471be5 | |||
e242c9288f | |||
c55a29e7cf | |||
6af047430c | |||
200c924acd | |||
9b2168466c | |||
7ae48bf370 | |||
ee3d76fb96 | |||
40d192524b | |||
c659e05005 | |||
676a45c222 | |||
1da0761b13 | |||
32939874f2 | |||
43a4bf389a | |||
33c7c5fa00 | |||
216b53f224 | |||
059b0a2e1c | |||
3ab952f168 | |||
4f676d6770 | |||
e980bc847b | |||
174efc9080 | |||
3228789375 | |||
36e461795a | |||
d6e7641364 | |||
15cebd6e06 | |||
e9a074d4d1 | |||
4b7fd39e57 | |||
fa005f1327 | |||
c7a9f40baa | |||
d5b9726158 | |||
f659e66cf7 | |||
801bdbf298 | |||
09da93cfb3 | |||
70ace02e80 | |||
1f758e953d | |||
ffad2cab81 | |||
dbb10644de | |||
4848392185 | |||
956f4ac30f | |||
c09ff28fd5 | |||
4d40e0aa38 | |||
045e66b631 | |||
62e60d78de | |||
23bdaa1517 |
17
.github/workflows/main.yml
vendored
@ -9,18 +9,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Dockerhub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: Login to Docker & GHCR
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
@ -36,11 +37,5 @@ jobs:
|
||||
--provenance=false \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--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 \
|
||||
.
|
||||
|
50
CHANGELOG.md
@ -1,3 +1,37 @@
|
||||
# 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)
|
||||
@ -14,20 +48,20 @@
|
||||
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
|
||||
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
|
||||
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
|
||||
+ Added custom acme feature in 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)
|
||||
+ 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
|
||||
|
||||
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
|
||||
+ Fixed potential memory leak in acme handler logic
|
||||
+ Added "Do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
|
||||
+ Fixed potential memory leak in ACME handler logic
|
||||
+ Added "Do you want to get a TLS certificate for this subdomain?" dialogue when a new subdomain proxy rule is created
|
||||
|
||||
# v2.6.5 Jul 19 2023
|
||||
|
||||
+ Added Import / Export-Feature
|
||||
+ Moved 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)
|
||||
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
|
||||
+ Added Whois
|
||||
@ -37,7 +71,7 @@
|
||||
+ Added force TLS v1.2 above toggle
|
||||
+ Added trace route
|
||||
+ 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
|
||||
+ Optimized UI for TCP Proxy
|
||||
|
||||
@ -47,7 +81,7 @@
|
||||
+ Split blacklist and whitelist from geodb script file
|
||||
+ Optimized compile binary size
|
||||
+ Added access control to TCP proxy
|
||||
+ Added "invalid config detect" in up time monitor for isse [#7](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
|
||||
+ Reduced file size of embedded materials
|
||||
|
||||
@ -74,6 +108,6 @@
|
||||
+ Basic auth
|
||||
+ Support TLS verification skip (for self signed certs)
|
||||
+ Added trend analysis
|
||||
+ Added referer and file type analysis
|
||||
+ Added referrer and file type analysis
|
||||
+ Added cert expire day display
|
||||
+ Moved subdomain proxy logic to dpcore
|
||||
|
128
README.md
@ -2,24 +2,28 @@
|
||||
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
||||
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||
|
||||
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
- Reverse Proxy
|
||||
|
||||
- Subdomain Reverse Proxy
|
||||
|
||||
- Virtual Directory Reverse Proxy
|
||||
- Virtual Directory
|
||||
- Basic Auth
|
||||
- Custom Headers
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (one certificate contains multiple host names)
|
||||
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- TCP Tunneling / Proxy
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Utilities
|
||||
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- IP Scanner
|
||||
@ -28,10 +32,17 @@ General purpose request (reverse) proxy and forwarding tool for low power device
|
||||
- External permission management system for easy system integration
|
||||
- SMTP config for password reset
|
||||
|
||||
## Build from Source
|
||||
Require Go 1.20 or above
|
||||
## Downloads
|
||||
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
|
||||
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
|
||||
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
|
||||
|
||||
```
|
||||
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
|
||||
## Build from Source
|
||||
Requires Go 1.22 or higher
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
cd ./zoraxy/src/
|
||||
go mod tidy
|
||||
@ -42,11 +53,11 @@ sudo ./zoraxy -port=:8000
|
||||
|
||||
## Usage
|
||||
|
||||
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
|
||||
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start growing their web services into multiple servers.
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
|
||||
|
||||
#### Linux
|
||||
|
||||
@ -60,18 +71,49 @@ Download the binary executable and double click the binary file to start it.
|
||||
|
||||
#### Raspberry Pi
|
||||
|
||||
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
The installation method is same as Linux. If you are using a Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
|
||||
#### Other ARM SBCs or Android phone with Termux
|
||||
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||
|
||||
### Start Paramters
|
||||
```
|
||||
Usage of zoraxy:
|
||||
-autorenew int
|
||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-info
|
||||
Show information about this program in JSON
|
||||
-log
|
||||
Log terminal output to file (default true)
|
||||
-mdns
|
||||
Enable mDNS scanner and transponder (default true)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-sshlb
|
||||
Allow loopback web ssh connection (DANGER)
|
||||
-version
|
||||
Show version of this server
|
||||
-webfm
|
||||
Enable web file manager for static web server root folder (default true)
|
||||
-webroot string
|
||||
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
||||
-ztauth string
|
||||
ZeroTier authtoken for the local node
|
||||
-ztport int
|
||||
ZeroTier controller API port (default 9993)
|
||||
```
|
||||
|
||||
### External Permission Management Mode
|
||||
|
||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag
|
||||
If you already have an upstream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag:
|
||||
|
||||
```bash
|
||||
./zoraxy -noauth=true
|
||||
@ -79,45 +121,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.*
|
||||
|
||||
#### 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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||
|
||||
## FAQ
|
||||
@ -128,22 +137,21 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
|
||||
|
||||
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
||||
|
||||
Assuming you already have a valid license, to use Zoraxy with ZeroTier, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken at correct location in your host.
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
|
||||
|
||||
```
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
```
|
||||
|
||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||
|
||||
This allows you to have infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
|
||||
## Web.SSH
|
||||
|
||||
Web SSH currently only support Linux based OS. The following platforms are supported
|
||||
## Web SSH
|
||||
|
||||
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
@ -151,9 +159,9 @@ Web SSH currently only support Linux based OS. The following platforms are suppo
|
||||
|
||||
### Loopback Connection
|
||||
|
||||
Loopback web ssh connection, by default, is disabled. This means that if you are trying to connect to address like 127.0.0.1 or localhost, the system will reject your connection due to security issues. To enable loopback for testing or development purpose, use the following flags to override the loopback checking.
|
||||
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||
|
||||
```
|
||||
```bash
|
||||
./zoraxy -sshlb=true
|
||||
```
|
||||
|
||||
@ -165,5 +173,5 @@ If you like the project and want to support us, please consider a donation. You
|
||||
|
||||
## 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.**
|
||||
|
||||
|
@ -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. |
|
||||
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
|
||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
|
||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
|
||||
|
||||
## Examples: </br>
|
||||
### Docker Run </br>
|
||||
|
BIN
docs/img/bg.png
Before Width: | Height: | Size: 4.5 MiB |
BIN
docs/img/bg2.png
Before Width: | Height: | Size: 9.4 MiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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
After Width: | Height: | Size: 202 KiB |
Before Width: | Height: | Size: 42 KiB |
BIN
docs/img/screenshots/10.png
Normal file
After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 62 KiB |
BIN
docs/img/screenshots/2.png
Normal file
After Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 32 KiB |
BIN
docs/img/screenshots/3.png
Normal file
After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/img/screenshots/4.png
Normal file
After Width: | Height: | Size: 203 KiB |
Before Width: | Height: | Size: 41 KiB |
BIN
docs/img/screenshots/5.png
Normal file
After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 46 KiB |
BIN
docs/img/screenshots/6.png
Normal file
After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 48 KiB |
BIN
docs/img/screenshots/7.png
Normal file
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 55 KiB |
BIN
docs/img/screenshots/8.png
Normal file
After Width: | Height: | Size: 185 KiB |
Before Width: | Height: | Size: 68 KiB |
BIN
docs/img/screenshots/9.png
Normal file
After Width: | Height: | Size: 867 KiB |
Before Width: | Height: | Size: 153 KiB |
@ -8,7 +8,7 @@
|
||||
<meta name="author" content="tobychui">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
@ -74,21 +74,16 @@
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- 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 id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui 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>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
@ -126,6 +121,22 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
@ -148,7 +159,7 @@
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use, noobs friendly reverse proxy server that can be easily set-up using a web form and a few toggle switches.</p>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -158,7 +169,7 @@
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most of the simple use cases.</p>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -168,7 +179,7 @@
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allow easy setup for regional services.</p>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -189,7 +200,7 @@
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integrated with Gotty Web SSH terminal, allow one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -199,7 +210,7 @@
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real time analytic tools, provide you the best insights of visitors data without cookies.</p>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -209,7 +220,7 @@
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovering service, enable automatic service discovery within LAN.</p>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -219,7 +230,7 @@
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -240,34 +251,34 @@
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<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 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 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 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 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 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 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 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 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 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>
|
||||
|
135
docs/style.css
@ -1,5 +1,5 @@
|
||||
body{
|
||||
background: #f6f6f6 !important;
|
||||
background: #ffffff !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
@ -18,7 +18,7 @@ body{
|
||||
.left-menu {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
background-color: #ffffff;
|
||||
background-color: #fcfcfc;
|
||||
min-height: 100vh;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
@ -48,17 +48,19 @@ body{
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
width: 100%;
|
||||
border-right: 0.4em solid var(--themeTextColor);
|
||||
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.menu-item.active{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
background-color: #f0f8ff;
|
||||
background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
|
||||
}
|
||||
|
||||
.menu-item .item-icon{
|
||||
fill: #fcfcfc;
|
||||
}
|
||||
|
||||
.menu-item:hover{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
background: rgba(35,35,35,0.1);
|
||||
}
|
||||
|
||||
.menu-item img{
|
||||
@ -69,18 +71,6 @@ body{
|
||||
|
||||
|
||||
/* 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{
|
||||
padding-top: 15em;
|
||||
@ -91,11 +81,13 @@ body{
|
||||
.bannerHeader{
|
||||
font-size: 8em;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bannerSubheader{
|
||||
font-weight: 400;
|
||||
font-size: 1.2em;
|
||||
color: #ebebeb;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
@ -104,6 +96,21 @@ body{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#home{
|
||||
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
|
||||
}
|
||||
|
||||
#home .table th, #home .table h4{
|
||||
color: white;
|
||||
}
|
||||
|
||||
#home .table h4 .content, #home .table h4 .sub.header{
|
||||
color: white;
|
||||
}
|
||||
#home .table td a{
|
||||
color: #d6ddff;
|
||||
}
|
||||
|
||||
/* features */
|
||||
#features{
|
||||
padding-top: 4em;
|
||||
@ -173,56 +180,58 @@ body{
|
||||
}
|
||||
}
|
||||
|
||||
/* Decorative Animation */
|
||||
.dot-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 2em;
|
||||
}
|
||||
|
||||
.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;
|
||||
/*
|
||||
Waves CSS
|
||||
*/
|
||||
|
||||
#wavesWrapper{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
.waves {
|
||||
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) {
|
||||
animation-delay: 2s;
|
||||
.parallax > use {
|
||||
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(4) {
|
||||
animation-delay: 3s;
|
||||
.parallax > use:nth-child(1) {
|
||||
animation-delay: -8s;
|
||||
animation-duration: 28s;
|
||||
}
|
||||
|
||||
@keyframes dot-animation {
|
||||
0% {
|
||||
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(2) {
|
||||
animation-delay: -12s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
.parallax > use:nth-child(3) {
|
||||
animation-delay: -16s;
|
||||
animation-duration: 52s;
|
||||
}
|
||||
.parallax > use:nth-child(4) {
|
||||
animation-delay: -20s;
|
||||
animation-duration: 80s;
|
||||
}
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px,0,0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(85px,0,0);
|
||||
}
|
||||
}
|
||||
/*Shrinking for mobile*/
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height:40px;
|
||||
min-height:40px;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 74 KiB |
@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
resulst := []*geodb.WhitelistEntry{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
@ -134,7 +136,11 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode)
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -158,7 +164,11 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToWhiteList(ipAddr)
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
geodbStore.AddIPToWhiteList(ipAddr, comment)
|
||||
}
|
||||
|
||||
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -103,6 +103,9 @@ 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)")
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
|
21
src/api.go
@ -47,6 +47,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
@ -56,9 +57,16 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
//Reverse proxy root related APIs
|
||||
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
|
||||
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
|
||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||
//Reverse proxy virtual directory APIs
|
||||
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||
//Reverse proxy user define header apis
|
||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||
//Reverse proxy auth related APIs
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||
@ -77,6 +85,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||
|
||||
//Blacklist APIs
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
@ -115,6 +124,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
||||
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
@ -154,6 +165,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
||||
|
||||
//Account Reset
|
||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
@ -175,7 +188,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||
if *allowWebFileManager {
|
||||
//Web Directory Manager file operation functions
|
||||
|
@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
results := []*CertInfo{}
|
||||
|
||||
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")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
172
src/config.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -35,97 +36,118 @@ type Record struct {
|
||||
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
// Save a reverse proxy config record to file
|
||||
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//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)
|
||||
/*
|
||||
Load Reverse Proxy Config from file and append it to current runtime proxy router
|
||||
*/
|
||||
func LoadReverseProxyConfig(configFilepath string) error {
|
||||
//Load the config file from disk
|
||||
endpointConfig, err := os.ReadFile(configFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SaveReverseProxyConfigToFile(recordToSave)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfigFile(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
SystemWideLogger.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
|
||||
return err
|
||||
}
|
||||
//Parse it into dynamic proxy endpoint
|
||||
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||
if err != nil {
|
||||
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 ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{
|
||||
ProxyType: "",
|
||||
Rootname: "",
|
||||
ProxyTarget: "",
|
||||
UseTLS: false,
|
||||
func filterProxyConfigFilename(filename string) string {
|
||||
//Filter out wildcard characters
|
||||
filename = strings.ReplaceAll(filename, "*", "(ST)")
|
||||
filename = strings.ReplaceAll(filename, "?", "(QM)")
|
||||
filename = strings.ReplaceAll(filename, "[", "(OB)")
|
||||
filename = strings.ReplaceAll(filename, "]", "(CB)")
|
||||
filename = strings.ReplaceAll(filename, "#", "(HT)")
|
||||
return filepath.ToSlash(filename)
|
||||
}
|
||||
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//Get filename for saving
|
||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
//Save config to file
|
||||
js, err := json.MarshalIndent(endpoint, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(endpoint string) error {
|
||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
||||
if endpoint == "/" {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
if !utils.FileExists(filename) {
|
||||
return errors.New("target endpoint not exists")
|
||||
}
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// Get the default root config that point to the internal static web server
|
||||
// this will be used if root config is not found (new deployment / missing root.config file)
|
||||
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
//Default settings
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
|
||||
ProxyType: dynamicproxy.ProxyType_Root,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
|
||||
RequireTLS: false,
|
||||
BypassGlobalTLS: false,
|
||||
SkipTlsValidation: false,
|
||||
SkipCertValidations: false,
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(filename)
|
||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||
DefaultSiteValue: "",
|
||||
})
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Unmarshal the content into config
|
||||
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,
|
||||
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
|
||||
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
|
||||
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: targetProxyEndpoint.BasicAuthExceptionRules,
|
||||
}
|
||||
|
||||
return &thisProxyConfigRecord, nil
|
||||
}
|
||||
|
||||
func getFilenameFromRootName(rootname string) string {
|
||||
//Generate a filename for this rootname
|
||||
filename := strings.ReplaceAll(rootname, ".", "_")
|
||||
filename = strings.ReplaceAll(filename, "/", "-")
|
||||
filename = filename + ".config"
|
||||
return filename
|
||||
return rootProxyEndpoint, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
34
src/go.mod
@ -1,18 +1,34 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.16
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
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/google/uuid v1.3.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
)
|
||||
|
1805
src/go.sum
29
src/main.go
@ -13,11 +13,11 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@ -35,6 +35,7 @@ import (
|
||||
)
|
||||
|
||||
// General flags
|
||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
@ -49,7 +50,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.8"
|
||||
version = "3.0.1"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -63,7 +64,6 @@ var (
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
@ -80,6 +80,7 @@ var (
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
@ -128,20 +129,8 @@ func ShutdownSeq() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
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},
|
||||
})
|
||||
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
@ -166,7 +155,7 @@ func main() {
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
@ -179,8 +168,8 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL.
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
|
@ -1,9 +1,6 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -26,16 +23,10 @@ import (
|
||||
- Vitrual Directory Routing
|
||||
*/
|
||||
|
||||
var (
|
||||
//go:embed tld.json
|
||||
rawTldMap []byte
|
||||
)
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Special Routing Rules, bypass most of the limitations
|
||||
*/
|
||||
|
||||
//Check if there are external routing rule matches.
|
||||
//If yes, route them via external rr
|
||||
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
||||
@ -53,10 +44,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Inject headers
|
||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
@ -80,38 +73,45 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
/*
|
||||
Subdomain Routing
|
||||
Host Routing
|
||||
*/
|
||||
if strings.Contains(r.Host, ".") {
|
||||
//This might be a subdomain. See if there are any subdomain proxy router for this
|
||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.subdomainRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Routing
|
||||
*/
|
||||
//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)
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
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 + "/")
|
||||
if potentialProxtEndpoint != nil {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
@ -136,52 +136,71 @@ Once entered this routing segment, the root routing options will take over
|
||||
for the routing logic.
|
||||
*/
|
||||
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
|
||||
//Route to custom domain
|
||||
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
|
||||
//Not set. Redirect to first level of domain redirectable
|
||||
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
|
||||
if err != nil {
|
||||
//Redirect to proxy root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
|
||||
}
|
||||
//Get the proxy root config
|
||||
proot := h.Parent.Root
|
||||
switch proot.DefaultSiteOption {
|
||||
case DefaultSite_InternalStaticWebServer:
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if h.isTopLevelRedirectableDomain(domainOnly) {
|
||||
//This is requesting a top level private domain that should be serving root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
//Validate the redirection target URL
|
||||
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
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)
|
||||
|
||||
//No vdir match. Route via root router
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
case DefaultSite_Redirect:
|
||||
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
|
||||
if redirectTarget == "" {
|
||||
redirectTarget = "about:blank"
|
||||
}
|
||||
|
||||
//Check if it is an infinite loopback redirect
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
return
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
if hostname == domainOnly {
|
||||
h.logRequest(r, false, 500, "root-redirect", domainOnly)
|
||||
http.Error(w, "Loopback redirects due to invalid settings", 500)
|
||||
return
|
||||
}
|
||||
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||
case DefaultSite_NotFoundPage:
|
||||
//Serve the not found page, use template if exists
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||
if err != nil {
|
||||
w.Write(page_hosterror)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,44 +238,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -26,10 +26,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()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
@ -48,7 +44,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if !matchingFound {
|
||||
h.logRequest(r, false, 401, proxyType, pe.Domain)
|
||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@ -8,12 +9,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var onExitFlushLoop func()
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
// client, support http, also support https tunnel using http.hijacker
|
||||
@ -60,6 +58,7 @@ type ResponseRewriteRuleSet struct {
|
||||
ProxyDomain string
|
||||
OriginalHost string
|
||||
UseTLS bool
|
||||
NoCache bool
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
}
|
||||
|
||||
@ -67,7 +66,12 @@ type requestCanceler interface {
|
||||
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
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
@ -79,10 +83,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
||||
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
|
||||
@ -94,16 +94,17 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).DisableCompression = true
|
||||
|
||||
if ignoreTLSVerification {
|
||||
if dpcOptions.IgnoreTLSVerification {
|
||||
//Ignore TLS certificate validation error
|
||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
return &ReverseProxy{
|
||||
Director: director,
|
||||
Prepender: prepender,
|
||||
Verbal: false,
|
||||
Transport: thisTransporter,
|
||||
Director: director,
|
||||
Prepender: prepender,
|
||||
FlushInterval: dpcOptions.FlushInterval,
|
||||
Verbal: false,
|
||||
Transport: thisTransporter,
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,64 +178,66 @@ var hopHeaders = []string{
|
||||
//"Upgrade",
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
|
||||
if p.FlushInterval != 0 {
|
||||
if wf, ok := dst.(writeFlusher); ok {
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: wf,
|
||||
latency: p.FlushInterval,
|
||||
done: make(chan bool),
|
||||
}
|
||||
|
||||
go mlw.flushLoop()
|
||||
defer mlw.stop()
|
||||
dst = mlw
|
||||
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
|
||||
func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
||||
var w io.Writer = dst
|
||||
if flushInterval != 0 {
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: dst,
|
||||
flush: http.NewResponseController(dst).Flush,
|
||||
latency: flushInterval,
|
||||
}
|
||||
|
||||
defer mlw.stop()
|
||||
// set up initial timer so headers get flushed even if body writes are delayed
|
||||
mlw.flushPending = true
|
||||
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
|
||||
w = mlw
|
||||
}
|
||||
|
||||
io.Copy(dst, src)
|
||||
var buf []byte
|
||||
_, err := p.copyBuffer(w, src, buf)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
type writeFlusher interface {
|
||||
io.Writer
|
||||
http.Flusher
|
||||
}
|
||||
// Copy with given buffer size. Default to 64k
|
||||
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
||||
if len(buf) == 0 {
|
||||
buf = make([]byte, 64*1024)
|
||||
}
|
||||
|
||||
type maxLatencyWriter struct {
|
||||
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()
|
||||
var written int64
|
||||
for {
|
||||
select {
|
||||
case <-m.done:
|
||||
if onExitFlushLoop != nil {
|
||||
onExitFlushLoop()
|
||||
nr, rerr := src.Read(buf)
|
||||
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
|
||||
p.logf("dpcore read error during body copy: %v", rerr)
|
||||
}
|
||||
|
||||
if nr > 0 {
|
||||
nw, werr := dst.Write(buf[:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
return
|
||||
case <-t.C:
|
||||
m.mu.Lock()
|
||||
m.dst.Flush()
|
||||
m.mu.Unlock()
|
||||
|
||||
if werr != nil {
|
||||
return written, werr
|
||||
}
|
||||
|
||||
if nr != nw {
|
||||
return written, io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
|
||||
if rerr != nil {
|
||||
if rerr == io.EOF {
|
||||
rerr = nil
|
||||
}
|
||||
return written, rerr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) stop() {
|
||||
m.done <- true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
if p.ErrorLog != nil {
|
||||
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.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
@ -260,10 +263,25 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
|
||||
//Disable cache if nocache is set
|
||||
if noCache {
|
||||
header.Del("Cache-Control")
|
||||
header.Set("Cache-Control", "no-store")
|
||||
}
|
||||
|
||||
//Hide Go-HTTP-Client UA if the client didnt sent us one
|
||||
if _, ok := header["User-Agent"]; !ok {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
// don't send the default Go HTTP client User-Agent.
|
||||
header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func addXForwardedForHeader(req *http.Request) {
|
||||
@ -281,6 +299,22 @@ func addXForwardedForHeader(req *http.Request) {
|
||||
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)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
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 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
|
||||
if 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
|
||||
res.Body.Close()
|
||||
copyHeader(rw.Header(), res.Trailer)
|
||||
|
38
src/mod/dynamicproxy/dpcore/flush.go
Normal 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 == "")
|
||||
}
|
73
src/mod/dynamicproxy/dpcore/maxLatencyWriter.go
Normal 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()
|
||||
}
|
||||
}
|
@ -22,27 +22,19 @@ import (
|
||||
|
||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
Parent: &thisRouter,
|
||||
}
|
||||
|
||||
//Prase the tld map for tld redirection in main router
|
||||
//See Server.go declarations
|
||||
if len(rawTldMap) > 0 {
|
||||
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
|
||||
}
|
||||
|
||||
return &thisRouter, nil
|
||||
}
|
||||
|
||||
@ -76,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
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
|
||||
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
|
||||
if router.Option.ForceTLSLatest {
|
||||
minVersion = tls.VersionTLS12
|
||||
@ -101,16 +86,6 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
/*
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
*/
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
Handler: router.mux,
|
||||
@ -129,7 +104,7 @@ func (router *Router) StartProxyService() error {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
@ -140,7 +115,7 @@ func (router *Router) StartProxyService() error {
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
@ -225,7 +200,7 @@ func (router *Router) StartProxyService() error {
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
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)
|
||||
defer cancel()
|
||||
@ -253,13 +228,13 @@ func (router *Router) StopProxyService() error {
|
||||
// Restart the current router if it is running.
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
var err error = nil
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
@ -267,7 +242,7 @@ func (router *Router) Restart() error {
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
@ -280,128 +255,17 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||
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
|
||||
*/
|
||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
||||
if ptype == "vdir" {
|
||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
||||
if !ok {
|
||||
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)
|
||||
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||
var targetProxyEndpoint *ProxyEndpoint
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
key, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
@ -409,13 +273,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
|
||||
if key == matchingDomain {
|
||||
targetProxyEndpoint = v
|
||||
}
|
||||
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)
|
||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
|
158
src/mod/dynamicproxy/endpoints.go
Normal 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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
@ -28,13 +30,41 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
||||
return targetProxyEndpoint
|
||||
}
|
||||
|
||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
if ok {
|
||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||
}
|
||||
|
||||
//No hit. Try with wildcard
|
||||
matchProxyEndpoints := []*ProxyEndpoint{}
|
||||
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
||||
ep := v.(*ProxyEndpoint)
|
||||
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
||||
if err != nil {
|
||||
//Continue
|
||||
return true
|
||||
}
|
||||
if match {
|
||||
//targetSubdomainEndpoint = ep
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(matchProxyEndpoints) == 1 {
|
||||
//Only 1 match
|
||||
return matchProxyEndpoints[0]
|
||||
} else if len(matchProxyEndpoints) > 1 {
|
||||
//More than one match. Get the best match one
|
||||
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
|
||||
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
|
||||
})
|
||||
return matchProxyEndpoints[0]
|
||||
}
|
||||
|
||||
return targetSubdomainEndpoint
|
||||
}
|
||||
|
||||
@ -54,14 +84,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
return rewrittenURL
|
||||
}
|
||||
|
||||
// Handle subdomain request
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
// Handle host request
|
||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
//Append / to the end of the redirection endpoint if not exists
|
||||
@ -76,7 +114,10 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -89,10 +130,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
})
|
||||
|
||||
@ -113,15 +155,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.parent.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.parent.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
@ -131,7 +181,10 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -144,11 +197,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.RootOrMatchingDomain,
|
||||
PathPrefix: target.MatchingPath,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
|
@ -2,19 +2,25 @@ package redirection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type RuleTable struct {
|
||||
AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity
|
||||
Logger *logger.Logger
|
||||
configPath string //The location where the redirection rules is stored
|
||||
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
||||
|
||||
}
|
||||
|
||||
type RedirectRules struct {
|
||||
@ -24,10 +30,11 @@ type RedirectRules struct {
|
||||
StatusCode int //Status Code for redirection
|
||||
}
|
||||
|
||||
func NewRuleTable(configPath string) (*RuleTable, error) {
|
||||
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
|
||||
thisRuleTable := RuleTable{
|
||||
rules: sync.Map{},
|
||||
configPath: configPath,
|
||||
AllowRegex: allowRegex,
|
||||
}
|
||||
//Load all the rules from the config path
|
||||
if !utils.FileExists(configPath) {
|
||||
@ -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 "_"
|
||||
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
|
||||
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 {
|
||||
// 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
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
fmt.Println(redirectURL, filename, filepath)
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to delete
|
||||
@ -145,18 +153,47 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
|
||||
// Iterate through all the keys in the rules map
|
||||
var targetRedirectionRule *RedirectRules = nil
|
||||
var maxMatch int = 0
|
||||
|
||||
t.rules.Range(func(key interface{}, value interface{}) bool {
|
||||
// Check if the requested URL starts with the key as a prefix
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
if t.AllowRegex {
|
||||
//Regexp matching rule
|
||||
matched, err := regexp.MatchString(key.(string), requestedURL)
|
||||
if err != nil {
|
||||
//Something wrong with the regex?
|
||||
t.log("Unable to match regex", err)
|
||||
return true
|
||||
}
|
||||
if matched {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
|
||||
} else {
|
||||
//Default: prefix matching redirect
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return targetRedirectionRule
|
||||
}
|
||||
|
||||
// Log the message to log file, use STDOUT if logger not set
|
||||
func (t *RuleTable) log(message string, err error) {
|
||||
if t.Logger == nil {
|
||||
if err == nil {
|
||||
log.Println("[Redirect] " + message)
|
||||
} else {
|
||||
log.Println("[Redirect] " + message + ": " + err.Error())
|
||||
}
|
||||
} else {
|
||||
t.Logger.PrintAndLog("Redirect", message, err)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
114
src/mod/dynamicproxy/router.go
Normal file
@ -0,0 +1,114 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Dynamic Proxy Router Functions
|
||||
|
||||
This script handle the proxy rules router spawning
|
||||
and preparation
|
||||
*/
|
||||
|
||||
// Prepare proxy route generate a proxy handler service object for your endpoint
|
||||
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
|
||||
//Filter the tailing slash if any
|
||||
domain := endpoint.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
endpoint.Domain = domain
|
||||
|
||||
//Parse the web proxy endpoint
|
||||
webProxyEndpoint := domain
|
||||
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
|
||||
//TLS is not hardcoded in proxy target domain
|
||||
if endpoint.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Create the proxy routing handler
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
|
||||
IgnoreTLSVerification: endpoint.SkipCertValidations,
|
||||
})
|
||||
endpoint.proxy = proxy
|
||||
endpoint.parent = router
|
||||
|
||||
//Prepare proxy routing hjandler for each of the virtual directories
|
||||
for _, vdir := range endpoint.VirtualDirectories {
|
||||
domain := vdir.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
//Parse the web proxy endpoint
|
||||
webProxyEndpoint = domain
|
||||
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
|
||||
//TLS is not hardcoded in proxy target domain
|
||||
if vdir.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, &dpcore.DpcoreOptions{
|
||||
IgnoreTLSVerification: vdir.SkipCertValidations,
|
||||
})
|
||||
vdir.proxy = proxy
|
||||
vdir.parent = endpoint
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
|
||||
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
|
||||
if endpoint.proxy == nil {
|
||||
//This endpoint is not prepared
|
||||
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
|
||||
}
|
||||
// Push record into running subdomain endpoints
|
||||
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
|
||||
func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
|
||||
if endpoint.proxy == nil {
|
||||
//This endpoint is not prepared
|
||||
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
|
||||
}
|
||||
// Push record into running root endpoints
|
||||
router.Root = endpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProxyEndpoint remove provide global access by key
|
||||
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
|
||||
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return targetEpt.Remove()
|
||||
}
|
@ -1,50 +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,
|
||||
BypassGlobalTLS: options.BypassGlobalTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
||||
})
|
||||
|
||||
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
|
||||
return nil
|
||||
}
|
157
src/mod/dynamicproxy/templates/hosterror.html
Normal 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>
|
@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyType_Subdomain = 0
|
||||
ProxyType_Vdir = 1
|
||||
ProxyType_Root = 0
|
||||
ProxyType_Host = 1
|
||||
ProxyType_Vdir = 2
|
||||
)
|
||||
|
||||
type ProxyHandler struct {
|
||||
@ -24,9 +25,11 @@ type ProxyHandler struct {
|
||||
|
||||
type RouterOption struct {
|
||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||
HostVersion string //The version of Zoraxy, use for heading mod
|
||||
Port int //Incoming port
|
||||
UseTls bool //Use TLS to serve incoming requsts
|
||||
ForceTLSLatest bool //Force TLS1.2 or above
|
||||
NoCache bool //Force set Cache-Control: no-store
|
||||
ListenOnPort80 bool //Enable port 80 http listener
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
@ -37,16 +40,14 @@ type RouterOption struct {
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
SubdomainEndpoint *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
RootRoutingOptions *RootRoutingOptions
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
|
||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||
tldMap map[string]int //Top level domain map, see tld.json
|
||||
@ -69,63 +70,72 @@ type BasicAuthExceptionRule struct {
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
// A proxy endpoint record
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
|
||||
Domain string //Domain or IP to proxy to
|
||||
RequireTLS bool //Target domain require TLS
|
||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||
Proxy *dpcore.ReverseProxy `json:"-"`
|
||||
|
||||
parent *Router
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
// program structure than directly using ProxyEndpoint
|
||||
type VirtualDirectoryEndpoint struct {
|
||||
MatchingPath string //Matching prefix of the request path, also act as key
|
||||
Domain string //Domain or IP to proxy to
|
||||
RequireTLS bool //Target domain require TLS
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
Disabled bool //If the rule is enabled
|
||||
proxy *dpcore.ReverseProxy `json:"-"`
|
||||
parent *ProxyEndpoint `json:"-"`
|
||||
}
|
||||
|
||||
// A proxy endpoint record, a general interface for handling inbound routing
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||
Domain string //Domain or IP to proxy to
|
||||
|
||||
//TLS/SSL Related
|
||||
RequireTLS bool //Target domain require TLS
|
||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
|
||||
|
||||
//Virtual Directories
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
||||
//Custom Headers
|
||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
|
||||
//Authentication
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||
|
||||
//Fallback routing logic
|
||||
DefaultSiteOption int //Fallback routing logic options
|
||||
DefaultSiteValue string //Fallback routing target, optional
|
||||
|
||||
Disabled bool //If the rule is disabled
|
||||
|
||||
//Internal Logic Elements
|
||||
parent *Router
|
||||
proxy *dpcore.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
/*
|
||||
Routing type specific interface
|
||||
These are options that only avaible for a specific interface
|
||||
when running, these are converted into "ProxyEndpoint" objects
|
||||
for more generic routing logic
|
||||
*/
|
||||
|
||||
// Root options are those that are required for reverse proxy handler to work
|
||||
type RootOptions struct {
|
||||
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
|
||||
RequireTLS bool //Proxy root target require TLS connection (not recommended)
|
||||
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
|
||||
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
|
||||
|
||||
//Basic Auth Related
|
||||
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
// Additional options are here for letting router knows how to route exception cases for root
|
||||
type RootRoutingOptions struct {
|
||||
//Root only configs
|
||||
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
|
||||
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
|
||||
}
|
||||
|
||||
type VdirOptions struct {
|
||||
RootName string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
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
|
||||
}
|
||||
const (
|
||||
DefaultSite_InternalStaticWebServer = 0
|
||||
DefaultSite_ReverseProxy = 1
|
||||
DefaultSite_Redirect = 2
|
||||
DefaultSite_NotFoundPage = 3
|
||||
)
|
||||
|
||||
/*
|
||||
Web Templates
|
||||
@ -133,4 +143,6 @@ Web Templates
|
||||
var (
|
||||
//go:embed templates/forbidden.html
|
||||
page_forbidden []byte
|
||||
//go:embed templates/hosterror.html
|
||||
page_hosterror []byte
|
||||
)
|
||||
|
25
src/mod/forwardproxy/cproxy/LICENSE.md
Normal 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.
|
109
src/mod/forwardproxy/cproxy/config.go
Normal 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 }
|
19
src/mod/forwardproxy/cproxy/default_client_connector.go
Normal 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
|
||||
}
|
||||
}
|
25
src/mod/forwardproxy/cproxy/default_dialer.go
Normal 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
|
||||
}
|
9
src/mod/forwardproxy/cproxy/default_filter.go
Normal 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 }
|
56
src/mod/forwardproxy/cproxy/default_handler.go
Normal 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")
|
||||
)
|
54
src/mod/forwardproxy/cproxy/default_proxy.go
Normal 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()
|
||||
}
|
24
src/mod/forwardproxy/cproxy/default_server_connector.go
Normal 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)
|
||||
}
|
32
src/mod/forwardproxy/cproxy/hostname_filter.go
Normal 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
|
||||
}
|
26
src/mod/forwardproxy/cproxy/hostname_suffix_filter.go
Normal 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
|
||||
}
|
67
src/mod/forwardproxy/cproxy/interfaces.go
Normal 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
|
||||
)
|
24
src/mod/forwardproxy/cproxy/logging_initializer.go
Normal 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
|
||||
}
|
36
src/mod/forwardproxy/cproxy/proxy_protocol_initializer.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type proxyProtocolInitializer struct{}
|
||||
|
||||
func newProxyProtocolInitializer() *proxyProtocolInitializer {
|
||||
return &proxyProtocolInitializer{}
|
||||
}
|
||||
|
||||
func (this *proxyProtocolInitializer) Initialize(client, server Socket) bool {
|
||||
header := formatHeader(client.RemoteAddr(), server.RemoteAddr())
|
||||
_, err := io.WriteString(server, header)
|
||||
return err == nil
|
||||
}
|
||||
func formatHeader(client, server net.Addr) string {
|
||||
clientAddress, clientPort := parseAddress(client.String())
|
||||
serverAddress, serverPort := parseAddress(server.String())
|
||||
if strings.Contains(clientAddress, ":") {
|
||||
return fmt.Sprintf(proxyProtocolIPv6Preamble, clientAddress, serverAddress, clientPort, serverPort)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(proxyProtocolIPv4Preamble, clientAddress, serverAddress, clientPort, serverPort)
|
||||
}
|
||||
func parseAddress(address string) (string, string) {
|
||||
address, port, _ := net.SplitHostPort(address)
|
||||
return address, port
|
||||
}
|
||||
|
||||
const proxyProtocolIPv4Preamble = "PROXY TCP4 %s %s %s %s\r\n"
|
||||
const proxyProtocolIPv6Preamble = "PROXY TCP6 %s %s %s %s\r\n"
|
18
src/mod/forwardproxy/cproxy/routing_dialer.go
Normal file
@ -0,0 +1,18 @@
|
||||
package cproxy
|
||||
|
||||
type routingDialer struct {
|
||||
inner Dialer
|
||||
targetAddress string
|
||||
}
|
||||
|
||||
func newRoutingDialer(config *configuration) Dialer {
|
||||
if len(config.DialAddress) == 0 {
|
||||
return config.Dialer
|
||||
}
|
||||
|
||||
return &routingDialer{inner: config.Dialer, targetAddress: config.DialAddress}
|
||||
}
|
||||
|
||||
func (this *routingDialer) Dial(string) Socket {
|
||||
return this.inner.Dial(this.targetAddress)
|
||||
}
|
137
src/mod/forwardproxy/forwardproxy.go
Normal file
@ -0,0 +1,137 @@
|
||||
package forwardproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy/cproxy"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type ZrFilter struct {
|
||||
//To be implemented
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
server *http.Server
|
||||
handler *http.Handler
|
||||
running bool
|
||||
db *database.Database
|
||||
logger *logger.Logger
|
||||
Port int
|
||||
}
|
||||
|
||||
func NewForwardProxy(sysdb *database.Database, port int, logger *logger.Logger) *Handler {
|
||||
thisFilter := ZrFilter{}
|
||||
handler := cproxy.New(cproxy.Options.Filter(thisFilter))
|
||||
|
||||
return &Handler{
|
||||
db: sysdb,
|
||||
server: nil,
|
||||
handler: &handler,
|
||||
running: false,
|
||||
logger: logger,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Start the forward proxy
|
||||
func (h *Handler) Start() error {
|
||||
if h.running {
|
||||
return errors.New("forward proxy already running")
|
||||
}
|
||||
server := &http.Server{Addr: ":" + strconv.Itoa(h.Port), Handler: *h.handler}
|
||||
h.server = server
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
h.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop the forward proxy
|
||||
func (h *Handler) Stop() error {
|
||||
if h.running && h.server != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := h.server.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
h.running = false
|
||||
h.server = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the port number of the forward proxy
|
||||
func (h *Handler) UpdatePort(newPort int) error {
|
||||
h.Stop()
|
||||
h.Port = newPort
|
||||
return h.Start()
|
||||
}
|
||||
|
||||
func (it ZrFilter) IsAuthorized(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle port change of the forward proxy
|
||||
func (h *Handler) HandlePort(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := utils.PostInt(r, "port")
|
||||
if err != nil {
|
||||
js, _ := json.Marshal(h.Port)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Update the port
|
||||
err = h.UpdatePort(port)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy port updated to :"+strconv.Itoa(h.Port), nil)
|
||||
h.db.Write("fwdproxy", "port", port)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle power toggle of the forward proxys
|
||||
func (h *Handler) HandleToogle(w http.ResponseWriter, r *http.Request) {
|
||||
enabled, err := utils.PostBool(r, "enable")
|
||||
if err != nil {
|
||||
//Get the current state of the forward proxy
|
||||
js, _ := json.Marshal(h.running)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enabled {
|
||||
err = h.Start()
|
||||
if err != nil {
|
||||
h.logger.PrintAndLog("Forward Proxy", "Unable to start forward proxy server", err)
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Started, listening on :"+strconv.Itoa(h.Port), nil)
|
||||
} else {
|
||||
err = h.Stop()
|
||||
if err != nil {
|
||||
h.logger.PrintAndLog("Forward Proxy", "Unable to stop forward proxy server", err)
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Stopped", nil)
|
||||
}
|
||||
h.db.Write("fwdproxy", "enabled", enabled)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
@ -207,7 +207,7 @@ func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
//Handle listing of network members. Set details=true for listing all details
|
||||
// Handle listing of network members. Set details=true for listing all details
|
||||
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.GetPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -241,7 +241,7 @@ func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
//Handle Authorization of members
|
||||
// Handle Authorization of members
|
||||
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -281,7 +281,7 @@ func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
//Handle Delete or Add IP for a member in a network
|
||||
// Handle Delete or Add IP for a member in a network
|
||||
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -356,7 +356,7 @@ func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
//Handle naming for members
|
||||
// Handle naming for members
|
||||
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -391,7 +391,7 @@ func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
//Handle delete of a given memver
|
||||
// Handle delete of a given memver
|
||||
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -426,3 +426,79 @@ func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Check if a given network id is a network hosted on this zoraxy node
|
||||
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
|
||||
networks, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
if network == networkId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle server instant joining a given network
|
||||
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if m.memberExistsInNetwork(netid, m.ControllerID) {
|
||||
utils.SendErrorResponse(w, "controller already inside network")
|
||||
return
|
||||
}
|
||||
|
||||
//Join the network
|
||||
err = m.joinNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle server instant leaving a given network
|
||||
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
//Leave the network
|
||||
err = m.leaveNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove it from target network if it is authorized
|
||||
err = m.deleteMember(netid, m.ControllerID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ type MemberInfo struct {
|
||||
VRev int `json:"vRev"`
|
||||
}
|
||||
|
||||
//Get the zerotier node info from local service
|
||||
// Get the zerotier node info from local service
|
||||
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
|
||||
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
||||
|
||||
@ -187,7 +187,7 @@ func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
|
||||
return &networkInfo, nil
|
||||
}
|
||||
|
||||
//List network details
|
||||
// List network details
|
||||
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
|
||||
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
|
||||
if err != nil {
|
||||
@ -249,7 +249,7 @@ func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *Ne
|
||||
return nil
|
||||
}
|
||||
|
||||
//List network IDs
|
||||
// List network IDs
|
||||
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
||||
if err != nil {
|
||||
@ -281,7 +281,7 @@ func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
return networkIds, nil
|
||||
}
|
||||
|
||||
//wrapper for checking if a network id exists
|
||||
// wrapper for checking if a network id exists
|
||||
func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
networkIds, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
@ -297,7 +297,7 @@ func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//delete a network
|
||||
// delete a network
|
||||
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
client := &http.Client{}
|
||||
@ -330,8 +330,8 @@ func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Configure network
|
||||
//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
// Configure network
|
||||
// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
data := map[string]interface{}{
|
||||
@ -545,7 +545,7 @@ func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool
|
||||
return false
|
||||
}
|
||||
|
||||
//Get a network memeber info by netid and memberid
|
||||
// Get a network memeber info by netid and memberid
|
||||
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
|
||||
if err != nil {
|
||||
@ -573,7 +573,7 @@ func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*M
|
||||
return thisMemeberInfo, nil
|
||||
}
|
||||
|
||||
//Set the authorization state of a member
|
||||
// Set the authorization state of a member
|
||||
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
||||
payload := []byte(`{"authorized": true}`)
|
||||
@ -600,7 +600,7 @@ func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAutho
|
||||
return nil
|
||||
}
|
||||
|
||||
//Delete a member from the network
|
||||
// Delete a member from the network
|
||||
func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
|
||||
if err != nil {
|
||||
@ -620,3 +620,45 @@ func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to join a given network
|
||||
func (m *NetworkManager) joinNetwork(netid string) error {
|
||||
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to leave a given network
|
||||
func (m *NetworkManager) leaveNetwork(netid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package geodb
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Whitelist.go
|
||||
@ -8,11 +11,29 @@ import "strings"
|
||||
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 *Store) AddCountryCodeToWhitelist(countryCode string) {
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Write("whitelist-cn", countryCode, true)
|
||||
entry := WhitelistEntry{
|
||||
EntryType: EntryType_CountryCode,
|
||||
CC: countryCode,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-cn", countryCode, entry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
|
||||
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
|
||||
return isWhitelisted
|
||||
return s.sysdb.KeyExists("whitelist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
whitelistedCountryCode := []string{}
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||
whitelistedCountryCode := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, ip)
|
||||
thisWhitelistEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
|
||||
}
|
||||
|
||||
return whitelistedCountryCode
|
||||
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
|
||||
//IP Whitelist
|
||||
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string) {
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, true)
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
|
||||
thisIpEntry := WhitelistEntry{
|
||||
EntryType: EntryType_IP,
|
||||
IP: ipAddr,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
}
|
||||
|
||||
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
|
||||
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
|
||||
if isWhitelisted {
|
||||
//single IP whitelist entry
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIp()
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
|
||||
for _, whitelistRules := range AllWhitelistedIps {
|
||||
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
|
||||
if wildcardMatch {
|
||||
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIp() []string {
|
||||
whitelistedIp := []string{}
|
||||
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
|
||||
whitelistedIp := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedIp = append(whitelistedIp, ip)
|
||||
//ip := string(keypairs[0])
|
||||
thisEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisEntry)
|
||||
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||
}
|
||||
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
|
||||
allWhitelistedIPs := []string{}
|
||||
entries := s.GetAllWhitelistedIp()
|
||||
for _, entry := range entries {
|
||||
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
|
||||
}
|
||||
|
||||
return allWhitelistedIPs
|
||||
}
|
||||
|
@ -85,10 +85,6 @@ func NewReverseProxy(target *url.URL) *ReverseProxy {
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
|
||||
return &ReverseProxy{Director: director, Verbal: false}
|
||||
@ -211,9 +207,9 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,10 +82,13 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
requestURL = strings.TrimPrefix(requestURL, "/")
|
||||
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
|
||||
wspHandler := websocketproxy.NewProxy(u, false)
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: false,
|
||||
SkipOriginCheck: false,
|
||||
})
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -5,22 +5,22 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//This remove the certificates in the list where either the
|
||||
//public key or the private key is missing
|
||||
// This remove the certificates in the list where either the
|
||||
// public key or the private key is missing
|
||||
func getCertPairs(certFiles []string) []string {
|
||||
crtMap := make(map[string]bool)
|
||||
pemMap := make(map[string]bool)
|
||||
keyMap := make(map[string]bool)
|
||||
|
||||
for _, filename := range certFiles {
|
||||
if filepath.Ext(filename) == ".crt" {
|
||||
crtMap[strings.TrimSuffix(filename, ".crt")] = true
|
||||
if filepath.Ext(filename) == ".pem" {
|
||||
pemMap[strings.TrimSuffix(filename, ".pem")] = true
|
||||
} else if filepath.Ext(filename) == ".key" {
|
||||
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for domain := range crtMap {
|
||||
for domain := range pemMap {
|
||||
if keyMap[domain] {
|
||||
result = append(result, domain)
|
||||
}
|
||||
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
//Get the cloest subdomain certificate from a list of domains
|
||||
// Get the cloest subdomain certificate from a list of domains
|
||||
func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
var matchingDomain string = ""
|
||||
maxLength := 0
|
||||
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
|
||||
return matchingDomain
|
||||
}
|
||||
|
||||
//Check if a requesting domain is a subdomain of a given domain
|
||||
func isSubdomain(subdomain, domain string) bool {
|
||||
subdomainParts := strings.Split(subdomain, ".")
|
||||
domainParts := strings.Split(domain, ".")
|
||||
if len(subdomainParts) < len(domainParts) {
|
||||
return false
|
||||
}
|
||||
for i := range domainParts {
|
||||
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|