Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
21
.github/workflows/main.yml
vendored
@ -9,18 +9,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.release.tag_name }}
|
ref: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Dockerhub
|
- name: Login to Docker & GHCR
|
||||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
run: |
|
||||||
|
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
|
#echo "${{ secrets.GHCR_PASSWORD }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: Setup building file structure
|
- name: Setup building file structure
|
||||||
run: |
|
run: |
|
||||||
@ -36,11 +38,8 @@ jobs:
|
|||||||
--provenance=false \
|
--provenance=false \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||||
.
|
|
||||||
|
|
||||||
docker buildx build --push \
|
|
||||||
--build-arg VERSION=${{ github.event.release.tag_name }} \
|
|
||||||
--provenance=false \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--tag zoraxydocker/zoraxy:latest \
|
--tag zoraxydocker/zoraxy:latest \
|
||||||
|
# Since this is still undetermined, I will leave it commented
|
||||||
|
#--tag ghcr.io/zoraxydocker/zoraxy:${{ steps.get_latest_release_tag.outputs.latest_tag }} \
|
||||||
|
#--tag ghcr.io/zoraxydocker/zoraxy:latest \
|
||||||
.
|
.
|
||||||
|
26
CHANGELOG.md
@ -1,3 +1,13 @@
|
|||||||
|
# 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
|
# v2.6.7 Sep 26 2023
|
||||||
|
|
||||||
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
|
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
|
||||||
@ -14,20 +24,20 @@
|
|||||||
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
|
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
|
||||||
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
|
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
|
||||||
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
|
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
|
||||||
+ Added custom acme feature in back-end, thx [@daluntw](https://github.com/daluntw)
|
+ Added custom ACME feature in backend, thx [@daluntw](https://github.com/daluntw)
|
||||||
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
|
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
|
||||||
+ Introduce new startparameter `-fastgeoip=true`, see [Releases](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
|
+ Introduce new start parameter `-fastgeoip=true`: see [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
|
||||||
|
|
||||||
# v2.6.5.1 Jul 26 2023
|
# v2.6.5.1 Jul 26 2023
|
||||||
|
|
||||||
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
|
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
|
||||||
+ Fixed potential memory leak in acme handler logic
|
+ Fixed potential memory leak in ACME handler logic
|
||||||
+ Added "Do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
|
+ Added "Do you want to get a TLS certificate for this subdomain?" dialogue when a new subdomain proxy rule is created
|
||||||
|
|
||||||
# v2.6.5 Jul 19 2023
|
# v2.6.5 Jul 19 2023
|
||||||
|
|
||||||
+ Added Import / Export-Feature
|
+ Added Import / Export-Feature
|
||||||
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
|
+ Moved configuration files to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
|
||||||
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
|
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
|
||||||
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
|
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
|
||||||
+ Added Whois
|
+ Added Whois
|
||||||
@ -37,7 +47,7 @@
|
|||||||
+ Added force TLS v1.2 above toggle
|
+ Added force TLS v1.2 above toggle
|
||||||
+ Added trace route
|
+ Added trace route
|
||||||
+ Added ICMP ping
|
+ Added ICMP ping
|
||||||
+ Added special routing rules module for up-coming acme integration
|
+ Added special routing rules module for up-coming ACME integration
|
||||||
+ Fixed IPv6 check bug in black/whitelist
|
+ Fixed IPv6 check bug in black/whitelist
|
||||||
+ Optimized UI for TCP Proxy
|
+ Optimized UI for TCP Proxy
|
||||||
|
|
||||||
@ -47,7 +57,7 @@
|
|||||||
+ Split blacklist and whitelist from geodb script file
|
+ Split blacklist and whitelist from geodb script file
|
||||||
+ Optimized compile binary size
|
+ Optimized compile binary size
|
||||||
+ Added access control to TCP proxy
|
+ Added access control to TCP proxy
|
||||||
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7)
|
+ Added "invalid config detect" in up time monitor for issue [#7](https://github.com/tobychui/zoraxy/issues/7)
|
||||||
+ Fixed minor bugs in advance stats panel
|
+ Fixed minor bugs in advance stats panel
|
||||||
+ Reduced file size of embedded materials
|
+ Reduced file size of embedded materials
|
||||||
|
|
||||||
@ -74,6 +84,6 @@
|
|||||||
+ Basic auth
|
+ Basic auth
|
||||||
+ Support TLS verification skip (for self signed certs)
|
+ Support TLS verification skip (for self signed certs)
|
||||||
+ Added trend analysis
|
+ Added trend analysis
|
||||||
+ Added referer and file type analysis
|
+ Added referrer and file type analysis
|
||||||
+ Added cert expire day display
|
+ Added cert expire day display
|
||||||
+ Moved subdomain proxy logic to dpcore
|
+ Moved subdomain proxy logic to dpcore
|
||||||
|
119
README.md
@ -2,24 +2,28 @@
|
|||||||
|
|
||||||
# Zoraxy
|
# Zoraxy
|
||||||
|
|
||||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||||
|
|
||||||
|
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Simple to use interface with detail in-system instructions
|
- Simple to use interface with detail in-system instructions
|
||||||
- Reverse Proxy
|
- Reverse Proxy
|
||||||
|
- Virtual Directory
|
||||||
- Subdomain Reverse Proxy
|
- Basic Auth
|
||||||
|
- Custom Headers
|
||||||
- Virtual Directory Reverse Proxy
|
|
||||||
- Redirection Rules
|
- Redirection Rules
|
||||||
- TLS / SSL setup and deploy
|
- TLS / SSL setup and deploy
|
||||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners)
|
- ACME features like auto-renew to serve your sites in http**s**
|
||||||
|
- SNI support (one certificate contains multiple host names)
|
||||||
|
|
||||||
|
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||||
|
- TCP Tunneling / Proxy
|
||||||
- Integrated Up-time Monitor
|
- Integrated Up-time Monitor
|
||||||
- Web-SSH Terminal
|
- Web-SSH Terminal
|
||||||
- Utilities
|
- Utilities
|
||||||
|
|
||||||
- CIDR IP converters
|
- CIDR IP converters
|
||||||
- mDNS Scanner
|
- mDNS Scanner
|
||||||
- IP Scanner
|
- IP Scanner
|
||||||
@ -29,9 +33,9 @@ General purpose request (reverse) proxy and forwarding tool for low power device
|
|||||||
- SMTP config for password reset
|
- SMTP config for password reset
|
||||||
|
|
||||||
## Build from Source
|
## Build from Source
|
||||||
Require Go 1.20 or above
|
Requires Go 1.20 or higher
|
||||||
|
|
||||||
```
|
```bash
|
||||||
git clone https://github.com/tobychui/zoraxy
|
git clone https://github.com/tobychui/zoraxy
|
||||||
cd ./zoraxy/src/
|
cd ./zoraxy/src/
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@ -42,11 +46,11 @@ sudo ./zoraxy -port=:8000
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
|
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
|
||||||
|
|
||||||
### Standalone Mode
|
### Standalone Mode
|
||||||
|
|
||||||
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start growing their web services into multiple servers.
|
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
@ -60,18 +64,49 @@ Download the binary executable and double click the binary file to start it.
|
|||||||
|
|
||||||
#### Raspberry Pi
|
#### Raspberry Pi
|
||||||
|
|
||||||
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
The installation method is same as Linux. If you are using a Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||||
|
|
||||||
#### Other ARM SBCs or Android phone with Termux
|
#### Other ARM SBCs or Android phone with Termux
|
||||||
|
|
||||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
|
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||||
|
|
||||||
|
### Start Paramters
|
||||||
|
```
|
||||||
|
Usage of zoraxy:
|
||||||
|
-autorenew int
|
||||||
|
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||||
|
-fastgeoip
|
||||||
|
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||||
|
-info
|
||||||
|
Show information about this program in JSON
|
||||||
|
-log
|
||||||
|
Log terminal output to file (default true)
|
||||||
|
-mdns
|
||||||
|
Enable mDNS scanner and transponder (default true)
|
||||||
|
-noauth
|
||||||
|
Disable authentication for management interface
|
||||||
|
-port string
|
||||||
|
Management web interface listening port (default ":8000")
|
||||||
|
-sshlb
|
||||||
|
Allow loopback web ssh connection (DANGER)
|
||||||
|
-version
|
||||||
|
Show version of this server
|
||||||
|
-webfm
|
||||||
|
Enable web file manager for static web server root folder (default true)
|
||||||
|
-webroot string
|
||||||
|
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
||||||
|
-ztauth string
|
||||||
|
ZeroTier authtoken for the local node
|
||||||
|
-ztport int
|
||||||
|
ZeroTier controller API port (default 9993)
|
||||||
|
```
|
||||||
|
|
||||||
### External Permission Management Mode
|
### External Permission Management Mode
|
||||||
|
|
||||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag
|
If you already have an upstream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./zoraxy -noauth=true
|
./zoraxy -noauth=true
|
||||||
@ -79,45 +114,12 @@ If you already have a up-stream reverse proxy server in place with permission ma
|
|||||||
|
|
||||||
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||||
|
|
||||||
#### Use with ArozOS
|
|
||||||
|
|
||||||
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
|
|
||||||
cd ~/arozos/subservices
|
|
||||||
mkdir zoraxy
|
|
||||||
cd ./zoraxy
|
|
||||||
|
|
||||||
# Download the release binary from Github release
|
|
||||||
wget {binary executable link from release page}
|
|
||||||
|
|
||||||
# Set permission. Change this if required
|
|
||||||
sudo chmod 775 -R ./
|
|
||||||
|
|
||||||
# Start zoraxy to see if the downloaded arch is correct.
|
|
||||||
./zoraxy
|
|
||||||
|
|
||||||
# After the unzip done, press Ctrl + C to kill it
|
|
||||||
# Rename it to valid arozos subservice binary format
|
|
||||||
mv ./zoraxy zoraxy_linux_amd64
|
|
||||||
|
|
||||||
# If you are using SBCs with different CPU arch, use the following names
|
|
||||||
# mv ./zoraxy zoraxy_linux_arm
|
|
||||||
# mv ./zoraxy zoraxy_linux_arm64
|
|
||||||
|
|
||||||
# Restart arozos
|
|
||||||
sudo systemctl restart arozos
|
|
||||||
```
|
|
||||||
|
|
||||||
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
@ -128,22 +130,21 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
|
|||||||
|
|
||||||
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
||||||
|
|
||||||
Assuming you already have a valid license, to use Zoraxy with ZeroTier, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken at correct location in your host.
|
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||||
|
|
||||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags
|
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
|
||||||
|
|
||||||
```
|
```bash
|
||||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||||
```
|
```
|
||||||
|
|
||||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||||
|
|
||||||
This allows you to have infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||||
|
|
||||||
## Web.SSH
|
## Web SSH
|
||||||
|
|
||||||
Web SSH currently only support Linux based OS. The following platforms are supported
|
|
||||||
|
|
||||||
|
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
- linux/arm64
|
- linux/arm64
|
||||||
- linux/armv6 (experimental)
|
- linux/armv6 (experimental)
|
||||||
@ -151,9 +152,9 @@ Web SSH currently only support Linux based OS. The following platforms are suppo
|
|||||||
|
|
||||||
### Loopback Connection
|
### Loopback Connection
|
||||||
|
|
||||||
Loopback web ssh connection, by default, is disabled. This means that if you are trying to connect to address like 127.0.0.1 or localhost, the system will reject your connection due to security issues. To enable loopback for testing or development purpose, use the following flags to override the loopback checking.
|
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
./zoraxy -sshlb=true
|
./zoraxy -sshlb=true
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -165,5 +166,5 @@ If you like the project and want to support us, please consider a donation. You
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open source under AGPL. I open source this project so everyone can check for security issues and benefit all users. **If your plans to use this project in commercial environment which violate the AGPL terms, please contact toby@imuslab.com for an alternative commercial license.**
|
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative license.**
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ services:
|
|||||||
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
||||||
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||||
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
|
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
|
||||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
|
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
|
||||||
|
|
||||||
## Examples: </br>
|
## Examples: </br>
|
||||||
### Docker Run </br>
|
### Docker Run </br>
|
||||||
|
@ -148,7 +148,7 @@
|
|||||||
Reverse Proxy
|
Reverse Proxy
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Simple to use, noobs friendly reverse proxy server that can be easily set-up using a web form and a few toggle switches.</p>
|
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -158,7 +158,7 @@
|
|||||||
Redirection
|
Redirection
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most of the simple use cases.</p>
|
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -168,7 +168,7 @@
|
|||||||
Geo-IP & Blacklist
|
Geo-IP & Blacklist
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Blacklist with GeoIP support. Allow easy setup for regional services.</p>
|
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -189,7 +189,7 @@
|
|||||||
Web SSH
|
Web SSH
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Integrated with Gotty Web SSH terminal, allow one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -199,7 +199,7 @@
|
|||||||
Real Time Statistics
|
Real Time Statistics
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Traffic data collection and real time analytic tools, provide you the best insights of visitors data without cookies.</p>
|
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -209,7 +209,7 @@
|
|||||||
Scanner & Utilities
|
Scanner & Utilities
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Build in IP scanner and mDNS discovering service, enable automatic service discovery within LAN.</p>
|
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="four wide column featureItem">
|
<div class="four wide column featureItem">
|
||||||
@ -219,7 +219,7 @@
|
|||||||
Open Source
|
Open Source
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<p>Project is open source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
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 |
@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
strip "github.com/grokify/html-strip-tags-go"
|
||||||
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
|||||||
bltype = "country"
|
bltype = "country"
|
||||||
}
|
}
|
||||||
|
|
||||||
resulst := []string{}
|
resulst := []*geodb.WhitelistEntry{}
|
||||||
if bltype == "country" {
|
if bltype == "country" {
|
||||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||||
} else if bltype == "ip" {
|
} else if bltype == "ip" {
|
||||||
@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
geodbStore.AddCountryCodeToWhitelist(countryCode)
|
comment, _ := utils.PostPara(r, "comment")
|
||||||
|
comment = strip.StripTags(comment)
|
||||||
|
|
||||||
|
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
geodbStore.AddIPToWhiteList(ipAddr)
|
comment, _ := utils.PostPara(r, "comment")
|
||||||
|
comment = strip.StripTags(comment)
|
||||||
|
|
||||||
|
geodbStore.AddIPToWhiteList(ipAddr, comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
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)")
|
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Add a 3 second delay to make sure everything is settle down
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
// Pass over to the acmeHandler to deal with the communication
|
// Pass over to the acmeHandler to deal with the communication
|
||||||
acmeHandler.HandleRenewCertificate(w, r)
|
acmeHandler.HandleRenewCertificate(w, r)
|
||||||
|
|
||||||
|
17
src/api.go
@ -56,9 +56,16 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||||
//Reverse proxy root related APIs
|
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||||
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
|
//Reverse proxy virtual directory APIs
|
||||||
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
|
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||||
|
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||||
|
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||||
|
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||||
|
//Reverse proxy user define header apis
|
||||||
|
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||||
|
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||||
|
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||||
//Reverse proxy auth related APIs
|
//Reverse proxy auth related APIs
|
||||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||||
@ -115,6 +122,8 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||||
|
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||||
|
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||||
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
||||||
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||||
@ -175,7 +184,7 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||||
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
|
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||||
if *allowWebFileManager {
|
if *allowWebFileManager {
|
||||||
//Web Directory Manager file operation functions
|
//Web Directory Manager file operation functions
|
||||||
|
@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|||||||
results := []*CertInfo{}
|
results := []*CertInfo{}
|
||||||
|
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
|
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
||||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||||
fileInfo, err := os.Stat(certFilepath)
|
fileInfo, err := os.Stat(certFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if keytype == "pub" {
|
if keytype == "pub" {
|
||||||
overWriteFilename = domain + ".crt"
|
overWriteFilename = domain + ".pem"
|
||||||
} else if keytype == "pri" {
|
} else if keytype == "pri" {
|
||||||
overWriteFilename = domain + ".key"
|
overWriteFilename = domain + ".key"
|
||||||
} else {
|
} else {
|
||||||
@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Update cert list
|
||||||
|
tlsCertManager.UpdateLoadedCertList()
|
||||||
|
|
||||||
// send response
|
// send response
|
||||||
fmt.Fprintln(w, "File upload successful!")
|
fmt.Fprintln(w, "File upload successful!")
|
||||||
}
|
}
|
||||||
|
166
src/config.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -35,97 +36,118 @@ type Record struct {
|
|||||||
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a reverse proxy config record to file
|
/*
|
||||||
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
|
Load Reverse Proxy Config from file and append it to current runtime proxy router
|
||||||
//TODO: Make this accept new def types
|
*/
|
||||||
os.MkdirAll("./conf/proxy/", 0775)
|
func LoadReverseProxyConfig(configFilepath string) error {
|
||||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
//Load the config file from disk
|
||||||
|
endpointConfig, err := os.ReadFile(configFilepath)
|
||||||
//Generate record
|
|
||||||
thisRecord := proxyConfigRecord
|
|
||||||
|
|
||||||
//Write to file
|
|
||||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
|
||||||
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save a running reverse proxy endpoint to file (with automatic endpoint to record conversion)
|
|
||||||
func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) error {
|
|
||||||
recordToSave, err := ConvertProxyEndpointToRecord(proxyEndpoint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return SaveReverseProxyConfigToFile(recordToSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveReverseProxyConfigFile(rootname string) error {
|
//Parse it into dynamic proxy endpoint
|
||||||
filename := getFilenameFromRootName(rootname)
|
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||||
SystemWideLogger.Println("Config Removed: ", removePendingFile)
|
|
||||||
if utils.FileExists(removePendingFile) {
|
|
||||||
err := os.Remove(removePendingFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Matching domain not set. Assume root
|
||||||
|
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||||
|
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
//File already gone
|
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||||
|
//This is a root config file
|
||||||
|
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||||
|
|
||||||
|
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
|
||||||
|
//This is a host config file
|
||||||
|
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
||||||
|
} else {
|
||||||
|
return errors.New("not supported proxy type")
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return ptype, rootname and proxyTarget, error if any
|
func filterProxyConfigFilename(filename string) string {
|
||||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
//Filter out wildcard characters
|
||||||
thisRecord := Record{
|
filename = strings.ReplaceAll(filename, "*", "(ST)")
|
||||||
ProxyType: "",
|
filename = strings.ReplaceAll(filename, "?", "(QM)")
|
||||||
Rootname: "",
|
filename = strings.ReplaceAll(filename, "[", "(OB)")
|
||||||
ProxyTarget: "",
|
filename = strings.ReplaceAll(filename, "]", "(CB)")
|
||||||
UseTLS: false,
|
filename = strings.ReplaceAll(filename, "#", "(HT)")
|
||||||
|
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,
|
BypassGlobalTLS: false,
|
||||||
SkipTlsValidation: false,
|
SkipCertValidations: false,
|
||||||
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
RequireBasicAuth: false,
|
RequireBasicAuth: false,
|
||||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||||
}
|
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||||
|
DefaultSiteValue: "",
|
||||||
configContent, err := os.ReadFile(filename)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &thisRecord, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//Unmarshal the content into config
|
return rootProxyEndpoint, nil
|
||||||
err = json.Unmarshal(configContent, &thisRecord)
|
|
||||||
if err != nil {
|
|
||||||
return &thisRecord, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return it
|
|
||||||
return &thisRecord, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a running proxy endpoint object into a save-able record struct
|
|
||||||
func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoint) (*Record, error) {
|
|
||||||
thisProxyConfigRecord := Record{
|
|
||||||
ProxyType: targetProxyEndpoint.GetProxyTypeString(),
|
|
||||||
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
|
|
||||||
ProxyTarget: targetProxyEndpoint.Domain,
|
|
||||||
UseTLS: targetProxyEndpoint.RequireTLS,
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -10,9 +10,11 @@ require (
|
|||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0
|
||||||
github.com/likexian/whois v1.15.1
|
github.com/likexian/whois v1.15.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25
|
github.com/microcosm-cc/bluemonday v1.0.25
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.14.0
|
||||||
golang.org/x/sys v0.11.0
|
golang.org/x/sys v0.11.0
|
||||||
|
golang.org/x/text v0.12.0
|
||||||
golang.org/x/tools v0.12.0 // indirect
|
golang.org/x/tools v0.12.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -740,6 +740,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
|||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
||||||
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
|
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||||
|
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
27
src/main.go
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/aroz"
|
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
@ -35,6 +34,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// General flags
|
// General flags
|
||||||
|
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||||
var showver = flag.Bool("version", false, "Show version of this server")
|
var showver = flag.Bool("version", false, "Show version of this server")
|
||||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||||
@ -49,7 +49,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
name = "Zoraxy"
|
name = "Zoraxy"
|
||||||
version = "2.6.8"
|
version = "3.0.0"
|
||||||
nodeUUID = "generic"
|
nodeUUID = "generic"
|
||||||
development = false //Set this to false to use embedded web fs
|
development = false //Set this to false to use embedded web fs
|
||||||
bootTime = time.Now().Unix()
|
bootTime = time.Now().Unix()
|
||||||
@ -63,7 +63,6 @@ var (
|
|||||||
/*
|
/*
|
||||||
Handler Modules
|
Handler Modules
|
||||||
*/
|
*/
|
||||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
|
||||||
sysdb *database.Database //System database
|
sysdb *database.Database //System database
|
||||||
authAgent *auth.AuthAgent //Authentication agent
|
authAgent *auth.AuthAgent //Authentication agent
|
||||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||||
@ -128,20 +127,8 @@ func ShutdownSeq() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
//Parse startup flags
|
||||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
flag.Parse()
|
||||||
Name: name,
|
|
||||||
Desc: "Dynamic Reverse Proxy Server",
|
|
||||||
Group: "Network",
|
|
||||||
IconPath: "zoraxy/img/small_icon.png",
|
|
||||||
Version: version,
|
|
||||||
StartDir: "zoraxy/index.html",
|
|
||||||
SupportFW: true,
|
|
||||||
LaunchFWDir: "zoraxy/index.html",
|
|
||||||
SupportEmb: false,
|
|
||||||
InitFWSize: []int{1080, 580},
|
|
||||||
})
|
|
||||||
|
|
||||||
if *showver {
|
if *showver {
|
||||||
fmt.Println(name + " - Version " + version)
|
fmt.Println(name + " - Version " + version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -166,7 +153,7 @@ func main() {
|
|||||||
startupSequence()
|
startupSequence()
|
||||||
|
|
||||||
//Initiate management interface APIs
|
//Initiate management interface APIs
|
||||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
requireAuth = !(*noauth)
|
||||||
initAPIs()
|
initAPIs()
|
||||||
|
|
||||||
//Start the reverse proxy server in go routine
|
//Start the reverse proxy server in go routine
|
||||||
@ -179,8 +166,8 @@ func main() {
|
|||||||
//Start the finalize sequences
|
//Start the finalize sequences
|
||||||
finalSequence()
|
finalSequence()
|
||||||
|
|
||||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||||
err = http.ListenAndServe(handler.Port, nil)
|
err = http.ListenAndServe(*webUIPort, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -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
|
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||||
// private key, and a certificate URL.
|
// private key, and a certificate URL.
|
||||||
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package dynamicproxy
|
package dynamicproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -26,11 +23,6 @@ import (
|
|||||||
- Vitrual Directory Routing
|
- Vitrual Directory Routing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed tld.json
|
|
||||||
rawTldMap []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
/*
|
/*
|
||||||
Special Routing Rules, bypass most of the limitations
|
Special Routing Rules, bypass most of the limitations
|
||||||
@ -53,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Inject headers
|
||||||
|
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
General Access Check
|
General Access Check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
respWritten := h.handleAccessRouting(w, r)
|
respWritten := h.handleAccessRouting(w, r)
|
||||||
if respWritten {
|
if respWritten {
|
||||||
return
|
return
|
||||||
@ -80,38 +74,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.getProxyEndpointFromHostname(domainOnly)
|
||||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
|
if sep != nil && !sep.Disabled {
|
||||||
if sep != nil {
|
|
||||||
if sep.RequireBasicAuth {
|
if sep.RequireBasicAuth {
|
||||||
err := h.handleBasicAuthRouting(w, r, sep)
|
err := h.handleBasicAuthRouting(w, r, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.subdomainRequest(w, r, sep)
|
|
||||||
|
//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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Fallback to handle by the host proxy forwarder
|
||||||
|
h.hostRequest(w, r, sep)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Virtual Directory Routing
|
Root Router Handling
|
||||||
*/
|
*/
|
||||||
//Clean up the request URI
|
//Clean up the request URI
|
||||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
if !strings.HasSuffix(proxyingPath, "/") {
|
||||||
if targetProxyEndpoint != nil {
|
|
||||||
if targetProxyEndpoint.RequireBasicAuth {
|
|
||||||
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
|
||||||
} else if !strings.HasSuffix(proxyingPath, "/") {
|
|
||||||
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
|
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
|
||||||
if potentialProxtEndpoint != nil {
|
if potentialProxtEndpoint != nil {
|
||||||
//Missing tailing slash. Redirect to target proxy endpoint
|
//Missing tailing slash. Redirect to target proxy endpoint
|
||||||
@ -136,52 +137,63 @@ Once entered this routing segment, the root routing options will take over
|
|||||||
for the routing logic.
|
for the routing logic.
|
||||||
*/
|
*/
|
||||||
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
domainOnly := r.Host
|
domainOnly := r.Host
|
||||||
if strings.Contains(r.Host, ":") {
|
if strings.Contains(r.Host, ":") {
|
||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
|
//Get the proxy root config
|
||||||
//Route to custom domain
|
proot := h.Parent.Root
|
||||||
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
|
switch proot.DefaultSiteOption {
|
||||||
//Not set. Redirect to first level of domain redirectable
|
case DefaultSite_InternalStaticWebServer:
|
||||||
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
|
fallthrough
|
||||||
if err != nil {
|
case DefaultSite_ReverseProxy:
|
||||||
//Redirect to proxy root
|
//They both share the same behavior
|
||||||
h.proxyRequest(w, r, h.Parent.Root)
|
|
||||||
} else {
|
//Check if any virtual directory rules matches
|
||||||
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
|
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||||
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
|
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||||
}
|
//Virtual directory routing rule found. Route via vdir mode
|
||||||
|
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||||
return
|
return
|
||||||
} else if h.isTopLevelRedirectableDomain(domainOnly) {
|
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
|
||||||
//This is requesting a top level private domain that should be serving root
|
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||||
h.proxyRequest(w, r, h.Parent.Root)
|
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||||
} else {
|
//Missing tailing slash. Redirect to target proxy endpoint
|
||||||
//Validate the redirection target URL
|
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||||
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//No vdir match. Route via root router
|
||||||
|
h.hostRequest(w, r, h.Parent.Root)
|
||||||
|
case DefaultSite_Redirect:
|
||||||
|
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
|
||||||
|
if redirectTarget == "" {
|
||||||
|
redirectTarget = "about:blank"
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if it is an infinite loopback redirect
|
||||||
|
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Error when parsing target. Send to root
|
//Error when parsing target. Send to root
|
||||||
h.proxyRequest(w, r, h.Parent.Root)
|
h.hostRequest(w, r, h.Parent.Root)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hostname := parsedURL.Hostname()
|
hostname := parsedURL.Hostname()
|
||||||
if domainOnly != hostname {
|
if hostname == domainOnly {
|
||||||
//Redirect to target
|
h.logRequest(r, false, 500, "root-redirect", domainOnly)
|
||||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
http.Error(w, "Loopback redirects due to invalid settings", 500)
|
||||||
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
|
|
||||||
return
|
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 {
|
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||||
//Route to root
|
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||||
h.proxyRequest(w, r, h.Parent.Root)
|
case DefaultSite_NotFoundPage:
|
||||||
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,44 +231,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
return false
|
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()
|
u, p, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
@ -48,7 +44,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matchingFound {
|
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.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
w.WriteHeader(401)
|
w.WriteHeader(401)
|
||||||
return errors.New("unauthorized")
|
return errors.New("unauthorized")
|
||||||
|
@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
|
|||||||
ProxyDomain string
|
ProxyDomain string
|
||||||
OriginalHost string
|
OriginalHost string
|
||||||
UseTLS bool
|
UseTLS bool
|
||||||
|
NoCache bool
|
||||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +244,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeHeaders(header http.Header) {
|
func removeHeaders(header http.Header, noCache bool) {
|
||||||
// Remove hop-by-hop headers listed in the "Connection" header.
|
// Remove hop-by-hop headers listed in the "Connection" header.
|
||||||
if c := header.Get("Connection"); c != "" {
|
if c := header.Get("Connection"); c != "" {
|
||||||
for _, f := range strings.Split(c, ",") {
|
for _, f := range strings.Split(c, ",") {
|
||||||
@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if header.Get("A-Upgrade") != "" {
|
//Restore the Upgrade header if any
|
||||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||||
header.Del("A-Upgrade")
|
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||||
|
header.Del("Zr-Origin-Upgrade")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Disable cache if nocache is set
|
||||||
|
if noCache {
|
||||||
|
header.Del("Cache-Control")
|
||||||
|
header.Set("Cache-Control", "no-store")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
|
|||||||
req.Header.Set("X-Forwarded-Proto", "http")
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Header.Get("X-Real-Ip") == "" {
|
||||||
|
//Not exists. Fill it in with client IP
|
||||||
|
req.Header.Set("X-Real-Ip", clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
copyHeader(outreq.Header, req.Header)
|
copyHeader(outreq.Header, req.Header)
|
||||||
|
|
||||||
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||||
removeHeaders(outreq.Header)
|
removeHeaders(outreq.Header, rrr.NoCache)
|
||||||
|
|
||||||
// Add X-Forwarded-For Header.
|
// Add X-Forwarded-For Header.
|
||||||
addXForwardedForHeader(outreq)
|
addXForwardedForHeader(outreq)
|
||||||
@ -339,7 +352,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
|
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
|
||||||
removeHeaders(res.Header)
|
removeHeaders(res.Header, rrr.NoCache)
|
||||||
|
|
||||||
if p.ModifyResponse != nil {
|
if p.ModifyResponse != nil {
|
||||||
if err := p.ModifyResponse(res); err != nil {
|
if err := p.ModifyResponse(res); err != nil {
|
||||||
|
@ -22,11 +22,9 @@ import (
|
|||||||
|
|
||||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||||
proxyMap := sync.Map{}
|
proxyMap := sync.Map{}
|
||||||
domainMap := sync.Map{}
|
|
||||||
thisRouter := Router{
|
thisRouter := Router{
|
||||||
Option: &option,
|
Option: &option,
|
||||||
ProxyEndpoints: &proxyMap,
|
ProxyEndpoints: &proxyMap,
|
||||||
SubdomainEndpoint: &domainMap,
|
|
||||||
Running: false,
|
Running: false,
|
||||||
server: nil,
|
server: nil,
|
||||||
routingRules: []*RoutingRule{},
|
routingRules: []*RoutingRule{},
|
||||||
@ -37,12 +35,6 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
|
|||||||
Parent: &thisRouter,
|
Parent: &thisRouter,
|
||||||
}
|
}
|
||||||
|
|
||||||
//Prase the tld map for tld redirection in main router
|
|
||||||
//See Server.go declarations
|
|
||||||
if len(rawTldMap) > 0 {
|
|
||||||
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &thisRouter, nil
|
return &thisRouter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
|||||||
func (router *Router) StartProxyService() error {
|
func (router *Router) StartProxyService() error {
|
||||||
//Create a new server object
|
//Create a new server object
|
||||||
if router.server != nil {
|
if router.server != nil {
|
||||||
return errors.New("Reverse proxy server already running")
|
return errors.New("reverse proxy server already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if root route is set
|
//Check if root route is set
|
||||||
if router.Root == nil {
|
if router.Root == nil {
|
||||||
return errors.New("Reverse proxy router root not set")
|
return errors.New("reverse proxy router root not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load root options from file
|
|
||||||
loadedRootOption, err := loadRootRoutingOptionsFromFile()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
router.RootRoutingOptions = loadedRootOption
|
|
||||||
|
|
||||||
minVersion := tls.VersionTLS10
|
minVersion := tls.VersionTLS10
|
||||||
if router.Option.ForceTLSLatest {
|
if router.Option.ForceTLSLatest {
|
||||||
minVersion = tls.VersionTLS12
|
minVersion = tls.VersionTLS12
|
||||||
@ -101,16 +86,6 @@ func (router *Router) StartProxyService() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if router.Option.UseTls {
|
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{
|
router.server = &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||||
Handler: router.mux,
|
Handler: router.mux,
|
||||||
@ -129,7 +104,7 @@ func (router *Router) StartProxyService() error {
|
|||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
|
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && sep.BypassGlobalTLS {
|
if sep != nil && sep.BypassGlobalTLS {
|
||||||
//Allow routing via non-TLS handler
|
//Allow routing via non-TLS handler
|
||||||
originalHostHeader := r.Host
|
originalHostHeader := r.Host
|
||||||
@ -140,7 +115,7 @@ func (router *Router) StartProxyService() error {
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
r.URL, _ = url.Parse(originalHostHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: sep.Domain,
|
ProxyDomain: sep.Domain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
UseTLS: sep.RequireTLS,
|
UseTLS: sep.RequireTLS,
|
||||||
@ -225,7 +200,7 @@ func (router *Router) StartProxyService() error {
|
|||||||
|
|
||||||
func (router *Router) StopProxyService() error {
|
func (router *Router) StopProxyService() error {
|
||||||
if router.server == nil {
|
if router.server == nil {
|
||||||
return errors.New("Reverse proxy server already stopped")
|
return errors.New("reverse proxy server already stopped")
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -253,13 +228,13 @@ func (router *Router) StopProxyService() error {
|
|||||||
// Restart the current router if it is running.
|
// Restart the current router if it is running.
|
||||||
func (router *Router) Restart() error {
|
func (router *Router) Restart() error {
|
||||||
//Stop the router if it is already running
|
//Stop the router if it is already running
|
||||||
var err error = nil
|
|
||||||
if router.Running {
|
if router.Running {
|
||||||
err := router.StopProxyService()
|
err := router.StopProxyService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
// Start the server
|
// Start the server
|
||||||
err = router.StartProxyService()
|
err = router.StartProxyService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -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 = r.Host
|
||||||
}
|
}
|
||||||
hostname = strings.Split(hostname, ":")[0]
|
hostname = strings.Split(hostname, ":")[0]
|
||||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||||
return subdEndpoint != nil
|
return subdEndpoint != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Add an URL into a custom proxy services
|
|
||||||
*/
|
|
||||||
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
|
|
||||||
domain := options.Domain
|
|
||||||
if domain[len(domain)-1:] == "/" {
|
|
||||||
domain = domain[:len(domain)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if rootname[len(rootname)-1:] == "/" {
|
|
||||||
rootname = rootname[:len(rootname)-1]
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
webProxyEndpoint := domain
|
|
||||||
if options.RequireTLS {
|
|
||||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
|
||||||
} else {
|
|
||||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
|
||||||
}
|
|
||||||
//Create a new proxy agent for this root
|
|
||||||
path, err := url.Parse(webProxyEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
|
|
||||||
|
|
||||||
endpointObject := ProxyEndpoint{
|
|
||||||
ProxyType: ProxyType_Vdir,
|
|
||||||
RootOrMatchingDomain: options.RootName,
|
|
||||||
Domain: domain,
|
|
||||||
RequireTLS: options.RequireTLS,
|
|
||||||
SkipCertValidations: options.SkipCertValidations,
|
|
||||||
RequireBasicAuth: options.RequireBasicAuth,
|
|
||||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
|
||||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
|
||||||
Proxy: proxy,
|
|
||||||
}
|
|
||||||
|
|
||||||
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
|
|
||||||
|
|
||||||
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Load routing from RP
|
Load routing from RP
|
||||||
*/
|
*/
|
||||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||||
if ptype == "vdir" {
|
var targetProxyEndpoint *ProxyEndpoint
|
||||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||||
if !ok {
|
key, ok := key.(string)
|
||||||
return nil, errors.New("target proxy not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetProxy := proxy.(*ProxyEndpoint)
|
|
||||||
targetProxy.parent = router
|
|
||||||
return targetProxy, nil
|
|
||||||
} else if ptype == "subd" {
|
|
||||||
proxy, ok := router.SubdomainEndpoint.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("target proxy not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetProxy := proxy.(*ProxyEndpoint)
|
|
||||||
targetProxy.parent = router
|
|
||||||
return targetProxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("unsupported ptype")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Add an default router for the proxy server
|
|
||||||
*/
|
|
||||||
func (router *Router) SetRootProxy(options *RootOptions) error {
|
|
||||||
proxyLocation := options.ProxyLocation
|
|
||||||
if proxyLocation[len(proxyLocation)-1:] == "/" {
|
|
||||||
proxyLocation = proxyLocation[:len(proxyLocation)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
webProxyEndpoint := proxyLocation
|
|
||||||
if options.RequireTLS {
|
|
||||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
|
||||||
} else {
|
|
||||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
|
||||||
}
|
|
||||||
//Create a new proxy agent for this root
|
|
||||||
path, err := url.Parse(webProxyEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
|
||||||
|
|
||||||
rootEndpoint := ProxyEndpoint{
|
|
||||||
ProxyType: ProxyType_Vdir,
|
|
||||||
RootOrMatchingDomain: "/",
|
|
||||||
Domain: proxyLocation,
|
|
||||||
RequireTLS: options.RequireTLS,
|
|
||||||
SkipCertValidations: options.SkipCertValidations,
|
|
||||||
RequireBasicAuth: options.RequireBasicAuth,
|
|
||||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
|
||||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
|
||||||
Proxy: proxy,
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Root = &rootEndpoint
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers to export the syncmap for easier processing
|
|
||||||
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
|
||||||
m := make(map[string]*ProxyEndpoint)
|
|
||||||
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
|
||||||
k, ok := key.(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -409,13 +273,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
m[k] = v
|
|
||||||
|
if key == matchingDomain {
|
||||||
|
targetProxyEndpoint = v
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return m
|
|
||||||
|
if targetProxyEndpoint == nil {
|
||||||
|
return nil, errors.New("target routing rule not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetProxyEndpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
// Deep copy a proxy endpoint, excluding runtime paramters
|
||||||
|
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
|
||||||
|
js, _ := json.Marshal(endpoint)
|
||||||
|
newProxyEndpoint := ProxyEndpoint{}
|
||||||
|
err := json.Unmarshal(js, &newProxyEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &newProxyEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||||
m := make(map[string]*ProxyEndpoint)
|
m := make(map[string]*ProxyEndpoint)
|
||||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||||
k, ok := key.(string)
|
k, ok := key.(string)
|
||||||
|
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"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
@ -28,13 +30,41 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
|||||||
return targetProxyEndpoint
|
return targetProxyEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||||
if ok {
|
if ok {
|
||||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
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
|
return targetSubdomainEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,14 +84,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
|||||||
return rewrittenURL
|
return rewrittenURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle subdomain request
|
// Handle host request
|
||||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||||
|
|
||||||
|
//Inject custom headers
|
||||||
|
if len(target.UserDefinedHeaders) > 0 {
|
||||||
|
for _, customHeader := range target.UserDefinedHeaders {
|
||||||
|
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestURL := r.URL.String()
|
requestURL := r.URL.String()
|
||||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||||
r.Header.Set("A-Upgrade", "websocket")
|
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||||
wsRedirectionEndpoint := target.Domain
|
wsRedirectionEndpoint := target.Domain
|
||||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||||
//Append / to the end of the redirection endpoint if not exists
|
//Append / to the end of the redirection endpoint if not exists
|
||||||
@ -89,10 +127,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
r.URL, _ = url.Parse(originalHostHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: target.Domain,
|
ProxyDomain: target.Domain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
UseTLS: target.RequireTLS,
|
UseTLS: target.RequireTLS,
|
||||||
|
NoCache: h.Parent.Option.NoCache,
|
||||||
PathPrefix: "",
|
PathPrefix: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -113,15 +152,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle vdir type request
|
// Handle vdir type request
|
||||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||||
r.URL, _ = url.Parse(rewriteURL)
|
r.URL, _ = url.Parse(rewriteURL)
|
||||||
|
|
||||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||||
|
|
||||||
|
//Inject custom headers
|
||||||
|
if len(target.parent.UserDefinedHeaders) > 0 {
|
||||||
|
for _, customHeader := range target.parent.UserDefinedHeaders {
|
||||||
|
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||||
r.Header.Set("A-Upgrade", "websocket")
|
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||||
wsRedirectionEndpoint := target.Domain
|
wsRedirectionEndpoint := target.Domain
|
||||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||||
@ -144,11 +191,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
r.URL, _ = url.Parse(originalHostHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: target.Domain,
|
ProxyDomain: target.Domain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
UseTLS: target.RequireTLS,
|
UseTLS: target.RequireTLS,
|
||||||
PathPrefix: target.RootOrMatchingDomain,
|
PathPrefix: target.MatchingPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
|
@ -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
|
|
||||||
}
|
|
110
src/mod/dynamicproxy/router.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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, "", 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, 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
|
|
||||||
}
|
|
@ -14,8 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ProxyType_Subdomain = 0
|
ProxyType_Root = 0
|
||||||
ProxyType_Vdir = 1
|
ProxyType_Host = 1
|
||||||
|
ProxyType_Vdir = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyHandler struct {
|
type ProxyHandler struct {
|
||||||
@ -24,9 +25,11 @@ type ProxyHandler struct {
|
|||||||
|
|
||||||
type RouterOption struct {
|
type RouterOption struct {
|
||||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||||
|
HostVersion string //The version of Zoraxy, use for heading mod
|
||||||
Port int //Incoming port
|
Port int //Incoming port
|
||||||
UseTls bool //Use TLS to serve incoming requsts
|
UseTls bool //Use TLS to serve incoming requsts
|
||||||
ForceTLSLatest bool //Force TLS1.2 or above
|
ForceTLSLatest bool //Force TLS1.2 or above
|
||||||
|
NoCache bool //Force set Cache-Control: no-store
|
||||||
ListenOnPort80 bool //Enable port 80 http listener
|
ListenOnPort80 bool //Enable port 80 http listener
|
||||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||||
TlsManager *tlscert.Manager
|
TlsManager *tlscert.Manager
|
||||||
@ -39,10 +42,8 @@ type RouterOption struct {
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
Option *RouterOption
|
Option *RouterOption
|
||||||
ProxyEndpoints *sync.Map
|
ProxyEndpoints *sync.Map
|
||||||
SubdomainEndpoint *sync.Map
|
|
||||||
Running bool
|
Running bool
|
||||||
Root *ProxyEndpoint
|
Root *ProxyEndpoint
|
||||||
RootRoutingOptions *RootRoutingOptions
|
|
||||||
mux http.Handler
|
mux http.Handler
|
||||||
server *http.Server
|
server *http.Server
|
||||||
tlsListener net.Listener
|
tlsListener net.Listener
|
||||||
@ -69,63 +70,70 @@ type BasicAuthExceptionRule struct {
|
|||||||
PathPrefix string
|
PathPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// A proxy endpoint record
|
// 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 {
|
type ProxyEndpoint struct {
|
||||||
ProxyType int //The type of this proxy, see const def
|
ProxyType int //The type of this proxy, see const def
|
||||||
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
|
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||||
Domain string //Domain or IP to proxy to
|
Domain string //Domain or IP to proxy to
|
||||||
|
|
||||||
|
//TLS/SSL Related
|
||||||
RequireTLS bool //Target domain require TLS
|
RequireTLS bool //Target domain require TLS
|
||||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||||
SkipCertValidations bool //Set to true to accept self signed certs
|
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:"-"`
|
|
||||||
|
|
||||||
|
//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
|
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
|
// Root options are those that are required for reverse proxy handler to work
|
||||||
type RootOptions struct {
|
const (
|
||||||
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
|
DefaultSite_InternalStaticWebServer = 0
|
||||||
RequireTLS bool //Proxy root target require TLS connection (not recommended)
|
DefaultSite_ReverseProxy = 1
|
||||||
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
|
DefaultSite_Redirect = 2
|
||||||
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
|
DefaultSite_NotFoundPage = 3
|
||||||
|
)
|
||||||
//Basic Auth Related
|
|
||||||
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
|
|
||||||
BasicAuthCredentials []*BasicAuthCredentials
|
|
||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional options are here for letting router knows how to route exception cases for root
|
|
||||||
type RootRoutingOptions struct {
|
|
||||||
//Root only configs
|
|
||||||
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
|
|
||||||
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
|
|
||||||
}
|
|
||||||
|
|
||||||
type VdirOptions struct {
|
|
||||||
RootName string
|
|
||||||
Domain string
|
|
||||||
RequireTLS bool
|
|
||||||
BypassGlobalTLS bool
|
|
||||||
SkipCertValidations bool
|
|
||||||
RequireBasicAuth bool
|
|
||||||
BasicAuthCredentials []*BasicAuthCredentials
|
|
||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubdOptions struct {
|
|
||||||
MatchingDomain string
|
|
||||||
Domain string
|
|
||||||
RequireTLS bool
|
|
||||||
BypassGlobalTLS bool
|
|
||||||
SkipCertValidations bool
|
|
||||||
RequireBasicAuth bool
|
|
||||||
BasicAuthCredentials []*BasicAuthCredentials
|
|
||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Web Templates
|
Web Templates
|
||||||
|
@ -207,7 +207,7 @@ func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request)
|
|||||||
utils.SendOK(w)
|
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) {
|
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
|
||||||
netid, err := utils.GetPara(r, "netid")
|
netid, err := utils.GetPara(r, "netid")
|
||||||
if err != nil {
|
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) {
|
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||||
netid, err := utils.PostPara(r, "netid")
|
netid, err := utils.PostPara(r, "netid")
|
||||||
if err != nil {
|
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) {
|
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
|
||||||
netid, err := utils.PostPara(r, "netid")
|
netid, err := utils.PostPara(r, "netid")
|
||||||
if err != nil {
|
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) {
|
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
|
||||||
netid, err := utils.PostPara(r, "netid")
|
netid, err := utils.PostPara(r, "netid")
|
||||||
if err != nil {
|
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) {
|
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
netid, err := utils.PostPara(r, "netid")
|
netid, err := utils.PostPara(r, "netid")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -426,3 +426,79 @@ func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
utils.SendOK(w)
|
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"`
|
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) {
|
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
|
||||||
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
|
|||||||
return &networkInfo, nil
|
return &networkInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//List network details
|
// List network details
|
||||||
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
|
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)
|
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -249,7 +249,7 @@ func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *Ne
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//List network IDs
|
// List network IDs
|
||||||
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -281,7 +281,7 @@ func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
|||||||
return networkIds, nil
|
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 {
|
func (m *NetworkManager) networkExists(networkId string) bool {
|
||||||
networkIds, err := m.listNetworkIds()
|
networkIds, err := m.listNetworkIds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -297,7 +297,7 @@ func (m *NetworkManager) networkExists(networkId string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
//delete a network
|
// delete a network
|
||||||
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
@ -330,8 +330,8 @@ func (m *NetworkManager) deleteNetwork(networkID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Configure network
|
// Configure network
|
||||||
//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
// 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 {
|
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
|
||||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
@ -545,7 +545,7 @@ func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool
|
|||||||
return false
|
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) {
|
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)
|
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -573,7 +573,7 @@ func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*M
|
|||||||
return thisMemeberInfo, nil
|
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 {
|
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
|
||||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
||||||
payload := []byte(`{"authorized": true}`)
|
payload := []byte(`{"authorized": true}`)
|
||||||
@ -600,7 +600,7 @@ func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAutho
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Delete a member from the network
|
// Delete a member from the network
|
||||||
func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
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)
|
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -620,3 +620,45 @@ func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
package geodb
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Whitelist.go
|
Whitelist.go
|
||||||
@ -8,11 +11,29 @@ import "strings"
|
|||||||
This script handles whitelist related functions
|
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
|
//Geo Whitelist
|
||||||
|
|
||||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
|
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||||
countryCode = strings.ToLower(countryCode)
|
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) {
|
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||||
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
|||||||
|
|
||||||
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
|
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
|
||||||
countryCode = strings.ToLower(countryCode)
|
countryCode = strings.ToLower(countryCode)
|
||||||
var isWhitelisted bool = false
|
return s.sysdb.KeyExists("whitelist-cn", countryCode)
|
||||||
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
|
|
||||||
return isWhitelisted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetAllWhitelistedCountryCode() []string {
|
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||||
whitelistedCountryCode := []string{}
|
whitelistedCountryCode := []*WhitelistEntry{}
|
||||||
entries, err := s.sysdb.ListTable("whitelist-cn")
|
entries, err := s.sysdb.ListTable("whitelist-cn")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return whitelistedCountryCode
|
return whitelistedCountryCode
|
||||||
}
|
}
|
||||||
for _, keypairs := range entries {
|
for _, keypairs := range entries {
|
||||||
ip := string(keypairs[0])
|
thisWhitelistEntry := WhitelistEntry{}
|
||||||
whitelistedCountryCode = append(whitelistedCountryCode, ip)
|
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
|
||||||
|
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return whitelistedCountryCode
|
return whitelistedCountryCode
|
||||||
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
|
|||||||
|
|
||||||
//IP Whitelist
|
//IP Whitelist
|
||||||
|
|
||||||
func (s *Store) AddIPToWhiteList(ipAddr string) {
|
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
|
||||||
s.sysdb.Write("whitelist-ip", ipAddr, true)
|
thisIpEntry := WhitelistEntry{
|
||||||
|
EntryType: EntryType_IP,
|
||||||
|
IP: ipAddr,
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||||
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||||
var isWhitelisted bool = false
|
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
|
||||||
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
|
|
||||||
if isWhitelisted {
|
if isWhitelisted {
|
||||||
|
//single IP whitelist entry
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check for IP wildcard and CIRD rules
|
//Check for IP wildcard and CIRD rules
|
||||||
AllWhitelistedIps := s.GetAllWhitelistedIp()
|
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
|
||||||
for _, whitelistRules := range AllWhitelistedIps {
|
for _, whitelistRules := range AllWhitelistedIps {
|
||||||
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
|
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
|
||||||
if wildcardMatch {
|
if wildcardMatch {
|
||||||
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetAllWhitelistedIp() []string {
|
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
|
||||||
whitelistedIp := []string{}
|
whitelistedIp := []*WhitelistEntry{}
|
||||||
entries, err := s.sysdb.ListTable("whitelist-ip")
|
entries, err := s.sysdb.ListTable("whitelist-ip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return whitelistedIp
|
return whitelistedIp
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, keypairs := range entries {
|
for _, keypairs := range entries {
|
||||||
ip := string(keypairs[0])
|
//ip := string(keypairs[0])
|
||||||
whitelistedIp = append(whitelistedIp, ip)
|
thisEntry := WhitelistEntry{}
|
||||||
|
json.Unmarshal(keypairs[1], &thisEntry)
|
||||||
|
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return whitelistedIp
|
return whitelistedIp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
|
||||||
|
allWhitelistedIPs := []string{}
|
||||||
|
entries := s.GetAllWhitelistedIp()
|
||||||
|
for _, entry := range entries {
|
||||||
|
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allWhitelistedIPs
|
||||||
|
}
|
||||||
|
@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if header.Get("A-Upgrade") != "" {
|
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||||
header.Del("A-Upgrade")
|
header.Del("Zr-Origin-Upgrade")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
|
|||||||
requestURL := r.URL.String()
|
requestURL := r.URL.String()
|
||||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||||
r.Header.Set("A-Upgrade", "websocket")
|
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||||
requestURL = strings.TrimPrefix(requestURL, "/")
|
requestURL = strings.TrimPrefix(requestURL, "/")
|
||||||
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
|
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
|
||||||
wspHandler := websocketproxy.NewProxy(u, false)
|
wspHandler := websocketproxy.NewProxy(u, false)
|
||||||
|
@ -5,22 +5,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//This remove the certificates in the list where either the
|
// This remove the certificates in the list where either the
|
||||||
//public key or the private key is missing
|
// public key or the private key is missing
|
||||||
func getCertPairs(certFiles []string) []string {
|
func getCertPairs(certFiles []string) []string {
|
||||||
crtMap := make(map[string]bool)
|
pemMap := make(map[string]bool)
|
||||||
keyMap := make(map[string]bool)
|
keyMap := make(map[string]bool)
|
||||||
|
|
||||||
for _, filename := range certFiles {
|
for _, filename := range certFiles {
|
||||||
if filepath.Ext(filename) == ".crt" {
|
if filepath.Ext(filename) == ".pem" {
|
||||||
crtMap[strings.TrimSuffix(filename, ".crt")] = true
|
pemMap[strings.TrimSuffix(filename, ".pem")] = true
|
||||||
} else if filepath.Ext(filename) == ".key" {
|
} else if filepath.Ext(filename) == ".key" {
|
||||||
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
for domain := range crtMap {
|
for domain := range pemMap {
|
||||||
if keyMap[domain] {
|
if keyMap[domain] {
|
||||||
result = append(result, domain)
|
result = append(result, domain)
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
|
|||||||
return result
|
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 {
|
func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||||
var matchingDomain string = ""
|
var matchingDomain string = ""
|
||||||
maxLength := 0
|
maxLength := 0
|
||||||
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
|||||||
|
|
||||||
return matchingDomain
|
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
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,12 +14,19 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CertCache struct {
|
||||||
|
Cert *x509.Certificate
|
||||||
|
PubKey string
|
||||||
|
PriKey string
|
||||||
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
CertStore string
|
CertStore string //Path where all the certs are stored
|
||||||
|
LoadedCerts []*CertCache //A list of loaded certs
|
||||||
verbal bool
|
verbal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed localhost.crt localhost.key
|
//go:embed localhost.pem localhost.key
|
||||||
var buildinCertStore embed.FS
|
var buildinCertStore embed.FS
|
||||||
|
|
||||||
func NewManager(certStore string, verbal bool) (*Manager, error) {
|
func NewManager(certStore string, verbal bool) (*Manager, error) {
|
||||||
@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
|
|||||||
os.MkdirAll(certStore, 0775)
|
os.MkdirAll(certStore, 0775)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pubKey := "./tmp/localhost.pem"
|
||||||
|
priKey := "./tmp/localhost.key"
|
||||||
|
|
||||||
|
//Check if this is initial setup
|
||||||
|
if !utils.FileExists(pubKey) {
|
||||||
|
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
|
||||||
|
os.WriteFile(pubKey, buildInPubKey, 0775)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.FileExists(priKey) {
|
||||||
|
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
|
||||||
|
os.WriteFile(priKey, buildInPriKey, 0775)
|
||||||
|
}
|
||||||
|
|
||||||
thisManager := Manager{
|
thisManager := Manager{
|
||||||
CertStore: certStore,
|
CertStore: certStore,
|
||||||
|
LoadedCerts: []*CertCache{},
|
||||||
verbal: verbal,
|
verbal: verbal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := thisManager.UpdateLoadedCertList()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &thisManager, nil
|
return &thisManager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update domain mapping from file
|
||||||
|
func (m *Manager) UpdateLoadedCertList() error {
|
||||||
|
//Get a list of certificates from file
|
||||||
|
domainList, err := m.ListCertDomains()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load each of the certificates into memory
|
||||||
|
certList := []*CertCache{}
|
||||||
|
for _, certname := range domainList {
|
||||||
|
//Read their certificate into memory
|
||||||
|
pubKey := filepath.Join(m.CertStore, certname+".pem")
|
||||||
|
priKey := filepath.Join(m.CertStore, certname+".key")
|
||||||
|
certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Certificate loaded failed: " + certname)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, thisCert := range certificate.Certificate {
|
||||||
|
loadedCert, err := x509.ParseCertificate(thisCert)
|
||||||
|
if err != nil {
|
||||||
|
//Error pasring cert, skip this byte segment
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
thisCacheEntry := CertCache{
|
||||||
|
Cert: loadedCert,
|
||||||
|
PubKey: pubKey,
|
||||||
|
PriKey: priKey,
|
||||||
|
}
|
||||||
|
certList = append(certList, &thisCacheEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Replace runtime cert array
|
||||||
|
m.LoadedCerts = certList
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match cert by CN
|
||||||
|
func (m *Manager) CertMatchExists(serverName string) bool {
|
||||||
|
for _, certCacheEntry := range m.LoadedCerts {
|
||||||
|
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cert entry by matching server name, return pubKey and priKey if found
|
||||||
|
// check with CertMatchExists before calling to the load function
|
||||||
|
func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
|
||||||
|
for _, certCacheEntry := range m.LoadedCerts {
|
||||||
|
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
|
||||||
|
return certCacheEntry.PubKey, certCacheEntry.PriKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a list of domains by filename
|
||||||
func (m *Manager) ListCertDomains() ([]string, error) {
|
func (m *Manager) ListCertDomains() ([]string, error) {
|
||||||
filenames, err := m.ListCerts()
|
filenames, err := m.ListCerts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
|
|||||||
return filenames, nil
|
return filenames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a list of cert files (public and private keys)
|
||||||
func (m *Manager) ListCerts() ([]string, error) {
|
func (m *Manager) ListCerts() ([]string, error) {
|
||||||
certs, err := ioutil.ReadDir(m.CertStore)
|
certs, err := os.ReadDir(m.CertStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
@ -64,36 +156,31 @@ func (m *Manager) ListCerts() ([]string, error) {
|
|||||||
return filenames, nil
|
return filenames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a certificate from disk where its certificate matches with the helloinfo
|
||||||
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
//Check if the domain corrisponding cert exists
|
//Check if the domain corrisponding cert exists
|
||||||
pubKey := "./tmp/localhost.crt"
|
pubKey := "./tmp/localhost.pem"
|
||||||
priKey := "./tmp/localhost.key"
|
priKey := "./tmp/localhost.key"
|
||||||
|
|
||||||
//Check if this is initial setup
|
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
||||||
if !utils.FileExists(pubKey) {
|
//Direct hit
|
||||||
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
|
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
|
||||||
os.WriteFile(pubKey, buildInPubKey, 0775)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.FileExists(priKey) {
|
|
||||||
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
|
|
||||||
os.WriteFile(priKey, buildInPriKey, 0775)
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
|
||||||
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
|
|
||||||
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
||||||
|
} else if m.CertMatchExists(helloInfo.ServerName) {
|
||||||
|
//Use x509
|
||||||
|
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
|
||||||
} else {
|
} else {
|
||||||
|
//Fallback to legacy method of matching certificates
|
||||||
|
/*
|
||||||
domainCerts, _ := m.ListCertDomains()
|
domainCerts, _ := m.ListCertDomains()
|
||||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||||
if cloestDomainCert != "" {
|
if cloestDomainCert != "" {
|
||||||
//There is a matching parent domain for this subdomain. Use this instead.
|
//There is a matching parent domain for this subdomain. Use this instead.
|
||||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
|
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
|
||||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||||
} else if m.DefaultCertExists() {
|
} else if m.DefaultCertExists() {
|
||||||
//Use default.crt and default.key
|
//Use default.pem and default.key
|
||||||
pubKey = filepath.Join(m.CertStore, "default.crt")
|
pubKey = filepath.Join(m.CertStore, "default.pem")
|
||||||
priKey = filepath.Join(m.CertStore, "default.key")
|
priKey = filepath.Join(m.CertStore, "default.key")
|
||||||
if m.verbal {
|
if m.verbal {
|
||||||
log.Println("No matching certificate found. Serving with default")
|
log.Println("No matching certificate found. Serving with default")
|
||||||
@ -102,6 +189,19 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
|
|||||||
if m.verbal {
|
if m.verbal {
|
||||||
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
|
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if m.DefaultCertExists() {
|
||||||
|
//Use default.pem and default.key
|
||||||
|
pubKey = filepath.Join(m.CertStore, "default.pem")
|
||||||
|
priKey = filepath.Join(m.CertStore, "default.key")
|
||||||
|
//if m.verbal {
|
||||||
|
// log.Println("No matching certificate found. Serving with default")
|
||||||
|
//}
|
||||||
|
} else {
|
||||||
|
//if m.verbal {
|
||||||
|
// log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,17 +217,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
|
|||||||
|
|
||||||
// Check if both the default cert public key and private key exists
|
// Check if both the default cert public key and private key exists
|
||||||
func (m *Manager) DefaultCertExists() bool {
|
func (m *Manager) DefaultCertExists() bool {
|
||||||
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the default cert exists returning seperate results for pubkey and prikey
|
// Check if the default cert exists returning seperate results for pubkey and prikey
|
||||||
func (m *Manager) DefaultCertExistsSep() (bool, bool) {
|
func (m *Manager) DefaultCertExistsSep() (bool, bool) {
|
||||||
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the cert if exists
|
// Delete the cert if exists
|
||||||
func (m *Manager) RemoveCert(domain string) error {
|
func (m *Manager) RemoveCert(domain string) error {
|
||||||
pubKey := filepath.Join(m.CertStore, domain+".crt")
|
pubKey := filepath.Join(m.CertStore, domain+".pem")
|
||||||
priKey := filepath.Join(m.CertStore, domain+".key")
|
priKey := filepath.Join(m.CertStore, domain+".key")
|
||||||
if utils.FileExists(pubKey) {
|
if utils.FileExists(pubKey) {
|
||||||
err := os.Remove(pubKey)
|
err := os.Remove(pubKey)
|
||||||
@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Update the cert list
|
||||||
|
m.UpdateLoadedCertList()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Check if the certificate is a valid TLS/SSL certificate
|
// Check if the certificate is a valid TLS/SSL certificate
|
||||||
return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
|
return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
|
||||||
} else if strings.Contains(block.Type, "PRIVATE KEY") {
|
} else if strings.Contains(block.Type, "PRIVATE KEY") {
|
||||||
// The file contains a private key
|
// The file contains a private key
|
||||||
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
if err != nil {
|
return err == nil
|
||||||
// Handle the error
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getWebsiteStatus(url string) (int, error) {
|
func getWebsiteStatus(url string) (int, error) {
|
||||||
resp, err := http.Get(url)
|
client := http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Try replace the http with https and vise versa
|
//Try replace the http with https and vise versa
|
||||||
rewriteURL := ""
|
rewriteURL := ""
|
||||||
@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
|
|||||||
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = http.Get(rewriteURL)
|
resp, err = client.Get(rewriteURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
|
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
|
||||||
//Invalid downstream reverse proxy settings, but it is online
|
//Invalid downstream reverse proxy settings, but it is online
|
||||||
|
@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ func (ws *WebServer) RestorePreviousState() {
|
|||||||
ws.option.EnableDirectoryListing = enableDirList
|
ws.option.EnableDirectoryListing = enableDirList
|
||||||
|
|
||||||
//Check the running state
|
//Check the running state
|
||||||
webservRunning := false
|
webservRunning := true
|
||||||
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
||||||
if webservRunning {
|
if webservRunning {
|
||||||
ws.Start()
|
ws.Start()
|
||||||
@ -124,6 +124,11 @@ func (ws *WebServer) ChangePort(port string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current using port in options
|
||||||
|
func (ws *WebServer) GetListeningPort() string {
|
||||||
|
return ws.option.Port
|
||||||
|
}
|
||||||
|
|
||||||
// Start starts the web server.
|
// Start starts the web server.
|
||||||
func (ws *WebServer) Start() error {
|
func (ws *WebServer) Start() error {
|
||||||
ws.mu.Lock()
|
ws.mu.Lock()
|
||||||
|
@ -21,6 +21,9 @@ var (
|
|||||||
|
|
||||||
// Add user customizable reverse proxy
|
// Add user customizable reverse proxy
|
||||||
func ReverseProxtInit() {
|
func ReverseProxtInit() {
|
||||||
|
/*
|
||||||
|
Load Reverse Proxy Global Settings
|
||||||
|
*/
|
||||||
inboundPort := 80
|
inboundPort := 80
|
||||||
if sysdb.KeyExists("settings", "inbound") {
|
if sysdb.KeyExists("settings", "inbound") {
|
||||||
sysdb.Read("settings", "inbound", &inboundPort)
|
sysdb.Read("settings", "inbound", &inboundPort)
|
||||||
@ -45,6 +48,14 @@ func ReverseProxtInit() {
|
|||||||
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
|
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
developmentMode := false
|
||||||
|
sysdb.Read("settings", "devMode", &developmentMode)
|
||||||
|
if useTls {
|
||||||
|
SystemWideLogger.Println("Development mode enabled. Using no-store Cache Control policy")
|
||||||
|
} else {
|
||||||
|
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
|
||||||
|
}
|
||||||
|
|
||||||
listenOnPort80 := false
|
listenOnPort80 := false
|
||||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||||
if listenOnPort80 {
|
if listenOnPort80 {
|
||||||
@ -63,11 +74,19 @@ func ReverseProxtInit() {
|
|||||||
SystemWideLogger.Println("Force HTTPS mode disabled")
|
SystemWideLogger.Println("Force HTTPS mode disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a new proxy object
|
||||||
|
The DynamicProxy is the parent of all reverse proxy handlers,
|
||||||
|
use for managemening and provide functions to access proxy handlers
|
||||||
|
*/
|
||||||
|
|
||||||
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
||||||
HostUUID: nodeUUID,
|
HostUUID: nodeUUID,
|
||||||
|
HostVersion: version,
|
||||||
Port: inboundPort,
|
Port: inboundPort,
|
||||||
UseTls: useTls,
|
UseTls: useTls,
|
||||||
ForceTLSLatest: forceLatestTLSVersion,
|
ForceTLSLatest: forceLatestTLSVersion,
|
||||||
|
NoCache: developmentMode,
|
||||||
ListenOnPort80: listenOnPort80,
|
ListenOnPort80: listenOnPort80,
|
||||||
ForceHttpsRedirect: forceHttpsRedirect,
|
ForceHttpsRedirect: forceHttpsRedirect,
|
||||||
TlsManager: tlsCertManager,
|
TlsManager: tlsCertManager,
|
||||||
@ -83,45 +102,28 @@ func ReverseProxtInit() {
|
|||||||
|
|
||||||
dynamicProxyRouter = dprouter
|
dynamicProxyRouter = dprouter
|
||||||
|
|
||||||
//Load all conf from files
|
/*
|
||||||
|
|
||||||
|
Load all conf from files
|
||||||
|
|
||||||
|
*/
|
||||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||||
for _, conf := range confs {
|
for _, conf := range confs {
|
||||||
record, err := LoadReverseProxyConfig(conf)
|
err := LoadReverseProxyConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.ProxyType == "root" {
|
|
||||||
dynamicProxyRouter.SetRootProxy(&dynamicproxy.RootOptions{
|
|
||||||
ProxyLocation: record.ProxyTarget,
|
|
||||||
RequireTLS: record.UseTLS,
|
|
||||||
})
|
|
||||||
} else if record.ProxyType == "subd" {
|
|
||||||
dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
|
|
||||||
MatchingDomain: record.Rootname,
|
|
||||||
Domain: record.ProxyTarget,
|
|
||||||
RequireTLS: record.UseTLS,
|
|
||||||
BypassGlobalTLS: record.BypassGlobalTLS,
|
|
||||||
SkipCertValidations: record.SkipTlsValidation,
|
|
||||||
RequireBasicAuth: record.RequireBasicAuth,
|
|
||||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
|
||||||
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
|
|
||||||
})
|
|
||||||
} else if record.ProxyType == "vdir" {
|
|
||||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
|
|
||||||
RootName: record.Rootname,
|
|
||||||
Domain: record.ProxyTarget,
|
|
||||||
RequireTLS: record.UseTLS,
|
|
||||||
BypassGlobalTLS: record.BypassGlobalTLS,
|
|
||||||
SkipCertValidations: record.SkipTlsValidation,
|
|
||||||
RequireBasicAuth: record.RequireBasicAuth,
|
|
||||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
|
||||||
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
SystemWideLogger.PrintAndLog("Proxy", "Unsupported endpoint type: "+record.ProxyType+". Skipping "+filepath.Base(conf), nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dynamicProxyRouter.Root == nil {
|
||||||
|
//Root config not set (new deployment?), use internal static web server as root
|
||||||
|
defaultRootRouter, err := GetDefaultRootConfig()
|
||||||
|
if err != nil {
|
||||||
|
SystemWideLogger.PrintAndLog("Proxy", "Failed to generate default root routing", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dynamicProxyRouter.SetProxyRouteAsRoot(defaultRootRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start Service
|
//Start Service
|
||||||
@ -173,7 +175,7 @@ func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "type not defined")
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
return
|
return
|
||||||
@ -241,96 +243,112 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootname := ""
|
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||||
if eptype == "vdir" {
|
if eptype == "host" {
|
||||||
vdir, err := utils.PostPara(r, "rootname")
|
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "vdir not defined")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Vdir must start with /
|
|
||||||
if !strings.HasPrefix(vdir, "/") {
|
|
||||||
vdir = "/" + vdir
|
|
||||||
}
|
|
||||||
rootname = vdir
|
|
||||||
|
|
||||||
thisOption := dynamicproxy.VdirOptions{
|
|
||||||
RootName: vdir,
|
|
||||||
Domain: endpoint,
|
|
||||||
RequireTLS: useTLS,
|
|
||||||
BypassGlobalTLS: useBypassGlobalTLS,
|
|
||||||
SkipCertValidations: skipTlsValidation,
|
|
||||||
RequireBasicAuth: requireBasicAuth,
|
|
||||||
BasicAuthCredentials: basicAuthCredentials,
|
|
||||||
}
|
|
||||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
|
|
||||||
|
|
||||||
} else if eptype == "subd" {
|
|
||||||
subdomain, err := utils.PostPara(r, "rootname")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "subdomain not defined")
|
utils.SendErrorResponse(w, "subdomain not defined")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rootname = subdomain
|
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
|
||||||
thisOption := dynamicproxy.SubdOptions{
|
//I/O
|
||||||
MatchingDomain: subdomain,
|
ProxyType: dynamicproxy.ProxyType_Host,
|
||||||
|
RootOrMatchingDomain: rootOrMatchingDomain,
|
||||||
Domain: endpoint,
|
Domain: endpoint,
|
||||||
|
//TLS
|
||||||
RequireTLS: useTLS,
|
RequireTLS: useTLS,
|
||||||
BypassGlobalTLS: useBypassGlobalTLS,
|
BypassGlobalTLS: useBypassGlobalTLS,
|
||||||
SkipCertValidations: skipTlsValidation,
|
SkipCertValidations: skipTlsValidation,
|
||||||
|
//VDir
|
||||||
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
|
//Custom headers
|
||||||
|
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
||||||
|
//Auth
|
||||||
RequireBasicAuth: requireBasicAuth,
|
RequireBasicAuth: requireBasicAuth,
|
||||||
BasicAuthCredentials: basicAuthCredentials,
|
BasicAuthCredentials: basicAuthCredentials,
|
||||||
|
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||||
|
DefaultSiteOption: 0,
|
||||||
|
DefaultSiteValue: "",
|
||||||
}
|
}
|
||||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
|
||||||
} else if eptype == "root" {
|
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||||
rootname = "root"
|
if err != nil {
|
||||||
thisOption := dynamicproxy.RootOptions{
|
utils.SendErrorResponse(w, "unable to prepare proxy route to target endpoint: "+err.Error())
|
||||||
ProxyLocation: endpoint,
|
|
||||||
RequireTLS: useTLS,
|
|
||||||
}
|
|
||||||
dynamicProxyRouter.SetRootProxy(&thisOption)
|
|
||||||
} else {
|
|
||||||
//Invalid eptype
|
|
||||||
utils.SendErrorResponse(w, "Invalid endpoint type")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Save it
|
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
|
||||||
thisProxyConfigRecord := Record{
|
proxyEndpointCreated = &thisProxyEndpoint
|
||||||
ProxyType: eptype,
|
} else if eptype == "root" {
|
||||||
Rootname: rootname,
|
//Get the default site options and target
|
||||||
ProxyTarget: endpoint,
|
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
|
||||||
UseTLS: useTLS,
|
if err != nil {
|
||||||
BypassGlobalTLS: useBypassGlobalTLS,
|
utils.SendErrorResponse(w, "default site action not defined")
|
||||||
SkipTlsValidation: skipTlsValidation,
|
return
|
||||||
RequireBasicAuth: requireBasicAuth,
|
}
|
||||||
BasicAuthCredentials: basicAuthCredentials,
|
|
||||||
|
var defaultSiteOption int = 1
|
||||||
|
opt, err := strconv.Atoi(dsOptString)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid default site option")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSiteOption = opt
|
||||||
|
|
||||||
|
dsVal, err := utils.PostPara(r, "defaultSiteVal")
|
||||||
|
if err != nil && (defaultSiteOption == 1 || defaultSiteOption == 2) {
|
||||||
|
//Reverse proxy or redirect, must require value to be set
|
||||||
|
utils.SendErrorResponse(w, "target not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Write the root options to file
|
||||||
|
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
|
||||||
|
ProxyType: dynamicproxy.ProxyType_Root,
|
||||||
|
RootOrMatchingDomain: "/",
|
||||||
|
Domain: endpoint,
|
||||||
|
RequireTLS: useTLS,
|
||||||
|
BypassGlobalTLS: false,
|
||||||
|
SkipCertValidations: false,
|
||||||
|
|
||||||
|
DefaultSiteOption: defaultSiteOption,
|
||||||
|
DefaultSiteValue: dsVal,
|
||||||
|
}
|
||||||
|
preparedRootProxyRoute, err := dynamicProxyRouter.PrepareProxyRoute(&rootRoutingEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to prepare root routing: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
|
||||||
|
proxyEndpointCreated = &rootRoutingEndpoint
|
||||||
|
} else {
|
||||||
|
//Invalid eptype
|
||||||
|
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Save the config to file
|
||||||
|
err = SaveReverseProxyConfig(proxyEndpointCreated)
|
||||||
|
if err != nil {
|
||||||
|
SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
|
|
||||||
|
|
||||||
//Update utm if exists
|
//Update utm if exists
|
||||||
if uptimeMonitor != nil {
|
UpdateUptimeMonitorTargets()
|
||||||
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
|
|
||||||
uptimeMonitor.CleanRecords()
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ReverseProxyHandleEditEndpoint handles proxy endpoint edit
|
ReverseProxyHandleEditEndpoint handles proxy endpoint edit
|
||||||
This endpoint do not handle
|
(host only, for root use Default Site page to edit)
|
||||||
basic auth credential update. The credential
|
This endpoint do not handle basic auth credential update.
|
||||||
will be loaded from old config and reused
|
The credential will be loaded from old config and reused
|
||||||
*/
|
*/
|
||||||
func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "type not defined")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rootNameOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
rootNameOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "Target proxy rule not defined")
|
utils.SendErrorResponse(w, "Target proxy rule not defined")
|
||||||
@ -371,50 +389,31 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
requireBasicAuth := (rba == "true")
|
requireBasicAuth := (rba == "true")
|
||||||
|
|
||||||
//Load the previous basic auth credentials from current proxy rules
|
//Load the previous basic auth credentials from current proxy rules
|
||||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(eptype, rootNameOrMatchingDomain)
|
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
|
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if eptype == "vdir" {
|
//Generate a new proxyEndpoint from the new config
|
||||||
thisOption := dynamicproxy.VdirOptions{
|
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||||
RootName: targetProxyEntry.RootOrMatchingDomain,
|
newProxyEndpoint.Domain = endpoint
|
||||||
Domain: endpoint,
|
newProxyEndpoint.RequireTLS = useTLS
|
||||||
RequireTLS: useTLS,
|
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||||
BypassGlobalTLS: false,
|
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||||
SkipCertValidations: skipTlsValidation,
|
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||||
RequireBasicAuth: requireBasicAuth,
|
|
||||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
|
||||||
}
|
|
||||||
targetProxyEntry.Remove()
|
|
||||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
|
|
||||||
|
|
||||||
} else if eptype == "subd" {
|
//Prepare to replace the current routing rule
|
||||||
thisOption := dynamicproxy.SubdOptions{
|
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||||
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
|
if err != nil {
|
||||||
Domain: endpoint,
|
utils.SendErrorResponse(w, err.Error())
|
||||||
RequireTLS: useTLS,
|
return
|
||||||
BypassGlobalTLS: bypassGlobalTLS,
|
|
||||||
SkipCertValidations: skipTlsValidation,
|
|
||||||
RequireBasicAuth: requireBasicAuth,
|
|
||||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
|
||||||
}
|
}
|
||||||
targetProxyEntry.Remove()
|
targetProxyEntry.Remove()
|
||||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||||
}
|
|
||||||
|
|
||||||
//Save it to file
|
//Save it to file
|
||||||
thisProxyConfigRecord := Record{
|
SaveReverseProxyConfig(newProxyEndpoint)
|
||||||
ProxyType: eptype,
|
|
||||||
Rootname: targetProxyEntry.RootOrMatchingDomain,
|
|
||||||
ProxyTarget: endpoint,
|
|
||||||
UseTLS: useTLS,
|
|
||||||
SkipTlsValidation: skipTlsValidation,
|
|
||||||
RequireBasicAuth: requireBasicAuth,
|
|
||||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
|
||||||
}
|
|
||||||
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
|
|
||||||
|
|
||||||
//Update uptime monitor
|
//Update uptime monitor
|
||||||
UpdateUptimeMonitorTargets()
|
UpdateUptimeMonitorTargets()
|
||||||
@ -429,21 +428,19 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.PostPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Remove the config from runtime
|
//Remove the config from runtime
|
||||||
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ptype, ep)
|
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remove the config from file
|
//Remove the config from file
|
||||||
RemoveReverseProxyConfigFile(ep)
|
err = RemoveReverseProxyConfig(ep)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//Update utm if exists
|
//Update utm if exists
|
||||||
if uptimeMonitor != nil {
|
if uptimeMonitor != nil {
|
||||||
@ -473,14 +470,8 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.GetPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Load the target proxy object from router
|
//Load the target proxy object from router
|
||||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -502,17 +493,6 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.PostPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ptype != "vdir" && ptype != "subd" {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
creds, err := utils.PostPara(r, "creds")
|
creds, err := utils.PostPara(r, "creds")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||||
@ -520,7 +500,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Load the target proxy object from router
|
//Load the target proxy object from router
|
||||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -570,7 +550,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
|||||||
targetProxy.BasicAuthCredentials = mergedCredentials
|
targetProxy.BasicAuthCredentials = mergedCredentials
|
||||||
|
|
||||||
//Save it to file
|
//Save it to file
|
||||||
SaveReverseProxyEndpointToFile(targetProxy)
|
SaveReverseProxyConfig(targetProxy)
|
||||||
|
|
||||||
//Replace runtime configuration
|
//Replace runtime configuration
|
||||||
targetProxy.UpdateToRuntime()
|
targetProxy.UpdateToRuntime()
|
||||||
@ -593,14 +573,8 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.GetPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Load the target proxy object from router
|
//Load the target proxy object from router
|
||||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -624,12 +598,6 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.PostPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
||||||
@ -637,7 +605,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Load the target proxy object from router
|
//Load the target proxy object from router
|
||||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -666,7 +634,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
//Save configs to runtime and file
|
//Save configs to runtime and file
|
||||||
targetProxy.UpdateToRuntime()
|
targetProxy.UpdateToRuntime()
|
||||||
SaveReverseProxyEndpointToFile(targetProxy)
|
SaveReverseProxyConfig(targetProxy)
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -679,12 +647,6 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ptype, err := utils.PostPara(r, "ptype")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
||||||
@ -692,7 +654,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the target proxy object from router
|
// Load the target proxy object from router
|
||||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -717,7 +679,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Save configs to runtime and file
|
// Save configs to runtime and file
|
||||||
targetProxy.UpdateToRuntime()
|
targetProxy.UpdateToRuntime()
|
||||||
SaveReverseProxyEndpointToFile(targetProxy)
|
SaveReverseProxyConfig(targetProxy)
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -728,16 +690,28 @@ func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||||
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "type not defined")
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if eptype == "vdir" {
|
if eptype == "host" {
|
||||||
results := []*dynamicproxy.ProxyEndpoint{}
|
results := []*dynamicproxy.ProxyEndpoint{}
|
||||||
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||||
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
|
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
|
||||||
|
|
||||||
|
//Clear the auth passwords before showing to front-end
|
||||||
|
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
|
||||||
|
for _, user := range thisEndpoint.BasicAuthCredentials {
|
||||||
|
cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{
|
||||||
|
Username: user.Username,
|
||||||
|
PasswordHash: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
thisEndpoint.BasicAuthCredentials = cleanedCredentials
|
||||||
|
results = append(results, thisEndpoint)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -745,19 +719,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
|||||||
return results[i].Domain < results[j].Domain
|
return results[i].Domain < results[j].Domain
|
||||||
})
|
})
|
||||||
|
|
||||||
js, _ := json.Marshal(results)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
} else if eptype == "subd" {
|
|
||||||
results := []*dynamicproxy.ProxyEndpoint{}
|
|
||||||
dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
|
||||||
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
|
|
||||||
})
|
|
||||||
|
|
||||||
js, _ := json.Marshal(results)
|
js, _ := json.Marshal(results)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else if eptype == "root" {
|
} else if eptype == "root" {
|
||||||
@ -789,7 +750,7 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if enabled == "false" {
|
} else if enabled == "false" {
|
||||||
sysdb.Write("settings", "listenP80", false)
|
sysdb.Write("settings", "listenP80", false)
|
||||||
SystemWideLogger.Println("Disabling port 80 listener")
|
SystemWideLogger.Println("Disabling port 80 listener")
|
||||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||||
} else {
|
} else {
|
||||||
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
|
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
|
||||||
}
|
}
|
||||||
@ -837,6 +798,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleDevelopmentModeChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
enableDevelopmentModeStr, err := utils.GetPara(r, "enable")
|
||||||
|
if err != nil {
|
||||||
|
//Load the current development mode toggle state
|
||||||
|
js, _ := json.Marshal(dynamicProxyRouter.Option.NoCache)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
} else {
|
||||||
|
//Write changes to runtime
|
||||||
|
enableDevelopmentMode := false
|
||||||
|
if enableDevelopmentModeStr == "true" {
|
||||||
|
enableDevelopmentMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//Write changes to runtime
|
||||||
|
dynamicProxyRouter.Option.NoCache = enableDevelopmentMode
|
||||||
|
|
||||||
|
//Write changes to database
|
||||||
|
sysdb.Write("settings", "devMode", enableDevelopmentMode)
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Handle incoming port set. Change the current proxy incoming port
|
// Handle incoming port set. Change the current proxy incoming port
|
||||||
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||||
newIncomingPort, err := utils.PostPara(r, "incoming")
|
newIncomingPort, err := utils.PostPara(r, "incoming")
|
||||||
@ -882,33 +867,138 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle list of root route options
|
/* Handle Custom Header Rules */
|
||||||
func HandleRootRouteOptionList(w http.ResponseWriter, r *http.Request) {
|
//List all the custom header defined in this proxy rule
|
||||||
js, _ := json.Marshal(dynamicProxyRouter.RootRoutingOptions)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle update of the root route edge case options. See dynamicproxy/rootRoute.go
|
func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
||||||
func HandleRootRouteOptionsUpdate(w http.ResponseWriter, r *http.Request) {
|
epType, err := utils.PostPara(r, "type")
|
||||||
enableUnsetSubdomainRedirect, err := utils.PostBool(r, "unsetRedirect")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
unsetRedirectTarget, _ := utils.PostPara(r, "unsetRedirectTarget")
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
newRootOption := dynamicproxy.RootRoutingOptions{
|
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||||
EnableRedirectForUnsetRules: enableUnsetSubdomainRedirect,
|
return
|
||||||
UnsetRuleRedirectTarget: unsetRedirectTarget,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicProxyRouter.RootRoutingOptions = &newRootOption
|
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
err = newRootOption.SaveToFile()
|
if epType == "root" {
|
||||||
|
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else {
|
||||||
|
ep, err := dynamicProxyRouter.LoadProxy(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProxyEndpoint = ep
|
||||||
|
}
|
||||||
|
|
||||||
|
//List all custom headers
|
||||||
|
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
||||||
|
if customHeaderList == nil {
|
||||||
|
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
|
||||||
|
}
|
||||||
|
js, _ := json.Marshal(customHeaderList)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new header to the target endpoint
|
||||||
|
func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
epType, err := utils.PostPara(r, "type")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := utils.PostPara(r, "name")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "HTTP header name not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := utils.PostPara(r, "value")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "HTTP header value not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if epType == "root" {
|
||||||
|
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else {
|
||||||
|
ep, err := dynamicProxyRouter.LoadProxy(domain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProxyEndpoint = ep
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a new custom header object
|
||||||
|
targetProxyEndpoint.AddUserDefinedHeader(name, value)
|
||||||
|
|
||||||
|
//Save it (no need reload as header are not handled by dpcore)
|
||||||
|
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to save update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove a header from the target endpoint
|
||||||
|
func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
epType, err := utils.PostPara(r, "type")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := utils.PostPara(r, "name")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "HTTP header name not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if epType == "root" {
|
||||||
|
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else {
|
||||||
|
ep, err := dynamicProxyRouter.LoadProxy(domain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProxyEndpoint = ep
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProxyEndpoint.RemoveUserDefinedHeader(name)
|
||||||
|
|
||||||
|
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to save update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
|
|
||||||
This script holds the static resources router
|
This script holds the static resources router
|
||||||
for the reverse proxy service
|
for the reverse proxy service
|
||||||
|
|
||||||
|
If you are looking for reverse proxy handler, see Server.go in mod/dynamicproxy/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func FSHandler(handler http.Handler) http.Handler {
|
func FSHandler(handler http.Handler) http.Handler {
|
||||||
|
35
src/routingrule.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Routing Rule
|
||||||
|
|
||||||
|
This script handle special routing rules for some utilities functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Register the system build-in routing rules into the core
|
||||||
|
func registerBuildInRoutingRules() {
|
||||||
|
//Cloudflare email decoder
|
||||||
|
//It decode the email address if you are proxying a cloudflare protected site
|
||||||
|
//[email-protected] -> real@email.com
|
||||||
|
dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||||
|
ID: "cloudflare-decoder",
|
||||||
|
MatchRule: func(r *http.Request) bool {
|
||||||
|
return strings.HasSuffix(r.RequestURI, "cloudflare-static/email-decode.min.js")
|
||||||
|
},
|
||||||
|
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
decoder := "function fixObfuscatedEmails(){let t=document.getElementsByClassName(\"__cf_email__\");for(let e=0;e<t.length;e++){let r=t[e],l=r.getAttribute(\"data-cfemail\");if(l){let a=decrypt(l);r.setAttribute(\"href\",\"mailto:\"+a),r.innerHTML=a}}}function decrypt(t){let e=\"\",r=parseInt(t.substr(0,2),16);for(let l=2;l<t.length;l+=2){let a=parseInt(t.substr(l,2),16)^r;e+=String.fromCharCode(a)}try{e=decodeURIComponent(escape(e))}catch(f){console.error(f)}return e}fixObfuscatedEmails();"
|
||||||
|
w.Header().Set("Content-type", "text/javascript")
|
||||||
|
w.Write([]byte(decoder))
|
||||||
|
},
|
||||||
|
Enabled: false,
|
||||||
|
UseSystemAccessControl: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
32
src/start.go
@ -101,6 +101,18 @@ func startupSequence() {
|
|||||||
} else {
|
} else {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Start the static web server
|
||||||
|
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
|
||||||
|
Sysdb: sysdb,
|
||||||
|
Port: "5487", //Default Port
|
||||||
|
WebRoot: *staticWebServerRoot,
|
||||||
|
EnableDirectoryListing: true,
|
||||||
|
EnableWebDirManager: *allowWebFileManager,
|
||||||
|
})
|
||||||
|
//Restore the web server to previous shutdown state
|
||||||
|
staticWebServer.RestorePreviousState()
|
||||||
|
|
||||||
//Create a netstat buffer
|
//Create a netstat buffer
|
||||||
netstatBuffers, err = netstat.NewNetStatBuffer(300)
|
netstatBuffers, err = netstat.NewNetStatBuffer(300)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -128,7 +140,7 @@ func startupSequence() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if *allowMdnsScanning {
|
if *allowMdnsScanning {
|
||||||
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
|
portInt, err := strconv.Atoi(strings.Split(*webUIPort, ":")[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portInt = 8000
|
portInt = 8000
|
||||||
}
|
}
|
||||||
@ -220,25 +232,13 @@ func startupSequence() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Static Web Server
|
|
||||||
|
|
||||||
Start the static web server
|
|
||||||
*/
|
|
||||||
|
|
||||||
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
|
|
||||||
Sysdb: sysdb,
|
|
||||||
Port: "5487", //Default Port
|
|
||||||
WebRoot: *staticWebServerRoot,
|
|
||||||
EnableDirectoryListing: true,
|
|
||||||
EnableWebDirManager: *allowWebFileManager,
|
|
||||||
})
|
|
||||||
//Restore the web server to previous shutdown state
|
|
||||||
staticWebServer.RestorePreviousState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This sequence start after everything is initialized
|
// This sequence start after everything is initialized
|
||||||
func finalSequence() {
|
func finalSequence() {
|
||||||
//Start ACME renew agent
|
//Start ACME renew agent
|
||||||
acmeRegisterSpecialRoutingRule()
|
acmeRegisterSpecialRoutingRule()
|
||||||
|
|
||||||
|
//Inject routing rules
|
||||||
|
registerBuildInRoutingRules()
|
||||||
}
|
}
|
||||||
|
290
src/vdir.go
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
Vdir.go
|
||||||
|
|
||||||
|
This script handle virtual directory functions
|
||||||
|
in global scopes
|
||||||
|
|
||||||
|
Author: tobychui
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List the Virtual directory under given proxy rule
|
||||||
|
func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if eptype == "host" {
|
||||||
|
endpoint, err := utils.PostPara(r, "ep") //Support root and host
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "endpoint not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEndpoint, err = dynamicProxyRouter.LoadProxy(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target endpoint not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if eptype == "root" {
|
||||||
|
targetEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else {
|
||||||
|
utils.SendErrorResponse(w, "invalid type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parse result to json
|
||||||
|
vdirs := targetEndpoint.VirtualDirectories
|
||||||
|
if targetEndpoint.VirtualDirectories == nil {
|
||||||
|
//Avoid returning null to front-end
|
||||||
|
vdirs = []*dynamicproxy.VirtualDirectoryEndpoint{}
|
||||||
|
}
|
||||||
|
js, _ := json.Marshal(vdirs)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Virtual Directory to a host
|
||||||
|
func ReverseProxyAddVdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingPath, err := utils.PostPara(r, "path")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "matching path not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Must start with /
|
||||||
|
if !strings.HasPrefix(matchingPath, "/") {
|
||||||
|
matchingPath = "/" + matchingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target domain not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqTLSStr, err := utils.PostPara(r, "reqTLS")
|
||||||
|
if err != nil {
|
||||||
|
//Assume false
|
||||||
|
reqTLSStr = "false"
|
||||||
|
}
|
||||||
|
reqTLS := (reqTLSStr == "true")
|
||||||
|
|
||||||
|
skipValidStr, err := utils.PostPara(r, "skipValid")
|
||||||
|
if err != nil {
|
||||||
|
//Assume false
|
||||||
|
skipValidStr = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
skipValid := (skipValidStr == "true")
|
||||||
|
|
||||||
|
//Load the target proxy endpoint from runtime
|
||||||
|
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if eptype == "root" {
|
||||||
|
//Check if root is running at reverse proxy mode
|
||||||
|
if dynamicProxyRouter.Root.DefaultSiteOption != dynamicproxy.DefaultSite_ReverseProxy {
|
||||||
|
utils.SendErrorResponse(w, "virtual directory can only be added to root router under proxy mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else if eptype == "host" {
|
||||||
|
endpointID, err := utils.PostPara(r, "endpoint")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "endpoint not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedEndpoint, err := dynamicProxyRouter.LoadProxy(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "selected proxy host not exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetProxyEndpoint = loadedEndpoint
|
||||||
|
} else {
|
||||||
|
utils.SendErrorResponse(w, "invalid proxy type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a virtual directory entry base on the above info
|
||||||
|
newVirtualDirectoryRouter := dynamicproxy.VirtualDirectoryEndpoint{
|
||||||
|
MatchingPath: matchingPath,
|
||||||
|
Domain: domain,
|
||||||
|
RequireTLS: reqTLS,
|
||||||
|
SkipCertValidations: skipValid,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add Virtual Directory Rule to this Proxy Endpoint
|
||||||
|
activatedProxyEndpoint, err := targetProxyEndpoint.AddVirtualDirectoryRule(&newVirtualDirectoryRouter)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Save it to file
|
||||||
|
SaveReverseProxyConfig(activatedProxyEndpoint)
|
||||||
|
|
||||||
|
// Update uptime monitor
|
||||||
|
UpdateUptimeMonitorTargets()
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReverseProxyDeleteVdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vdir, err := utils.PostPara(r, "vdir")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "vdir matching key not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if eptype == "root" {
|
||||||
|
targetEndpoint = dynamicProxyRouter.Root
|
||||||
|
} else if eptype == "host" {
|
||||||
|
//Proxy rule
|
||||||
|
matchingPath, err := utils.PostPara(r, "path")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "matching path not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ept, err := dynamicProxyRouter.LoadProxy(matchingPath)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target proxy rule not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEndpoint = ept
|
||||||
|
} else {
|
||||||
|
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Delete the Vdir from endpoint
|
||||||
|
err = targetEndpoint.RemoveVirtualDirectoryRuleByMatchingPath(vdir)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SaveReverseProxyConfig(targetEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
SystemWideLogger.PrintAndLog("Config", "Fail to write vdir rules update to config file", err)
|
||||||
|
utils.SendErrorResponse(w, "unable to write changes to file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle update of reverse proxy vdir rules
|
||||||
|
func ReverseProxyEditVdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "type not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vdir, err := utils.PostPara(r, "vdir")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "vdir matching key not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target domain not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqTLSStr, err := utils.PostPara(r, "reqTLS")
|
||||||
|
if err != nil {
|
||||||
|
//Assume false
|
||||||
|
reqTLSStr = "false"
|
||||||
|
}
|
||||||
|
reqTLS := (reqTLSStr == "true")
|
||||||
|
|
||||||
|
skipValidStr, err := utils.PostPara(r, "skipValid")
|
||||||
|
if err != nil {
|
||||||
|
//Assume false
|
||||||
|
skipValidStr = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
skipValid := (skipValidStr == "true")
|
||||||
|
|
||||||
|
var targetEndpoint *dynamicproxy.ProxyEndpoint
|
||||||
|
if eptype == "root" {
|
||||||
|
targetEndpoint = dynamicProxyRouter.Root
|
||||||
|
|
||||||
|
} else if eptype == "host" {
|
||||||
|
//Proxy rule
|
||||||
|
matchingPath, err := utils.PostPara(r, "path")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "matching path not defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ept, err := dynamicProxyRouter.LoadProxy(matchingPath)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target proxy rule not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEndpoint = ept
|
||||||
|
} else {
|
||||||
|
utils.SendErrorResponse(w, "invalid endpoint type given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the target vdir exists
|
||||||
|
if targetEndpoint.GetVirtualDirectoryRuleByMatchingPath(vdir) == nil {
|
||||||
|
utils.SendErrorResponse(w, "target virtual directory rule not exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Overwrite the target endpoint
|
||||||
|
newVdirRule := dynamicproxy.VirtualDirectoryEndpoint{
|
||||||
|
MatchingPath: vdir,
|
||||||
|
Domain: domain,
|
||||||
|
RequireTLS: reqTLS,
|
||||||
|
SkipCertValidations: skipValid,
|
||||||
|
Disabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEndpoint.RemoveVirtualDirectoryRuleByMatchingPath(vdir)
|
||||||
|
activatedProxyEndpoint, err := targetEndpoint.AddVirtualDirectoryRule(&newVdirRule)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Save changes to file
|
||||||
|
SaveReverseProxyConfig(activatedProxyEndpoint)
|
||||||
|
|
||||||
|
UpdateUptimeMonitorTargets()
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
@ -618,6 +618,10 @@
|
|||||||
<label>IP Address</label>
|
<label>IP Address</label>
|
||||||
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
|
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Remarks (Optional)</label>
|
||||||
|
<input id="ipAddressCommentsWhitelist" type="text" placeholder="Comments or remarks for this IP range">
|
||||||
|
</div>
|
||||||
<button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button">
|
<button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button">
|
||||||
<i class="green add icon"></i> Whitelist IP
|
<i class="green add icon"></i> Whitelist IP
|
||||||
</button>
|
</button>
|
||||||
@ -634,6 +638,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>IP Address</th>
|
<th>IP Address</th>
|
||||||
|
<th>Remarks</th>
|
||||||
<th>Remove</th>
|
<th>Remove</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -793,11 +798,12 @@
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
$('#whitelistIpTable').append(`
|
$('#whitelistIpTable').append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td>
|
<td colspan="3"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
$.each(data, function(index, ip) {
|
$.each(data, function(index, ipEntry) {
|
||||||
|
let ip = ipEntry.IP;
|
||||||
let icon = "globe icon";
|
let icon = "globe icon";
|
||||||
if (isLAN(ip)){
|
if (isLAN(ip)){
|
||||||
icon = "desktop icon";
|
icon = "desktop icon";
|
||||||
@ -807,6 +813,7 @@
|
|||||||
$('#whitelistIpTable').append(`
|
$('#whitelistIpTable').append(`
|
||||||
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
|
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
|
||||||
<td><i class="${icon}"></i> ${ip}</td>
|
<td><i class="${icon}"></i> ${ip}</td>
|
||||||
|
<td>${ipEntry.Comment}</td>
|
||||||
<td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td>
|
<td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
@ -1003,6 +1010,7 @@
|
|||||||
|
|
||||||
function addIpWhitelist(){
|
function addIpWhitelist(){
|
||||||
let targetIp = $("#ipAddressInputWhitelist").val().trim();
|
let targetIp = $("#ipAddressInputWhitelist").val().trim();
|
||||||
|
let remarks = $("#ipAddressCommentsWhitelist").val().trim();
|
||||||
if (targetIp == ""){
|
if (targetIp == ""){
|
||||||
alert("IP address is empty")
|
alert("IP address is empty")
|
||||||
return
|
return
|
||||||
@ -1016,7 +1024,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/whitelist/ip/add",
|
url: "/api/whitelist/ip/add",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {ip: targetIp.toLowerCase()},
|
data: {ip: targetIp.toLowerCase(), "comment": remarks},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.error !== undefined) {
|
if (response.error !== undefined) {
|
||||||
msgbox(response.error, false, 6000);
|
msgbox(response.error, false, 6000);
|
||||||
@ -1025,6 +1033,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("#ipAddressInputWhitelist").val("");
|
$("#ipAddressInputWhitelist").val("");
|
||||||
|
$("#ipAddressCommentsWhitelist").val("");
|
||||||
$("#ipAddressInputWhitelist").parent().remvoeClass("error");
|
$("#ipAddressInputWhitelist").parent().remvoeClass("error");
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function() {
|
||||||
|
@ -15,42 +15,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<h4>Default Certificates</h4>
|
<h3>Hosts Certificates</h3>
|
||||||
<small>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</small></p>
|
|
||||||
<table class="ui very basic unstackable celled table">
|
|
||||||
<thead>
|
|
||||||
<tr><th class="no-sort">Key Type</th>
|
|
||||||
<th class="no-sort">Exists</th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><i class="globe icon"></i> Default Public Key</td>
|
|
||||||
<td id="pubkeyExists"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><i class="lock icon"></i> Default Private Key</td>
|
|
||||||
<td id="prikeyExists"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
|
|
||||||
<div class="ui buttons">
|
|
||||||
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
|
||||||
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<h4>Sub-domain Certificates</h4>
|
|
||||||
<p>Provide certificates for multiple domains reverse proxy</p>
|
<p>Provide certificates for multiple domains reverse proxy</p>
|
||||||
<div class="ui fluid form">
|
<div class="ui fluid form">
|
||||||
<div class="three fields">
|
<div class="three fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Server Name (Domain)</label>
|
<label>Server Name (Domain)</label>
|
||||||
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
|
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
|
||||||
|
<small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Public Key (.pem)</label>
|
<label>Public Key (.pem)</label>
|
||||||
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
||||||
|
<small>or .crt files in order systems</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Private Key (.key)</label>
|
<label>Private Key (.key)</label>
|
||||||
@ -63,14 +40,33 @@
|
|||||||
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
||||||
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<div class="ui message">
|
||||||
|
<h4>Tips about Server Names & SNI</h4>
|
||||||
|
<div class="ui bulleted list">
|
||||||
|
<div class="item">
|
||||||
|
If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
|
||||||
|
for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
|
||||||
|
<code>a.example.com</code> and <code>b.example.com</code>).
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
If you have a wildcard certificate that covers <code>*.example.com</code>,
|
||||||
|
you can just enter <code>example.com</code> as server name to add a certificate.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
If you have a certificate contain multiple host, you can enter the first domain in your certificate
|
||||||
|
and Zoraxy will try to match the remaining CN/DNS for you.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>Current list of loaded certificates</p>
|
||||||
<div>
|
<div>
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||||
<table class="ui sortable unstackable celled table">
|
<table class="ui sortable unstackable basic celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Domain</th>
|
<tr><th>Domain</th>
|
||||||
<th>Last Update</th>
|
<th>Last Update</th>
|
||||||
<th>Expire At</th>
|
<th>Expire At</th>
|
||||||
|
<th class="no-sort">Renew</th>
|
||||||
<th class="no-sort">Remove</th>
|
<th class="no-sort">Remove</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody id="certifiedDomainList">
|
<tbody id="certifiedDomainList">
|
||||||
@ -81,15 +77,34 @@
|
|||||||
|
|
||||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui message">
|
<div class="ui divider"></div>
|
||||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
<h3>Fallback Certificate</h3>
|
||||||
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
|
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
|
||||||
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
<table class="ui very basic unstackable celled table">
|
||||||
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
<thead>
|
||||||
|
<tr><th class="no-sort">Key Type</th>
|
||||||
|
<th class="no-sort">Found</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i class="globe icon"></i> Fallback Public Key</td>
|
||||||
|
<td id="pubkeyExists"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="lock icon"></i> Fallback Private Key</td>
|
||||||
|
<td id="prikeyExists"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
|
||||||
|
<div class="ui buttons">
|
||||||
|
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
||||||
|
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
|
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
|
||||||
<p>Management features regarding CA and ACME</p>
|
<p>Management features regarding CA and ACME</p>
|
||||||
|
<h4>Prefered Certificate Authority</h4>
|
||||||
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
||||||
<div class="ui fluid form">
|
<div class="ui fluid form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -112,12 +127,12 @@
|
|||||||
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
||||||
</div><br>
|
</div><br>
|
||||||
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment acmeRenewStateWrapper">
|
||||||
<h4 class="ui header" id="acmeAutoRenewer">
|
<h4 class="ui header" id="acmeAutoRenewer">
|
||||||
<i class="red circle icon"></i>
|
<i class="white remove icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span id="acmeAutoRenewerStatus">Disabled</span>
|
<span id="acmeAutoRenewerStatus">Disabled</span>
|
||||||
<div class="sub header">Auto-Renewer Status</div>
|
<div class="sub header">ACME Auto-Renewer</div>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -130,6 +145,110 @@
|
|||||||
|
|
||||||
$("#defaultCA").dropdown();
|
$("#defaultCA").dropdown();
|
||||||
|
|
||||||
|
|
||||||
|
//Renew certificate by button press
|
||||||
|
function renewCertificate(domain, btn=undefined){
|
||||||
|
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||||
|
if (defaultCA.trim() == ""){
|
||||||
|
defaultCA = "Let's Encrypt";
|
||||||
|
}
|
||||||
|
//Get a new cert using ACME
|
||||||
|
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||||
|
|
||||||
|
//Request ACME for certificate
|
||||||
|
if (btn != undefined){
|
||||||
|
$(btn).addClass('disabled');
|
||||||
|
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
||||||
|
}
|
||||||
|
obtainCertificate(domain, defaultCA.trim(), function(succ){
|
||||||
|
if (btn != undefined){
|
||||||
|
$(btn).removeClass('disabled');
|
||||||
|
if (succ){
|
||||||
|
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||||
|
}else{
|
||||||
|
$(btn).html(`<i class="ui red times icon"></i>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
initManagedDomainCertificateList();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Obtain Certificate via ACME
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Obtain certificate from API, only support one domain
|
||||||
|
function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
|
||||||
|
//Load the ACME email from server side
|
||||||
|
let acmeEmail = "";
|
||||||
|
$.get("/api/acme/autoRenew/email", function(data){
|
||||||
|
if (data != "" && data != undefined && data != null){
|
||||||
|
acmeEmail = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = "";
|
||||||
|
let email = acmeEmail;
|
||||||
|
if (acmeEmail == ""){
|
||||||
|
msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
|
||||||
|
if (callback != undefined){
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.trim() == "" && !domains.includes(",")){
|
||||||
|
//Zoraxy filename are the matching name for domains.
|
||||||
|
//Use the same as domains
|
||||||
|
filename = domains;
|
||||||
|
}else if (filename != "" && !domains.includes(",")){
|
||||||
|
//Invalid settings. Force the filename to be same as domain
|
||||||
|
//if there are only 1 domain
|
||||||
|
filename = domains;
|
||||||
|
}else{
|
||||||
|
msgbox("Filename cannot be empty for certs containing multiple domains.")
|
||||||
|
if (callback != undefined){
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/acme/obtainCert",
|
||||||
|
method: "GET",
|
||||||
|
data: {
|
||||||
|
domains: domains,
|
||||||
|
filename: filename,
|
||||||
|
email: email,
|
||||||
|
ca: usingCa,
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.error) {
|
||||||
|
console.log("Error:", response.error);
|
||||||
|
// Show error message
|
||||||
|
msgbox(response.error, false, 12000);
|
||||||
|
if (callback != undefined){
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Certificate installed successfully");
|
||||||
|
// Show success message
|
||||||
|
msgbox("Certificate installed successfully");
|
||||||
|
|
||||||
|
if (callback != undefined){
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
console.log("Failed to install certificate:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Delete the certificate by its domain
|
//Delete the certificate by its domain
|
||||||
function deleteCertificate(domain){
|
function deleteCertificate(domain){
|
||||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||||
@ -154,6 +273,12 @@
|
|||||||
//Initialize the current default CA options
|
//Initialize the current default CA options
|
||||||
$.get("/api/acme/autoRenew/email", function(data){
|
$.get("/api/acme/autoRenew/email", function(data){
|
||||||
$("#prefACMEEmail").val(data);
|
$("#prefACMEEmail").val(data);
|
||||||
|
if (data.trim() == ""){
|
||||||
|
//acme email is not yet set
|
||||||
|
$(".renewButton").addClass('disabled');
|
||||||
|
}else{
|
||||||
|
$(".renewButton").removeClass('disabled');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$.get("/api/acme/autoRenew/ca", function(data){
|
$.get("/api/acme/autoRenew/ca", function(data){
|
||||||
@ -167,7 +292,13 @@
|
|||||||
//Set the status of the acme enable icon
|
//Set the status of the acme enable icon
|
||||||
function setACMEEnableStates(enabled){
|
function setACMEEnableStates(enabled){
|
||||||
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
||||||
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
|
if (enabled){
|
||||||
|
$(".acmeRenewStateWrapper").addClass("enabled");
|
||||||
|
}else{
|
||||||
|
$(".acmeRenewStateWrapper").removeClass("enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
|
||||||
}
|
}
|
||||||
initAcmeStatus();
|
initAcmeStatus();
|
||||||
|
|
||||||
@ -187,6 +318,9 @@
|
|||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
msgbox(data.error, false);
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
//Update the renew button states
|
||||||
|
$(".renewButton").removeClass('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -223,13 +357,14 @@
|
|||||||
<td>${entry.Domain}</td>
|
<td>${entry.Domain}</td>
|
||||||
<td>${entry.LastModifiedDate}</td>
|
<td>${entry.LastModifiedDate}</td>
|
||||||
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
|
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
|
||||||
|
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', this);"><i class="ui green refresh icon"></i></button></td>
|
||||||
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
|
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length == 0){
|
if (data.length == 0){
|
||||||
$("#certifiedDomainList").append(`<tr>
|
$("#certifiedDomainList").append(`<tr>
|
||||||
<td colspan="4"><i class="ui times circle icon"></i> No valid keypairs found</td>
|
<td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
|
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
|
||||||
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
|
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<h2>Members</h2>
|
<h2>Members</h2>
|
||||||
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
|
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
|
||||||
<div class="ui checkbox" style="margin-bottom: 1em;">
|
<div class="ui checkbox" style="margin-bottom: 1em;">
|
||||||
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);">
|
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);" checked>
|
||||||
<label>Show Unauthorized Members</label>
|
<label>Show Unauthorized Members</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="" style="overflow-x: auto;">
|
<div class="" style="overflow-x: auto;">
|
||||||
@ -84,6 +84,11 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h4>Add Controller as Member</h4>
|
||||||
|
<p>Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.</p>
|
||||||
|
<button class="ui basic small button addControllerToNetworkBtn" onclick="ganAddControllerToNetwork(this);"><i class="green add icon"></i> Add Controller as Member</button>
|
||||||
|
<button class="ui basic small button removeControllerFromNetworkBtn" onclick="ganRemoveControllerFromNetwork(this);"><i class="red sign-out icon"></i> Remove Controller from Member</button>
|
||||||
<br><br>
|
<br><br>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -355,7 +360,10 @@
|
|||||||
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
const tableBody = $('#networkMemeberTable');
|
let tableBody = $('#networkMemeberTable');
|
||||||
|
if (tableBody.length == 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
data.sort((a, b) => a.address.localeCompare(b.address));
|
data.sort((a, b) => a.address.localeCompare(b.address));
|
||||||
//Check if the new object equal to the old one
|
//Check if the new object equal to the old one
|
||||||
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
|
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
|
||||||
@ -592,6 +600,55 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Add and remove this controller node to network as member
|
||||||
|
function ganAddControllerToNetwork(){
|
||||||
|
$(".addControllerToNetworkBtn").addClass("disabled");
|
||||||
|
$(".addControllerToNetworkBtn").addClass("loading");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/gan/network/join",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
netid:currentGANetID,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||||
|
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false, 6000);
|
||||||
|
}else{
|
||||||
|
msgbox("Controller joint " + currentGANetID);
|
||||||
|
}
|
||||||
|
setTimeout(function(){
|
||||||
|
renderMemeberTable(true);
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ganRemoveControllerFromNetwork(){
|
||||||
|
$(".removeControllerFromNetworkBtn").addClass("disabled");
|
||||||
|
$(".removeControllerFromNetworkBtn").addClass("loading");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/gan/network/leave",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
netid:currentGANetID,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false, 6000);
|
||||||
|
}else{
|
||||||
|
msgbox("Controller left " + currentGANetID);
|
||||||
|
}
|
||||||
|
renderMemeberTable(true);
|
||||||
|
$(".removeControllerFromNetworkBtn").removeClass("disabled");
|
||||||
|
$(".removeControllerFromNetworkBtn").removeClass("loading");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//Entry points
|
//Entry points
|
||||||
function initGanetDetails(ganetId){
|
function initGanetDetails(ganetId){
|
||||||
currentGANetID = ganetId;
|
currentGANetID = ganetId;
|
||||||
@ -612,6 +669,11 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Switch from other tabs back to this, exit to GAN list
|
||||||
|
tabSwitchEventBind["gan"] = function(){
|
||||||
|
exitToGanList();
|
||||||
|
}
|
||||||
|
|
||||||
//Exit point
|
//Exit point
|
||||||
function exitToGanList(){
|
function exitToGanList(){
|
||||||
$("#gan").load("./components/gan.html", function(){
|
$("#gan").load("./components/gan.html", function(){
|
||||||
|
275
src/web/components/httprp.html
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
<div class="standardContainer">
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h2>HTTP Proxy</h2>
|
||||||
|
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||||
|
<table class="ui celled sortable unstackable compact table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Destination</th>
|
||||||
|
<th>Virtual Directory</th>
|
||||||
|
<th>Basic Auth</th>
|
||||||
|
<th class="no-sort" style="min-width:100px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="httpProxyList">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
|
||||||
|
<br><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function listProxyEndpoints(){
|
||||||
|
$.get("/api/proxy/list?type=host", function(data){
|
||||||
|
$("#httpProxyList").html(``);
|
||||||
|
if (data.error !== undefined){
|
||||||
|
$("#httpProxyList").append(`<tr>
|
||||||
|
<td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
|
||||||
|
</tr>`);
|
||||||
|
}else if (data.length == 0){
|
||||||
|
$("#httpProxyList").append(`<tr>
|
||||||
|
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
||||||
|
</tr>`);
|
||||||
|
}else{
|
||||||
|
data.forEach(subd => {
|
||||||
|
let tlsIcon = "";
|
||||||
|
let subdData = encodeURIComponent(JSON.stringify(subd));
|
||||||
|
if (subd.RequireTLS){
|
||||||
|
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||||
|
if (subd.SkipCertValidations){
|
||||||
|
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inboundTlsIcon = "";
|
||||||
|
if ($("#tls").checkbox("is checked")){
|
||||||
|
inboundTlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||||
|
if (subd.BypassGlobalTLS){
|
||||||
|
inboundTlsIcon = `<i class="grey lock icon" title="TLS Bypass Enabled"></i>`;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
inboundTlsIcon = `<i class="yellow lock open icon" title="Plain Text Mode"></i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build the virtual directory list
|
||||||
|
var vdList = `<div class="ui list">`;
|
||||||
|
subd.VirtualDirectories.forEach(vdir => {
|
||||||
|
vdList += `<div class="item">${vdir.MatchingPath} <i class="green angle double right icon"></i> ${vdir.Domain}</div>`;
|
||||||
|
});
|
||||||
|
vdList += `</div>`;
|
||||||
|
|
||||||
|
if (subd.VirtualDirectories.length == 0){
|
||||||
|
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||||
|
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td>
|
||||||
|
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
|
||||||
|
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
|
||||||
|
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
||||||
|
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||||
|
<button class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
|
||||||
|
<button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Inline editor for httprp.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
function editEndpoint(uuid) {
|
||||||
|
uuid = uuid.hexDecode();
|
||||||
|
var row = $('tr[eptuuid="' + uuid + '"]');
|
||||||
|
var columns = row.find('td[data-label]');
|
||||||
|
var payload = $(row).attr("payload");
|
||||||
|
payload = JSON.parse(decodeURIComponent(payload));
|
||||||
|
console.log(payload);
|
||||||
|
//console.log(payload);
|
||||||
|
columns.each(function(index) {
|
||||||
|
var column = $(this);
|
||||||
|
var oldValue = column.text().trim();
|
||||||
|
|
||||||
|
if ($(this).attr("editable") == "false"){
|
||||||
|
//This col do not allow edit. Skip
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an input element based on the column content
|
||||||
|
var input;
|
||||||
|
var datatype = $(this).attr("datatype");
|
||||||
|
if (datatype == "domain"){
|
||||||
|
let domain = payload.Domain;
|
||||||
|
//Target require TLS for proxying
|
||||||
|
let tls = payload.RequireTLS;
|
||||||
|
if (tls){
|
||||||
|
tls = "checked";
|
||||||
|
}else{
|
||||||
|
tls = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Require TLS validation
|
||||||
|
let skipTLSValidation = payload.SkipCertValidations;
|
||||||
|
let checkstate = "";
|
||||||
|
if (skipTLSValidation){
|
||||||
|
checkstate = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
|
input = `
|
||||||
|
<div class="ui mini fluid input">
|
||||||
|
<input type="text" class="Domain" value="${domain}">
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="RequireTLS" ${tls}>
|
||||||
|
<label>Require TLS<br>
|
||||||
|
<small>Proxy target require HTTPS connection</small></label>
|
||||||
|
</div><br>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||||
|
<label>Skip Verification<br>
|
||||||
|
<small>Check this if proxy target is using self signed certificates</small></label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
column.empty().append(input);
|
||||||
|
}else if (datatype == "vdir"){
|
||||||
|
//Append a quick access button for vdir page
|
||||||
|
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
|
||||||
|
<i class="ui yellow folder icon"></i> Edit Virtual Directories
|
||||||
|
</button>`);
|
||||||
|
|
||||||
|
}else if (datatype == "basicauth"){
|
||||||
|
let requireBasicAuth = payload.RequireBasicAuth;
|
||||||
|
let checkstate = "";
|
||||||
|
if (requireBasicAuth){
|
||||||
|
checkstate = "checked";
|
||||||
|
}
|
||||||
|
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
|
||||||
|
<label>Require Basic Auth</label>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
|
||||||
|
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
|
||||||
|
<div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Advance Configs
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
|
||||||
|
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
}else if (datatype == 'action'){
|
||||||
|
column.empty().append(`
|
||||||
|
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
|
||||||
|
<button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
|
||||||
|
|
||||||
|
`);
|
||||||
|
}else if (datatype == "inbound"){
|
||||||
|
let originalContent = $(column).html();
|
||||||
|
column.empty().append(`${originalContent}
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
|
||||||
|
<label>Allow plain HTTP access<br>
|
||||||
|
<small>Allow inbound connections without TLS/SSL</small></label>
|
||||||
|
</div><br>
|
||||||
|
`);
|
||||||
|
}else{
|
||||||
|
//Unknown field. Leave it untouched
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".endpointAdvanceConfig").accordion();
|
||||||
|
$("#httpProxyList").find(".editBtn").addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitProxyInlineEdit(){
|
||||||
|
listProxyEndpoints();
|
||||||
|
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProxyInlineEdit(uuid){
|
||||||
|
uuid = uuid.hexDecode();
|
||||||
|
var row = $('tr[eptuuid="' + uuid + '"]');
|
||||||
|
if (row.length == 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var epttype = "host";
|
||||||
|
let newDomain = $(row).find(".Domain").val();
|
||||||
|
let requireTLS = $(row).find(".RequireTLS")[0].checked;
|
||||||
|
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
||||||
|
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
||||||
|
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||||
|
|
||||||
|
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/edit",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"type": epttype,
|
||||||
|
"rootname": uuid,
|
||||||
|
"ep":newDomain,
|
||||||
|
"bpgtls": bypassGlobalTLS,
|
||||||
|
"tls" :requireTLS,
|
||||||
|
"tlsval": skipCertValidations,
|
||||||
|
"bauth" :requireBasicAuth,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error !== undefined){
|
||||||
|
msgbox(data.error, false, 6000);
|
||||||
|
}else{
|
||||||
|
msgbox("Proxy endpoint updated");
|
||||||
|
listProxyEndpoints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button events */
|
||||||
|
function editBasicAuthCredentials(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickEditVdir(uuid){
|
||||||
|
openTabById("vdir");
|
||||||
|
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editCustomHeaders(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editLoadBalanceOptions(uuid){
|
||||||
|
alert(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Bind on tab switch events
|
||||||
|
tabSwitchEventBind["httprp"] = function(){
|
||||||
|
listProxyEndpoints();
|
||||||
|
}
|
||||||
|
</script>
|
@ -72,25 +72,10 @@
|
|||||||
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
||||||
</div>
|
</div>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<div class="advancezone ui basic segment">
|
|
||||||
<div class="ui accordion advanceSettings">
|
|
||||||
<div class="title">
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
Advance Options
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
|
|
||||||
<button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(".advanceSettings").accordion();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Redirection functions
|
Redirection functions
|
||||||
@ -125,6 +110,7 @@
|
|||||||
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||||
}
|
}
|
||||||
initRedirectionRuleList();
|
initRedirectionRuleList();
|
||||||
|
resetForm();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -151,16 +137,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAdvanceRules(){
|
|
||||||
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initRedirectionRuleList(){
|
function initRedirectionRuleList(){
|
||||||
$("#redirectionRuleList").html("");
|
$("#redirectionRuleList").html("");
|
||||||
$.get("/api/redirect/list", function(data){
|
$.get("/api/redirect/list", function(data){
|
||||||
data.forEach(function(entry){
|
data.forEach(function(entry){
|
||||||
$("#redirectionRuleList").append(`<tr>
|
$("#redirectionRuleList").append(`<tr>
|
||||||
<td>${entry.RedirectURL} </td>
|
<td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></td>
|
||||||
<td>${entry.TargetURL}</td>
|
<td>${entry.TargetURL}</td>
|
||||||
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
|
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
|
||||||
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
||||||
@ -169,7 +151,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (data.length == 0){
|
if (data.length == 0){
|
||||||
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="checkmark icon"></i> No redirection rule</td></tr>`);
|
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,92 +1,122 @@
|
|||||||
<div class="standardContainer">
|
<div class="standardContainer">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h2>Set Proxy Root</h2>
|
<h2>Default Site</h2>
|
||||||
<p>The default routing point for all incoming traffics. For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
|
<p>Default routing options for inbound traffic (previously called Proxy Root)</p>
|
||||||
|
<div class="ui form">
|
||||||
|
<div class="grouped fields">
|
||||||
|
<label>What to show when Zoraxy is hit with an unknown Host?</label>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio defaultsite checkbox">
|
||||||
|
<input type="radio" name="defaultsiteOption" checked="checked" value="webserver">
|
||||||
|
<label>Internal Static Web Server<br>
|
||||||
|
<small>Check this if you prefer a more Apache / Nginx like experience</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio defaultsite checkbox">
|
||||||
|
<input type="radio" name="defaultsiteOption" value="proxy">
|
||||||
|
<label>Reverse Proxy Target<br>
|
||||||
|
<small>Proxy the request to a target IP / domain</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio defaultsite checkbox">
|
||||||
|
<input type="radio" name="defaultsiteOption" value="redirect">
|
||||||
|
<label>Redirect<br>
|
||||||
|
<small>Redirect the user to a new location</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio defaultsite checkbox">
|
||||||
|
<input type="radio" name="defaultsiteOption" value="notfound">
|
||||||
|
<label>Show 404 NOT FOUND<br>
|
||||||
|
<small>Respond to request with a 404 page</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reverse Proxy as Default Site Options -->
|
||||||
|
<div id="defaultSiteProxyOptions" class="ui basic segment advanceoptions defaultSiteOptionDetails" style="display:none; ">
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Proxy Root</label>
|
<label>Reverse Proxy Target</label>
|
||||||
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
|
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
|
||||||
<small>E.g. localhost:8080</small>
|
<small>e.g. localhost:8080 / 192.168.0.100:80 / example.com</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input type="checkbox" id="rootReqTLS">
|
<input type="checkbox" id="rootReqTLS">
|
||||||
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
|
<label>Reverse proxy target require TLS connection <br><small>Check this if your proxy target URL require connection with https://</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui horizontal divider">OR</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
|
|
||||||
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="field">
|
|
||||||
<h4>Root Routing Options</h4>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
<input type="checkbox" id="unsetRedirect">
|
|
||||||
<label>Enable redirect for unset subdomains <br><small>Redirect subdomain that is not found to custom domain</small></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui basic segment" id="unsetRedirectDomainWrapper" style="background-color: #f7f7f7; border-radius: 1em; margin-left: 2em; padding-left: 2em; display:none;">
|
|
||||||
<div style="
|
|
||||||
position: absolute;
|
|
||||||
top:0;
|
|
||||||
left: 1em;
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
margin-top: -10px;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #f7f7f7;">
|
|
||||||
|
|
||||||
</div>
|
<!-- Redirect as default site Options-->
|
||||||
|
<div id="defaultSiteRedirectOptions" class="ui basic segment advanceoptions defaultSiteOptionDetails" style="display:none;"">
|
||||||
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Redirect target domain</label>
|
<label>Redirect target domain</label>
|
||||||
<div class="ui input">
|
<div class="ui input">
|
||||||
<input id="unsetRedirectDomain" type="text" placeholder="http://example.com">
|
<input id="redirectDomain" type="text" placeholder="http://example.com">
|
||||||
</div>
|
</div>
|
||||||
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)<br>
|
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)</small>
|
||||||
Leave empty for redirecting to upper level domain (e.g. notfound.example.com <i class="right arrow icon"></i> example.com)</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
<button class="ui basic button" onclick="updateRootOptions()"><i class="blue save icon" ></i> Save Root Options</button>
|
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
|
|
||||||
|
<button class="ui basic button" onclick="setProxyRoot(this)"><i class="green checkmark icon" ></i> Apply Changes</button>
|
||||||
|
<button class="ui basic button" onclick="initRootInfo()"><i class="refresh icon" ></i> Reset</button>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
var currentDefaultSiteOption = 0; //For enum see typedef.go
|
||||||
$("#advanceRootSettings").accordion();
|
$("#advanceRootSettings").accordion();
|
||||||
|
|
||||||
function handleUseStaticWebServerAsRoot(){
|
//Handle toggle events of option radio boxes
|
||||||
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
|
function updateAvaibleDefaultSiteOptions(){
|
||||||
if (useStaticWebServer){
|
let selectedDefaultSite = $('input[name="defaultsiteOption"]:checked').val();
|
||||||
|
|
||||||
|
$(".defaultSiteOptionDetails").hide();
|
||||||
|
$("#useRootProxyRouterForVdir").parent().addClass("disabled");
|
||||||
|
if (selectedDefaultSite == "webserver"){
|
||||||
|
//Use build in web server as target
|
||||||
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
|
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||||
$("#proxyRoot").val(staticWebServerURL);
|
$("#proxyRoot").val(staticWebServerURL);
|
||||||
$("#proxyRoot").parent().addClass("disabled");
|
$("#proxyRoot").parent().addClass("disabled");
|
||||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||||
$("#rootReqTLS").parent().addClass("disabled");
|
$("#rootReqTLS").parent().addClass("disabled");
|
||||||
|
$("#useRootProxyRouterForVdir").parent().removeClass("disabled");
|
||||||
//Check if web server is enabled. If not, ask if the user want to enable it
|
currentDefaultSiteOption = 0;
|
||||||
/*if (!$("#webserv_enable").parent().checkbox("is checked")){
|
}else if (selectedDefaultSite == "proxy"){
|
||||||
confirmBox("Enable static web server now?", function(choice){
|
$("#defaultSiteProxyOptions").show();
|
||||||
if (choice == true){
|
|
||||||
$("#webserv_enable").parent().checkbox("set checked");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
}else{
|
|
||||||
$("#rootReqTLS").parent().removeClass("disabled");
|
$("#rootReqTLS").parent().removeClass("disabled");
|
||||||
$("#proxyRoot").parent().removeClass("disabled");
|
$("#proxyRoot").parent().removeClass("disabled");
|
||||||
initRootInfo();
|
$("#useRootProxyRouterForVdir").parent().removeClass("disabled");
|
||||||
|
currentDefaultSiteOption = 1;
|
||||||
|
}else if (selectedDefaultSite == "redirect"){
|
||||||
|
$("#defaultSiteRedirectOptions").show();
|
||||||
|
currentDefaultSiteOption = 2;
|
||||||
|
}else if (selectedDefaultSite == "notfound"){
|
||||||
|
currentDefaultSiteOption = 3;
|
||||||
|
}else{
|
||||||
|
//Unknown option
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bind events to the radio boxes
|
||||||
|
function bindDefaultSiteRadioCheckboxEvents(){
|
||||||
|
$('input[type=radio][name=defaultsiteOption]').off("change").on("change", function() {
|
||||||
|
updateAvaibleDefaultSiteOptions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initRootInfo(callback=undefined){
|
function initRootInfo(callback=undefined){
|
||||||
@ -94,6 +124,22 @@
|
|||||||
if (data == null){
|
if (data == null){
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
|
var $radios = $('input:radio[name=defaultsiteOption]');
|
||||||
|
let proxyType = data.DefaultSiteOption;
|
||||||
|
//See typedef.go for enum conversion
|
||||||
|
if (proxyType == 0){
|
||||||
|
$radios.filter('[value=webserver]').prop('checked', true);
|
||||||
|
}else if (proxyType == 1){
|
||||||
|
$radios.filter('[value=proxy]').prop('checked', true);
|
||||||
|
$("#proxyRoot").val(data.DefaultSiteValue);
|
||||||
|
}else if (proxyType == 2){
|
||||||
|
$radios.filter('[value=redirect]').prop('checked', true);
|
||||||
|
$("#redirectDomain").val(data.DefaultSiteValue);
|
||||||
|
}else if (proxyType == 3){
|
||||||
|
$radios.filter('[value=notfound]').prop('checked', true);
|
||||||
|
}
|
||||||
|
updateAvaibleDefaultSiteOptions();
|
||||||
|
|
||||||
$("#proxyRoot").val(data.Domain);
|
$("#proxyRoot").val(data.Domain);
|
||||||
checkRootRequireTLS(data.Domain);
|
checkRootRequireTLS(data.Domain);
|
||||||
}
|
}
|
||||||
@ -104,21 +150,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
initRootInfo(function(){
|
initRootInfo(function(){
|
||||||
updateWebServerLinkSettings();
|
bindDefaultSiteRadioCheckboxEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Update the current web server port settings
|
|
||||||
function updateWebServerLinkSettings(){
|
|
||||||
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
|
|
||||||
if (isUsingWebServ){
|
|
||||||
$(".webservRootDisabled").addClass("disabled");
|
|
||||||
$("#useStaticWebServer").parent().checkbox("set checked");
|
|
||||||
}else{
|
|
||||||
$(".webservRootDisabled").removeClass("disabled");
|
|
||||||
$("#useStaticWebServer").parent().checkbox("set unchecked");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUsingStaticWebServerAsRoot(callback){
|
function isUsingStaticWebServerAsRoot(callback){
|
||||||
let currentProxyRoot = $("#proxyRoot").val().trim();
|
let currentProxyRoot = $("#proxyRoot").val().trim();
|
||||||
@ -131,46 +165,11 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRootSettingStates(){
|
|
||||||
$.get("/api/cert/tls", function(data){
|
|
||||||
if (data == true){
|
|
||||||
$("#disableRootTLS").parent().removeClass('disabled').attr("title", "");
|
|
||||||
}else{
|
|
||||||
$("#disableRootTLS").parent().addClass('disabled').attr("title", "TLS listener is not enabled");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Bind event to tab switch
|
//Bind event to tab switch
|
||||||
tabSwitchEventBind["setroot"] = function(){
|
tabSwitchEventBind["setroot"] = function(){
|
||||||
//On switch over to this page, update root info
|
|
||||||
updateRootSettingStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Toggle the display status of the input box for domain setting
|
|
||||||
function updateRedirectionDomainSettingInputBox(useRedirect){
|
|
||||||
if(useRedirect){
|
|
||||||
$("#unsetRedirectDomainWrapper").stop().finish().slideDown("fast");
|
|
||||||
}else{
|
|
||||||
$("#unsetRedirectDomainWrapper").stop().finish().slideUp("fast");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCustomRedirectForUnsetSubd(){
|
|
||||||
$.get("/api/proxy/root/listOptions", function(data){
|
|
||||||
$("#unsetRedirect")[0].checked = data.EnableRedirectForUnsetRules || false;
|
|
||||||
$("#unsetRedirectDomain").val(data.UnsetRuleRedirectTarget);
|
|
||||||
updateRedirectionDomainSettingInputBox(data.EnableRedirectForUnsetRules);
|
|
||||||
|
|
||||||
//Bind event to the checkbox
|
|
||||||
$("#unsetRedirect").off("change").on("change", function(){
|
|
||||||
let useRedirect = $("#unsetRedirect")[0].checked;
|
|
||||||
updateRedirectionDomainSettingInputBox(useRedirect);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
checkCustomRedirectForUnsetSubd();
|
|
||||||
|
|
||||||
//Check if the given domain will redirect to https
|
//Check if the given domain will redirect to https
|
||||||
function checkRootRequireTLS(targetDomain){
|
function checkRootRequireTLS(targetDomain){
|
||||||
@ -193,28 +192,56 @@
|
|||||||
}else if (data == "http"){
|
}else if (data == "http"){
|
||||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//Set the new proxy root option
|
//Set the new proxy root option
|
||||||
function setProxyRoot(){
|
function setProxyRoot(btn=undefined){
|
||||||
var newpr = $("#proxyRoot").val();
|
var newpr = $("#proxyRoot").val();
|
||||||
if (newpr.trim() == ""){
|
if (newpr.trim() == "" && currentDefaultSiteOption == 0){
|
||||||
$("#proxyRoot").parent().addClass('error');
|
//Fill in the web server info
|
||||||
return
|
newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||||
}else{
|
$("#proxyRoot").val(newpr);
|
||||||
$("#proxyRoot").parent().removeClass('error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootReqTls = $("#rootReqTLS")[0].checked;
|
var rootReqTls = $("#rootReqTLS")[0].checked;
|
||||||
|
|
||||||
|
if (btn != undefined){
|
||||||
|
$(btn).addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
//proxy mode or redirect mode, check for input values
|
||||||
|
var defaultSiteValue = "";
|
||||||
|
if (currentDefaultSiteOption == 1){
|
||||||
|
if ($("#proxyRoot").val().trim() == ""){
|
||||||
|
$("#proxyRoot").parent().addClass("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
defaultSiteValue = $("#proxyRoot").val().trim();
|
||||||
|
$("#proxyRoot").parent().removeClass("error");
|
||||||
|
|
||||||
|
}else if (currentDefaultSiteOption == 2){
|
||||||
|
if ($("#redirectDomain").val().trim() == ""){
|
||||||
|
$("#redirectDomain").parent().addClass("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
defaultSiteValue = $("#redirectDomain").val().trim();
|
||||||
|
$("#redirectDomain").parent().removeClass("error");
|
||||||
|
}
|
||||||
|
|
||||||
//Create the endpoint by calling add
|
//Create the endpoint by calling add
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/add",
|
url: "/api/proxy/add",
|
||||||
data: {"type": "root", tls: rootReqTls, ep: newpr},
|
data: {
|
||||||
|
"type": "root",
|
||||||
|
"tls": rootReqTls,
|
||||||
|
"ep": newpr,
|
||||||
|
"defaultSiteOpt": currentDefaultSiteOption,
|
||||||
|
"defaultSiteVal":defaultSiteValue,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
msgbox(data.error, false, 5000);
|
msgbox(data.error, false, 5000);
|
||||||
@ -231,37 +258,20 @@
|
|||||||
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
//Update the checkbox
|
//Update the checkbox
|
||||||
updateWebServerLinkSettings();
|
|
||||||
msgbox("Proxy Root Updated");
|
msgbox("Proxy Root Updated");
|
||||||
}, 1000);
|
}, 100);
|
||||||
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (btn != undefined){
|
||||||
|
$(btn).removeClass("disabled");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRootOptions(){
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: "/api/proxy/root/updateOptions",
|
|
||||||
data: {
|
|
||||||
unsetRedirect: $("#unsetRedirect")[0].checked,
|
|
||||||
unsetRedirectTarget: $("#unsetRedirectDomain").val().trim(),
|
|
||||||
},
|
|
||||||
success: function(data) {
|
|
||||||
if (data.error != undefined){
|
|
||||||
msgbox(data.error, false);
|
|
||||||
}else{
|
|
||||||
msgbox("Root Routing Options updated");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(error) {
|
|
||||||
console.log("Error:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
@ -1,25 +1,22 @@
|
|||||||
<div class="ui stackable grid">
|
<!-- Proxy Create Form-->
|
||||||
|
<style>
|
||||||
|
.rulesInstructions{
|
||||||
|
background: var(--theme_background) !important;
|
||||||
|
color: var(--theme_lgrey);
|
||||||
|
border-radius: 1em !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="standardContainer">
|
||||||
|
<div class="ui stackable grid">
|
||||||
<div class="ten wide column">
|
<div class="ten wide column">
|
||||||
<div class="standardContainer">
|
<div class="ui basic segment" style="border-radius: 1em; padding: 1em !important;">
|
||||||
<div class="ui basic segment" style="margin-top: 1em;">
|
|
||||||
<h2>New Proxy Rule</h2>
|
<h2>New Proxy Rule</h2>
|
||||||
<p>You can create a proxy endpoing by subdomain or virtual directories</p>
|
<p>You can add more proxy rules to support more site via domain / subdomains</p>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Proxy Type</label>
|
<label>Matching Keyword / Domain</label>
|
||||||
<div class="ui selection dropdown">
|
<input type="text" id="rootname" placeholder="mydomain.com">
|
||||||
<input type="hidden" id="ptype" value="subd" onchange="handleProxyTypeOptionChange(this.value)">
|
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small>
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
<div class="default text">Proxy Type</div>
|
|
||||||
<div class="menu">
|
|
||||||
<div class="item" data-value="subd">Sub-domain</div>
|
|
||||||
<div class="item" data-value="vdir">Virtual Directory</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Subdomain Matching Keyword / Virtual Directory Name</label>
|
|
||||||
<input type="text" id="rootname" placeholder="s1.mydomain.com">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Target IP Address or Domain Name with port</label>
|
<label>Target IP Address or Domain Name with port</label>
|
||||||
@ -90,30 +87,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="blue add icon"></i> Create Endpoint</button>
|
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
|
||||||
<br><br>
|
<br><br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="six wide column">
|
<div class="six wide column">
|
||||||
<div class="ui basic segment" style="height: 100%; background-color: var(--theme_grey); color: var(--theme_lgrey);">
|
<div class="ui basic segment rulesInstructions">
|
||||||
<br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||||
<span style="font-size: 1.2em; font-weight: 300;">Subdomain</span><br>
|
Example of domain matching keyword:<br>
|
||||||
Example of subdomain matching keyword:<br>
|
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
|
||||||
<code>s1.arozos.com</code> <br>(Any access starting with s1.arozos.com will be proxy to the IP address below)<br>
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<span style="font-size: 1.2em; font-weight: 300;">Virtual Directory</span><br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
||||||
Example of virtual directory name: <br>
|
Example of subdomain matching keyword:<br>
|
||||||
<code>/s1/home/</code> <br>(Any access to {this_server}/s1/home/ will be proxy to the IP address below)<br>
|
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
|
||||||
You can also ignore the tailing slash for wildcard like usage.<br>
|
<div class="ui divider"></div>
|
||||||
<code>/s1/room-</code> <br>Any access to {this_server}/s1/classroom_* will be proxied, for example: <br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
||||||
|
Example of wildcard matching keyword:<br>
|
||||||
|
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br>
|
||||||
<div class="ui list">
|
<div class="ui list">
|
||||||
<div class="item"><code>/s1/room-101</code></div>
|
<div class="item"><code>www.arozos.com</code></div>
|
||||||
<div class="item"><code>/s1/room-102/</code></div>
|
<div class="item"><code>foo.bar.arozos.com</code></div>
|
||||||
<div class="item"><code>/s1/room-103/map.txt</code></div>
|
</div>
|
||||||
</div><br>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,9 +117,9 @@
|
|||||||
<script>
|
<script>
|
||||||
$("#advanceProxyRules").accordion();
|
$("#advanceProxyRules").accordion();
|
||||||
|
|
||||||
|
|
||||||
//New Proxy Endpoint
|
//New Proxy Endpoint
|
||||||
function newProxyEndpoint(){
|
function newProxyEndpoint(){
|
||||||
var type = $("#ptype").val();
|
|
||||||
var rootname = $("#rootname").val();
|
var rootname = $("#rootname").val();
|
||||||
var proxyDomain = $("#proxyDomain").val();
|
var proxyDomain = $("#proxyDomain").val();
|
||||||
var useTLS = $("#reqTls")[0].checked;
|
var useTLS = $("#reqTls")[0].checked;
|
||||||
@ -132,20 +127,6 @@
|
|||||||
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
||||||
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
||||||
|
|
||||||
if (type === "vdir") {
|
|
||||||
if (!rootname.startsWith("/")) {
|
|
||||||
rootname = "/" + rootname
|
|
||||||
$("#rootname").val(rootname);
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (!isSubdomainDomain(rootname)){
|
|
||||||
//This doesn't seems like a subdomain
|
|
||||||
if (!confirm(rootname + " does not looks like a subdomain. Continue anyway?")){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootname.trim() == ""){
|
if (rootname.trim() == ""){
|
||||||
$("#rootname").parent().addClass("error");
|
$("#rootname").parent().addClass("error");
|
||||||
return
|
return
|
||||||
@ -164,7 +145,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/add",
|
url: "/api/proxy/add",
|
||||||
data: {
|
data: {
|
||||||
type: type,
|
type: "host",
|
||||||
rootname: rootname,
|
rootname: rootname,
|
||||||
tls: useTLS,
|
tls: useTLS,
|
||||||
ep: proxyDomain,
|
ep: proxyDomain,
|
||||||
@ -177,19 +158,14 @@
|
|||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
msgbox(data.error, false, 5000);
|
msgbox(data.error, false, 5000);
|
||||||
}else{
|
}else{
|
||||||
//OK
|
|
||||||
listVdirs();
|
|
||||||
listSubd();
|
|
||||||
|
|
||||||
|
|
||||||
//Clear old data
|
//Clear old data
|
||||||
$("#rootname").val("");
|
$("#rootname").val("");
|
||||||
$("#proxyDomain").val("");
|
$("#proxyDomain").val("");
|
||||||
credentials = [];
|
credentials = [];
|
||||||
updateTable();
|
updateTable();
|
||||||
|
reloadUptimeList();
|
||||||
//Check if it is a new subdomain and TLS enabled
|
//Check if it is a new subdomain and TLS enabled
|
||||||
if (type == "subd" && $("#tls").checkbox("is checked")){
|
if ($("#tls").checkbox("is checked")){
|
||||||
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
|
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
|
||||||
if (choice == true){
|
if (choice == true){
|
||||||
//Load the prefer CA from TLS page
|
//Load the prefer CA from TLS page
|
||||||
@ -200,7 +176,12 @@
|
|||||||
//Get a new cert using ACME
|
//Get a new cert using ACME
|
||||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||||
console.log("Trying to get a new certificate via ACME");
|
console.log("Trying to get a new certificate via ACME");
|
||||||
obtainCertificate(rootname, defaultCA.trim());
|
|
||||||
|
//Request ACME for certificate, see cert.html component
|
||||||
|
obtainCertificate(rootname, defaultCA.trim(), function(){
|
||||||
|
// Renew the parent certificate list
|
||||||
|
initManagedDomainCertificateList();
|
||||||
|
});
|
||||||
}else{
|
}else{
|
||||||
msgbox("Proxy Endpoint Added");
|
msgbox("Proxy Endpoint Added");
|
||||||
}
|
}
|
||||||
@ -214,23 +195,17 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProxyTypeOptionChange(newType){
|
|
||||||
if (newType == "subd"){
|
|
||||||
$("#bypassGlobalTLS").parent().removeClass("disabled");
|
|
||||||
}else if (newType == "vdir"){
|
|
||||||
$("#bypassGlobalTLS").parent().addClass("disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Generic functions for delete rp endpoints
|
//Generic functions for delete rp endpoints
|
||||||
function deleteEndpoint(ptype, epoint){
|
function deleteEndpoint(epoint){
|
||||||
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
|
epoint = decodeURIComponent(epoint).hexDecode();
|
||||||
|
if (confirm("Confirm remove proxy for :" + epoint + "?")){
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/del",
|
url: "/api/proxy/del",
|
||||||
data: {ep: epoint, ptype: ptype},
|
data: {ep: epoint, },
|
||||||
success: function(){
|
success: function(){
|
||||||
listVdirs();
|
listProxyEndpoints();
|
||||||
listSubd();
|
msgbox("Proxy Rule Deleted", true);
|
||||||
|
reloadUptimeList();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -331,226 +306,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Check if a string is a valid subdomain
|
|
||||||
function isSubdomainDomain(str) {
|
//Update v3.0.0
|
||||||
const regex = /^(localhost|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}\.)$/i;
|
//Since some proxy rules now contains wildcard characters
|
||||||
return regex.test(str);
|
//all uuid are converted to hex code before use in DOM selector
|
||||||
|
|
||||||
|
String.prototype.hexEncode = function(){
|
||||||
|
var hex, i;
|
||||||
|
|
||||||
|
var result = "";
|
||||||
|
for (i=0; i<this.length; i++) {
|
||||||
|
hex = this.charCodeAt(i).toString(16);
|
||||||
|
result += ("000"+hex).slice(-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
/*
|
|
||||||
Inline editor for subd.html and vdir.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function editEndpoint(endpointType, uuid) {
|
|
||||||
var row = $('tr[eptuuid="' + uuid + '"]');
|
|
||||||
var columns = row.find('td[data-label]');
|
|
||||||
var payload = $(row).attr("payload");
|
|
||||||
payload = JSON.parse(decodeURIComponent(payload));
|
|
||||||
console.log(payload);
|
|
||||||
//console.log(payload);
|
|
||||||
columns.each(function(index) {
|
|
||||||
var column = $(this);
|
|
||||||
var oldValue = column.text().trim();
|
|
||||||
|
|
||||||
if ($(this).attr("editable") == "false"){
|
|
||||||
//This col do not allow edit. Skip
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an input element based on the column content
|
String.prototype.hexDecode = function(){
|
||||||
var input;
|
var j;
|
||||||
var datatype = $(this).attr("datatype");
|
var hexes = this.match(/.{1,4}/g) || [];
|
||||||
if (datatype == "domain"){
|
var back = "";
|
||||||
let domain = payload.Domain;
|
for(j = 0; j<hexes.length; j++) {
|
||||||
//Target require TLS for proxying
|
back += String.fromCharCode(parseInt(hexes[j], 16));
|
||||||
let tls = payload.RequireTLS;
|
|
||||||
if (tls){
|
|
||||||
tls = "checked";
|
|
||||||
}else{
|
|
||||||
tls = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Require TLS validation
|
return back;
|
||||||
let skipTLSValidation = payload.SkipCertValidations;
|
|
||||||
let checkstate = "";
|
|
||||||
if (skipTLSValidation){
|
|
||||||
checkstate = "checked";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input = `
|
|
||||||
<div class="ui mini fluid input">
|
|
||||||
<input type="text" class="Domain" value="${domain}">
|
|
||||||
</div>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="RequireTLS" ${tls}>
|
|
||||||
<label>Require TLS<br>
|
|
||||||
<small>Proxy target require HTTPS connection</small></label>
|
|
||||||
</div><br>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
|
||||||
<label>Skip Verification<br>
|
|
||||||
<small>Check this if proxy target is using self signed certificates</small></label>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
column.empty().append(input);
|
|
||||||
}else if (datatype == "basicauth"){
|
|
||||||
let requireBasicAuth = payload.RequireBasicAuth;
|
|
||||||
let checkstate = "";
|
|
||||||
if (requireBasicAuth){
|
|
||||||
checkstate = "checked";
|
|
||||||
}
|
|
||||||
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
|
|
||||||
<label>Require Basic Auth</label>
|
|
||||||
</div>
|
|
||||||
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Settings</button>`);
|
|
||||||
|
|
||||||
}else if (datatype == 'action'){
|
|
||||||
column.empty().append(`
|
|
||||||
<button title="Cancel" onclick="exitProxyInlineEdit('${endpointType}');" class="ui basic small circular icon button"><i class="ui remove icon"></i></button>
|
|
||||||
<button title="Save" onclick="saveProxyInlineEdit('${uuid}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
|
|
||||||
`);
|
|
||||||
}else if (datatype == "inbound" && payload.ProxyType == 0){
|
|
||||||
let originalContent = $(column).html();
|
|
||||||
column.empty().append(`${originalContent}
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
|
|
||||||
<label>Allow plain HTTP access<br>
|
|
||||||
<small>Allow inbound connections without TLS/SSL</small></label>
|
|
||||||
</div><br>
|
|
||||||
`);
|
|
||||||
}else{
|
|
||||||
//Unknown field. Leave it untouched
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#" + endpointType).find(".editBtn").addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitProxyInlineEdit(){
|
|
||||||
listSubd();
|
|
||||||
listVdirs();
|
|
||||||
$("#" + endpointType).find(".editBtn").removeClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveProxyInlineEdit(uuid){
|
|
||||||
var row = $('tr[eptuuid="' + uuid + '"]');
|
|
||||||
if (row.length == 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var epttype = $(row).attr("class");
|
|
||||||
if (epttype == "subdEntry"){
|
|
||||||
epttype = "subd";
|
|
||||||
}else if (epttype == "vdirEntry"){
|
|
||||||
epttype = "vdir";
|
|
||||||
}
|
|
||||||
|
|
||||||
let newDomain = $(row).find(".Domain").val();
|
|
||||||
let requireTLS = $(row).find(".RequireTLS")[0].checked;
|
|
||||||
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
|
||||||
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
|
||||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
|
||||||
|
|
||||||
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "/api/proxy/edit",
|
|
||||||
method: "POST",
|
|
||||||
data: {
|
|
||||||
"type": epttype,
|
|
||||||
"rootname": uuid,
|
|
||||||
"ep":newDomain,
|
|
||||||
"bpgtls": bypassGlobalTLS,
|
|
||||||
"tls" :requireTLS,
|
|
||||||
"tlsval": skipCertValidations,
|
|
||||||
"bauth" :requireBasicAuth,
|
|
||||||
},
|
|
||||||
success: function(data){
|
|
||||||
if (data.error !== undefined){
|
|
||||||
msgbox(data.error, false, 6000);
|
|
||||||
}else{
|
|
||||||
msgbox("Proxy endpoint updated");
|
|
||||||
if (epttype == "subd"){
|
|
||||||
listSubd();
|
|
||||||
}else if (epttype == "vdir"){
|
|
||||||
listVdirs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function editBasicAuthCredentials(endpointType, uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: endpointType,
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Obtain Certificate via ACME
|
|
||||||
*/
|
|
||||||
|
|
||||||
//Load the ACME email from server side
|
|
||||||
let acmeEmail = "";
|
|
||||||
$.get("/api/acme/autoRenew/email", function(data){
|
|
||||||
if (data != "" && data != undefined && data != null){
|
|
||||||
acmeEmail = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obtain certificate from API, only support one domain
|
|
||||||
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
|
|
||||||
let filename = "";
|
|
||||||
let email = acmeEmail;
|
|
||||||
if (acmeEmail == ""){
|
|
||||||
let rootDomain = domains.split(".").pop();
|
|
||||||
email = "admin@" + rootDomain;
|
|
||||||
}
|
|
||||||
if (filename.trim() == "" && !domains.includes(",")){
|
|
||||||
//Zoraxy filename are the matching name for domains.
|
|
||||||
//Use the same as domains
|
|
||||||
filename = domains;
|
|
||||||
}else if (filename != "" && !domains.includes(",")){
|
|
||||||
//Invalid settings. Force the filename to be same as domain
|
|
||||||
//if there are only 1 domain
|
|
||||||
filename = domains;
|
|
||||||
}else{
|
|
||||||
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "/api/acme/obtainCert",
|
|
||||||
method: "GET",
|
|
||||||
data: {
|
|
||||||
domains: domains,
|
|
||||||
filename: filename,
|
|
||||||
email: email,
|
|
||||||
ca: usingCa,
|
|
||||||
},
|
|
||||||
success: function(response) {
|
|
||||||
if (response.error) {
|
|
||||||
console.log("Error:", response.error);
|
|
||||||
// Show error message
|
|
||||||
msgbox(response.error, false, 12000);
|
|
||||||
} else {
|
|
||||||
console.log("Certificate installed successfully");
|
|
||||||
// Show success message
|
|
||||||
msgbox("Certificate installed successfully");
|
|
||||||
|
|
||||||
// Renew the parent certificate list
|
|
||||||
initManagedDomainCertificateList();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(error) {
|
|
||||||
console.log("Failed to install certificate:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="six wide column statisticWrapper">
|
<div class="six wide column statisticWrapper">
|
||||||
<div class="ui greybackground statustab segment">
|
<div class="ui statustab segment">
|
||||||
<h5 class="ui header">
|
<h5 class="ui header">
|
||||||
<i class="exchange icon"></i>
|
<i class="exchange icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<h5 class="ui header">
|
<h5 class="ui header" style="margin-top: 0px;">
|
||||||
<i class="arrows alternate horizontal icon"></i>
|
<i class="arrows alternate horizontal icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span id="forwardtype"></span>
|
<span id="forwardtype"></span>
|
||||||
@ -39,32 +39,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<h5 class="ui header">
|
<h5 class="ui header" style="margin-top: 0px;">
|
||||||
<i class="map marker alternate icon"></i>
|
<i class="map marker alternate icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span id="country"></span>
|
<span id="country"></span>
|
||||||
<div class="sub header" id="countryList">
|
<div class="sub header" id="countryList">
|
||||||
|
<i class="ui loading circle notch icon"></i> Resolving GeoIP
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="standardContainer" style="padding-bottom: 0 !important;">
|
||||||
<div id="networkActWrapper" class="standardContainer" style="position: relative; margin-top: 1em;">
|
<!-- Power Buttons-->
|
||||||
|
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
|
||||||
|
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h4>Network Status</h4>
|
||||||
|
<p>Overall Network I/O in Current Host Server</p>
|
||||||
|
</div>
|
||||||
|
<div id="networkActWrapper" class="standardContainer" style="position: relative;">
|
||||||
<canvas id="networkActivity"></canvas>
|
<canvas id="networkActivity"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div id="networkActivityPlaceHolder">
|
<div id="networkActivityPlaceHolder">
|
||||||
<p style="opacity: 0.5;"><i class="ui pause icon"></i> Graph Render Paused</p>
|
<p style="opacity: 0.5;"> Graph Render Paused</p>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
<div class="standardContainer">
|
<div class="standardContainer">
|
||||||
<h4>Basic Settings</h4>
|
<div class="ui divider"></div>
|
||||||
|
<h4>Global Settings</h4>
|
||||||
<p>Inbound Port (Port to be proxied)</p>
|
<p>Inbound Port (Port to be proxied)</p>
|
||||||
<div class="ui action fluid notloopbackOnly input">
|
<div class="ui action fluid notloopbackOnly input">
|
||||||
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
|
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
|
||||||
<button class="ui basic green notloopbackOnly button" onclick="handlePortChange();">Apply</button>
|
<button class="ui basic notloopbackOnly button" onclick="handlePortChange();"><i class="ui green checkmark icon"></i> Apply</button>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
||||||
@ -89,21 +96,22 @@
|
|||||||
Advance Settings
|
Advance Settings
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>If you have no idea what are these, you can leave them as default :)</p>
|
|
||||||
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<label>Force TLS v1.2 or above<br>
|
<label>Force TLS v1.2 or above<br>
|
||||||
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
|
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
<div id="developmentMode" class="ui toggle checkbox" style="margin-top: 0.6em;">
|
||||||
|
<input type="checkbox">
|
||||||
|
<label>Development Mode<br>
|
||||||
|
<small>(Set Cache-Control to no-store so browser will always fetch new contents from your sites)</small></label>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
|
|
||||||
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
|
|
||||||
<div id="rploopbackWarning" class="ui segment" style="display:none;">
|
<div id="rploopbackWarning" class="ui segment" style="display:none;">
|
||||||
<b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br>
|
<b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br>
|
||||||
<small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small>
|
<small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small>
|
||||||
@ -114,7 +122,7 @@
|
|||||||
<div class="ui two column stackable grid">
|
<div class="ui two column stackable grid">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>Visitor Counts</p>
|
<p>Visitor Counts</p>
|
||||||
<table class="ui unstackable inverted celled table">
|
<table class="ui unstackable very basic celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Country ISO Code</th>
|
<th>Country ISO Code</th>
|
||||||
@ -130,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>Proxy Request Types</p>
|
<p>Proxy Request Types</p>
|
||||||
<table class="ui unstackable inverted celled table">
|
<table class="ui unstackable very basic celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Proxy Type</th>
|
<th>Proxy Type</th>
|
||||||
@ -175,13 +183,13 @@
|
|||||||
}
|
}
|
||||||
$("#serverstatus").addClass("green");
|
$("#serverstatus").addClass("green");
|
||||||
$("#statusTitle").text("Online");
|
$("#statusTitle").text("Online");
|
||||||
$("#rpStatusIcon").attr("class", "green circle check icon");
|
$("#rpStatusIcon").attr("class", "white circle check icon");
|
||||||
$("#statusText").text("Serving request on port: " + data.Option.Port);
|
$("#statusText").text("Serving request on port: " + data.Option.Port);
|
||||||
}else{
|
}else{
|
||||||
$("#startbtn").removeClass("disabled");
|
$("#startbtn").removeClass("disabled");
|
||||||
$("#stopbtn").addClass("disabled");
|
$("#stopbtn").addClass("disabled");
|
||||||
$("#statusTitle").text("Offline");
|
$("#statusTitle").text("Offline");
|
||||||
$("#rpStatusIcon").attr("class", "black circle times icon")
|
$("#rpStatusIcon").attr("class", "yellow moon icon")
|
||||||
$("#statusText").text("Reverse proxy server is offline");
|
$("#statusText").text("Reverse proxy server is offline");
|
||||||
$("#serverstatus").removeClass("green");
|
$("#serverstatus").removeClass("green");
|
||||||
}
|
}
|
||||||
@ -433,6 +441,30 @@
|
|||||||
}
|
}
|
||||||
initTlsVersionSetting();
|
initTlsVersionSetting();
|
||||||
|
|
||||||
|
function initDevelopmentMode(){
|
||||||
|
$.get("/api/proxy/developmentMode", function(data){
|
||||||
|
if (data === true){
|
||||||
|
$("#developmentMode").checkbox("set checked")
|
||||||
|
}else{
|
||||||
|
$("#developmentMode").checkbox("set unchecked")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bind change events
|
||||||
|
$("#developmentMode").off("change").on("change", function(data){
|
||||||
|
let enableDevMode = ($(this).find("input[type='checkbox']")[0].checked);
|
||||||
|
$.get("/api/proxy/developmentMode?enable=" + enableDevMode, function(data){
|
||||||
|
if (enableDevMode){
|
||||||
|
msgbox("Development mode enabled");
|
||||||
|
}else{
|
||||||
|
msgbox("Development mode disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initDevelopmentMode();
|
||||||
|
|
||||||
function initTlsSetting(){
|
function initTlsSetting(){
|
||||||
$.get("/api/cert/tls", function(data){
|
$.get("/api/cert/tls", function(data){
|
||||||
if (data == true){
|
if (data == true){
|
||||||
@ -561,10 +593,11 @@
|
|||||||
{
|
{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
responsive: true,
|
responsive: true,
|
||||||
resizeDelay: 100,
|
resizeDelay: 300,
|
||||||
options: {
|
options: {
|
||||||
animation: false,
|
animation: false,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
bezierCurve: true,
|
||||||
tooltips: {enabled: false},
|
tooltips: {enabled: false},
|
||||||
hover: {mode: null},
|
hover: {mode: null},
|
||||||
//stepped: 'middle',
|
//stepped: 'middle',
|
||||||
@ -606,18 +639,18 @@
|
|||||||
{
|
{
|
||||||
label: 'Inbound',
|
label: 'Inbound',
|
||||||
data: rxValues,
|
data: rxValues,
|
||||||
borderColor: "#4d9dd9",
|
borderColor: "#484bb8",
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
backgroundColor: 'rgba(77, 157, 217, 0.2)',
|
backgroundColor: 'rgba(72, 75, 184, 0.2)',
|
||||||
fill: true,
|
fill: true,
|
||||||
pointStyle: false,
|
pointStyle: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Outbound',
|
label: 'Outbound',
|
||||||
data: txValues,
|
data: txValues,
|
||||||
borderColor: '#ffe32b',
|
borderColor: '#02a9c1',
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
backgroundColor: 'rgba(255, 227, 43, 0.2)',
|
backgroundColor: 'rgba(2, 169, 193, 0.2)',
|
||||||
fill: true,
|
fill: true,
|
||||||
pointStyle: false,
|
pointStyle: false,
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
<div class="standardContainer">
|
|
||||||
<div class="ui basic segment">
|
|
||||||
<h2>Subdomain</h2>
|
|
||||||
<p>Subdomains are a way to organize and identify different sections of a website or domain. They are essentially a prefix to the main domain name, separated by a dot. <br>For example, in the domain "blog.example.com," "blog" is the subdomain.</p>
|
|
||||||
</div>
|
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
|
||||||
<table class="ui celled sortable unstackable compact table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Matching Domain</th>
|
|
||||||
<th>Proxy To</th>
|
|
||||||
<th>Basic Auth</th>
|
|
||||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="subdList">
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<button class="ui icon right floated basic button" onclick="listSubd();"><i class="green refresh icon"></i> Refresh</button>
|
|
||||||
<br><br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function listSubd(){
|
|
||||||
$.get("/api/proxy/list?type=subd", function(data){
|
|
||||||
$("#subdList").html(``);
|
|
||||||
if (data.error !== undefined){
|
|
||||||
$("#subdList").append(`<tr>
|
|
||||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
|
|
||||||
</tr>`);
|
|
||||||
}else if (data.length == 0){
|
|
||||||
$("#subdList").append(`<tr>
|
|
||||||
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Subdomain Proxy Record</td>
|
|
||||||
</tr>`);
|
|
||||||
}else{
|
|
||||||
data.forEach(subd => {
|
|
||||||
let tlsIcon = "";
|
|
||||||
let subdData = encodeURIComponent(JSON.stringify(subd));
|
|
||||||
if (subd.RequireTLS){
|
|
||||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
|
||||||
if (subd.SkipCertValidations){
|
|
||||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
|
||||||
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
|
|
||||||
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
|
|
||||||
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
|
||||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
|
||||||
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
|
||||||
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Bind on tab switch events
|
|
||||||
tabSwitchEventBind["subd"] = function(){
|
|
||||||
listSubd();
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -3,11 +3,33 @@
|
|||||||
<h2>TCP Proxy</h2>
|
<h2>TCP Proxy</h2>
|
||||||
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
|
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic orange button" id="addProxyConfigButton"><i class="ui add icon"></i> Add Proxy Config</button>
|
|
||||||
<button class="ui basic circular right floated icon button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i></button>
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui basic segment" id="addproxyConfig" style="display:none;">
|
<div class="ui basic segment" style="margin-top: 0;">
|
||||||
<h3>TCP Proxy Config</h3>
|
<h4>TCP Proxy Rules</h4>
|
||||||
|
<p>A list of TCP proxy rules created on this host. To enable them, use the toggle button on the right.</p>
|
||||||
|
<div style="overflow-x: auto; min-height: 400px;">
|
||||||
|
<table id="proxyTable" class="ui celled unstackable table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Port/Addr A</th>
|
||||||
|
<th>Port/Addr B</th>
|
||||||
|
<th>Mode</th>
|
||||||
|
<th>Timeout (s)</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
|
||||||
|
<br><br>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui basic segment" id="addproxyConfig">
|
||||||
|
<h4>Add or Edit TCP Proxy</h4>
|
||||||
<p>Create or edit a new proxy instance</p>
|
<p>Create or edit a new proxy instance</p>
|
||||||
<form id="tcpProxyForm" class="ui form">
|
<form id="tcpProxyForm" class="ui form">
|
||||||
<div class="field" style="display:none;">
|
<div class="field" style="display:none;">
|
||||||
@ -39,11 +61,10 @@
|
|||||||
<option value="starter">Starter</option>
|
<option value="starter">Starter</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
|
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
||||||
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
|
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
|
||||||
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
||||||
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
|
<div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
|
||||||
<h3>Proxy Mode Instructions</h3>
|
|
||||||
<p>TCP Proxy support the following TCP sockets proxy modes</p>
|
<p>TCP Proxy support the following TCP sockets proxy modes</p>
|
||||||
<table class="ui celled padded inverted basic table">
|
<table class="ui celled padded inverted basic table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -108,28 +129,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="ui divider"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ui basic segment" style="margin-top: 0;">
|
|
||||||
<h3>TCP Proxy Configs</h3>
|
|
||||||
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
|
|
||||||
<div style="overflow-x: auto; min-height: 400px;">
|
|
||||||
<table id="proxyTable" class="ui celled unstackable table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Port/Addr A</th>
|
|
||||||
<th>Port/Addr B</th>
|
|
||||||
<th>Mode</th>
|
|
||||||
<th>Timeout (s)</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -138,6 +137,13 @@
|
|||||||
$("#tcpProxyForm .dropdown").dropdown();
|
$("#tcpProxyForm .dropdown").dropdown();
|
||||||
$('#tcpProxyForm').on('submit', function(event) {
|
$('#tcpProxyForm').on('submit', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
//Check if update mode
|
||||||
|
if ($("#editTcpProxyButton").is(":visible")){
|
||||||
|
confirmEditTCPProxyConfig(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var form = $(this);
|
var form = $(this);
|
||||||
|
|
||||||
var formValid = validateTCPProxyConfig(form);
|
var formValid = validateTCPProxyConfig(form);
|
||||||
@ -166,22 +172,15 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//Add proxy button pressed. Show add TCP proxy menu
|
|
||||||
$("#addProxyConfigButton").on("click", function(){
|
|
||||||
$('#addproxyConfig').slideToggle('fast');
|
|
||||||
$("#addTcpProxyButton").show();
|
|
||||||
$("#editTcpProxyButton").hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function clearTCPProxyAddEditForm(){
|
function clearTCPProxyAddEditForm(){
|
||||||
$('#tcpProxyForm input, #tcpProxyForm select').val('');
|
$('#tcpProxyForm input, #tcpProxyForm select').val('');
|
||||||
$('#tcpProxyForm select').dropdown('clear');
|
$('#tcpProxyForm select').dropdown('clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelTCPProxyEdit(event) {
|
function cancelTCPProxyEdit(event=undefined) {
|
||||||
clearTCPProxyAddEditForm();
|
clearTCPProxyAddEditForm();
|
||||||
$('#addproxyConfig').slideUp('fast');
|
$("#addTcpProxyButton").show();
|
||||||
|
$("#editTcpProxyButton").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTCPProxyConfig(form){
|
function validateTCPProxyConfig(form){
|
||||||
@ -231,7 +230,7 @@
|
|||||||
proxyConfigs.forEach(function(config) {
|
proxyConfigs.forEach(function(config) {
|
||||||
var runningLogo = 'Stopped';
|
var runningLogo = 'Stopped';
|
||||||
var runningClass = "stopped";
|
var runningClass = "stopped";
|
||||||
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="play icon"></i> Start Proxy</button>`;
|
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
|
||||||
if (config.Running){
|
if (config.Running){
|
||||||
runningLogo = 'Running';
|
runningLogo = 'Running';
|
||||||
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
|
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
|
||||||
@ -354,8 +353,8 @@
|
|||||||
msgbox("Config Updated");
|
msgbox("Config Updated");
|
||||||
}
|
}
|
||||||
initProxyConfigList();
|
initProxyConfigList();
|
||||||
clearTCPProxyAddEditForm();
|
cancelTCPProxyEdit();
|
||||||
$("#addproxyConfig").slideUp("fast");
|
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function() {
|
||||||
msgbox('An error occurred while processing the request', false);
|
msgbox('An error occurred while processing the request', false);
|
||||||
|
@ -177,7 +177,7 @@
|
|||||||
$("#utmrender").append(`<div class="ui basic segment statusbar">
|
$("#utmrender").append(`<div class="ui basic segment statusbar">
|
||||||
<div class="domain">
|
<div class="domain">
|
||||||
<div style="position: absolute; top: 0; right: 0.4em;">
|
<div style="position: absolute; top: 0; right: 0.4em;">
|
||||||
<p class="onlineStatus" style="display: inline-block; font-size: 1.3em; padding-right: 0.5em; padding-left: 0.3em; ${onlineStatusCss}">${currentOnlineStatus}</p>
|
<p class="onlineStatus" style="display: inline-block; font-size: 1.2em; padding-right: 0.5em; padding-left: 0.3em; ${onlineStatusCss}">${currentOnlineStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>
|
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>
|
||||||
|
@ -3,12 +3,21 @@
|
|||||||
<h2>Utilities</h2>
|
<h2>Utilities</h2>
|
||||||
<p>You might find these tools or information helpful when setting up your gateway server</p>
|
<p>You might find these tools or information helpful when setting up your gateway server</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui top attached tabular menu">
|
||||||
|
<a class="nettools item active" data-tab="tab1"><i class="ui user circle blue icon"></i> Accounts</a>
|
||||||
|
<a class="nettools item" data-tab="tab2">Toolbox</a>
|
||||||
|
<a class="nettools item" data-tab="tab3">System</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
|
||||||
|
<div class="extAuthOnly" style="display:none;">
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<i class="ui green circle check icon"></i> Account options are not available due to -noauth flag is set to true.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="selfauthOnly">
|
<div class="selfauthOnly">
|
||||||
<h3>Account Management</h3>
|
<h3>Change Password</h3>
|
||||||
<p>Functions to help management the current account</p>
|
<p>Update the current account credentials</p>
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h5><i class="chevron down icon"></i> Change Password</h5>
|
<h5><i class="chevron down icon"></i> Change Password</h5>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
@ -89,7 +98,8 @@
|
|||||||
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
|
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
</div>
|
||||||
|
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
|
||||||
<h3> IP Address to CIDR</h3>
|
<h3> IP Address to CIDR</h3>
|
||||||
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
@ -116,13 +126,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Config Tools -->
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab3">
|
||||||
|
<!-- Config Tools -->
|
||||||
<h3>System Backup & Restore</h3>
|
<h3>System Backup & Restore</h3>
|
||||||
<p>Options related to system backup, migrate and restore.</p>
|
<p>Options related to system backup, migrate and restore.</p>
|
||||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
||||||
<!-- System Information -->
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
<!-- System Information -->
|
||||||
<div id="zoraxyinfo">
|
<div id="zoraxyinfo">
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
System Information
|
System Information
|
||||||
@ -159,10 +171,11 @@
|
|||||||
</table>
|
</table>
|
||||||
<p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p>
|
<p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
$('.menu .nettools.item').tab();
|
||||||
/*
|
/*
|
||||||
Account Password utilities
|
Account Password utilities
|
||||||
*/
|
*/
|
||||||
@ -171,6 +184,7 @@
|
|||||||
if (data == 0){
|
if (data == 0){
|
||||||
//Using external auth manager. Hide options
|
//Using external auth manager. Hide options
|
||||||
$(".selfauthOnly").hide();
|
$(".selfauthOnly").hide();
|
||||||
|
$(".extAuthOnly").show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,43 +3,166 @@
|
|||||||
<h2>Virtual Directory</h2>
|
<h2>Virtual Directory</h2>
|
||||||
<p>A virtual directory is a consolidated view of multiple directories that provides a unified entry point for users to access disparate sources.</p>
|
<p>A virtual directory is a consolidated view of multiple directories that provides a unified entry point for users to access disparate sources.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="currentVirtualDirectoryAttachingHost" class="ui basic segment">
|
||||||
|
Select a host / routing rule to start editing Virtual Directory
|
||||||
|
</div>
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="six wide column">
|
||||||
|
<h4>Select a Target Host / Site</h4>
|
||||||
|
<p>Attach Virtual Directory routing rule to root proxy router</p>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="useRootProxyRouterForVdir" onchange="handleVdirAttachTargetChange(this);">
|
||||||
|
<label>Root Proxy Router<br>
|
||||||
|
<small>Only applicable when Default Site is set to "Reverse Proxy" mode</small></label>
|
||||||
|
</div>
|
||||||
|
<div class="ui horizontal divider">OR</div>
|
||||||
|
<p>Create Virtual Directory routing in existing host / routing rule entries</p>
|
||||||
|
<div class="ui fluid search selection dropdown">
|
||||||
|
<input type="hidden" id="vdirBaseRoutingRule" name="vdirBaseRoutingRule" onchange="handleVdirAttachTargetChange();">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="default text">Select a host to edit</div>
|
||||||
|
<div class="menu" id="hostDomainList">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ten wide column">
|
||||||
|
<h4>Edit Virtual Directory Routing Rules</h4>
|
||||||
|
<p>The following are the list of Virtual Directories currently handled by the host router above</p>
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||||
<table class="ui celled sortable unstackable compact table">
|
<table class="ui celled sortable unstackable compact table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Virtual Directory</th>
|
<th>Virtual Directory</th>
|
||||||
<th>Proxy To</th>
|
<th>Destination</th>
|
||||||
<th>Basic Auth</th>
|
|
||||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="vdirList">
|
<tbody id="vdirList">
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td>
|
<td data-label="" colspan="3">No Selected Host</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
|
<button class="ui icon right floated basic button" onclick="reloadVdirList();"><i class="green refresh icon"></i> Refresh</button>
|
||||||
|
<br><br>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div id="newVDSection" class="disabled section">
|
||||||
|
<h4>New Virtual Directory Rule</h4>
|
||||||
|
<form class="ui form">
|
||||||
|
<div class="field">
|
||||||
|
<label>Matching Path Prefix</label>
|
||||||
|
<input type="text" id="virtualDirectoryPath" placeholder="/mysite/">
|
||||||
|
<small>Path that follows your select host / domain, e.g. <code>/mysite/</code> as path prefix will forward all request that matches <code>mydomain.com/mysite/*</code></small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Target IP Address or Domain Name with port</label>
|
||||||
|
<input type="text" id="virtualDirectoryDomain" onchange="updateVDTargetTLSState();">
|
||||||
|
<small>E.g. 192.168.0.101:8000 or example.com</small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="vdReqTls">
|
||||||
|
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advance configs -->
|
||||||
|
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
||||||
|
<div id="advanceProxyRules" class="ui fluid accordion">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Advance Settings
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p></p>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="vdSkipTLSValidation">
|
||||||
|
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" onclick="addVdirToHost(); event.preventDefault();"><i class="green add icon"></i> Create Virtual Directory</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<br><br>
|
<br><br>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
//Virtual directories functions
|
//Virtual directories functions
|
||||||
function listVdirs(){
|
|
||||||
$.get("/api/proxy/list?type=vdir", function(data){
|
//Initialize the list of hosts that can be used to attach vdirs config
|
||||||
$("#vdirList").html(``);
|
function initVdirList(){
|
||||||
if (data.error !== undefined){
|
//Load the hosts into the dropdown
|
||||||
|
$("#hostDomainList").html("");
|
||||||
|
$.get("/api/proxy/list?type=host", function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
if (data.length == 0){
|
||||||
|
//No hosts found
|
||||||
|
}else{
|
||||||
|
data.forEach(proxyEndpoint => {
|
||||||
|
let domain = proxyEndpoint.RootOrMatchingDomain;
|
||||||
|
$("#hostDomainList").append(`<div class="item" data-value="${domain}">${domain}</div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Update the dropdown events
|
||||||
|
$("#vdirBaseRoutingRule").parent().dropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleVdirAttachTargetChange(targetCheckbox=undefined){
|
||||||
|
if (targetCheckbox != undefined && targetCheckbox.checked){
|
||||||
|
$("#vdirBaseRoutingRule").parent().addClass("disabled");
|
||||||
|
|
||||||
|
//Load the vdir list for root
|
||||||
|
loadVdirList("root");
|
||||||
|
$("#newVDSection").removeClass("disabled");
|
||||||
|
}else{
|
||||||
|
$("#vdirBaseRoutingRule").parent().removeClass("disabled");
|
||||||
|
let selectedEndpointRule = $("#vdirBaseRoutingRule").val();
|
||||||
|
if (selectedEndpointRule != ""){
|
||||||
|
loadVdirList(selectedEndpointRule);
|
||||||
|
$("#newVDSection").removeClass("disabled");
|
||||||
|
}else{
|
||||||
|
$("#newVDSection").addClass("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//List the Vdir of the given endpoint, use "root" for root router
|
||||||
|
function loadVdirList(endpoint){
|
||||||
|
$("#currentVirtualDirectoryAttachingHost").html(`Editing Host: ${endpoint}`);
|
||||||
|
let reqURL = "/api/proxy/vdir/list?type=host&ep=" + endpoint;
|
||||||
|
if (endpoint == "root"){
|
||||||
|
//Load root endpoint vdir list
|
||||||
|
reqURL = "/api/proxy/vdir/list?type=root";
|
||||||
|
}
|
||||||
|
|
||||||
|
$.get(reqURL, function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
$("#vdirList").html("");
|
||||||
|
if (data.length == 0){
|
||||||
|
//No virtual directory for this host
|
||||||
$("#vdirList").append(`<tr>
|
$("#vdirList").append(`<tr>
|
||||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
|
<td data-label="" colspan="3"><i class="green check circle icon"></i> No Virtual Directory Routing Rule</td>
|
||||||
</tr>`);
|
|
||||||
}else if (data.length == 0){
|
|
||||||
$("#vdirList").append(`<tr>
|
|
||||||
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
|
|
||||||
</tr>`);
|
</tr>`);
|
||||||
}else{
|
}else{
|
||||||
|
//List the vdirs
|
||||||
|
console.log(data);
|
||||||
data.forEach(vdir => {
|
data.forEach(vdir => {
|
||||||
let tlsIcon = "";
|
var tlsIcon = "";
|
||||||
let vdirData = encodeURIComponent(JSON.stringify(vdir));
|
|
||||||
if (vdir.RequireTLS){
|
if (vdir.RequireTLS){
|
||||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||||
if (vdir.SkipCertValidations){
|
if (vdir.SkipCertValidations){
|
||||||
@ -47,29 +170,253 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tlsVerificationField = "";
|
let payload = JSON.stringify(vdir).hexEncode();
|
||||||
if (vdir.RequireTLS){
|
|
||||||
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
|
|
||||||
}else{
|
|
||||||
tlsVerificationField = "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
|
$("#vdirList").append(`<tr vdirid="${vdir.MatchingPath.hexEncode()}" class="vdirEntry" payload="${payload}">
|
||||||
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
|
<td data-label="" editable="false" >${vdir.MatchingPath}</td>
|
||||||
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
|
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
|
||||||
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
|
||||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||||
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
<button class="ui circular mini basic icon button editBtn" onclick='editVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="edit icon"></i></button>
|
||||||
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
|
<button class="ui circular mini red basic icon button" onclick='deleteVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="trash icon"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if the entered domain require TLS
|
||||||
|
function updateVDTargetTLSState(){
|
||||||
|
var targetDomain = $("#virtualDirectoryDomain").val().trim();
|
||||||
|
if (targetDomain != ""){
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/tlscheck",
|
||||||
|
data: {url: targetDomain},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
|
||||||
|
}else if (data == "https"){
|
||||||
|
$("#vdReqTls").parent().checkbox("set checked");
|
||||||
|
}else if (data == "http"){
|
||||||
|
$("#vdReqTls").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadVdirList(){
|
||||||
|
$("#vdirList").find(".editBtn").removeClass("disabled");
|
||||||
|
if ($("#useRootProxyRouterForVdir")[0].checked){
|
||||||
|
loadVdirList("root");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let endpoint = $("#vdirBaseRoutingRule").val().trim();
|
||||||
|
if (endpoint != ""){
|
||||||
|
loadVdirList(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a virtual directory routing rule and attach to this endpoint
|
||||||
|
function addVdirToHost(){
|
||||||
|
var matchingPath = $("#virtualDirectoryPath").val().trim();
|
||||||
|
var targetDomain = $("#virtualDirectoryDomain").val().trim();
|
||||||
|
var reqTLS = $("#vdReqTls")[0].checked;
|
||||||
|
var skipTLSValidation = $("#vdSkipTLSValidation")[0].checked;
|
||||||
|
|
||||||
|
//Validate the input data
|
||||||
|
if (matchingPath == ""){
|
||||||
|
$("#virtualDirectoryPath").parent().addClass('error');
|
||||||
|
return;
|
||||||
|
}else{
|
||||||
|
$("#virtualDirectoryPath").parent().removeClass('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDomain == ""){
|
||||||
|
$("#virtualDirectoryDomain").parent().addClass('error');
|
||||||
|
return;
|
||||||
|
}else{
|
||||||
|
$("#virtualDirectoryDomain").parent().removeClass('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if we are editing host
|
||||||
|
let epType = "host";
|
||||||
|
let endpoint = "root";
|
||||||
|
if ($("#useRootProxyRouterForVdir")[0].checked){
|
||||||
|
//Editing root virtual directory
|
||||||
|
epType = "root";
|
||||||
|
}else{
|
||||||
|
//Editing hosts virtual directory
|
||||||
|
endpoint = $("#vdirBaseRoutingRule").val().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a virtual directory endpoint
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/vdir/add",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"type": epType,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"path": matchingPath,
|
||||||
|
"domain":targetDomain,
|
||||||
|
"reqTLS":reqTLS,
|
||||||
|
"skipValid":skipTLSValidation,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("New Virtual Directory rule added");
|
||||||
|
reloadVdirList();
|
||||||
|
resetVdirForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(){
|
||||||
|
msgbox("Add Virtual Directory failed due to unknown reasons", false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Reset the vdir form
|
||||||
|
function resetVdirForm(){
|
||||||
|
$("#virtualDirectoryPath").val("");
|
||||||
|
$("#virtualDirectoryDomain").val("");
|
||||||
|
$("#vdReqTls").parent().checkbox("set unchecked");
|
||||||
|
$("#vdSkipTLSValidation").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove Vdir
|
||||||
|
function deleteVdir(matchingPath, endpoint){
|
||||||
|
var epType = "host";
|
||||||
|
var path = $("#vdirBaseRoutingRule").val().trim();
|
||||||
|
if (endpoint.trim() == "root"){
|
||||||
|
epType = "root";
|
||||||
|
path = "";
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/vdir/del",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"type":epType,
|
||||||
|
"vdir": matchingPath,
|
||||||
|
"path": path
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Virtual Directory rule removed", true);
|
||||||
|
reloadVdirList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function editVdir(matchingPath, ept){
|
||||||
|
let targetDOM = $(".vdirEntry[vdirid='" + matchingPath.hexEncode() + "']");
|
||||||
|
$("#vdirList").find(".editBtn").addClass("disabled");
|
||||||
|
let payload = $(targetDOM).attr("payload").hexDecode();
|
||||||
|
payload = JSON.parse(payload);
|
||||||
|
console.log(payload);
|
||||||
|
$(targetDOM).find("td[editable='true']").each(function(){
|
||||||
|
let datatype = $(this).attr("datatype");
|
||||||
|
let column = $(this);
|
||||||
|
|
||||||
|
if (datatype == "domain"){
|
||||||
|
let domain = payload.Domain;
|
||||||
|
//Target require TLS for proxying
|
||||||
|
let tls = payload.RequireTLS;
|
||||||
|
if (tls){
|
||||||
|
tls = "checked";
|
||||||
|
}else{
|
||||||
|
tls = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Require TLS validation
|
||||||
|
let skipTLSValidation = payload.SkipCertValidations;
|
||||||
|
let checkstate = "";
|
||||||
|
if (skipTLSValidation){
|
||||||
|
checkstate = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
|
input = `
|
||||||
|
<div class="ui mini fluid input">
|
||||||
|
<input type="text" class="Domain" value="${domain}">
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="RequireTLS" ${tls}>
|
||||||
|
<label>Require TLS<br>
|
||||||
|
<small>Proxy target require HTTPS connection</small></label>
|
||||||
|
</div><br>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||||
|
<label>Skip Verification<br>
|
||||||
|
<small>Check this if proxy target is using self signed certificates</small></label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
column.empty().append(input);
|
||||||
|
}else if (datatype == 'action'){
|
||||||
|
column.empty().append(`
|
||||||
|
<button title="Save" onclick="saveVdirInlineEdit('${payload.MatchingPath.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
|
||||||
|
<button title="Cancel" onclick="exitVdirInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveVdirInlineEdit(mathingPath){
|
||||||
|
mathingPath = mathingPath.hexDecode();
|
||||||
|
|
||||||
|
var epType = "host";
|
||||||
|
var path = $("#vdirBaseRoutingRule").val().trim();
|
||||||
|
if ($("#useRootProxyRouterForVdir")[0].checked){
|
||||||
|
epType = "root";
|
||||||
|
path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load new setting from inline editor
|
||||||
|
let newDomain = $("#vdirList").find(".Domain").val();
|
||||||
|
let requireTLS = $("#vdirList").find(".RequireTLS")[0].checked;
|
||||||
|
let skipValidation = $("#vdirList").find(".SkipCertValidations")[0].checked;
|
||||||
|
|
||||||
|
//console.log(mathingPath, newDomain, requireTLS, skipValidation);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/vdir/edit",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"type": epType,
|
||||||
|
"vdir": mathingPath,
|
||||||
|
"domain":newDomain,
|
||||||
|
"path":path,
|
||||||
|
"reqTLS":requireTLS,
|
||||||
|
"skipValid": skipValidation
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Virtual Directory rule updated", true);
|
||||||
|
exitVdirInlineEdit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(){
|
||||||
|
msgbox("unknown error occured", false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitVdirInlineEdit(){
|
||||||
|
reloadVdirList();
|
||||||
|
}
|
||||||
|
|
||||||
//Bind on tab switch events
|
//Bind on tab switch events
|
||||||
tabSwitchEventBind["vdir"] = function(){
|
tabSwitchEventBind["vdir"] = function(){
|
||||||
listVdirs();
|
initVdirList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initVdirList();
|
||||||
</script>
|
</script>
|
@ -4,7 +4,7 @@
|
|||||||
<p>A simple static web server that serve html css and js files</p>
|
<p>A simple static web server that serve html css and js files</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment webservRunningStateWrapper">
|
||||||
<h4 class="ui header" id="webservRunningState">
|
<h4 class="ui header" id="webservRunningState">
|
||||||
<i class="green circle icon"></i>
|
<i class="green circle icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -102,12 +102,14 @@
|
|||||||
function setWebServerRunningState(running){
|
function setWebServerRunningState(running){
|
||||||
if (running){
|
if (running){
|
||||||
$("#webserv_enable").parent().checkbox("set checked");
|
$("#webserv_enable").parent().checkbox("set checked");
|
||||||
$("#webservRunningState").find("i").attr("class", "green circle icon");
|
$("#webservRunningState").find("i").attr("class", "white circle check icon");
|
||||||
$("#webservRunningState").find(".webserv_status").text("Running");
|
$("#webservRunningState").find(".webserv_status").text("Running");
|
||||||
|
$(".webservRunningStateWrapper").addClass("enabled")
|
||||||
}else{
|
}else{
|
||||||
$("#webserv_enable").parent().checkbox("set unchecked");
|
$("#webserv_enable").parent().checkbox("set unchecked");
|
||||||
$("#webservRunningState").find("i").attr("class", "red circle icon");
|
$("#webservRunningState").find("i").attr("class", "white circle times icon");
|
||||||
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
||||||
|
$(".webservRunningStateWrapper").removeClass("enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<p>You do not have permission to view this directory or page. <br>
|
<p>You do not have permission to view this directory or page. <br>
|
||||||
This might cause by the region limit setting of this site.</p>
|
This might be caused by the region limit setting of this site.</p>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div style="text-align: left;">
|
<div style="text-align: left;">
|
||||||
<small>Request time: <span id="reqtime"></span></small><br>
|
<small>Request time: <span id="reqtime"></span></small><br>
|
||||||
|
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 694 KiB |
Before Width: | Height: | Size: 818 KiB |
Before Width: | Height: | Size: 1.1 MiB |
@ -26,9 +26,12 @@
|
|||||||
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
|
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
|
||||||
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
|
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui right floated buttons" style="padding-top: 2px;">
|
<div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
|
||||||
<button class="ui basic icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
<button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="ui right floated buttons" style="padding-top: 2px;">
|
||||||
|
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -36,21 +39,25 @@
|
|||||||
<a class="item active" tag="status">
|
<a class="item active" tag="status">
|
||||||
<i class="simplistic info circle icon"></i>Status
|
<i class="simplistic info circle icon"></i>Status
|
||||||
</a>
|
</a>
|
||||||
|
<a class="item" tag="setroot">
|
||||||
|
<i class="simplistic home icon"></i> Default Site
|
||||||
|
</a>
|
||||||
|
<div class="ui divider menudivider">Reverse Proxy</div>
|
||||||
|
<a class="item" tag="httprp">
|
||||||
|
<i class="simplistic sitemap icon"></i> HTTP Proxy
|
||||||
|
</a>
|
||||||
<a class="item" tag="vdir">
|
<a class="item" tag="vdir">
|
||||||
<i class="simplistic folder icon"></i> Virtual Directory
|
<i class="simplistic folder icon"></i> Virtual Directory
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="subd">
|
|
||||||
<i class="simplistic sitemap icon"></i> Subdomain Proxy
|
|
||||||
</a>
|
|
||||||
<a class="item" tag="rules">
|
<a class="item" tag="rules">
|
||||||
<i class="simplistic plus square icon"></i> Create Proxy Rules
|
<i class="simplistic plus square icon"></i> Create Proxy Rules
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="setroot">
|
<a class="item" tag="tcpprox">
|
||||||
<i class="simplistic home icon"></i> Set Proxy Root
|
<i class="simplistic exchange icon"></i> TCP Proxy
|
||||||
</a>
|
</a>
|
||||||
<div class="ui divider menudivider">Access & Connections</div>
|
<div class="ui divider menudivider">Access & Connections</div>
|
||||||
<a class="item" tag="cert">
|
<a class="item" tag="cert">
|
||||||
<i class="simplistic lock icon"></i> TLS / SSL certificate
|
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="redirectset">
|
<a class="item" tag="redirectset">
|
||||||
<i class="simplistic level up alternate icon"></i> Redirection
|
<i class="simplistic level up alternate icon"></i> Redirection
|
||||||
@ -65,9 +72,6 @@
|
|||||||
<a class="item" tag="zgrok">
|
<a class="item" tag="zgrok">
|
||||||
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="tcpprox">
|
|
||||||
<i class="simplistic exchange icon"></i> TCP Proxy
|
|
||||||
</a>
|
|
||||||
<div class="ui divider menudivider">Others</div>
|
<div class="ui divider menudivider">Others</div>
|
||||||
<a class="item" tag="webserv">
|
<a class="item" tag="webserv">
|
||||||
<i class="simplistic globe icon"></i> Static Web Server
|
<i class="simplistic globe icon"></i> Static Web Server
|
||||||
@ -97,7 +101,7 @@
|
|||||||
<div id="vdir" class="functiontab" target="vdir.html"></div>
|
<div id="vdir" class="functiontab" target="vdir.html"></div>
|
||||||
|
|
||||||
<!-- Subdomain Proxy -->
|
<!-- Subdomain Proxy -->
|
||||||
<div id="subd" class="functiontab" target="subd.html"></div>
|
<div id="httprp" class="functiontab" target="httprp.html"></div>
|
||||||
|
|
||||||
<!-- Create Rules -->
|
<!-- Create Rules -->
|
||||||
<div id="rules" class="functiontab" target="rules.html"></div>
|
<div id="rules" class="functiontab" target="rules.html"></div>
|
||||||
@ -150,14 +154,13 @@
|
|||||||
<br><br>
|
<br><br>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui container" style="color: grey; font-size: 90%">
|
<div class="ui container" style="color: grey; font-size: 90%">
|
||||||
<p>CopyRight Zoraxy project and its author, 2022 - <span class="year"></span></p>
|
<p>CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messageBox" class="ui green floating big compact message">
|
<div id="messageBox" class="ui green floating big compact message">
|
||||||
<p><i class="green check circle icon"></i> There are no message</p>
|
<p><i class="green check circle icon"></i> There are no messages</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="confirmBox" style="display:none;">
|
<div id="confirmBox" style="display:none;">
|
||||||
<div class="ui top attached progress">
|
<div class="ui top attached progress">
|
||||||
<div class="bar" style="width: 100%; min-width: 0px;"></div>
|
<div class="bar" style="width: 100%; min-width: 0px;"></div>
|
||||||
@ -236,12 +239,22 @@
|
|||||||
$.get("/api/auth/logout", function(response) {
|
$.get("/api/auth/logout", function(response) {
|
||||||
if (response === "OK") {
|
if (response === "OK") {
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
window.location.href = "/";
|
window.location.href = "/login.html";
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTheme(){
|
||||||
|
if ($("body").hasClass("darkTheme")){
|
||||||
|
$("body").removeClass("darkTheme")
|
||||||
|
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
|
||||||
|
}else{
|
||||||
|
$("body").addClass("darkTheme");
|
||||||
|
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTabButtonById(targetTabId){
|
function getTabButtonById(targetTabId){
|
||||||
let targetTabBtn = undefined;
|
let targetTabBtn = undefined;
|
||||||
$("#mainmenu").find(".item").each(function(){
|
$("#mainmenu").find(".item").each(function(){
|
||||||
@ -272,9 +285,11 @@
|
|||||||
$(targetBtn).addClass("active");
|
$(targetBtn).addClass("active");
|
||||||
$(".functiontab").hide();
|
$(".functiontab").hide();
|
||||||
$("#" + tabID).fadeIn('fast', function(){
|
$("#" + tabID).fadeIn('fast', function(){
|
||||||
|
setTimeout(function(){
|
||||||
if (tabSwitchEventBind[tabID]){
|
if (tabSwitchEventBind[tabID]){
|
||||||
tabSwitchEventBind[tabID]();
|
tabSwitchEventBind[tabID]();
|
||||||
}
|
}
|
||||||
|
},100)
|
||||||
});
|
});
|
||||||
$('html,body').animate({scrollTop: 0}, 'fast');
|
$('html,body').animate({scrollTop: 0}, 'fast');
|
||||||
window.location.hash = tabID;
|
window.location.hash = tabID;
|
||||||
|
@ -7,48 +7,13 @@
|
|||||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||||
<title>Login | Zoraxy</title>
|
<title>Login | Zoraxy</title>
|
||||||
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
||||||
|
<link href="script/aos.css" rel="stylesheet">
|
||||||
|
<script src="script/aos.js"></script>
|
||||||
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
|
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
|
||||||
<script type="application/javascript" src="script/semantic/semantic.min.js"></script>
|
<script type="application/javascript" src="script/semantic/semantic.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: rgb(245,245,245);
|
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
|
||||||
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background{
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0.8;
|
|
||||||
z-index: -99;
|
|
||||||
background-image: url("img/public/bg.jpg");
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-position: right top;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
margin:auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loginForm{
|
|
||||||
height: 100%;
|
|
||||||
background-color: white;
|
|
||||||
width: 25em;
|
|
||||||
margin-left: 10em;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 550px) {
|
|
||||||
/* CSS rules here for screens lower than 750px */
|
|
||||||
#loginForm{
|
|
||||||
width: calc(100% - 4em);
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#errmsg{
|
#errmsg{
|
||||||
@ -65,13 +30,97 @@
|
|||||||
.ui.fluid.button.registerOnly{
|
.ui.fluid.button.registerOnly{
|
||||||
display:none;
|
display:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loginForm {
|
||||||
|
border-radius: 1em;
|
||||||
|
width: 25em;
|
||||||
|
height: 450px;
|
||||||
|
position: absolute; /*Can also be `fixed`*/
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
/*Solves a problem in which the content is being cut when the div is smaller than its' wrapper:*/
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wavebase {
|
||||||
|
position:fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height:5vh;
|
||||||
|
text-align:center;
|
||||||
|
padding-top: 1em;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Waves CSS
|
||||||
|
*/
|
||||||
|
|
||||||
|
#wavesWrapper{
|
||||||
|
position: fixed;
|
||||||
|
bottom: 5vh;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waves {
|
||||||
|
position:relative;
|
||||||
|
width: 100%;
|
||||||
|
height:15vh;
|
||||||
|
margin-bottom:-7px; /*Fix for safari gap*/
|
||||||
|
min-height:100px;
|
||||||
|
max-height:150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.parallax > use {
|
||||||
|
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(1) {
|
||||||
|
animation-delay: -8s;
|
||||||
|
animation-duration: 28s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(2) {
|
||||||
|
animation-delay: -12s;
|
||||||
|
animation-duration: 40s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(3) {
|
||||||
|
animation-delay: -16s;
|
||||||
|
animation-duration: 52s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(4) {
|
||||||
|
animation-delay: -20s;
|
||||||
|
animation-duration: 80s;
|
||||||
|
}
|
||||||
|
@keyframes move-forever {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(-90px,0,0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(85px,0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*Shrinking for mobile*/
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.waves {
|
||||||
|
height:40px;
|
||||||
|
min-height:40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="background"></div>
|
<div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
|
||||||
<div id="loginForm" class="ui middle aligned center aligned grid">
|
<div class="column" style="padding-top: 0 !important;">
|
||||||
<div class="column">
|
<form class="ui large form content">
|
||||||
<form class="ui large form">
|
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
||||||
<p class="registerOnly">Account Setup</p>
|
<p class="registerOnly">Account Setup</p>
|
||||||
@ -99,19 +148,37 @@
|
|||||||
<label>Remember Me</label>
|
<label>Remember Me</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="loginbtn" class="ui fluid basic blue button loginOnly">Login</div>
|
<div id="loginbtn" class="ui fluid basic button loginOnly"> <i class="ui blue sign-in icon"></i> Login</div>
|
||||||
<div id="regsiterbtn" class="ui fluid basic blue button registerOnly">Create</div>
|
<div id="regsiterbtn" class="ui fluid basic button registerOnly"><i class="ui green checkmark icon"></i> Confirm</div>
|
||||||
<div id="errmsg"></div>
|
<div id="errmsg"></div>
|
||||||
<div id="forgetPassword" class="field loginOnly" style="text-align: right;">
|
<div id="forgetPassword" class="field loginOnly" style="text-align: right; margin-top: 2em;">
|
||||||
<a href="#" onclick="sendResetAccountEmail();">Forget Password</a>
|
<a href="#" onclick="sendResetAccountEmail();">Forget Password</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
|
||||||
<small>Proudly powered by Zoraxy</small>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="wavesWrapper">
|
||||||
|
<!-- CSS waves-->
|
||||||
|
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||||
|
<defs>
|
||||||
|
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||||
|
</defs>
|
||||||
|
<g class="parallax">
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="wavebase">
|
||||||
|
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
AOS.init();
|
||||||
|
|
||||||
var redirectionAddress = "/";
|
var redirectionAddress = "/";
|
||||||
var loginAddress = "/api/auth/login";
|
var loginAddress = "/api/auth/login";
|
||||||
$(".checkbox").checkbox();
|
$(".checkbox").checkbox();
|
||||||
|
245
src/web/main.css
@ -2,15 +2,65 @@
|
|||||||
index.html style overwrite
|
index.html style overwrite
|
||||||
*/
|
*/
|
||||||
:root{
|
:root{
|
||||||
--theme_grey: #414141;
|
|
||||||
--theme_lgrey: #f6f6f6;
|
|
||||||
--theme_green: #3c9c63;
|
|
||||||
--theme_fcolor: #979797;
|
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
|
||||||
--theme_advance: #f8f8f9;
|
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||||
|
--theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
|
||||||
|
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme Color Definations */
|
||||||
|
body:not(.darkTheme){
|
||||||
|
--theme_bg: #f6f6f6;
|
||||||
|
--theme_bg_primary: #ffffff;
|
||||||
|
--theme_bg_secondary: #ffffff;
|
||||||
|
--theme_bg_active: #ececec;
|
||||||
|
--theme_highlight: #a9d1f3;
|
||||||
|
--theme_bg_inverted: #27292d;
|
||||||
|
--theme_advance: #f8f8f9;
|
||||||
|
--item_color: #5e5d5d;
|
||||||
|
--item_color_select: rgba(0,0,0,.87);
|
||||||
|
--text_color: #414141;
|
||||||
|
--input_color: white;
|
||||||
|
--divider_color: #cacaca;
|
||||||
|
--text_color_inverted: #fcfcfc;
|
||||||
|
--button_text_color: #878787;
|
||||||
|
--button_border_color: #dedede;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme{
|
||||||
|
--theme_bg: #27292d;
|
||||||
|
--theme_bg_primary: #3d3f47;
|
||||||
|
--theme_bg_secondary: #373a42;
|
||||||
|
--theme_highlight: #6682c4;
|
||||||
|
--theme_bg_active: #292929;
|
||||||
|
--theme_bg_inverted: #f8f8f9;
|
||||||
|
--theme_advance: #333333;
|
||||||
|
--item_color: #cacaca;
|
||||||
|
--text_color: #fcfcfc;
|
||||||
|
--text_color_secondary: #dfdfdf;
|
||||||
|
--input_color: black;
|
||||||
|
--divider_color: #3b3b3b;
|
||||||
|
--item_color_select: rgba(255, 255, 255, 0.87);
|
||||||
|
--text_color_inverted: #414141;
|
||||||
|
--button_text_color: #e9e9e9;
|
||||||
|
--button_border_color: #646464;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Toggle Css */
|
||||||
|
#themeColorButton{
|
||||||
|
background-color: black;
|
||||||
|
color: var(--text_color_inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme #themeColorButton{
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
body{
|
body{
|
||||||
background-color:#f6f6f6;
|
background-color:var(--theme_bg);
|
||||||
color: #414141;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.functiontab{
|
.functiontab{
|
||||||
@ -31,14 +81,14 @@ body{
|
|||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
padding-left: 1.2em;
|
padding-left: 1.2em;
|
||||||
padding-right: 1.2em;
|
padding-right: 1.2em;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--theme_bg_secondary);
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
border-bottom: 1px solid var(--theme_highlight);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
box-shadow: 0px 1px 5px 0px rgba(38,38,38,0.26);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menubar .logo{
|
.menubar .logo{
|
||||||
@ -67,7 +117,7 @@ body{
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: calc(100% - 240px);
|
width: calc(100% - 240px);
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
background-color: white;
|
background-color: var(--theme_bg_primary);
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
@ -76,6 +126,9 @@ body{
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rulesInstructions, .rulesInstructions span{
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.divider{
|
.ui.divider{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@ -86,9 +139,17 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.statisticWrapper{
|
.statisticWrapper{
|
||||||
|
margin-top: 1em;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
padding-right: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statisticWrapper .statustab{
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Message Box */
|
/* Message Box */
|
||||||
#messageBox{
|
#messageBox{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -204,14 +265,6 @@ body{
|
|||||||
|
|
||||||
|
|
||||||
@media screen and (min-width: 750px) {
|
@media screen and (min-width: 750px) {
|
||||||
#serverstatus{
|
|
||||||
border-top-left-radius: 1em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greybackground.statustab{
|
|
||||||
border-top-right-radius: 1em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standardContainer{
|
.standardContainer{
|
||||||
padding-left: 2.4em;
|
padding-left: 2.4em;
|
||||||
padding-right: 2.4em;
|
padding-right: 2.4em;
|
||||||
@ -221,7 +274,18 @@ body{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 748px) {
|
||||||
|
#serverstatus{
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.statisticWrapper .statustab{
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -257,19 +321,10 @@ body{
|
|||||||
padding: 0em;
|
padding: 0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.grid > .stackable.stackable.row > .column, .ui.stackable.grid > .column.grid > .column, .ui.stackable.grid > .column.row > .column, .ui.stackable.grid > .column:not(.row), .ui.stackable.grid > .row > .column, .ui.stackable.grid > .row > .wide.column, .ui.stackable.grid > .wide.column.serverstatusWrapper {
|
|
||||||
padding: 0rem 0rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#serverstatus.green{
|
#serverstatus.green{
|
||||||
border-bottom: 0px solid transparent !important;
|
border-bottom: 0px solid transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.greybackground.statustab{
|
|
||||||
border-top-right-radius: 0em !important;
|
|
||||||
padding: 2em 2em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standardContainer{
|
.standardContainer{
|
||||||
padding-left: 1.2em;
|
padding-left: 1.2em;
|
||||||
padding-right: 1.2em;
|
padding-right: 1.2em;
|
||||||
@ -298,11 +353,16 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui.menu .item{
|
.ui.menu .item{
|
||||||
color: #5e5d5d;
|
color: var(--item_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.segment{
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.ui.secondary.vertical.menu .active.item{
|
.ui.secondary.vertical.menu .active.item{
|
||||||
background-color: #414141;
|
background: var(--theme_background);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@ -311,11 +371,15 @@ body{
|
|||||||
animation: blinker 3s ease-in-out infinite;
|
animation: blinker 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.basic.segment.advanceoptions{
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.bluefont{
|
.bluefont{
|
||||||
color: #417ac1 !important;
|
color: #417ac1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes blinker {
|
@keyframes blinker {
|
||||||
50% {
|
50% {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
@ -327,52 +391,26 @@ body{
|
|||||||
*/
|
*/
|
||||||
#serverstatus{
|
#serverstatus{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border-radius: 1em;
|
||||||
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#statusTitle{
|
#statusTitle{
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statustab{
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greybackground.statustab{
|
|
||||||
background-color: #414141 !important;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.greybackground.statustab .ui.header:not(:first-child){
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greybackground.statustab span,
|
|
||||||
.greybackground.statustab h1,
|
|
||||||
.greybackground.statustab h2,
|
|
||||||
.greybackground.statustab h3,
|
|
||||||
.greybackground.statustab h4,
|
|
||||||
.greybackground.statustab h5 {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greybackground.statustab .header{
|
|
||||||
color: #b7b7b7 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#serverstatus.green{
|
#serverstatus.green{
|
||||||
background-color: #fefefe !important;
|
background: linear-gradient(60deg, #27e7ff, #00ca52);
|
||||||
border-right: 5px solid #3d9c64;
|
|
||||||
}
|
}
|
||||||
#serverstatus.green .sub.header{
|
#serverstatus.green .sub.header{
|
||||||
color: rgb(224, 224, 224);
|
color: rgb(224, 224, 224);
|
||||||
}
|
}
|
||||||
#serverstatus.green i,
|
#serverstatus.green i,
|
||||||
#serverstatus.green #statusTitle{
|
#serverstatus.green #statusTitle{
|
||||||
color: #3d9c64;
|
color: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
#serverstatus.green #statusText{
|
#serverstatus.green #statusText{
|
||||||
color: #2c583d;
|
color: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -381,22 +419,20 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#serverstatus:not(.green){
|
#serverstatus:not(.green){
|
||||||
background-color: white !important;
|
background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||||
background-image: url("img/plant.png");
|
|
||||||
background-position: right;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: auto 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#serverstatus:not(.green) #statusTitle,
|
#serverstatus:not(.green) #statusTitle,
|
||||||
#serverstatus:not(.green) i,
|
#serverstatus:not(.green) i,
|
||||||
#serverstatus:not(.green) .sub.header{
|
#serverstatus:not(.green) .sub.header{
|
||||||
color: #4c4c4c;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.statustab{
|
.statustab{
|
||||||
min-height: 5.5em;
|
min-height: 5.5em;
|
||||||
|
margin: 1em;
|
||||||
|
border-radius: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summaryTotalCount{
|
#summaryTotalCount{
|
||||||
@ -467,7 +503,7 @@ body{
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
background-color: #3d9c64;
|
background-color: white;
|
||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
@ -476,6 +512,23 @@ body{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HTTP Proxy & Virtual Directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
#currentVirtualDirectoryAttachingHost{
|
||||||
|
background: var(--theme_background);
|
||||||
|
color: white;
|
||||||
|
border-radius: 1em;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section.disabled{
|
||||||
|
opacity: 0.5;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Access Control
|
Access Control
|
||||||
*/
|
*/
|
||||||
@ -506,24 +559,70 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tcproxConfig.running td:first-child{
|
.tcproxConfig.running td:first-child{
|
||||||
border-left: 0.6em solid #21ba45 !important;
|
border-left: 0.6em solid #02cb59 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tcproxConfig.stopped td:first-child{
|
.tcproxConfig.stopped td:first-child{
|
||||||
border-left: 0.6em solid #414141 !important;
|
border-left: 0.6em solid #02032a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tcproxConfig td:first-child .statusText{
|
.tcproxConfig td:first-child .statusText{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.3em;
|
bottom: 0.3em;
|
||||||
left: 0.2em;
|
left: 0.2em;
|
||||||
font-size: 2em;
|
font-size: 1.4em;
|
||||||
color:rgb(224, 224, 224);
|
color:rgb(224, 224, 224);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ACME Renewer Status Panel
|
||||||
|
*/
|
||||||
|
|
||||||
|
.acmeRenewStateWrapper{
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.acmeRenewStateWrapper .ui.header, .acmeRenewStateWrapper .sub.header{
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acmeRenewStateWrapper:not(.enabled){
|
||||||
|
background: var(--theme_red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.acmeRenewStateWrapper.enabled{
|
||||||
|
background: var(--theme_green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Static Web Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
.webservRunningStateWrapper{
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.webservRunningStateWrapper .ui.header, .webservRunningStateWrapper .sub.header{
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webservRunningStateWrapper:not(.enabled){
|
||||||
|
background: var(--theme_red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webservRunningStateWrapper.enabled{
|
||||||
|
background: var(--theme_green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Uptime Monitor
|
Uptime Monitor
|
||||||
*/
|
*/
|
||||||
|
@ -7,12 +7,14 @@
|
|||||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||||
<title>Account Reset | Zoraxy</title>
|
<title>Account Reset | Zoraxy</title>
|
||||||
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
||||||
|
<link href="script/aos.css" rel="stylesheet">
|
||||||
|
<script src="script/aos.js"></script>
|
||||||
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
|
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
|
||||||
<script type="application/javascript" src="script/semantic/semantic.min.js"></script>
|
<script type="application/javascript" src="script/semantic/semantic.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: rgb(245,245,245);
|
background: rgb(38,60,71);
|
||||||
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%);
|
background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.background{
|
.background{
|
||||||
@ -34,23 +36,6 @@
|
|||||||
margin:auto;
|
margin:auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loginForm{
|
|
||||||
height: 100%;
|
|
||||||
background-color: white;
|
|
||||||
width: 25em;
|
|
||||||
margin-left: 10em;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 550px) {
|
|
||||||
/* CSS rules here for screens lower than 750px */
|
|
||||||
#loginForm{
|
|
||||||
width: calc(100% - 4em);
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#errmsg{
|
#errmsg{
|
||||||
color: #9f3a38;
|
color: #9f3a38;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
@ -61,18 +46,102 @@
|
|||||||
.backBtn{
|
.backBtn{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0em;
|
top: 0em;
|
||||||
left: 1em;
|
left: 2em;
|
||||||
margin-top: -4em;
|
transition: opacity 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn:hover{
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginForm {
|
||||||
|
border-radius: 1em;
|
||||||
|
width: 25em;
|
||||||
|
height: 550px;
|
||||||
|
position: absolute; /*Can also be `fixed`*/
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
/*Solves a problem in which the content is being cut when the div is smaller than its' wrapper:*/
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wavebase {
|
||||||
|
position:fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height:5vh;
|
||||||
|
text-align:center;
|
||||||
|
padding-top: 1em;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Waves CSS
|
||||||
|
*/
|
||||||
|
|
||||||
|
#wavesWrapper{
|
||||||
|
position: fixed;
|
||||||
|
bottom: 5vh;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waves {
|
||||||
|
position:relative;
|
||||||
|
width: 100%;
|
||||||
|
height:15vh;
|
||||||
|
margin-bottom:-7px; /*Fix for safari gap*/
|
||||||
|
min-height:100px;
|
||||||
|
max-height:150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.parallax > use {
|
||||||
|
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(1) {
|
||||||
|
animation-delay: -8s;
|
||||||
|
animation-duration: 28s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(2) {
|
||||||
|
animation-delay: -12s;
|
||||||
|
animation-duration: 40s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(3) {
|
||||||
|
animation-delay: -16s;
|
||||||
|
animation-duration: 52s;
|
||||||
|
}
|
||||||
|
.parallax > use:nth-child(4) {
|
||||||
|
animation-delay: -20s;
|
||||||
|
animation-duration: 80s;
|
||||||
|
}
|
||||||
|
@keyframes move-forever {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(-90px,0,0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(85px,0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*Shrinking for mobile*/
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.waves {
|
||||||
|
height:40px;
|
||||||
|
min-height:40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="background"></div>
|
<div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
|
||||||
<div id="loginForm" class="ui middle aligned center aligned grid">
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<a class="backBtn" href="/">
|
|
||||||
<i class="huge black chevron circle left icon"></i>
|
|
||||||
</a>
|
|
||||||
<form class="ui large form">
|
<form class="ui large form">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
||||||
@ -95,7 +164,7 @@
|
|||||||
<input id="magic" type="password" name="New password" placeholder="New Password">
|
<input id="magic" type="password" name="New password" placeholder="New Password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="resetBtn" class="ui fluid basic green button">Set New Password</div>
|
<div id="resetBtn" class="ui fluid basic button"><i class="ui green lock open icon"></i> Reset Password</div>
|
||||||
<div id="errmsg" class="ui red message" style="display: none;">
|
<div id="errmsg" class="ui red message" style="display: none;">
|
||||||
<i class="red remove icon"></i> Unknown Error Occured
|
<i class="red remove icon"></i> Unknown Error Occured
|
||||||
</div>
|
</div>
|
||||||
@ -107,12 +176,36 @@
|
|||||||
<a href="#" id="resendEmailLink" onclick="sendResetAccountEmail();">Resend Email</a>
|
<a href="#" id="resendEmailLink" onclick="sendResetAccountEmail();">Resend Email</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
|
||||||
<small>Proudly powered by Zoraxy</small>
|
|
||||||
</form>
|
</form>
|
||||||
|
<a class="backBtn" href="/">
|
||||||
|
<i class="big chevron circle left icon" style="color: #121d37;"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wavesWrapper">
|
||||||
|
<!-- CSS waves-->
|
||||||
|
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||||
|
<defs>
|
||||||
|
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||||
|
</defs>
|
||||||
|
<g class="parallax">
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||||
|
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="wavebase">
|
||||||
|
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
AOS.init();
|
||||||
|
|
||||||
|
|
||||||
var redirectionAddress = "/";
|
var redirectionAddress = "/";
|
||||||
var loginAddress = "/api/auth/login";
|
var loginAddress = "/api/auth/login";
|
||||||
$(".checkbox").checkbox();
|
$(".checkbox").checkbox();
|
||||||
@ -155,6 +248,12 @@
|
|||||||
var token = $('#token').val();
|
var token = $('#token').val();
|
||||||
var newPassword = $('#magic').val();
|
var newPassword = $('#magic').val();
|
||||||
|
|
||||||
|
if (token.trim() == ""){
|
||||||
|
$("#errmsg").html(`<i class="red circle times icon"></i> Token cannot be empty!`);
|
||||||
|
$("#errmsg").show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Send POST request with input values as data
|
// Send POST request with input values as data
|
||||||
$.post('/api/account/new', { username: username, token: token, newpw: newPassword })
|
$.post('/api/account/new', { username: username, token: token, newpw: newPassword })
|
||||||
.done(function(data) {
|
.done(function(data) {
|
||||||
|
1
src/web/script/aos.css
Normal file
1
src/web/script/aos.js
Normal file
@ -107,6 +107,10 @@
|
|||||||
asc: 'sorted ascending',
|
asc: 'sorted ascending',
|
||||||
desc: 'sorted descending',
|
desc: 'sorted descending',
|
||||||
compare: function(a, b) {
|
compare: function(a, b) {
|
||||||
|
if (!isNaN(parseInt(a.trim())) && !isNaN(parseInt(b.trim())) ){
|
||||||
|
a = parseInt(a);
|
||||||
|
b = parseInt(b);
|
||||||
|
}
|
||||||
if (a > b) {
|
if (a > b) {
|
||||||
return 1;
|
return 1;
|
||||||
} else if (a < b) {
|
} else if (a < b) {
|
||||||
|
@ -191,6 +191,7 @@
|
|||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
parent.msgbox(data.error, false, 5000);
|
parent.msgbox(data.error, false, 5000);
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
parent.msgbox("Email updated");
|
parent.msgbox("Email updated");
|
||||||
$(btn).html(`<i class="green check icon"></i>`);
|
$(btn).html(`<i class="green check icon"></i>`);
|
||||||
@ -214,14 +215,18 @@
|
|||||||
$("#enableCertAutoRenew").parent().checkbox("set unchecked");
|
$("#enableCertAutoRenew").parent().checkbox("set unchecked");
|
||||||
enableTrigerOnChangeEvent = true;
|
enableTrigerOnChangeEvent = true;
|
||||||
}
|
}
|
||||||
|
if (parent && parent.setACMEEnableStates){
|
||||||
|
parent.setACMEEnableStates(!enabled);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parent && parent.setACMEEnableStates){
|
if (parent && parent.setACMEEnableStates){
|
||||||
parent.setACMEEnableStates(enabled);
|
parent.setACMEEnableStates(enabled);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Notes: This should be open in its original path-->
|
|
||||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
|
||||||
<script src="../script/jquery-3.6.0.min.js"></script>
|
|
||||||
<script src="../script/semantic/semantic.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<br>
|
|
||||||
<div class="ui container">
|
|
||||||
<!-- Path Rules -->
|
|
||||||
<div class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
Special Path Rules
|
|
||||||
<div class="sub header">Advanced customization for response on particular matching path or URL</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h4>Current list of special path rules.</h4>
|
|
||||||
<div style="width: 100%; overflow-x: auto;">
|
|
||||||
<table class="ui sortable unstackable celled table" >
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Matching Path</th>
|
|
||||||
<th>Status Code</th>
|
|
||||||
<th class="no-sort">Exact Match</th>
|
|
||||||
<th class="no-sort">Case Sensitive</th>
|
|
||||||
<th class="no-sort">Enabled</th>
|
|
||||||
<th class="no-sort">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="specialPathRules">
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<h4>Add Special Path Rule</h4>
|
|
||||||
<div class="ui form">
|
|
||||||
<div class="field">
|
|
||||||
<label>Matching URI</label>
|
|
||||||
<input type="text" name="matchingPath" placeholder="Matching URL">
|
|
||||||
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret</small>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
<input type="checkbox" name="exactMatch" tabindex="0" class="hidden">
|
|
||||||
<label>Require Exact Match</label>
|
|
||||||
</div>
|
|
||||||
<div class="ui message">
|
|
||||||
<p>Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of <b>example.com/secret/</b> and set it to return 401</p>
|
|
||||||
<i class="check square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> (content of image.png)<br>
|
|
||||||
<i class="square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> HTTP 401
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Response Status Code</label>
|
|
||||||
<input type="text"name="statusCode" placeholder="500">
|
|
||||||
<small><i class="ui circle info icon"></i> HTTP Status Code to be served by this rule</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br><br>
|
|
||||||
<button class="ui basic button iframeOnly" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -41,8 +41,7 @@
|
|||||||
<input id="inlineEditBasicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
|
<input id="inlineEditBasicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="field" >
|
<div class="field" >
|
||||||
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="blue add icon"></i> Add Credential</button>
|
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="green add icon"></i> Add Credential</button>
|
||||||
<button class="ui basic button" style="float: right;" onclick="saveCredentials();"><i class="green save icon"></i> Save Credential</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,7 +68,7 @@
|
|||||||
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
|
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" >
|
<div class="field" >
|
||||||
<button class="ui basic button" onclick="addExceptionPath();"><i class="blue add icon"></i> Add Exception</button>
|
<button class="ui basic button" onclick="addExceptionPath();"><i class="yellow add icon"></i> Add Exception</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui basic message">
|
<div class="ui basic message">
|
||||||
@ -99,7 +98,7 @@
|
|||||||
let payloadHash = window.location.hash.substr(1);
|
let payloadHash = window.location.hash.substr(1);
|
||||||
try{
|
try{
|
||||||
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||||
loadBasicAuthCredentials(payloadHash.ept, payloadHash.ep);
|
loadBasicAuthCredentials(payloadHash.ep);
|
||||||
$("#epname").text(payloadHash.ep);
|
$("#epname").text(payloadHash.ep);
|
||||||
editingEndpoint = payloadHash;
|
editingEndpoint = payloadHash;
|
||||||
}catch(ex){
|
}catch(ex){
|
||||||
@ -107,13 +106,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBasicAuthCredentials(endpointType, uuid){
|
function loadBasicAuthCredentials(uuid){
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/updateCredentials",
|
url: "/api/proxy/updateCredentials",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
data: {
|
data: {
|
||||||
ep: uuid,
|
ep: uuid,
|
||||||
ptype: endpointType
|
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
//Push the existing account to list
|
//Push the existing account to list
|
||||||
@ -163,6 +161,9 @@
|
|||||||
|
|
||||||
// Update the table body with the credentials
|
// Update the table body with the credentials
|
||||||
updateEditingCredentialList();
|
updateEditingCredentialList();
|
||||||
|
|
||||||
|
//Save the table
|
||||||
|
saveCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addExceptionPath(){
|
function addExceptionPath(){
|
||||||
@ -175,7 +176,6 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/auth/exceptions/add",
|
url: "/api/proxy/auth/exceptions/add",
|
||||||
data:{
|
data:{
|
||||||
ptype: editingEndpoint.ept,
|
|
||||||
ep: editingEndpoint.ep,
|
ep: editingEndpoint.ep,
|
||||||
prefix: newExclusionPathMatchingPrefix
|
prefix: newExclusionPathMatchingPrefix
|
||||||
},
|
},
|
||||||
@ -197,7 +197,6 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/proxy/auth/exceptions/delete",
|
url: "/api/proxy/auth/exceptions/delete",
|
||||||
data:{
|
data:{
|
||||||
ptype: editingEndpoint.ept,
|
|
||||||
ep: editingEndpoint.ep,
|
ep: editingEndpoint.ep,
|
||||||
prefix: matchingPrefix
|
prefix: matchingPrefix
|
||||||
},
|
},
|
||||||
@ -271,6 +270,8 @@
|
|||||||
|
|
||||||
// Update the table body
|
// Update the table body
|
||||||
updateEditingCredentialList();
|
updateEditingCredentialList();
|
||||||
|
|
||||||
|
saveCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
function alreadyExists(username){
|
function alreadyExists(username){
|
||||||
@ -293,7 +294,6 @@
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
ep: editingEndpoint.ep,
|
ep: editingEndpoint.ep,
|
||||||
ptype: editingEndpoint.ept,
|
|
||||||
creds: JSON.stringify(editingCredentials)
|
creds: JSON.stringify(editingCredentials)
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</b></p>
|
</b></p>
|
||||||
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
|
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
|
||||||
<input type="file" name="file" id="fileInput" accept=".zip">
|
<input type="file" name="file" id="fileInput" accept=".zip">
|
||||||
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button>
|
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Restore & Exit</button>
|
||||||
</form>
|
</form>
|
||||||
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
|
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
182
src/web/snippet/customHeaders.html
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Notes: This should be open in its original path-->
|
||||||
|
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
||||||
|
<script src="../script/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="../script/semantic/semantic.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<br>
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui header">
|
||||||
|
<div class="content">
|
||||||
|
Custom Headers
|
||||||
|
<div class="sub header" id="epname"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<p>You can define custom headers to be sent
|
||||||
|
together with the client request to the backend server in
|
||||||
|
this reverse proxy endpoint / host.</p>
|
||||||
|
|
||||||
|
<table class="ui very basic compacted unstackable celled table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="headerTable">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h4>Add Custom Header</h4>
|
||||||
|
<p>Add custom header(s) into this proxy target</p>
|
||||||
|
<div class="scrolling content ui form">
|
||||||
|
<div class="three small fields credentialEntry">
|
||||||
|
<div class="field">
|
||||||
|
<input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="field" >
|
||||||
|
<button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header</button>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="field" >
|
||||||
|
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br><br><br><br>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let editingEndpoint = {};
|
||||||
|
if (window.location.hash.length > 1){
|
||||||
|
let payloadHash = window.location.hash.substr(1);
|
||||||
|
try{
|
||||||
|
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||||
|
$("#epname").text(payloadHash.ep);
|
||||||
|
editingEndpoint = payloadHash;
|
||||||
|
}catch(ex){
|
||||||
|
console.log("Unable to load endpoint data from hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeThisWrapper(){
|
||||||
|
parent.hideSideWrapper(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//$("#debug").text(JSON.stringify(editingEndpoint));
|
||||||
|
|
||||||
|
function addCustomHeader(){
|
||||||
|
let name = $("#headerName").val().trim();
|
||||||
|
let value = $("#headerValue").val().trim();
|
||||||
|
|
||||||
|
if (name == ""){
|
||||||
|
$("#headerName").parent().addClass("error");
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
$("#headerName").parent().removeClass("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == ""){
|
||||||
|
$("#headerValue").parent().addClass("error");
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
$("#headerValue").parent().removeClass("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/header/add",
|
||||||
|
data: {
|
||||||
|
"type": editingEndpoint.ept,
|
||||||
|
"domain": editingEndpoint.ep,
|
||||||
|
"name": name,
|
||||||
|
"value": value
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
if (parent != undefined && parent.msgbox != undefined){
|
||||||
|
parent.msgbox(data.error,false);
|
||||||
|
}else{
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
listCustomHeaders();
|
||||||
|
if (parent != undefined && parent.msgbox != undefined){
|
||||||
|
parent.msgbox("Custom header added",true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clear the form
|
||||||
|
$("#headerName").val("");
|
||||||
|
$("#headerValue").val("");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCustomHeader(name){
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/header/remove",
|
||||||
|
data: {
|
||||||
|
"type": editingEndpoint.ept,
|
||||||
|
"domain": editingEndpoint.ep,
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
listCustomHeaders();
|
||||||
|
if (parent != undefined && parent.msgbox != undefined){
|
||||||
|
parent.msgbox("Custom header removed",true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listCustomHeaders(){
|
||||||
|
$("#headerTable").html(`<tr><td colspan="3"><i class="ui loading spinner icon"></i> Loading</td></tr>`);
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/proxy/header/list",
|
||||||
|
data: {
|
||||||
|
"type": editingEndpoint.ept,
|
||||||
|
"domain": editingEndpoint.ep,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
alert(data.error);
|
||||||
|
}else{
|
||||||
|
|
||||||
|
$("#headerTable").html("");
|
||||||
|
data.forEach(header => {
|
||||||
|
$("#headerTable").append(`
|
||||||
|
<tr>
|
||||||
|
<td>${header.Key}</td>
|
||||||
|
<td>${header.Value}</td>
|
||||||
|
<td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length == 0){
|
||||||
|
$("#headerTable").html(`<tr>
|
||||||
|
<td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
|
||||||
|
</tr>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
listCustomHeaders();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -20,6 +20,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -119,11 +120,10 @@ func UpdateUptimeMonitorTargets() {
|
|||||||
|
|
||||||
// Generate uptime monitor targets from reverse proxy rules
|
// Generate uptime monitor targets from reverse proxy rules
|
||||||
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
|
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
|
||||||
subds := dp.GetSDProxyEndpointsAsMap()
|
hosts := dp.GetProxyEndpointsAsMap()
|
||||||
vdirs := dp.GetVDProxyEndpointsAsMap()
|
|
||||||
|
|
||||||
UptimeTargets := []*uptime.Target{}
|
UptimeTargets := []*uptime.Target{}
|
||||||
for subd, target := range subds {
|
for hostid, target := range hosts {
|
||||||
url := "http://" + target.Domain
|
url := "http://" + target.Domain
|
||||||
protocol := "http"
|
protocol := "http"
|
||||||
if target.RequireTLS {
|
if target.RequireTLS {
|
||||||
@ -131,27 +131,31 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
|
|||||||
protocol = "https"
|
protocol = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Add the root url
|
||||||
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
||||||
ID: subd,
|
ID: hostid,
|
||||||
Name: subd,
|
Name: hostid,
|
||||||
URL: url,
|
URL: url,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
for vdir, target := range vdirs {
|
//Add each virtual directory into the list
|
||||||
url := "http://" + target.Domain
|
for _, vdir := range target.VirtualDirectories {
|
||||||
|
url := "http://" + vdir.Domain
|
||||||
protocol := "http"
|
protocol := "http"
|
||||||
if target.RequireTLS {
|
if target.RequireTLS {
|
||||||
url = "https://" + target.Domain
|
url = "https://" + vdir.Domain
|
||||||
protocol = "https"
|
protocol = "https"
|
||||||
}
|
}
|
||||||
|
//Add the root url
|
||||||
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
||||||
ID: vdir,
|
ID: hostid + vdir.MatchingPath,
|
||||||
Name: "*" + vdir,
|
Name: hostid + vdir.MatchingPath,
|
||||||
URL: url,
|
URL: url,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UptimeTargets
|
return UptimeTargets
|
||||||
@ -167,6 +171,48 @@ func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Static Web Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Handle port change, if root router is using internal static web server
|
||||||
|
// update the root router as well
|
||||||
|
func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
newPort, err := utils.PostInt(r, "port")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid port number given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dynamicProxyRouter.Root.DefaultSiteOption == dynamicproxy.DefaultSite_InternalStaticWebServer {
|
||||||
|
//Update the root site as well
|
||||||
|
newDraftingRoot := dynamicProxyRouter.Root.Clone()
|
||||||
|
newDraftingRoot.Domain = "127.0.0.1:" + strconv.Itoa(newPort)
|
||||||
|
activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to update root routing rule")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Replace the root
|
||||||
|
dynamicProxyRouter.Root = activatedNewRoot
|
||||||
|
|
||||||
|
SaveReverseProxyConfig(newDraftingRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = staticWebServer.ChangePort(strconv.Itoa(newPort))
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
mDNS Scanning
|
||||||
|
*/
|
||||||
|
|
||||||
// Handle listing current registered mdns nodes
|
// Handle listing current registered mdns nodes
|
||||||
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
|
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
|
||||||
if mdnsScanner == nil {
|
if mdnsScanner == nil {
|
||||||
|