Compare commits
85 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 | |||
20cf290d37 | |||
4ca0fcc6d1 | |||
ce4ce72820 | |||
e363d55899 | |||
172479e4fb | |||
156fa5dace | |||
4d40e0aa38 | |||
045e66b631 | |||
62e60d78de | |||
23bdaa1517 | |||
50f222cced | |||
640e1adf96 | |||
d4bb84180c | |||
bda47fc36b | |||
fd6ba56143 | |||
b63a0fc246 | |||
ed92cccf0e | |||
95892802fd | |||
8a5004e828 | |||
c6c523e005 | |||
a692ec818d | |||
c65f780613 | |||
507c2ab468 | |||
1180da8d11 | |||
83f574e3ab | |||
60837f307d | |||
50d5dedabe | |||
f15c774c70 | |||
069f4805f6 | |||
eb98624a6a | |||
6a0c7cf499 | |||
73ab9ca778 | |||
9f9e0750e1 | |||
5664965491 | |||
db4016e79f | |||
f84c4370cf | |||
b39cb6391b | |||
4f7f60188f | |||
dce58343db | |||
415838ad39 | |||
ce0b1a7585 | |||
352995e852 | |||
a3d55a3274 | |||
70adadf129 | |||
d42ac8a146 | |||
f304ff8862 | |||
7d91e02dc9 | |||
dae510ae0a | |||
cd382a78a5 | |||
987de4a7be | |||
52d3b2f8c2 | |||
5038429a70 | |||
2acbf0f3f5 | |||
aed703e260 | |||
5ece7c0da4 |
45
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Image Publisher
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker & GHCR
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
#echo "${{ secrets.GHCR_PASSWORD }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
|
||||
- name: Build the image
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/docker/
|
||||
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg VERSION=${{ github.event.release.tag_name }} \
|
||||
--provenance=false \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||
--tag zoraxydocker/zoraxy:latest \
|
||||
# Since this is still undetermined, I will leave it commented
|
||||
#--tag ghcr.io/zoraxydocker/zoraxy:${{ steps.get_latest_release_tag.outputs.latest_tag }} \
|
||||
#--tag ghcr.io/zoraxydocker/zoraxy:latest \
|
||||
.
|
46
CHANGELOG.md
@ -1,7 +1,43 @@
|
||||
v2.6.5 Jul 19 2023
|
||||
# v2.6.8 Nov 25 2023
|
||||
|
||||
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)
|
||||
+ Optimized subdomain / vdir editing interface
|
||||
+ Added system-wide logger (Work in progress)
|
||||
+ Fixed issue for uptime monitor bug [#77](https://github.com/tobychui/zoraxy/issues/77)
|
||||
+ Changed default static web port to 5487 (prevent already in use)
|
||||
+ Added automatic HTTP/2 to TLS mode
|
||||
+ Bug fix for webserver autostart [67](https://github.com/tobychui/zoraxy/issues/67)
|
||||
|
||||
# v2.6.7 Sep 26 2023
|
||||
|
||||
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
|
||||
+ Web Directory Manager (see static webserver tab)
|
||||
+ Added static web server and black / whitelist template [#38](https://github.com/tobychui/zoraxy/issues/38)
|
||||
+ Added default / preferred CA features for ACME [#47](https://github.com/tobychui/zoraxy/issues/47)
|
||||
+ Optimized TLS/SSL page and added dedicated section for ACME related features
|
||||
+ Bugfixes [#61](https://github.com/tobychui/zoraxy/issues/61) [#58](https://github.com/tobychui/zoraxy/issues/58)
|
||||
|
||||
# v2.6.6 Aug 30 2023
|
||||
|
||||
+ Added basic auth editor custom exception rules
|
||||
+ Fixed redirection bug under another reverse proxy and Apache location headers [#39](https://github.com/tobychui/zoraxy/issues/39)
|
||||
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
|
||||
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
|
||||
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
|
||||
+ Added custom ACME feature in backend, thx [@daluntw](https://github.com/daluntw)
|
||||
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
|
||||
+ Introduce new start parameter `-fastgeoip=true`: see [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
|
||||
|
||||
# v2.6.5.1 Jul 26 2023
|
||||
|
||||
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
|
||||
+ Fixed potential memory leak in ACME handler logic
|
||||
+ Added "Do you want to get a TLS certificate for this subdomain?" dialogue when a new subdomain proxy rule is created
|
||||
|
||||
# v2.6.5 Jul 19 2023
|
||||
|
||||
+ Added Import / Export-Feature
|
||||
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
|
||||
+ Moved configuration files to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
|
||||
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
|
||||
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
|
||||
+ Added Whois
|
||||
@ -11,7 +47,7 @@ v2.6.5 Jul 19 2023
|
||||
+ Added force TLS v1.2 above toggle
|
||||
+ Added trace route
|
||||
+ Added ICMP ping
|
||||
+ Added special routing rules module for up-coming acme integration
|
||||
+ Added special routing rules module for up-coming ACME integration
|
||||
+ Fixed IPv6 check bug in black/whitelist
|
||||
+ Optimized UI for TCP Proxy
|
||||
|
||||
@ -21,7 +57,7 @@ v2.6.5 Jul 19 2023
|
||||
+ Split blacklist and whitelist from geodb script file
|
||||
+ Optimized compile binary size
|
||||
+ Added access control to TCP proxy
|
||||
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7)
|
||||
+ Added "invalid config detect" in up time monitor for issue [#7](https://github.com/tobychui/zoraxy/issues/7)
|
||||
+ Fixed minor bugs in advance stats panel
|
||||
+ Reduced file size of embedded materials
|
||||
|
||||
@ -48,6 +84,6 @@ v2.6.5 Jul 19 2023
|
||||
+ Basic auth
|
||||
+ Support TLS verification skip (for self signed certs)
|
||||
+ Added trend analysis
|
||||
+ Added referer and file type analysis
|
||||
+ Added referrer and file type analysis
|
||||
+ Added cert expire day display
|
||||
+ Moved subdomain proxy logic to dpcore
|
||||
|
125
README.md
@ -2,24 +2,28 @@
|
||||
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
||||
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||
|
||||
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
- Reverse Proxy
|
||||
|
||||
- Subdomain Reverse Proxy
|
||||
|
||||
- Virtual Directory Reverse Proxy
|
||||
- Virtual Directory
|
||||
- Basic Auth
|
||||
- Custom Headers
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (one certificate contains multiple host names)
|
||||
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- TCP Tunneling / Proxy
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Utilities
|
||||
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- IP Scanner
|
||||
@ -29,9 +33,9 @@ General purpose request (reverse) proxy and forwarding tool for low power device
|
||||
- SMTP config for password reset
|
||||
|
||||
## Build from Source
|
||||
Require Go 1.20 or above
|
||||
Requires Go 1.20 or higher
|
||||
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
cd ./zoraxy/src/
|
||||
go mod tidy
|
||||
@ -42,11 +46,11 @@ sudo ./zoraxy -port=:8000
|
||||
|
||||
## Usage
|
||||
|
||||
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
|
||||
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start growing their web services into multiple servers.
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
|
||||
|
||||
#### Linux
|
||||
|
||||
@ -60,18 +64,49 @@ Download the binary executable and double click the binary file to start it.
|
||||
|
||||
#### Raspberry Pi
|
||||
|
||||
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
The installation method is same as Linux. If you are using a Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
|
||||
#### Other ARM SBCs or Android phone with Termux
|
||||
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||
|
||||
### Start Paramters
|
||||
```
|
||||
Usage of zoraxy:
|
||||
-autorenew int
|
||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-info
|
||||
Show information about this program in JSON
|
||||
-log
|
||||
Log terminal output to file (default true)
|
||||
-mdns
|
||||
Enable mDNS scanner and transponder (default true)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-sshlb
|
||||
Allow loopback web ssh connection (DANGER)
|
||||
-version
|
||||
Show version of this server
|
||||
-webfm
|
||||
Enable web file manager for static web server root folder (default true)
|
||||
-webroot string
|
||||
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
||||
-ztauth string
|
||||
ZeroTier authtoken for the local node
|
||||
-ztport int
|
||||
ZeroTier controller API port (default 9993)
|
||||
```
|
||||
|
||||
### External Permission Management Mode
|
||||
|
||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag
|
||||
If you already have an upstream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag:
|
||||
|
||||
```bash
|
||||
./zoraxy -noauth=true
|
||||
@ -79,45 +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.*
|
||||
|
||||
#### Use with ArozOS
|
||||
|
||||
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
|
||||
|
||||
```bash
|
||||
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
|
||||
cd ~/arozos/subservices
|
||||
mkdir zoraxy
|
||||
cd ./zoraxy
|
||||
|
||||
# Download the release binary from Github release
|
||||
wget {binary executable link from release page}
|
||||
|
||||
# Set permission. Change this if required
|
||||
sudo chmod 775 -R ./
|
||||
|
||||
# Start zoraxy to see if the downloaded arch is correct.
|
||||
./zoraxy
|
||||
|
||||
# After the unzip done, press Ctrl + C to kill it
|
||||
# Rename it to valid arozos subservice binary format
|
||||
mv ./zoraxy zoraxy_linux_amd64
|
||||
|
||||
# If you are using SBCs with different CPU arch, use the following names
|
||||
# mv ./zoraxy zoraxy_linux_arm
|
||||
# mv ./zoraxy zoraxy_linux_arm64
|
||||
|
||||
# Restart arozos
|
||||
sudo systemctl restart arozos
|
||||
```
|
||||
|
||||
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||
|
||||
## FAQ
|
||||
@ -128,22 +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.
|
||||
|
||||
Assuming you already have a valid license, to use Zoraxy with ZeroTier, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken at correct location in your host.
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
|
||||
|
||||
```
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
```
|
||||
|
||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||
|
||||
This allows you to have infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
|
||||
## Web.SSH
|
||||
|
||||
Web SSH currently only support Linux based OS. The following platforms are supported
|
||||
## Web SSH
|
||||
|
||||
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
@ -151,13 +152,19 @@ Web SSH currently only support Linux based OS. The following platforms are suppo
|
||||
|
||||
### Loopback Connection
|
||||
|
||||
Loopback web ssh connection, by default, is disabled. This means that if you are trying to connect to address like 127.0.0.1 or localhost, the system will reject your connection due to security issues. To enable loopback for testing or development purpose, use the following flags to override the loopback checking.
|
||||
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||
|
||||
```
|
||||
```bash
|
||||
./zoraxy -sshlb=true
|
||||
```
|
||||
|
||||
## Sponsor This Project
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is open source under AGPL. I open source this project so everyone can check for security issues and benefit all users. **If your plans to use this project in commercial environment which violate the AGPL terms, please contact toby@imuslab.com for an alternative commercial license.**
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative license.**
|
||||
|
||||
|
@ -1,19 +1,38 @@
|
||||
FROM alpine:latest
|
||||
FROM docker.io/golang:alpine
|
||||
# VERSION comes from the main.yml workflow --build-arg
|
||||
ARG VERSION
|
||||
|
||||
RUN apk update && apk upgrade &&\
|
||||
apk add bash curl jq sudo &&\
|
||||
mkdir -p /zoraxy/data/
|
||||
RUN apk add --no-cache bash netcat-openbsd sudo
|
||||
|
||||
VOLUME [ "/zoraxy/data/" ]
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
COPY entrypoint.sh /zoraxy/
|
||||
COPY entrypoint.sh /opt/zoraxy/
|
||||
|
||||
RUN chmod +x /zoraxy/entrypoint.sh
|
||||
RUN chmod -R 755 /opt/zoraxy/ &&\
|
||||
chmod +x /opt/zoraxy/entrypoint.sh
|
||||
|
||||
ENV ARGS="-port=:8000 -noauth=false"
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 8000
|
||||
# If you build it yourself, you will need to add the src directory into the docker directory.
|
||||
COPY ./src/ /opt/zoraxy/source/
|
||||
|
||||
WORKDIR /opt/zoraxy/source/
|
||||
|
||||
RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
|
||||
RUN chmod +x /usr/local/bin/zoraxy
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
ENV ARGS="-noauth=false"
|
||||
|
||||
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
|
||||
|
||||
ENTRYPOINT ["/zoraxy/entrypoint.sh"]
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 PassiveLemon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,7 +1,9 @@
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
|
||||
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
|
||||
## Setup: </br>
|
||||
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
|
||||
@ -10,24 +12,24 @@ You may also need to portforward your 80/443 to allow http and https traffic. If
|
||||
|
||||
### Using Docker run </br>
|
||||
```
|
||||
docker run -d --name (container name) -p (ports) -v (path to storage directory):/zoraxy/data/ -e ARGS=(your arguments) -e VERSION=(version) passivelemon/zoraxy-docker:latest
|
||||
docker run -d --name (container name) -p (ports) -v (path to storage directory):/opt/zoraxy/data/ -e ARGS='(your arguments)' zoraxydocker/zoraxy:latest
|
||||
```
|
||||
|
||||
### Using Docker Compose </br>
|
||||
```
|
||||
```yml
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: passivelemon/zoraxy-docker:latest
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: (container name)
|
||||
ports:
|
||||
- 80:80 # Http port
|
||||
- 443:443 # Https port
|
||||
- (external):8000 # Management portal port
|
||||
- 80:80
|
||||
- 443:443
|
||||
- (external):8000
|
||||
volumes:
|
||||
- (path to storage directory):/zoraxy/data/ # Host directory for Zoraxy file storage
|
||||
- (path to storage directory):/opt/zoraxy/config/
|
||||
environment:
|
||||
ARGS: '(your arguments)' # The arguments to run with Zoraxy. Enter them as they would be entered normally.
|
||||
ARGS: '(your arguments)'
|
||||
```
|
||||
|
||||
| Operator | Need | Details |
|
||||
@ -35,33 +37,29 @@ services:
|
||||
| `-d` | Yes | will run the container in the background. |
|
||||
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
|
||||
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
||||
| `-v (path to storage directory):/zoraxy/data/` | 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 `-port=:8000 -noauth=false` |
|
||||
| `-e VERSION=(version)` | No | Sets the version of Zoraxy that the container will download. Must be a supported version found on the Zoraxy Github. Defaults to the latest if not set. |
|
||||
| `passivelemon/zoraxy-docker:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
|
||||
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
|
||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
|
||||
|
||||
## Examples: </br>
|
||||
### Docker Run </br>
|
||||
```
|
||||
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/zoraxy/data/ -e ARGS="-port=:8000 -noauth=false" passivelemon/zoraxy-docker:latest
|
||||
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -e ARGS='-noauth=false' zoraxydocker/zoraxy:latest
|
||||
```
|
||||
|
||||
### Docker Compose </br>
|
||||
```
|
||||
```yml
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: passivelemon/zoraxy-docker:latest
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8005:8000/tcp
|
||||
volumes:
|
||||
- /home/docker/Containers/Zoraxy:/zoraxy/data/
|
||||
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/
|
||||
environment:
|
||||
ARGS: '-port=:8000 -noauth=false'
|
||||
ARGS: '-noauth=false'
|
||||
```
|
||||
|
||||
### Other </br>
|
||||
If the container doesn't start properly, you might be rate limited from GitHub for some amount of time. You can check this by running `curl -s https://api.github.com/repos/tobychui/zoraxy/releases` on the host. If you are, you will just have to wait for a little while or use a VPN. </br>
|
||||
|
@ -1,20 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Zoraxy version $VERSION"
|
||||
|
||||
cd /zoraxy/data/
|
||||
|
||||
if [ "$VERSION" != "" ]; then
|
||||
echo "|| Using release ${VERSION} ||"
|
||||
release=${VERSION}
|
||||
else
|
||||
echo "|| Using latest release ||"
|
||||
# Gets the latest pre-release version tag. Will be updated when official release comes out.
|
||||
release=$(curl -s https://api.github.com/repos/tobychui/zoraxy/releases | jq -r 'map(select(.prerelease)) | .[0].tag_name')
|
||||
fi
|
||||
|
||||
if [ ! -e "/zoraxy/data/zoraxy_linux_amd64-${release}" ]; then
|
||||
echo "|| Downloading version ${release} ||"
|
||||
curl -Lso /zoraxy/data/zoraxy_linux_amd64-${release} https://github.com/tobychui/zoraxy/releases/download/${release}/zoraxy_linux_amd64
|
||||
chmod u+x /zoraxy/data/zoraxy_linux_amd64-${release}
|
||||
fi
|
||||
|
||||
./zoraxy_linux_amd64-${release} ${ARGS}
|
||||
zoraxy -port=:8000 ${ARGS}
|
||||
|
@ -148,7 +148,7 @@
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use, noobs friendly reverse proxy server that can be easily set-up using a web form and a few toggle switches.</p>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -158,7 +158,7 @@
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most of the simple use cases.</p>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -168,7 +168,7 @@
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allow easy setup for regional services.</p>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -189,7 +189,7 @@
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integrated with Gotty Web SSH terminal, allow one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -199,7 +199,7 @@
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real time analytic tools, provide you the best insights of visitors data without cookies.</p>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -209,7 +209,7 @@
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovering service, enable automatic service discovery within LAN.</p>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
@ -219,7 +219,7 @@
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
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 |
@ -19,7 +19,8 @@ clean:
|
||||
|
||||
$(PLATFORMS):
|
||||
@echo "Building $(os)/$(arch)"
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
# GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
fixwindows:
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
strip "github.com/grokify/html-strip-tags-go"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
resulst := []*geodb.WhitelistEntry{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode)
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
comment = strip.StripTags(comment)
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
|
42
src/acme.go
@ -1,9 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@ -28,7 +28,7 @@ func getRandomPort(minPort int) int {
|
||||
|
||||
// init the new ACME instance
|
||||
func initACME() *acme.ACMEHandler {
|
||||
log.Println("Starting ACME handler")
|
||||
SystemWideLogger.Println("Starting ACME handler")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// Generate a random port above 30000
|
||||
port := getRandomPort(30000)
|
||||
@ -38,12 +38,12 @@ func initACME() *acme.ACMEHandler {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
func acmeRegisterSpecialRoutingRule() {
|
||||
log.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||
|
||||
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||
ID: "acme-autorenew",
|
||||
@ -65,7 +65,8 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
return
|
||||
}
|
||||
|
||||
resBody, err := ioutil.ReadAll(res.Body)
|
||||
resBody, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("error reading: %s\n", err)
|
||||
return
|
||||
@ -77,7 +78,7 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("[Err] " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("ACME", "Unable register temp port for DNS resolver", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +88,7 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
log.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
@ -102,14 +103,37 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
log.Println("Restoring HTTP to HTTPS redirect settings")
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
|
||||
func HandleACMEPreferredCA(w http.ResponseWriter, r *http.Request) {
|
||||
ca, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//Return the current ca to user
|
||||
prefCA := "Let's Encrypt"
|
||||
sysdb.Read("acmepref", "prefca", &prefCA)
|
||||
js, _ := json.Marshal(prefCA)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Check if the CA is supported
|
||||
acme.IsSupportedCA(ca)
|
||||
//Set the new config
|
||||
sysdb.Write("acmepref", "prefca", ca)
|
||||
SystemWideLogger.Println("Updating prefered ACME CA to " + ca)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
45
src/api.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
@ -53,7 +54,22 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||
//Reverse proxy virtual directory APIs
|
||||
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||
//Reverse proxy user define header apis
|
||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||
//Reverse proxy auth related APIs
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
|
||||
|
||||
//TLS / SSL config
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
@ -106,6 +122,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
||||
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
@ -154,6 +172,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
@ -161,10 +180,32 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Static Web Server
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||
if *allowWebFileManager {
|
||||
//Web Directory Manager file operation functions
|
||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
|
||||
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
|
||||
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
|
||||
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
|
||||
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
|
||||
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
|
||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
||||
}
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
http.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
http.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
}
|
||||
|
18
src/cert.go
@ -6,7 +6,6 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -52,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
@ -128,7 +127,7 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
log.Println("Unable to load certificate: " + certFilepath)
|
||||
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
@ -182,11 +181,11 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "usetls", true)
|
||||
log.Println("Enabling TLS mode on reverse proxy")
|
||||
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "usetls", false)
|
||||
log.Println("Disabling TLS mode on reverse proxy")
|
||||
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
|
||||
@ -213,11 +212,11 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "forceLatestTLS", true)
|
||||
log.Println("Updating minimum TLS version to v1.2 or above")
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "forceLatestTLS", false)
|
||||
log.Println("Updating minimum TLS version to v1.0 or above")
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given")
|
||||
@ -249,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
@ -288,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
180
src/config.go
@ -3,10 +3,9 @@ package main
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -26,68 +25,129 @@ import (
|
||||
*/
|
||||
|
||||
type Record struct {
|
||||
ProxyType string
|
||||
Rootname string
|
||||
ProxyTarget string
|
||||
UseTLS bool
|
||||
SkipTlsValidation bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
|
||||
ProxyType string
|
||||
Rootname string
|
||||
ProxyTarget string
|
||||
UseTLS bool
|
||||
BypassGlobalTLS bool
|
||||
SkipTlsValidation bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
|
||||
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//Generate record
|
||||
thisRecord := proxyConfigRecord
|
||||
|
||||
//Write to file
|
||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
||||
return ioutil.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
log.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return err
|
||||
}
|
||||
/*
|
||||
Load Reverse Proxy Config from file and append it to current runtime proxy router
|
||||
*/
|
||||
func LoadReverseProxyConfig(configFilepath string) error {
|
||||
//Load the config file from disk
|
||||
endpointConfig, err := os.ReadFile(configFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//File already gone
|
||||
//Parse it into dynamic proxy endpoint
|
||||
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Matching domain not set. Assume root
|
||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||
}
|
||||
|
||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
//This is a root config file
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||
|
||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
|
||||
//This is a host config file
|
||||
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
||||
} else {
|
||||
return errors.New("not supported proxy type")
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{}
|
||||
configContent, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
}
|
||||
|
||||
//Unmarshal the content into config
|
||||
err = json.Unmarshal(configContent, &thisRecord)
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
}
|
||||
|
||||
//Return it
|
||||
return &thisRecord, nil
|
||||
func filterProxyConfigFilename(filename string) string {
|
||||
//Filter out wildcard characters
|
||||
filename = strings.ReplaceAll(filename, "*", "(ST)")
|
||||
filename = strings.ReplaceAll(filename, "?", "(QM)")
|
||||
filename = strings.ReplaceAll(filename, "[", "(OB)")
|
||||
filename = strings.ReplaceAll(filename, "]", "(CB)")
|
||||
filename = strings.ReplaceAll(filename, "#", "(HT)")
|
||||
return filepath.ToSlash(filename)
|
||||
}
|
||||
|
||||
func getFilenameFromRootName(rootname string) string {
|
||||
//Generate a filename for this rootname
|
||||
filename := strings.ReplaceAll(rootname, ".", "_")
|
||||
filename = strings.ReplaceAll(filename, "/", "-")
|
||||
filename = filename + ".config"
|
||||
return filename
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//Get filename for saving
|
||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
//Save config to file
|
||||
js, err := json.MarshalIndent(endpoint, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(endpoint string) error {
|
||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
||||
if endpoint == "/" {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
if !utils.FileExists(filename) {
|
||||
return errors.New("target endpoint not exists")
|
||||
}
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// Get the default root config that point to the internal static web server
|
||||
// this will be used if root config is not found (new deployment / missing root.config file)
|
||||
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
//Default settings
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
|
||||
ProxyType: dynamicproxy.ProxyType_Root,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
|
||||
RequireTLS: false,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: false,
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||
DefaultSiteValue: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rootProxyEndpoint, nil
|
||||
}
|
||||
|
||||
/*
|
||||
@ -155,14 +215,14 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
//Also zip in the sysdb
|
||||
zipFile, err := zipWriter.Create("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to zip sysdb: " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("Backup", "Unable to zip sysdb", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to open sysdb: " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@ -170,7 +230,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
SystemWideLogger.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -275,12 +335,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Send a success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Println("Configuration restored")
|
||||
SystemWideLogger.Println("Configuration restored")
|
||||
fmt.Fprintln(w, "Configuration restored")
|
||||
|
||||
if restoreDatabase {
|
||||
go func() {
|
||||
log.Println("Database altered. Restarting in 3 seconds...")
|
||||
SystemWideLogger.Println("Database altered. Restarting in 3 seconds...")
|
||||
time.Sleep(3 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -180,7 +180,7 @@ func setSMTPAdminAddress(adminAddr string) error {
|
||||
return sysdb.Write("smtp", "admin", adminAddr)
|
||||
}
|
||||
|
||||
//Load SMTP admin address. Return empty string if not set
|
||||
// Load SMTP admin address. Return empty string if not set
|
||||
func loadSMTPAdminAddr() string {
|
||||
adminAddr := ""
|
||||
if sysdb.KeyExists("smtp", "admin") {
|
||||
@ -223,7 +223,7 @@ func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
passwordResetAccessToken = uuid.NewV4().String()
|
||||
passwordResetAccessToken = uuid.New().String()
|
||||
|
||||
//SMTP info exists. Send reset account email
|
||||
lastAccountResetEmail = time.Now().Unix()
|
||||
|
39
src/geoip.go
@ -1,39 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
func getCountryCodeFromRequest(r *http.Request) string {
|
||||
countryCode := ""
|
||||
|
||||
// Get the IP address of the user from the request headers
|
||||
ipAddress := r.Header.Get("X-Forwarded-For")
|
||||
if ipAddress == "" {
|
||||
ipAddress = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
|
||||
// Open the GeoIP database
|
||||
db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb")
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Look up the country code for the IP address
|
||||
record, err := db.Country(net.ParseIP(ipAddress))
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
|
||||
// Get the ISO country code from the record
|
||||
countryCode = record.Country.IsoCode
|
||||
|
||||
return countryCode
|
||||
}
|
19
src/go.mod
@ -4,16 +4,17 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/go-acme/lego/v4 v4.12.1 // indirect
|
||||
github.com/go-acme/lego/v4 v4.14.0
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.24
|
||||
github.com/oschwald/geoip2-golang v1.8.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/net v0.11.0
|
||||
golang.org/x/sys v0.9.0
|
||||
github.com/grokify/html-strip-tags-go v0.1.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/text v0.12.0
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
)
|
||||
|
289
src/go.sum
@ -123,7 +123,6 @@ cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOV
|
||||
cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
|
||||
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
|
||||
@ -392,34 +391,59 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS
|
||||
cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M=
|
||||
cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8=
|
||||
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
@ -427,7 +451,26 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27/go.mod h1:ZdjYvJpDlefgh8/hWelJhqgqJeodxu4SmbVsSdBlL7E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30/go.mod h1:qQtIBl5OVMfmeQkz8HaVyh5DzFmmFXyvK27UgIgOr4c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4/go.mod h1:JniVpqvw90sVjNqanGLufrVapWySL28fhBlYgl96Q/w=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0/go.mod h1:PwyKKVL0cNkC37QwLcrhyeCrAk+5bY8O2ou7USyAS2A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@ -438,12 +481,14 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
|
||||
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||
@ -455,7 +500,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cloudflare-go v0.49.0/go.mod h1:h0QgcIZ3qEXwFiwfBO8sQxjVdYsLX+PfD7NFEnANaKg=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/cloudflare-go v0.70.0/go.mod h1:VW6GuazkaZ4xEDkFt24lkXQUsE8q7BiGqDniC2s8WEM=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@ -487,9 +534,11 @@ github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+t
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dnsimple/dnsimple-go v0.71.1/go.mod h1:F9WHww9cC76hrnwGFfAfrqdW99j3MOYasQcIwTS/aUk=
|
||||
github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -503,7 +552,7 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/exoscale/egoscale v0.90.0/go.mod h1:wyXE5zrnFynMXA0jMhwQqSe24CfUhmBk2WI5wFZcq6Y=
|
||||
github.com/exoscale/egoscale v0.100.1/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
@ -511,22 +560,27 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-acme/lego/v4 v4.12.1 h1:Cy3FS7wADLNBqCLpz2wdfdNrThW9rZy8RCAfnUrL2uE=
|
||||
github.com/go-acme/lego/v4 v4.12.1/go.mod h1:UZoOlhVmUYP/N0z4tEbfUjoCNHRZNObzqWZtT76DIsc=
|
||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
||||
github.com/go-acme/lego/v4 v4.14.0 h1:/skZoRHgVh0d2RK7l1g3Ch8HqeqP9LB8ZEjLdGEpcDE=
|
||||
github.com/go-acme/lego/v4 v4.14.0/go.mod h1:zjmvNCDLGz7GrC1OqdVpVmZFKSRabEDtWbdzmcpBsGo=
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
|
||||
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
|
||||
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@ -540,14 +594,18 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
|
||||
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
@ -556,10 +614,14 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -611,6 +673,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
@ -640,8 +703,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
@ -671,10 +735,13 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
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/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-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
@ -695,38 +762,44 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI=
|
||||
github.com/hashicorp/hc-install v0.5.0/go.mod h1:JyzMfbzfSBSjoDCRPna1vi/24BEDxFaCPfdHtM5SCdo=
|
||||
github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/terraform-exec v0.17.2/go.mod h1:tuIbsL2l4MlwwIZx9HPM+LOV9vVyEfBYu2GsO1uH3/8=
|
||||
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
|
||||
github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980=
|
||||
github.com/hashicorp/terraform-json v0.15.0/go.mod h1:+L1RNzjDU5leLFZkHTFTbJXaoqUC6TqXlFgDoOXrtvk=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
@ -744,9 +817,10 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@ -757,12 +831,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
||||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A=
|
||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||
@ -770,11 +847,12 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=
|
||||
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
|
||||
github.com/likexian/whois v1.15.0 h1:AYYJ5bNUo8Qy2T1Z5GgMp1oIcIlCcTDfg1buYz6TdAE=
|
||||
github.com/likexian/whois v1.15.0/go.mod h1:456fUTkh+O8F8v09bGdVl7XxBjRaQ4LvYHyVWX5Bxyg=
|
||||
github.com/likexian/whois-parser v1.24.8/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
|
||||
github.com/linode/linodego v1.9.1/go.mod h1:h6AuFR/JpqwwM/vkj7s8KV3iGN8/jxn+zc437F8SZ8w=
|
||||
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
|
||||
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
|
||||
github.com/likexian/whois-parser v1.24.9/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
|
||||
github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||
@ -799,22 +877,24 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
|
||||
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/mimuret/golang-iij-dpf v0.7.1/go.mod h1:IXWYcQVIHYzuM+W7kDWX0mseHDfUoqMuarxMXHVTir0=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@ -825,27 +905,32 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
|
||||
github.com/nrdcg/desec v0.6.0/go.mod h1:wybWg5cRrNmtXLYpUCPCLvz4jfFNEGZQEnoUiX9WqcY=
|
||||
github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY=
|
||||
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
|
||||
github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4=
|
||||
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
|
||||
github.com/nrdcg/goinwx v0.8.2/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
|
||||
github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA=
|
||||
github.com/nrdcg/porkbun v0.2.0/go.mod h1:i0uLMn9ItFsLsSQIAeEu1wQ9/+6EvX1eQw15hulMMRw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -859,15 +944,14 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
|
||||
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
|
||||
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
|
||||
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/ovh/go-ovh v1.4.1/go.mod h1:6bL6pPyUT7tBfI0pqOegJgRjgjuO+mOo+MyXd1EEC0M=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -879,7 +963,7 @@ github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@ -907,30 +991,33 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sacloud/api-client-go v0.2.1/go.mod h1:8fmYy5OpT3W8ltV5ZxF8evultNwKpduGN4YKmU9Af7w=
|
||||
github.com/sacloud/go-http v0.1.2/go.mod h1:gvWaT8LFBFnSBFVrznOQXC62uad46bHZQM8w+xoH3eE=
|
||||
github.com/sacloud/iaas-api-go v1.3.2/go.mod h1:CoqpRYBG2NRB5xfqTfZNyh2lVLKyLkE/HV9ISqmbhGc=
|
||||
github.com/sacloud/packages-go v0.0.5/go.mod h1:XWMBSNHT9YKY3lCh6yJsx1o1RRQQGpuhNqJA6bSHdD4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
|
||||
github.com/sacloud/api-client-go v0.2.8/go.mod h1:0CV/kWNYlS1hCNdnk6Wx7Wdg8DPFCnv0zOIzdXjeAeY=
|
||||
github.com/sacloud/go-http v0.1.6/go.mod h1:oLAHoDJRkptf8sq4fE8oERLkdCh0kJWfWu+paoJY7I0=
|
||||
github.com/sacloud/iaas-api-go v1.11.1/go.mod h1:uBDSa06F/V0OnoR66jGdbH0PVnCJw+NeE9RVbVgMfss=
|
||||
github.com/sacloud/packages-go v0.0.8/go.mod h1:btPji+wtZ+Pk7MeCy+zo61o5IziBoLdHIrdGiYq9Kb8=
|
||||
github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIWIk1OA7x9j2Q=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/softlayer/softlayer-go v1.0.6/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qyx2MNvYyHof7ik4=
|
||||
github.com/softlayer/softlayer-go v1.1.2/go.mod h1:hvAbzGH4LRXA6yXY8BNx99yoqZ7urfDdtl9mvBf0G+g=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@ -950,7 +1037,6 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@ -960,31 +1046,35 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/transip/gotransip/v6 v6.17.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
||||
github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
|
||||
github.com/ultradns/ultradns-go-sdk v1.4.0-20221107152238-f3f1d1d/go.mod h1:IgdoVzrGYzq4H4IGI0DAVnM3CbcuQDSxEP4s/j6cztI=
|
||||
github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||
github.com/urfave/cli/v2 v2.14.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c/go.mod h1:F4UyVEmq4/m5lAmx+GccrxyRCXmnBjzUL09JLTQFp94=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
@ -1000,6 +1090,7 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@ -1017,6 +1108,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -1026,26 +1118,36 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -1083,9 +1185,13 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1121,6 +1227,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
@ -1136,6 +1243,7 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@ -1146,6 +1254,7 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
@ -1156,10 +1265,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1185,10 +1295,9 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -1203,8 +1312,9 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1257,6 +1367,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -1271,6 +1382,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1278,6 +1390,7 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1293,23 +1406,27 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
@ -1318,6 +1435,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1333,16 +1452,16 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -1408,9 +1527,12 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1653,7 +1775,6 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -1664,11 +1785,11 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.6.5/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
@ -1682,6 +1803,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@ -1692,6 +1814,7 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
|
||||
|
55
src/main.go
@ -13,13 +13,13 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -30,18 +30,26 @@ import (
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
// General flags
|
||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
|
||||
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
|
||||
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.5"
|
||||
version = "3.0.0"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -55,7 +63,6 @@ var (
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
@ -71,10 +78,12 @@ var (
|
||||
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
@ -96,9 +105,12 @@ func ShutdownSeq() {
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
fmt.Println("- Stopping mDNS Discoverer")
|
||||
//Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
if mdnsTickerStop != nil {
|
||||
fmt.Println("- Stopping mDNS Discoverer (might take a few minutes)")
|
||||
// Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
}
|
||||
|
||||
mdnsScanner.Close()
|
||||
fmt.Println("- Closing Certificates Auto Renewer")
|
||||
acmeAutoRenewer.Close()
|
||||
@ -106,26 +118,17 @@ func ShutdownSeq() {
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
fmt.Println("- Closing system wide logger")
|
||||
SystemWideLogger.Close()
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
Name: name,
|
||||
Desc: "Dynamic Reverse Proxy Server",
|
||||
Group: "Network",
|
||||
IconPath: "zoraxy/img/small_icon.png",
|
||||
Version: version,
|
||||
StartDir: "zoraxy/index.html",
|
||||
SupportFW: true,
|
||||
LaunchFWDir: "zoraxy/index.html",
|
||||
SupportEmb: false,
|
||||
InitFWSize: []int{1080, 580},
|
||||
})
|
||||
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
@ -141,7 +144,7 @@ func main() {
|
||||
}
|
||||
uuidBytes, err := os.ReadFile(uuidRecord)
|
||||
if err != nil {
|
||||
log.Println("Unable to read system uuid from file system")
|
||||
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
|
||||
panic(err)
|
||||
}
|
||||
nodeUUID = string(uuidBytes)
|
||||
@ -150,7 +153,7 @@ func main() {
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
@ -163,8 +166,8 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -5,12 +5,11 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -28,6 +27,12 @@ import (
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
}
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
type ACMEUser struct {
|
||||
Email string
|
||||
@ -65,7 +70,7 @@ func NewACME(acmeServer string, port string) *ACMEHandler {
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for the specified domains.
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) {
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) {
|
||||
log.Println("[ACME] Obtaining certificate...")
|
||||
|
||||
// generate private key
|
||||
@ -84,17 +89,41 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
// create config
|
||||
config := lego.NewConfig(&adminUser)
|
||||
|
||||
// setup who is the issuer and the key type
|
||||
config.CADirURL = a.DefaultAcmeServer
|
||||
// skip TLS verify if need
|
||||
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
|
||||
if skipTLS {
|
||||
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server")
|
||||
config.HTTPClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//Overwrite the CADir URL if set
|
||||
if ca != "" {
|
||||
caLinkOverwrite, err := loadCAApiServerFromName(ca)
|
||||
// setup the custom ACME url endpoint.
|
||||
if caUrl != "" {
|
||||
config.CADirURL = caUrl
|
||||
}
|
||||
|
||||
// if not custom ACME url, load it from ca.json
|
||||
if caName == "custom" {
|
||||
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
|
||||
} else {
|
||||
caLinkOverwrite, err := loadCAApiServerFromName(caName)
|
||||
if err == nil {
|
||||
config.CADirURL = caLinkOverwrite
|
||||
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
|
||||
} else {
|
||||
return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.")
|
||||
// (caName == "" || caUrl == "") will use default acme
|
||||
config.CADirURL = a.DefaultAcmeServer
|
||||
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,12 +163,31 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL.
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Save certificate's ACME info for renew usage
|
||||
certInfo := &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
@ -250,14 +298,39 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
var caUrl string
|
||||
|
||||
ca, err := utils.PostPara(r, "ca")
|
||||
if err != nil {
|
||||
log.Println("CA not set. Using default (Let's Encrypt)")
|
||||
log.Println("[INFO] CA not set. Using default")
|
||||
ca, caUrl = "", ""
|
||||
}
|
||||
|
||||
if ca == "custom" {
|
||||
caUrl, err = utils.PostPara(r, "caURL")
|
||||
if err != nil {
|
||||
log.Println("[INFO] Custom CA set but no URL provide, Using default")
|
||||
ca, caUrl = "", ""
|
||||
}
|
||||
}
|
||||
|
||||
if ca == "" {
|
||||
//default. Use Let's Encrypt
|
||||
ca = "Let's Encrypt"
|
||||
}
|
||||
|
||||
var skipTLS bool
|
||||
|
||||
if skipTLSString, err := utils.PostPara(r, "skipTLS"); err != nil {
|
||||
skipTLS = false
|
||||
} else if skipTLSString != "true" {
|
||||
skipTLS = false
|
||||
} else {
|
||||
skipTLS = true
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
result, err := a.ObtainCert(domains, filename, email, ca)
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
@ -285,4 +358,20 @@ func IsPortInUse(port int) bool {
|
||||
}
|
||||
defer listener.Close()
|
||||
return false // Port is not in use
|
||||
|
||||
}
|
||||
|
||||
// Load cert information from json file
|
||||
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
certInfoBytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certInfo := &CertificateInfoJSON{}
|
||||
if err = json.Unmarshal(certInfoBytes, certInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certInfo, nil
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package acme
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
@ -39,7 +40,6 @@ type AutoRenewer struct {
|
||||
type ExpiredCerts struct {
|
||||
Domains []string
|
||||
Filepath string
|
||||
CA string
|
||||
}
|
||||
|
||||
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
|
||||
@ -279,12 +279,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
CAName, err := ExtractIssuerName(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Unable to extract issuer name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
@ -295,7 +289,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
|
||||
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
||||
Filepath: filepath.Join(certFolder, file.Name()),
|
||||
CA: CAName,
|
||||
Domains: DNSName,
|
||||
})
|
||||
}
|
||||
@ -314,12 +307,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
CAName, err := ExtractIssuerName(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Unable to extract issuer name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
@ -330,7 +317,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
|
||||
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
||||
Filepath: filepath.Join(certFolder, file.Name()),
|
||||
CA: CAName,
|
||||
Domains: DNSName,
|
||||
})
|
||||
}
|
||||
@ -355,7 +341,22 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
||||
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
|
||||
fileName := filepath.Base(expiredCert.Filepath)
|
||||
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
||||
_, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA)
|
||||
|
||||
// Load certificate info for ACME detail
|
||||
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
|
||||
certInfo, err := loadCertInfoJSON(certInfoFilename)
|
||||
if err != nil {
|
||||
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
|
||||
|
||||
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
|
||||
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
|
||||
certInfo = &CertificateInfoJSON{}
|
||||
} else {
|
||||
certInfo = &CertificateInfoJSON{AcmeName: CAName}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
|
||||
if err != nil {
|
||||
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
||||
} else {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CA Defination, load from embeded json when startup
|
||||
@ -32,14 +33,24 @@ func init() {
|
||||
}
|
||||
|
||||
caDef = runtimeCaDef
|
||||
|
||||
}
|
||||
|
||||
// Get the CA ACME server endpoint and error if not found
|
||||
func loadCAApiServerFromName(caName string) (string, error) {
|
||||
// handle BuyPass cert org section (Buypass AS-983163327)
|
||||
if strings.HasPrefix(caName, "Buypass AS") {
|
||||
caName = "Buypass"
|
||||
}
|
||||
|
||||
val, ok := caDef.Production[caName]
|
||||
if !ok {
|
||||
return "", errors.New("This CA is not supported")
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func IsSupportedCA(caName string) bool {
|
||||
_, err := loadCAApiServerFromName(caName)
|
||||
return err == nil
|
||||
}
|
||||
|
@ -53,6 +53,11 @@ func ExtractIssuerName(certBytes []byte) (string, error) {
|
||||
return "", fmt.Errorf("failed to parse certificate: %v", err)
|
||||
}
|
||||
|
||||
// Check if exist incase some acme server didn't have org section
|
||||
if len(cert.Issuer.Organization) == 0 {
|
||||
return "", fmt.Errorf("cert didn't have org section exist")
|
||||
}
|
||||
|
||||
// Extract the issuer name
|
||||
issuer := cert.Issuer.Organization[0]
|
||||
|
||||
|
@ -2,7 +2,9 @@ package dynamicproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
@ -43,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Inject headers
|
||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
@ -70,49 +74,126 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
/*
|
||||
Subdomain Routing
|
||||
Host Routing
|
||||
*/
|
||||
if strings.Contains(r.Host, ".") {
|
||||
//This might be a subdomain. See if there are any subdomain proxy router for this
|
||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.subdomainRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Routing
|
||||
*/
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil {
|
||||
if targetProxyEndpoint.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") {
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to handle by the host proxy forwarder
|
||||
h.hostRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Root Router Handling
|
||||
*/
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
if !strings.HasSuffix(proxyingPath, "/") {
|
||||
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
//Passthrough the request to root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
h.handleRootRouting(w, r)
|
||||
}
|
||||
} else {
|
||||
//No routing rules found. Route to root.
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
//No routing rules found.
|
||||
h.handleRootRouting(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
handleRootRouting
|
||||
|
||||
This function handle root routing situations where there are no subdomain
|
||||
, vdir or special routing rule matches the requested URI.
|
||||
|
||||
Once entered this routing segment, the root routing options will take over
|
||||
for the routing logic.
|
||||
*/
|
||||
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
//Get the proxy root config
|
||||
proot := h.Parent.Root
|
||||
switch proot.DefaultSiteOption {
|
||||
case DefaultSite_InternalStaticWebServer:
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//No vdir match. Route via root router
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
case DefaultSite_Redirect:
|
||||
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
|
||||
if redirectTarget == "" {
|
||||
redirectTarget = "about:blank"
|
||||
}
|
||||
|
||||
//Check if it is an infinite loopback redirect
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
return
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
if hostname == domainOnly {
|
||||
h.logRequest(r, false, 500, "root-redirect", domainOnly)
|
||||
http.Error(w, "Loopback redirects due to invalid settings", 500)
|
||||
return
|
||||
}
|
||||
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||
case DefaultSite_NotFoundPage:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,9 +205,9 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
|
||||
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile("./web/forbidden.html")
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
|
||||
if err != nil {
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
w.Write(page_forbidden)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
@ -138,9 +219,9 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
|
||||
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile("./web/forbidden.html")
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
|
||||
if err != nil {
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
w.Write(page_forbidden)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package dynamicproxy
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
)
|
||||
@ -15,10 +16,16 @@ import (
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
proxyType := "vdir-auth"
|
||||
if pe.ProxyType == ProxyType_Subdomain {
|
||||
proxyType = "subd-auth"
|
||||
if len(pe.BasicAuthExceptionRules) > 0 {
|
||||
//Check if the current path matches the exception rules
|
||||
for _, exceptionRule := range pe.BasicAuthExceptionRules {
|
||||
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
|
||||
//This path is excluded from basic auth
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
@ -37,7 +44,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if !matchingFound {
|
||||
h.logRequest(r, false, 401, proxyType, pe.Domain)
|
||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
|
@ -14,10 +14,6 @@ import (
|
||||
|
||||
var onExitFlushLoop func()
|
||||
|
||||
const (
|
||||
defaultTimeout = time.Minute * 5
|
||||
)
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
// client, support http, also support https tunnel using http.hijacker
|
||||
@ -64,6 +60,7 @@ type ResponseRewriteRuleSet struct {
|
||||
ProxyDomain string
|
||||
OriginalHost string
|
||||
UseTLS bool
|
||||
NoCache bool
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
}
|
||||
|
||||
@ -247,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.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
@ -264,9 +261,16 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
|
||||
//Disable cache if nocache is set
|
||||
if noCache {
|
||||
header.Del("Cache-Control")
|
||||
header.Set("Cache-Control", "no-store")
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
copyHeader(outreq.Header, req.Header)
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||
removeHeaders(outreq.Header)
|
||||
removeHeaders(outreq.Header, rrr.NoCache)
|
||||
|
||||
// Add X-Forwarded-For Header.
|
||||
addXForwardedForHeader(outreq)
|
||||
@ -343,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.
|
||||
removeHeaders(res.Header)
|
||||
removeHeaders(res.Header, rrr.NoCache)
|
||||
|
||||
if p.ModifyResponse != nil {
|
||||
if err := p.ModifyResponse(res); err != nil {
|
||||
|
@ -21,6 +21,17 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
//Issue #39: Check if it is location target match the proxying domain
|
||||
//E.g. Proxy config: blog.example.com -> example.com/blog
|
||||
//Check if it is actually redirecting to example.com instead of a new domain
|
||||
//like news.example.com.
|
||||
// The later check bypass apache screw up method of redirection header
|
||||
// e.g. https://imuslab.com -> http://imuslab.com:443
|
||||
if rrr.ProxyDomain != u.Host && !strings.Contains(u.Host, rrr.OriginalHost+":") {
|
||||
//New location domain not matching proxy target domain.
|
||||
//Do not modify location header
|
||||
return urlString, nil
|
||||
}
|
||||
u.Host = rrr.OriginalHost
|
||||
|
||||
if strings.Contains(rrr.ProxyDomain, "/") {
|
||||
|
@ -3,6 +3,7 @@ package dynamicproxy
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -21,14 +22,13 @@ import (
|
||||
|
||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
@ -52,6 +52,12 @@ func (router *Router) UpdateTLSVersion(requireLatest bool) {
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update port 80 listener state
|
||||
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
|
||||
router.Option.ListenOnPort80 = useRedirect
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update https redirect, which will require updates
|
||||
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
router.Option.ForceHttpsRedirect = useRedirect
|
||||
@ -62,11 +68,12 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
if router.server != nil {
|
||||
return errors.New("Reverse proxy server already running")
|
||||
return errors.New("reverse proxy server already running")
|
||||
}
|
||||
|
||||
//Check if root route is set
|
||||
if router.Root == nil {
|
||||
return errors.New("Reverse proxy router root not set")
|
||||
return errors.New("reverse proxy router root not set")
|
||||
}
|
||||
|
||||
minVersion := tls.VersionTLS10
|
||||
@ -79,27 +86,63 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
Handler: router.mux,
|
||||
TLSConfig: config,
|
||||
}
|
||||
router.tlsListener = ln
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
|
||||
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
|
||||
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
|
||||
//Add a 80 to 443 redirector
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
protocol := "https://"
|
||||
if router.Option.Port == 443 {
|
||||
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
//Check if the domain requesting allow non TLS mode
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
PathPrefix: "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if router.Option.ForceHttpsRedirect {
|
||||
//Redirect to https is enabled
|
||||
protocol := "https://"
|
||||
if router.Option.Port == 443 {
|
||||
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
}
|
||||
} else {
|
||||
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
//Do not do redirection
|
||||
if sep != nil {
|
||||
//Sub-domain exists but not allow non-TLS access
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("400 - Bad Request"))
|
||||
} else {
|
||||
//No defined sub-domain
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}),
|
||||
@ -127,7 +170,7 @@ func (router *Router) StartProxyService() error {
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
//Unable to startup port 80 listener. Handle shutdown process gracefully
|
||||
stopChan <- true
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
log.Fatalf("Could not start redirection server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
router.tlsRedirectStop = stopChan
|
||||
@ -136,8 +179,8 @@ func (router *Router) StartProxyService() error {
|
||||
//Start the TLS server
|
||||
log.Println("Reverse proxy service started in the background (TLS mode)")
|
||||
go func() {
|
||||
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start proxy server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
@ -157,7 +200,7 @@ func (router *Router) StartProxyService() error {
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
return errors.New("Reverse proxy server already stopped")
|
||||
return errors.New("reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@ -185,13 +228,13 @@ func (router *Router) StopProxyService() error {
|
||||
// Restart the current router if it is running.
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
var err error = nil
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
@ -199,7 +242,7 @@ func (router *Router) Restart() error {
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
@ -212,148 +255,17 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||
return subdEndpoint != nil
|
||||
}
|
||||
|
||||
/*
|
||||
Add an URL into a custom proxy services
|
||||
*/
|
||||
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
/*
|
||||
if rootname[len(rootname)-1:] == "/" {
|
||||
rootname = rootname[:len(rootname)-1]
|
||||
}
|
||||
*/
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
|
||||
|
||||
endpointObject := ProxyEndpoint{
|
||||
ProxyType: ProxyType_Vdir,
|
||||
RootOrMatchingDomain: options.RootName,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
|
||||
|
||||
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Load routing from RP
|
||||
*/
|
||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
||||
if ptype == "vdir" {
|
||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
return proxy.(*ProxyEndpoint), nil
|
||||
} else if ptype == "subd" {
|
||||
proxy, ok := router.SubdomainEndpoint.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
return proxy.(*ProxyEndpoint), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Save routing from RP
|
||||
*/
|
||||
func (router *Router) SaveProxy(ptype string, key string, newConfig *ProxyEndpoint) {
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Store(key, newConfig)
|
||||
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Store(key, newConfig)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Remove routing from RP
|
||||
*/
|
||||
func (router *Router) RemoveProxy(ptype string, key string) error {
|
||||
//fmt.Println(ptype, key)
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Delete(key)
|
||||
return nil
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Add an default router for the proxy server
|
||||
*/
|
||||
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,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.Root = &rootEndpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers to export the syncmap for easier processing
|
||||
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||
var targetProxyEndpoint *ProxyEndpoint
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
key, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
@ -361,13 +273,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
|
||||
if key == matchingDomain {
|
||||
targetProxyEndpoint = v
|
||||
}
|
||||
return true
|
||||
})
|
||||
return m
|
||||
|
||||
if targetProxyEndpoint == nil {
|
||||
return nil, errors.New("target routing rule not found")
|
||||
}
|
||||
|
||||
return targetProxyEndpoint, nil
|
||||
}
|
||||
|
||||
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
// Deep copy a proxy endpoint, excluding runtime paramters
|
||||
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
|
||||
js, _ := json.Marshal(endpoint)
|
||||
newProxyEndpoint := ProxyEndpoint{}
|
||||
err := json.Unmarshal(js, &newProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &newProxyEndpoint
|
||||
}
|
||||
|
||||
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
|
158
src/mod/dynamicproxy/endpoints.go
Normal file
@ -0,0 +1,158 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
/*
|
||||
endpoint.go
|
||||
author: tobychui
|
||||
|
||||
This script handle the proxy endpoint object actions
|
||||
so proxyEndpoint can be handled like a proper oop object
|
||||
|
||||
Most of the functions are implemented in dynamicproxy.go
|
||||
*/
|
||||
|
||||
/*
|
||||
User Defined Header Functions
|
||||
*/
|
||||
|
||||
// Check if a user define header exists in this endpoint, ignore case
|
||||
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if strings.EqualFold(header.Key, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Remvoe a user defined header from the list
|
||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
newHeaderList := []*UserDefinedHeader{}
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if !strings.EqualFold(header.Key, key) {
|
||||
newHeaderList = append(newHeaderList, header)
|
||||
}
|
||||
}
|
||||
|
||||
ep.UserDefinedHeaders = newHeaderList
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a user defined header to the list, duplicates will be automatically removed
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
|
||||
if ep.UserDefinedHeaderExists(key) {
|
||||
ep.RemoveUserDefinedHeader(key)
|
||||
}
|
||||
|
||||
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
|
||||
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
|
||||
Value: value,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Functions
|
||||
*/
|
||||
|
||||
// Get virtual directory handler from given URI
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if strings.HasPrefix(requestURI, vdir.MatchingPath) {
|
||||
return vdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get virtual directory handler by matching path (exact match required)
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if vdir.MatchingPath == matchingPath {
|
||||
return vdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a vdir rule by its matching path
|
||||
func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath string) error {
|
||||
entryFound := false
|
||||
newVirtualDirectoryList := []*VirtualDirectoryEndpoint{}
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if vdir.MatchingPath == matchingPath {
|
||||
entryFound = true
|
||||
} else {
|
||||
newVirtualDirectoryList = append(newVirtualDirectoryList, vdir)
|
||||
}
|
||||
}
|
||||
|
||||
if entryFound {
|
||||
//Update the list of vdirs
|
||||
ep.VirtualDirectories = newVirtualDirectoryList
|
||||
return nil
|
||||
}
|
||||
return errors.New("target virtual directory routing rule not found")
|
||||
}
|
||||
|
||||
// Delete a vdir rule by its matching path
|
||||
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
|
||||
//Check for matching path duplicate
|
||||
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
|
||||
return nil, errors.New("rule with same matching path already exists")
|
||||
}
|
||||
|
||||
//Append it to the list of virtual directory
|
||||
ep.VirtualDirectories = append(ep.VirtualDirectories, vdir)
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
parentRouter := ep.parent
|
||||
readyRoutingRule, err := parentRouter.PrepareProxyRoute(ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ep.ProxyType == ProxyType_Root {
|
||||
parentRouter.Root = readyRoutingRule
|
||||
} else if ep.ProxyType == ProxyType_Host {
|
||||
ep.Remove()
|
||||
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
} else {
|
||||
return nil, errors.New("unsupported proxy type")
|
||||
}
|
||||
|
||||
return readyRoutingRule, nil
|
||||
}
|
||||
|
||||
// Create a deep clone object of the proxy endpoint
|
||||
// Note the returned object is not activated. Call to prepare function before pushing into runtime
|
||||
func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
|
||||
clonedProxyEndpoint := ProxyEndpoint{}
|
||||
js, _ := json.Marshal(ep)
|
||||
json.Unmarshal(js, &clonedProxyEndpoint)
|
||||
return &clonedProxyEndpoint
|
||||
}
|
||||
|
||||
// Remove this proxy endpoint from running proxy endpoint list
|
||||
func (ep *ProxyEndpoint) Remove() error {
|
||||
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write changes to runtime without respawning the proxy handler
|
||||
// use prepare -> remove -> add if you change anything in the endpoint
|
||||
// that effects the proxy routing src / dest
|
||||
func (ep *ProxyEndpoint) UpdateToRuntime() {
|
||||
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
@ -28,13 +30,41 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
||||
return targetProxyEndpoint
|
||||
}
|
||||
|
||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
if ok {
|
||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||
}
|
||||
|
||||
//No hit. Try with wildcard
|
||||
matchProxyEndpoints := []*ProxyEndpoint{}
|
||||
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
||||
ep := v.(*ProxyEndpoint)
|
||||
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
||||
if err != nil {
|
||||
//Continue
|
||||
return true
|
||||
}
|
||||
if match {
|
||||
//targetSubdomainEndpoint = ep
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(matchProxyEndpoints) == 1 {
|
||||
//Only 1 match
|
||||
return matchProxyEndpoints[0]
|
||||
} else if len(matchProxyEndpoints) > 1 {
|
||||
//More than one match. Get the best match one
|
||||
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
|
||||
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
|
||||
})
|
||||
return matchProxyEndpoints[0]
|
||||
}
|
||||
|
||||
return targetSubdomainEndpoint
|
||||
}
|
||||
|
||||
@ -54,14 +84,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
return rewrittenURL
|
||||
}
|
||||
|
||||
// Handle subdomain request
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
// Handle host request
|
||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
//Append / to the end of the redirection endpoint if not exists
|
||||
@ -89,10 +127,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
})
|
||||
|
||||
@ -113,15 +152,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.parent.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.parent.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
@ -144,11 +191,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.RootOrMatchingDomain,
|
||||
PathPrefix: target.MatchingPath,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
@ -183,6 +230,5 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
|
||||
}
|
||||
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
||||
|
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,48 +0,0 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Add an URL intoa custom subdomain service
|
||||
|
||||
*/
|
||||
|
||||
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
||||
|
||||
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
|
||||
RootOrMatchingDomain: options.MatchingDomain,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
Proxy: proxy,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
})
|
||||
|
||||
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
|
||||
return nil
|
||||
}
|
55
src/mod/dynamicproxy/templates/forbidden.html
Normal file
@ -0,0 +1,55 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Zoraxy Forbidden Template -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
|
||||
<title>Forbidden</title>
|
||||
<style>
|
||||
#msg{
|
||||
position: absolute;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 250px);
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer{
|
||||
position: fixed;
|
||||
padding: 2em;
|
||||
padding-left: 5em;
|
||||
padding-right: 5em;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
small{
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="msg">
|
||||
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
|
||||
<div>
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might cause by the region limit setting of this site.</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
|
||||
$("#requrl").text(window.location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,6 +1,7 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@ -13,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyType_Subdomain = 0
|
||||
ProxyType_Vdir = 1
|
||||
ProxyType_Root = 0
|
||||
ProxyType_Host = 1
|
||||
ProxyType_Vdir = 2
|
||||
)
|
||||
|
||||
type ProxyHandler struct {
|
||||
@ -23,28 +25,32 @@ type ProxyHandler struct {
|
||||
|
||||
type RouterOption struct {
|
||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||
HostVersion string //The version of Zoraxy, use for heading mod
|
||||
Port int //Incoming port
|
||||
UseTls bool //Use TLS to serve incoming requsts
|
||||
ForceTLSLatest bool //Force TLS1.2 or above
|
||||
NoCache bool //Force set Cache-Control: no-store
|
||||
ListenOnPort80 bool //Enable port 80 http listener
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
RedirectRuleTable *redirection.RuleTable
|
||||
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
|
||||
StatisticCollector *statistic.Collector
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
SubdomainEndpoint *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
|
||||
tlsRedirectStop chan bool
|
||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||
tldMap map[string]int //Top level domain map, see tld.json
|
||||
}
|
||||
|
||||
// Auth credential for basic auth on certain endpoints
|
||||
@ -59,56 +65,80 @@ type BasicAuthUnhashedCredentials struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
// A proxy endpoint record
|
||||
// Paths to exclude in basic auth enabled proxy handler
|
||||
type BasicAuthExceptionRule struct {
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
// program structure than directly using ProxyEndpoint
|
||||
type VirtualDirectoryEndpoint struct {
|
||||
MatchingPath string //Matching prefix of the request path, also act as key
|
||||
Domain string //Domain or IP to proxy to
|
||||
RequireTLS bool //Target domain require TLS
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
Disabled bool //If the rule is enabled
|
||||
proxy *dpcore.ReverseProxy `json:"-"`
|
||||
parent *ProxyEndpoint `json:"-"`
|
||||
}
|
||||
|
||||
// A proxy endpoint record, a general interface for handling inbound routing
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
|
||||
Domain string //Domain or IP to proxy to
|
||||
RequireTLS bool //Target domain require TLS
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
|
||||
Proxy *dpcore.ReverseProxy `json:"-"`
|
||||
}
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||
Domain string //Domain or IP to proxy to
|
||||
|
||||
type RootOptions struct {
|
||||
ProxyLocation string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
}
|
||||
//TLS/SSL Related
|
||||
RequireTLS bool //Target domain require TLS
|
||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
|
||||
type VdirOptions struct {
|
||||
RootName string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
}
|
||||
//Virtual Directories
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
||||
type SubdOptions struct {
|
||||
MatchingDomain string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
//Custom Headers
|
||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
|
||||
//Authentication
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||
|
||||
//Fallback routing logic
|
||||
DefaultSiteOption int //Fallback routing logic options
|
||||
DefaultSiteValue string //Fallback routing target, optional
|
||||
|
||||
Disabled bool //If the rule is disabled
|
||||
//Internal Logic Elements
|
||||
parent *Router
|
||||
proxy *dpcore.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
/*
|
||||
type ProxyEndpoint struct {
|
||||
Root string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
Proxy *reverseproxy.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
type SubdomainEndpoint struct {
|
||||
MatchingDomain string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
Proxy *reverseproxy.ReverseProxy `json:"-"`
|
||||
}
|
||||
Routing type specific interface
|
||||
These are options that only avaible for a specific interface
|
||||
when running, these are converted into "ProxyEndpoint" objects
|
||||
for more generic routing logic
|
||||
*/
|
||||
|
||||
// Root options are those that are required for reverse proxy handler to work
|
||||
const (
|
||||
DefaultSite_InternalStaticWebServer = 0
|
||||
DefaultSite_ReverseProxy = 1
|
||||
DefaultSite_Redirect = 2
|
||||
DefaultSite_NotFoundPage = 3
|
||||
)
|
||||
|
||||
/*
|
||||
Web Templates
|
||||
*/
|
||||
var (
|
||||
//go:embed templates/forbidden.html
|
||||
page_forbidden []byte
|
||||
)
|
||||
|
@ -207,7 +207,7 @@ func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
//Handle listing of network members. Set details=true for listing all details
|
||||
// Handle listing of network members. Set details=true for listing all details
|
||||
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.GetPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -241,7 +241,7 @@ func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
//Handle Authorization of members
|
||||
// Handle Authorization of members
|
||||
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -281,7 +281,7 @@ func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
//Handle Delete or Add IP for a member in a network
|
||||
// Handle Delete or Add IP for a member in a network
|
||||
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -356,7 +356,7 @@ func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
//Handle naming for members
|
||||
// Handle naming for members
|
||||
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -391,7 +391,7 @@ func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
//Handle delete of a given memver
|
||||
// Handle delete of a given memver
|
||||
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -426,3 +426,79 @@ func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Check if a given network id is a network hosted on this zoraxy node
|
||||
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
|
||||
networks, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
if network == networkId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle server instant joining a given network
|
||||
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if m.memberExistsInNetwork(netid, m.ControllerID) {
|
||||
utils.SendErrorResponse(w, "controller already inside network")
|
||||
return
|
||||
}
|
||||
|
||||
//Join the network
|
||||
err = m.joinNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle server instant leaving a given network
|
||||
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "net id not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the target network is a network hosted on this server
|
||||
if !m.IsLocalGAN(netid) {
|
||||
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
|
||||
return
|
||||
}
|
||||
|
||||
//Leave the network
|
||||
err = m.leaveNetwork(netid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove it from target network if it is authorized
|
||||
err = m.deleteMember(netid, m.ControllerID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ type MemberInfo struct {
|
||||
VRev int `json:"vRev"`
|
||||
}
|
||||
|
||||
//Get the zerotier node info from local service
|
||||
// Get the zerotier node info from local service
|
||||
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
|
||||
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
||||
|
||||
@ -187,7 +187,7 @@ func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
|
||||
return &networkInfo, nil
|
||||
}
|
||||
|
||||
//List network details
|
||||
// List network details
|
||||
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
|
||||
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
|
||||
if err != nil {
|
||||
@ -249,7 +249,7 @@ func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *Ne
|
||||
return nil
|
||||
}
|
||||
|
||||
//List network IDs
|
||||
// List network IDs
|
||||
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
||||
if err != nil {
|
||||
@ -281,7 +281,7 @@ func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
return networkIds, nil
|
||||
}
|
||||
|
||||
//wrapper for checking if a network id exists
|
||||
// wrapper for checking if a network id exists
|
||||
func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
networkIds, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
@ -297,7 +297,7 @@ func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//delete a network
|
||||
// delete a network
|
||||
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
client := &http.Client{}
|
||||
@ -330,8 +330,8 @@ func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Configure network
|
||||
//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
// Configure network
|
||||
// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
data := map[string]interface{}{
|
||||
@ -545,7 +545,7 @@ func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool
|
||||
return false
|
||||
}
|
||||
|
||||
//Get a network memeber info by netid and memberid
|
||||
// Get a network memeber info by netid and memberid
|
||||
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
|
||||
if err != nil {
|
||||
@ -573,7 +573,7 @@ func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*M
|
||||
return thisMemeberInfo, nil
|
||||
}
|
||||
|
||||
//Set the authorization state of a member
|
||||
// Set the authorization state of a member
|
||||
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
||||
payload := []byte(`{"authorized": true}`)
|
||||
@ -600,7 +600,7 @@ func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAutho
|
||||
return nil
|
||||
}
|
||||
|
||||
//Delete a member from the network
|
||||
// Delete a member from the network
|
||||
func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
|
||||
if err != nil {
|
||||
@ -620,3 +620,45 @@ func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to join a given network
|
||||
func (m *NetworkManager) joinNetwork(netid string) error {
|
||||
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the host to leave a given network
|
||||
func (m *NetworkManager) leaveNetwork(netid string) error {
|
||||
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -20,13 +20,16 @@ type Store struct {
|
||||
WhitelistEnabled bool
|
||||
geodb [][]string //Parsed geodb list
|
||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||
|
||||
geotrie *trie
|
||||
geotrieIpv6 *trie
|
||||
|
||||
geotrie *trie
|
||||
geotrieIpv6 *trie
|
||||
//geoipCache sync.Map
|
||||
sysdb *database.Database
|
||||
option *StoreOptions
|
||||
}
|
||||
|
||||
sysdb *database.Database
|
||||
type StoreOptions struct {
|
||||
AllowSlowIpv4LookUp bool
|
||||
AllowSloeIpv6Lookup bool
|
||||
}
|
||||
|
||||
type CountryInfo struct {
|
||||
@ -34,7 +37,7 @@ type CountryInfo struct {
|
||||
ContinetCode string
|
||||
}
|
||||
|
||||
func NewGeoDb(sysdb *database.Database) (*Store, error) {
|
||||
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
||||
parsedGeoData, err := parseCSV(geoipv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -79,14 +82,25 @@ func NewGeoDb(sysdb *database.Database) (*Store, error) {
|
||||
log.Println("Database pointer set to nil: Entering debug mode")
|
||||
}
|
||||
|
||||
var ipv4Trie *trie
|
||||
if !option.AllowSlowIpv4LookUp {
|
||||
ipv4Trie = constrctTrieTree(parsedGeoData)
|
||||
}
|
||||
|
||||
var ipv6Trie *trie
|
||||
if !option.AllowSloeIpv6Lookup {
|
||||
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
|
||||
}
|
||||
|
||||
return &Store{
|
||||
BlacklistEnabled: blacklistEnabled,
|
||||
WhitelistEnabled: whitelistEnabled,
|
||||
geodb: parsedGeoData,
|
||||
geotrie: constrctTrieTree(parsedGeoData),
|
||||
geotrie: ipv4Trie,
|
||||
geodbIpv6: parsedGeoDataIpv6,
|
||||
geotrieIpv6: constrctTrieTree(parsedGeoDataIpv6),
|
||||
geotrieIpv6: ipv6Trie,
|
||||
sysdb: sysdb,
|
||||
option: option,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -106,6 +120,7 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
|
||||
CountryIsoCode: cc,
|
||||
ContinetCode: "",
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
|
@ -41,7 +41,10 @@ func TestTrieConstruct(t *testing.T) {
|
||||
|
||||
func TestResolveCountryCodeFromIP(t *testing.T) {
|
||||
// Create a new store
|
||||
store, err := geodb.NewGeoDb(nil)
|
||||
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
|
||||
false,
|
||||
false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("error creating store: %v", err)
|
||||
return
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -26,9 +25,17 @@ func (s *Store) search(ip string) string {
|
||||
//Search in geotrie tree
|
||||
cc := ""
|
||||
if IsIPv6(ip) {
|
||||
cc = s.geotrieIpv6.search(ip)
|
||||
if s.geotrieIpv6 == nil {
|
||||
cc = s.slowSearchIpv6(ip)
|
||||
} else {
|
||||
cc = s.geotrieIpv6.search(ip)
|
||||
}
|
||||
} else {
|
||||
cc = s.geotrie.search(ip)
|
||||
if s.geotrie == nil {
|
||||
cc = s.slowSearchIpv4(ip)
|
||||
} else {
|
||||
cc = s.geotrie.search(ip)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -69,27 +76,3 @@ func parseCSV(content []byte) ([][]string, error) {
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// Check if a ip string is within the range of two others
|
||||
func isIPInRange(ip, start, end string) bool {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if ipAddr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
startAddr := net.ParseIP(start)
|
||||
if startAddr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
endAddr := net.ParseIP(end)
|
||||
if endAddr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ipAddr.To4() == nil || startAddr.To4() == nil || endAddr.To4() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Compare(ipAddr.To4(), startAddr.To4()) >= 0 && bytes.Compare(ipAddr.To4(), endAddr.To4()) <= 0
|
||||
}
|
||||
|
81
src/mod/geodb/slowSearch.go
Normal file
@ -0,0 +1,81 @@
|
||||
package geodb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
/*
|
||||
slowSearch.go
|
||||
|
||||
This script implement the slow search method for ip to country code
|
||||
lookup. If you have the memory allocation for near O(1) lookup,
|
||||
you should not be using slow search mode.
|
||||
*/
|
||||
|
||||
func ipv4ToUInt32(ip net.IP) uint32 {
|
||||
ip = ip.To4()
|
||||
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
|
||||
}
|
||||
|
||||
func isIPv4InRange(startIP, endIP, testIP string) (bool, error) {
|
||||
start := net.ParseIP(startIP)
|
||||
end := net.ParseIP(endIP)
|
||||
test := net.ParseIP(testIP)
|
||||
|
||||
if start == nil || end == nil || test == nil {
|
||||
return false, errors.New("invalid IP address format")
|
||||
}
|
||||
|
||||
startUint := ipv4ToUInt32(start)
|
||||
endUint := ipv4ToUInt32(end)
|
||||
testUint := ipv4ToUInt32(test)
|
||||
|
||||
return testUint >= startUint && testUint <= endUint, nil
|
||||
}
|
||||
|
||||
func isIPv6InRange(startIP, endIP, testIP string) (bool, error) {
|
||||
start := net.ParseIP(startIP)
|
||||
end := net.ParseIP(endIP)
|
||||
test := net.ParseIP(testIP)
|
||||
|
||||
if start == nil || end == nil || test == nil {
|
||||
return false, errors.New("invalid IP address format")
|
||||
}
|
||||
|
||||
startInt := new(big.Int).SetBytes(start.To16())
|
||||
endInt := new(big.Int).SetBytes(end.To16())
|
||||
testInt := new(big.Int).SetBytes(test.To16())
|
||||
|
||||
return testInt.Cmp(startInt) >= 0 && testInt.Cmp(endInt) <= 0, nil
|
||||
}
|
||||
|
||||
// Slow country code lookup for
|
||||
func (s *Store) slowSearchIpv4(ipAddr string) string {
|
||||
for _, ipRange := range s.geodb {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
cc := ipRange[2]
|
||||
|
||||
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
|
||||
if inRange {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Store) slowSearchIpv6(ipAddr string) string {
|
||||
for _, ipRange := range s.geodbIpv6 {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
cc := ipRange[2]
|
||||
|
||||
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
|
||||
if inRange {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,15 +1,12 @@
|
||||
package geodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type trie_Node struct {
|
||||
childrens [2]*trie_Node
|
||||
ends bool
|
||||
cc string
|
||||
}
|
||||
|
||||
@ -18,7 +15,7 @@ type trie struct {
|
||||
root *trie_Node
|
||||
}
|
||||
|
||||
func ipToBitString(ip string) string {
|
||||
func ipToBytes(ip string) []byte {
|
||||
// Parse the IP address string into a net.IP object
|
||||
parsedIP := net.ParseIP(ip)
|
||||
|
||||
@ -29,49 +26,7 @@ func ipToBitString(ip string) string {
|
||||
ipBytes = parsedIP.To16()
|
||||
}
|
||||
|
||||
// Convert each byte in the IP address to its 8-bit binary representation
|
||||
var result []string
|
||||
for _, b := range ipBytes {
|
||||
result = append(result, fmt.Sprintf("%08b", b))
|
||||
}
|
||||
|
||||
// Join the binary representation of each byte with dots to form the final bit string
|
||||
return strings.Join(result, "")
|
||||
}
|
||||
|
||||
func bitStringToIp(bitString string) string {
|
||||
// Check if the bit string represents an IPv4 or IPv6 address
|
||||
isIPv4 := len(bitString) == 32
|
||||
|
||||
// Split the bit string into 8-bit segments
|
||||
segments := make([]string, 0)
|
||||
if isIPv4 {
|
||||
for i := 0; i < 4; i++ {
|
||||
segments = append(segments, bitString[i*8:(i+1)*8])
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < 16; i++ {
|
||||
segments = append(segments, bitString[i*8:(i+1)*8])
|
||||
}
|
||||
}
|
||||
|
||||
// Convert each segment to its decimal equivalent
|
||||
decimalSegments := make([]int, len(segments))
|
||||
for i, s := range segments {
|
||||
val, _ := strconv.ParseInt(s, 2, 64)
|
||||
decimalSegments[i] = int(val)
|
||||
}
|
||||
|
||||
// Construct the IP address string based on the type (IPv4 or IPv6)
|
||||
if isIPv4 {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", decimalSegments[0], decimalSegments[1], decimalSegments[2], decimalSegments[3])
|
||||
} else {
|
||||
ip := make(net.IP, net.IPv6len)
|
||||
for i := 0; i < net.IPv6len; i++ {
|
||||
ip[i] = byte(decimalSegments[i])
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
return ipBytes
|
||||
}
|
||||
|
||||
// inititlaizing a new trie
|
||||
@ -83,20 +38,39 @@ func newTrie() *trie {
|
||||
|
||||
// Passing words to trie
|
||||
func (t *trie) insert(ipAddr string, cc string) {
|
||||
word := ipToBitString(ipAddr)
|
||||
ipBytes := ipToBytes(ipAddr)
|
||||
current := t.root
|
||||
for _, wr := range word {
|
||||
index := wr - '0'
|
||||
if current.childrens[index] == nil {
|
||||
current.childrens[index] = &trie_Node{
|
||||
childrens: [2]*trie_Node{},
|
||||
ends: false,
|
||||
cc: cc,
|
||||
for _, b := range ipBytes {
|
||||
//For each byte in the ip address
|
||||
//each byte is 8 bit
|
||||
for j := 0; j < 8; j++ {
|
||||
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
|
||||
bit := 0b0000
|
||||
if bitwise {
|
||||
bit = 0b0001
|
||||
}
|
||||
if current.childrens[bit] == nil {
|
||||
current.childrens[bit] = &trie_Node{
|
||||
childrens: [2]*trie_Node{},
|
||||
cc: cc,
|
||||
}
|
||||
}
|
||||
current = current.childrens[bit]
|
||||
}
|
||||
current = current.childrens[index]
|
||||
}
|
||||
current.ends = true
|
||||
|
||||
/*
|
||||
for i := 63; i >= 0; i-- {
|
||||
bit := (ipInt64 >> uint(i)) & 1
|
||||
if current.childrens[bit] == nil {
|
||||
current.childrens[bit] = &trie_Node{
|
||||
childrens: [2]*trie_Node{},
|
||||
cc: cc,
|
||||
}
|
||||
}
|
||||
current = current.childrens[bit]
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func isReservedIP(ip string) bool {
|
||||
@ -126,16 +100,34 @@ func (t *trie) search(ipAddr string) string {
|
||||
if isReservedIP(ipAddr) {
|
||||
return ""
|
||||
}
|
||||
word := ipToBitString(ipAddr)
|
||||
|
||||
ipBytes := ipToBytes(ipAddr)
|
||||
current := t.root
|
||||
for _, wr := range word {
|
||||
index := wr - '0'
|
||||
if current.childrens[index] == nil {
|
||||
return current.cc
|
||||
for _, b := range ipBytes {
|
||||
//For each byte in the ip address
|
||||
//each byte is 8 bit
|
||||
for j := 0; j < 8; j++ {
|
||||
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
|
||||
bit := 0b0000
|
||||
if bitwise {
|
||||
bit = 0b0001
|
||||
}
|
||||
if current.childrens[bit] == nil {
|
||||
return current.cc
|
||||
}
|
||||
current = current.childrens[bit]
|
||||
}
|
||||
current = current.childrens[index]
|
||||
}
|
||||
if current.ends {
|
||||
/*
|
||||
for i := 63; i >= 0; i-- {
|
||||
bit := (ipInt64 >> uint(i)) & 1
|
||||
if current.childrens[bit] == nil {
|
||||
return current.cc
|
||||
}
|
||||
current = current.childrens[bit]
|
||||
}
|
||||
*/
|
||||
if len(current.childrens) == 0 {
|
||||
return current.cc
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
package geodb
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Whitelist.go
|
||||
@ -8,11 +11,29 @@ import "strings"
|
||||
This script handles whitelist related functions
|
||||
*/
|
||||
|
||||
const (
|
||||
EntryType_CountryCode int = 0
|
||||
EntryType_IP int = 1
|
||||
)
|
||||
|
||||
type WhitelistEntry struct {
|
||||
EntryType int //Entry type of whitelist, Country Code or IP
|
||||
CC string //ISO Country Code
|
||||
IP string //IP address or range
|
||||
Comment string //Comment for this entry
|
||||
}
|
||||
|
||||
//Geo Whitelist
|
||||
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Write("whitelist-cn", countryCode, true)
|
||||
entry := WhitelistEntry{
|
||||
EntryType: EntryType_CountryCode,
|
||||
CC: countryCode,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-cn", countryCode, entry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
|
||||
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
|
||||
return isWhitelisted
|
||||
return s.sysdb.KeyExists("whitelist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
whitelistedCountryCode := []string{}
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||
whitelistedCountryCode := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, ip)
|
||||
thisWhitelistEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
|
||||
}
|
||||
|
||||
return whitelistedCountryCode
|
||||
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
|
||||
//IP Whitelist
|
||||
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string) {
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, true)
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
|
||||
thisIpEntry := WhitelistEntry{
|
||||
EntryType: EntryType_IP,
|
||||
IP: ipAddr,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
}
|
||||
|
||||
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
|
||||
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
|
||||
if isWhitelisted {
|
||||
//single IP whitelist entry
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIp()
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
|
||||
for _, whitelistRules := range AllWhitelistedIps {
|
||||
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
|
||||
if wildcardMatch {
|
||||
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIp() []string {
|
||||
whitelistedIp := []string{}
|
||||
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
|
||||
whitelistedIp := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedIp = append(whitelistedIp, ip)
|
||||
//ip := string(keypairs[0])
|
||||
thisEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisEntry)
|
||||
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||
}
|
||||
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
|
||||
allWhitelistedIPs := []string{}
|
||||
entries := s.GetAllWhitelistedIp()
|
||||
for _, entry := range entries {
|
||||
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
|
||||
}
|
||||
|
||||
return allWhitelistedIPs
|
||||
}
|
||||
|
103
src/mod/info/logger/logger.go
Normal file
@ -0,0 +1,103 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Zoraxy Logger
|
||||
|
||||
This script is designed to make a managed log for the Zoraxy
|
||||
and replace the ton of log.Println in the system core
|
||||
*/
|
||||
|
||||
type Logger struct {
|
||||
LogToFile bool //Set enable write to file
|
||||
Prefix string //Prefix for log files
|
||||
LogFolder string //Folder to store the log file
|
||||
CurrentLogFile string //Current writing filename
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
|
||||
err := os.MkdirAll(logFolder, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thisLogger := Logger{
|
||||
LogToFile: logToFile,
|
||||
Prefix: logFilePrefix,
|
||||
LogFolder: logFolder,
|
||||
}
|
||||
|
||||
logFilePath := thisLogger.getLogFilepath()
|
||||
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
thisLogger.CurrentLogFile = logFilePath
|
||||
thisLogger.file = f
|
||||
return &thisLogger, nil
|
||||
}
|
||||
|
||||
func (l *Logger) getLogFilepath() string {
|
||||
year, month, _ := time.Now().Date()
|
||||
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
|
||||
}
|
||||
|
||||
// PrintAndLog will log the message to file and print the log to STDOUT
|
||||
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
|
||||
go func() {
|
||||
l.Log(title, message, originalError)
|
||||
}()
|
||||
log.Println("[" + title + "] " + message)
|
||||
}
|
||||
|
||||
// Println is a fast snap-in replacement for log.Println
|
||||
func (l *Logger) Println(v ...interface{}) {
|
||||
//Convert the array of interfaces into string
|
||||
message := fmt.Sprint(v...)
|
||||
go func() {
|
||||
l.Log("info", string(message), nil)
|
||||
}()
|
||||
log.Println("[INFO] " + string(message))
|
||||
}
|
||||
|
||||
func (l *Logger) Log(title string, errorMessage string, originalError error) {
|
||||
l.ValidateAndUpdateLogFilepath()
|
||||
if l.LogToFile {
|
||||
if originalError == nil {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
|
||||
} else {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Validate if the logging target is still valid (detect any months change)
|
||||
func (l *Logger) ValidateAndUpdateLogFilepath() {
|
||||
expectedCurrentLogFilepath := l.getLogFilepath()
|
||||
if l.CurrentLogFile != expectedCurrentLogFilepath {
|
||||
//Change of month. Update to a new log file
|
||||
l.file.Close()
|
||||
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
|
||||
l.LogToFile = false
|
||||
return
|
||||
}
|
||||
l.CurrentLogFile = expectedCurrentLogFilepath
|
||||
l.file = f
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Close() {
|
||||
l.file.Close()
|
||||
}
|
122
src/mod/info/logviewer/logviewer.go
Normal file
@ -0,0 +1,122 @@
|
||||
package logviewer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type ViewerOption struct {
|
||||
RootFolder string //The root folder to scan for log
|
||||
Extension string //The extension the root files use, include the . in your ext (e.g. .log)
|
||||
}
|
||||
|
||||
type Viewer struct {
|
||||
option *ViewerOption
|
||||
}
|
||||
|
||||
type LogFile struct {
|
||||
Title string
|
||||
Filename string
|
||||
Fullpath string
|
||||
Filesize int64
|
||||
}
|
||||
|
||||
func NewLogViewer(option *ViewerOption) *Viewer {
|
||||
return &Viewer{option: option}
|
||||
}
|
||||
|
||||
/*
|
||||
Log Request Handlers
|
||||
*/
|
||||
//List all the log files in the log folder. Return in map[string]LogFile format
|
||||
func (v *Viewer) HandleListLog(w http.ResponseWriter, r *http.Request) {
|
||||
logFiles := v.ListLogFiles(false)
|
||||
js, _ := json.Marshal(logFiles)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Read log of a given catergory and filename
|
||||
// Require GET varaible: file and catergory
|
||||
func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
|
||||
filename, err := utils.GetPara(r, "file")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid filename given")
|
||||
return
|
||||
}
|
||||
|
||||
catergory, err := utils.GetPara(r, "catergory")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid catergory given")
|
||||
return
|
||||
}
|
||||
|
||||
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendTextResponse(w, content)
|
||||
}
|
||||
|
||||
/*
|
||||
Log Access Functions
|
||||
*/
|
||||
|
||||
func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
result := map[string][]*LogFile{}
|
||||
filepath.WalkDir(v.option.RootFolder, func(path string, di fs.DirEntry, err error) error {
|
||||
if filepath.Ext(path) == v.option.Extension {
|
||||
catergory := filepath.Base(filepath.Dir(path))
|
||||
logList, ok := result[catergory]
|
||||
if !ok {
|
||||
//this catergory hasn't been scanned before.
|
||||
logList = []*LogFile{}
|
||||
}
|
||||
|
||||
fullpath := filepath.ToSlash(path)
|
||||
if !showFullpath {
|
||||
fullpath = ""
|
||||
}
|
||||
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logList = append(logList, &LogFile{
|
||||
Title: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
|
||||
Filename: filepath.Base(path),
|
||||
Fullpath: fullpath,
|
||||
Filesize: st.Size(),
|
||||
})
|
||||
|
||||
result[catergory] = logList
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
|
||||
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
} else {
|
||||
return "", errors.New("log file not found")
|
||||
}
|
||||
}
|
@ -226,7 +226,7 @@ func (m *MDNSHost) Scan(timeout int, domainFilter string) []*NetworkHost {
|
||||
return discoveredHost
|
||||
}
|
||||
|
||||
//Get all mac address of all interfaces
|
||||
// Get all mac address of all interfaces
|
||||
func getMacAddr() ([]string, error) {
|
||||
ifas, err := net.Interfaces()
|
||||
if err != nil {
|
||||
|
@ -213,6 +213,7 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
callbackChan <- wmicResult{0, 0, err}
|
||||
return
|
||||
}
|
||||
|
||||
//Filter out the first line
|
||||
@ -251,18 +252,16 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
|
||||
|
||||
go func() {
|
||||
//Spawn a timer to terminate the cmd process if timeout
|
||||
var timer *time.Timer
|
||||
timer = time.AfterFunc(3*time.Second, func() {
|
||||
timer.Stop()
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
callbackChan <- wmicResult{0, 0, errors.New("wmic execution timeout")}
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
result := wmicResult{}
|
||||
result = <-callbackChan
|
||||
cmd = nil
|
||||
if result.Err != nil {
|
||||
log.Println("Unable to extract NIC info from wmic: " + result.Err.Error())
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -58,7 +58,7 @@ func (h *Handler) HandleAddBlockingPath(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
targetBlockingPath := BlockingPath{
|
||||
UUID: uuid.NewV4().String(),
|
||||
UUID: uuid.New().String(),
|
||||
MatchingPath: matchingPath,
|
||||
ExactMatch: exactMatch == "true",
|
||||
StatusCode: statusCode,
|
||||
|
@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
requestURL = strings.TrimPrefix(requestURL, "/")
|
||||
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
|
||||
wspHandler := websocketproxy.NewProxy(u, false)
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
@ -95,7 +95,7 @@ func NewTCProxy(options *Options) *Manager {
|
||||
|
||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
//Generate a new config from options
|
||||
configUUID := uuid.NewV4().String()
|
||||
configUUID := uuid.New().String()
|
||||
thisConfig := ProxyRelayConfig{
|
||||
UUID: configUUID,
|
||||
Name: config.Name,
|
||||
|
@ -5,22 +5,22 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//This remove the certificates in the list where either the
|
||||
//public key or the private key is missing
|
||||
// This remove the certificates in the list where either the
|
||||
// public key or the private key is missing
|
||||
func getCertPairs(certFiles []string) []string {
|
||||
crtMap := make(map[string]bool)
|
||||
pemMap := make(map[string]bool)
|
||||
keyMap := make(map[string]bool)
|
||||
|
||||
for _, filename := range certFiles {
|
||||
if filepath.Ext(filename) == ".crt" {
|
||||
crtMap[strings.TrimSuffix(filename, ".crt")] = true
|
||||
if filepath.Ext(filename) == ".pem" {
|
||||
pemMap[strings.TrimSuffix(filename, ".pem")] = true
|
||||
} else if filepath.Ext(filename) == ".key" {
|
||||
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for domain := range crtMap {
|
||||
for domain := range pemMap {
|
||||
if keyMap[domain] {
|
||||
result = append(result, domain)
|
||||
}
|
||||
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
//Get the cloest subdomain certificate from a list of domains
|
||||
// Get the cloest subdomain certificate from a list of domains
|
||||
func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
var matchingDomain string = ""
|
||||
maxLength := 0
|
||||
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
|
||||
return matchingDomain
|
||||
}
|
||||
|
||||
//Check if a requesting domain is a subdomain of a given domain
|
||||
func isSubdomain(subdomain, domain string) bool {
|
||||
subdomainParts := strings.Split(subdomain, ".")
|
||||
domainParts := strings.Split(domain, ".")
|
||||
if len(subdomainParts) < len(domainParts) {
|
||||
return false
|
||||
}
|
||||
for i := range domainParts {
|
||||
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"embed"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -15,12 +14,19 @@ import (
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
CertStore string
|
||||
verbal bool
|
||||
type CertCache struct {
|
||||
Cert *x509.Certificate
|
||||
PubKey string
|
||||
PriKey string
|
||||
}
|
||||
|
||||
//go:embed localhost.crt localhost.key
|
||||
type Manager struct {
|
||||
CertStore string //Path where all the certs are stored
|
||||
LoadedCerts []*CertCache //A list of loaded certs
|
||||
verbal bool
|
||||
}
|
||||
|
||||
//go:embed localhost.pem localhost.key
|
||||
var buildinCertStore embed.FS
|
||||
|
||||
func NewManager(certStore string, verbal bool) (*Manager, error) {
|
||||
@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
|
||||
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{
|
||||
CertStore: certStore,
|
||||
verbal: verbal,
|
||||
CertStore: certStore,
|
||||
LoadedCerts: []*CertCache{},
|
||||
verbal: verbal,
|
||||
}
|
||||
|
||||
err := thisManager.UpdateLoadedCertList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
filenames, err := m.ListCerts()
|
||||
if err != nil {
|
||||
@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
// Return a list of cert files (public and private keys)
|
||||
func (m *Manager) ListCerts() ([]string, error) {
|
||||
certs, err := ioutil.ReadDir(m.CertStore)
|
||||
certs, err := os.ReadDir(m.CertStore)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
|
||||
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) {
|
||||
//Check if the domain corrisponding cert exists
|
||||
pubKey := "./tmp/localhost.crt"
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
||||
//Direct hit
|
||||
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
|
||||
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
||||
|
||||
} else if m.CertMatchExists(helloInfo.ServerName) {
|
||||
//Use x509
|
||||
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
|
||||
} else {
|
||||
domainCerts, _ := m.ListCertDomains()
|
||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||
if cloestDomainCert != "" {
|
||||
//There is a matching parent domain for this subdomain. Use this instead.
|
||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
|
||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||
} else if m.DefaultCertExists() {
|
||||
//Use default.crt and default.key
|
||||
pubKey = filepath.Join(m.CertStore, "default.crt")
|
||||
//Fallback to legacy method of matching certificates
|
||||
/*
|
||||
domainCerts, _ := m.ListCertDomains()
|
||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||
if cloestDomainCert != "" {
|
||||
//There is a matching parent domain for this subdomain. Use this instead.
|
||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
|
||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||
} else 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)
|
||||
}
|
||||
}*/
|
||||
|
||||
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")
|
||||
}
|
||||
//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)
|
||||
}
|
||||
//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
|
||||
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
|
||||
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
|
||||
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")
|
||||
if utils.FileExists(pubKey) {
|
||||
err := os.Remove(pubKey)
|
||||
@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
|
||||
}
|
||||
}
|
||||
|
||||
//Update the cert list
|
||||
m.UpdateLoadedCertList()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
|
||||
return false
|
||||
}
|
||||
// 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") {
|
||||
// The file contains a private key
|
||||
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@ -93,8 +93,6 @@ func (m *Monitor) ExecuteUptimeCheck() {
|
||||
Latency: laterncy,
|
||||
}
|
||||
|
||||
//fmt.Println(thisRecord)
|
||||
|
||||
} else {
|
||||
log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
|
||||
continue
|
||||
@ -219,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
||||
}
|
||||
|
||||
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 {
|
||||
//Try replace the http with https and vise versa
|
||||
rewriteURL := ""
|
||||
@ -229,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
|
||||
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
||||
}
|
||||
|
||||
resp, err = http.Get(rewriteURL)
|
||||
resp, err = client.Get(rewriteURL)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
|
||||
//Invalid downstream reverse proxy settings, but it is online
|
||||
@ -238,9 +240,11 @@ func getWebsiteStatus(url string) (int, error) {
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
status_code := resp.StatusCode
|
||||
return status_code, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
status_code := resp.StatusCode
|
||||
resp.Body.Close()
|
||||
return status_code, nil
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -40,46 +38,6 @@ func SendOK(w http.ResponseWriter) {
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
/*
|
||||
The paramter move function (mv)
|
||||
|
||||
You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
|
||||
r (HTTP Request Object)
|
||||
getParamter (string, aka $_GET['This string])
|
||||
|
||||
Will return
|
||||
Paramter string (if any)
|
||||
Error (if error)
|
||||
|
||||
*/
|
||||
/*
|
||||
func Mv(r *http.Request, getParamter string, postMode bool) (string, error) {
|
||||
if postMode == false {
|
||||
//Access the paramter via GET
|
||||
keys, ok := r.URL.Query()[getParamter]
|
||||
|
||||
if !ok || len(keys[0]) < 1 {
|
||||
//log.Println("Url Param " + getParamter +" is missing")
|
||||
return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
|
||||
// Query()["key"] will return an array of items,
|
||||
// we only want the single item.
|
||||
key := keys[0]
|
||||
return string(key), nil
|
||||
} else {
|
||||
//Access the parameter via POST
|
||||
r.ParseForm()
|
||||
x := r.Form.Get(getParamter)
|
||||
if len(x) == 0 || x == "" {
|
||||
return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
return string(x), nil
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
// Get GET parameter
|
||||
func GetPara(r *http.Request, key string) (string, error) {
|
||||
keys, ok := r.URL.Query()[key]
|
||||
@ -101,6 +59,40 @@ func PostPara(r *http.Request, key string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get POST paramter as boolean, accept 1 or true
|
||||
func PostBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
|
||||
if x == "1" || strings.ToLower(x) == "true" {
|
||||
return true, nil
|
||||
} else if x == "0" || strings.ToLower(x) == "false" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST paramter as int
|
||||
func PostInt(r *http.Request, key string) (int, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
rx, err := strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return rx, nil
|
||||
}
|
||||
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
@ -131,30 +123,6 @@ func TimeToString(targetTime time.Time) string {
|
||||
return targetTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func LoadImageAsBase64(filepath string) (string, error) {
|
||||
if !FileExists(filepath) {
|
||||
return "", errors.New("File not exists")
|
||||
}
|
||||
f, _ := os.Open(filepath)
|
||||
reader := bufio.NewReader(f)
|
||||
content, _ := io.ReadAll(reader)
|
||||
encoded := base64.StdEncoding.EncodeToString(content)
|
||||
return string(encoded), nil
|
||||
}
|
||||
|
||||
// Use for redirections
|
||||
func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string {
|
||||
if strings.Count(requestURI, "/") == 1 {
|
||||
//Already root level
|
||||
return redirectionLocation
|
||||
}
|
||||
for i := 0; i < strings.Count(requestURI, "/")-1; i++ {
|
||||
redirectionLocation = "../" + redirectionLocation
|
||||
}
|
||||
|
||||
return redirectionLocation
|
||||
}
|
||||
|
||||
// Check if given string in a given slice
|
||||
func StringInArray(arr []string, str string) bool {
|
||||
for _, a := range arr {
|
||||
|
406
src/mod/webserv/filemanager/filemanager.go
Normal file
@ -0,0 +1,406 @@
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
File Manager
|
||||
|
||||
This is a simple package that handles file management
|
||||
under the web server directory
|
||||
*/
|
||||
|
||||
type FileManager struct {
|
||||
Directory string
|
||||
}
|
||||
|
||||
// Create a new file manager with directory as root
|
||||
func NewFileManager(directory string) *FileManager {
|
||||
return &FileManager{
|
||||
Directory: directory,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle listing of a given directory
|
||||
func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
directory, err := utils.GetPara(r, "dir")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid directory given")
|
||||
return
|
||||
}
|
||||
|
||||
// Construct the absolute path to the target directory
|
||||
targetDir := filepath.Join(fm.Directory, directory)
|
||||
|
||||
// Open the target directory
|
||||
dirEntries, err := os.ReadDir(targetDir)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to open directory")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a slice to hold the file information
|
||||
var files []map[string]interface{} = []map[string]interface{}{}
|
||||
|
||||
// Iterate through the directory entries
|
||||
for _, dirEntry := range dirEntries {
|
||||
fileInfo := make(map[string]interface{})
|
||||
fileInfo["filename"] = dirEntry.Name()
|
||||
fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name())
|
||||
fileInfo["isDir"] = dirEntry.IsDir()
|
||||
|
||||
// Get file size and last modified time
|
||||
finfo, err := dirEntry.Info()
|
||||
if err != nil {
|
||||
//unable to load its info. Skip this file
|
||||
continue
|
||||
}
|
||||
fileInfo["lastModified"] = finfo.ModTime().Unix()
|
||||
if !dirEntry.IsDir() {
|
||||
// If it's a file, get its size
|
||||
fileInfo["size"] = finfo.Size()
|
||||
} else {
|
||||
// If it's a directory, set size to 0
|
||||
fileInfo["size"] = 0
|
||||
}
|
||||
|
||||
// Append file info to the list
|
||||
files = append(files, fileInfo)
|
||||
}
|
||||
|
||||
// Serialize the file info slice to JSON
|
||||
jsonData, err := json.Marshal(files)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to marshal JSON")
|
||||
return
|
||||
}
|
||||
|
||||
// Set response headers and send the JSON response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
// Handle upload of a file (multi-part), 25MB max
|
||||
func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
dir, err := utils.PostPara(r, "dir")
|
||||
if err != nil {
|
||||
log.Println("no dir given")
|
||||
utils.SendErrorResponse(w, "invalid dir given")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the multi-part form data
|
||||
err = r.ParseMultipartForm(25 << 20)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to parse form data")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the uploaded file
|
||||
file, fheader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
utils.SendErrorResponse(w, "unable to get uploaded file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Specify the directory where you want to save the uploaded file
|
||||
uploadDir := filepath.Join(fm.Directory, dir)
|
||||
if !utils.FileExists(uploadDir) {
|
||||
utils.SendErrorResponse(w, "upload target directory not exists")
|
||||
return
|
||||
}
|
||||
|
||||
filename := sanitizeFilename(fheader.Filename)
|
||||
if !isValidFilename(filename) {
|
||||
utils.SendErrorResponse(w, "filename contain invalid or reserved characters")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the file on the server
|
||||
filePath := filepath.Join(uploadDir, filepath.Base(filename))
|
||||
out, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to create file on the server")
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Copy the uploaded file to the server
|
||||
_, err = io.Copy(out, file)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to copy file to server")
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with a success message or appropriate response
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle download of a selected file, serve with content dispose header
|
||||
func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
filename, err := utils.GetPara(r, "file")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid filepath given")
|
||||
return
|
||||
}
|
||||
|
||||
previewMode, _ := utils.GetPara(r, "preview")
|
||||
if previewMode == "true" {
|
||||
// Serve the file using http.ServeFile
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
http.ServeFile(w, r, filePath)
|
||||
} else {
|
||||
// Trigger a download with content disposition headers
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleNewFolder creates a new folder in the specified directory
|
||||
func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the directory name from the request
|
||||
dirName, err := utils.GetPara(r, "path")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid directory name")
|
||||
return
|
||||
}
|
||||
|
||||
//Prevent path escape
|
||||
dirName = strings.ReplaceAll(dirName, "\\", "/")
|
||||
dirName = strings.ReplaceAll(dirName, "../", "")
|
||||
|
||||
// Specify the directory where you want to create the new folder
|
||||
newFolderPath := filepath.Join(fm.Directory, dirName)
|
||||
|
||||
// Check if the folder already exists
|
||||
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
|
||||
// Create the new folder
|
||||
err := os.Mkdir(newFolderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to create the new folder")
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with a success message or appropriate response
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
// If the folder already exists, respond with an error
|
||||
utils.SendErrorResponse(w, "folder already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFileCopy copies a file or directory from the source path to the destination path
|
||||
func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the source and destination paths from the request
|
||||
srcPath, err := utils.PostPara(r, "srcpath")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid source path")
|
||||
return
|
||||
}
|
||||
|
||||
destPath, err := utils.PostPara(r, "destpath")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid destination path")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and sanitize the source and destination paths
|
||||
srcPath = filepath.Clean(srcPath)
|
||||
destPath = filepath.Clean(destPath)
|
||||
|
||||
// Construct the absolute paths
|
||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||
|
||||
// Check if the source path exists
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "source path does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the destination path exists
|
||||
if _, err := os.Stat(absDestPath); os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "destination path does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
//Join the name to create final paste filename
|
||||
absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath))
|
||||
//Reject opr if already exists
|
||||
if utils.FileExists(absDestPath) {
|
||||
utils.SendErrorResponse(w, "target already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Perform the copy operation based on whether the source is a file or directory
|
||||
if isDir(absSrcPath) {
|
||||
// Recursive copy for directories
|
||||
err := copyDirectory(absSrcPath, absDestPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Copy a single file
|
||||
err := copyFile(absSrcPath, absDestPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the source and destination paths from the request
|
||||
srcPath, err := utils.GetPara(r, "srcpath")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid source path")
|
||||
return
|
||||
}
|
||||
|
||||
destPath, err := utils.GetPara(r, "destpath")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid destination path")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and sanitize the source and destination paths
|
||||
srcPath = filepath.Clean(srcPath)
|
||||
destPath = filepath.Clean(destPath)
|
||||
|
||||
// Construct the absolute paths
|
||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||
|
||||
// Check if the source path exists
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "source path does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the destination path exists
|
||||
if _, err := os.Stat(absDestPath); !os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "destination path already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Rename the source to the destination
|
||||
err = os.Rename(absSrcPath, absDestPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err))
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the target file or directory path from the request
|
||||
filePath, err := utils.GetPara(r, "file")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid file path")
|
||||
return
|
||||
}
|
||||
|
||||
// Construct the absolute path to the target file or directory
|
||||
absPath := filepath.Join(fm.Directory, filePath)
|
||||
|
||||
// Check if the target path exists
|
||||
_, err = os.Stat(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "file or directory does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize a map to hold file properties
|
||||
fileProps := make(map[string]interface{})
|
||||
fileProps["filename"] = filepath.Base(absPath)
|
||||
fileProps["filepath"] = filePath
|
||||
fileProps["isDir"] = isDir(absPath)
|
||||
|
||||
// Get file size and last modified time
|
||||
finfo, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to retrieve file properties")
|
||||
return
|
||||
}
|
||||
fileProps["lastModified"] = finfo.ModTime().Unix()
|
||||
if !isDir(absPath) {
|
||||
// If it's a file, get its size
|
||||
fileProps["size"] = finfo.Size()
|
||||
} else {
|
||||
// If it's a directory, calculate its total size containing all child files and folders
|
||||
totalSize, err := calculateDirectorySize(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to calculate directory size")
|
||||
return
|
||||
}
|
||||
fileProps["size"] = totalSize
|
||||
}
|
||||
|
||||
// Count the number of sub-files and sub-folders
|
||||
numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to count sub-files and sub-folders")
|
||||
return
|
||||
}
|
||||
fileProps["fileCounts"] = numSubFiles
|
||||
fileProps["folderCounts"] = numSubFolders
|
||||
|
||||
// Serialize the file properties to JSON
|
||||
jsonData, err := json.Marshal(fileProps)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to marshal JSON")
|
||||
return
|
||||
}
|
||||
|
||||
// Set response headers and send the JSON response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
// HandleFileDelete deletes a file or directory
|
||||
func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the target file or directory path from the request
|
||||
filePath, err := utils.PostPara(r, "target")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid file path")
|
||||
return
|
||||
}
|
||||
|
||||
// Construct the absolute path to the target file or directory
|
||||
absPath := filepath.Join(fm.Directory, filePath)
|
||||
|
||||
// Check if the target path exists
|
||||
_, err = os.Stat(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "file or directory does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the file or directory
|
||||
err = os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "error deleting file or directory")
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with a success message or appropriate response
|
||||
utils.SendOK(w)
|
||||
}
|
156
src/mod/webserv/filemanager/utils.go
Normal file
@ -0,0 +1,156 @@
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isValidFilename checks if a given filename is safe and valid.
|
||||
func isValidFilename(filename string) bool {
|
||||
// Define a list of disallowed characters and reserved names
|
||||
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
|
||||
reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} // Add more if needed
|
||||
|
||||
// Check for disallowed characters
|
||||
for _, char := range disallowedChars {
|
||||
if strings.Contains(filename, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for reserved names (case-insensitive)
|
||||
lowerFilename := strings.ToUpper(filename)
|
||||
for _, reserved := range reservedNames {
|
||||
if lowerFilename == reserved {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty filename
|
||||
if filename == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// The filename is considered valid
|
||||
return true
|
||||
}
|
||||
|
||||
// sanitizeFilename sanitizes a given filename by removing disallowed characters.
|
||||
func sanitizeFilename(filename string) string {
|
||||
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
|
||||
|
||||
// Replace disallowed characters with underscores
|
||||
for _, char := range disallowedChars {
|
||||
filename = strings.ReplaceAll(filename, char, "_")
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
// copyFile copies a single file from source to destination
|
||||
func copyFile(srcPath, destPath string) error {
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyDirectory recursively copies a directory and its contents from source to destination
|
||||
func copyDirectory(srcPath, destPath string) error {
|
||||
// Create the destination directory
|
||||
err := os.MkdirAll(destPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcEntryPath := filepath.Join(srcPath, entry.Name())
|
||||
destEntryPath := filepath.Join(destPath, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
err := copyDirectory(srcEntryPath, destEntryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := copyFile(srcEntryPath, destEntryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDir checks if the given path is a directory
|
||||
func isDir(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
|
||||
// calculateDirectorySize calculates the total size of a directory and its contents
|
||||
func calculateDirectorySize(dirPath string) (int64, error) {
|
||||
var totalSize int64
|
||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalSize += info.Size()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return totalSize, nil
|
||||
}
|
||||
|
||||
// countSubFilesAndFolders counts the number of sub-files and sub-folders within a directory
|
||||
func countSubFilesAndFolders(dirPath string) (int, int, error) {
|
||||
var numSubFiles, numSubFolders int
|
||||
|
||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
numSubFolders++
|
||||
} else {
|
||||
numSubFiles++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// Subtract 1 from numSubFolders to exclude the root directory itself
|
||||
return numSubFiles, numSubFolders - 1, nil
|
||||
}
|
89
src/mod/webserv/handler.go
Normal file
@ -0,0 +1,89 @@
|
||||
package webserv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Handler.go
|
||||
|
||||
Handler for web server options change
|
||||
web server is directly listening to the TCP port
|
||||
handlers in this script are for setting change only
|
||||
*/
|
||||
|
||||
type StaticWebServerStatus struct {
|
||||
ListeningPort int
|
||||
EnableDirectoryListing bool
|
||||
WebRoot string
|
||||
Running bool
|
||||
EnableWebDirManager bool
|
||||
}
|
||||
|
||||
// Handle getting current static web server status
|
||||
func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
listeningPortInt, _ := strconv.Atoi(ws.option.Port)
|
||||
currentStatus := StaticWebServerStatus{
|
||||
ListeningPort: listeningPortInt,
|
||||
EnableDirectoryListing: ws.option.EnableDirectoryListing,
|
||||
WebRoot: ws.option.WebRoot,
|
||||
Running: ws.isRunning,
|
||||
EnableWebDirManager: ws.option.EnableWebDirManager,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(currentStatus)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle request for starting the static web server
|
||||
func (ws *WebServer) HandleStartServer(w http.ResponseWriter, r *http.Request) {
|
||||
err := ws.Start()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle request for stopping the static web server
|
||||
func (ws *WebServer) HandleStopServer(w http.ResponseWriter, r *http.Request) {
|
||||
err := ws.Stop()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle change server listening port request
|
||||
func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
||||
newPort, err := utils.PostInt(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port number given")
|
||||
return
|
||||
}
|
||||
|
||||
err = ws.ChangePort(strconv.Itoa(newPort))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Change enable directory listing settings
|
||||
func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Request) {
|
||||
enableList, err := utils.PostBool(r, "enable")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid setting given")
|
||||
return
|
||||
}
|
||||
|
||||
ws.option.EnableDirectoryListing = enableList
|
||||
utils.SendOK(w)
|
||||
}
|
41
src/mod/webserv/middleware.go
Normal file
@ -0,0 +1,41 @@
|
||||
package webserv
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Convert a request path (e.g. /index.html) into physical path on disk
|
||||
func (ws *WebServer) resolveFileDiskPath(requestPath string) string {
|
||||
fileDiskpath := filepath.Join(ws.option.WebRoot, "html", requestPath)
|
||||
|
||||
//Force convert it to slash even if the host OS is on Windows
|
||||
fileDiskpath = filepath.Clean(fileDiskpath)
|
||||
fileDiskpath = strings.ReplaceAll(fileDiskpath, "\\", "/")
|
||||
return fileDiskpath
|
||||
|
||||
}
|
||||
|
||||
// File server middleware to handle directory listing (and future expansion)
|
||||
func (ws *WebServer) fsMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !ws.option.EnableDirectoryListing {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//This is a folder. Let check if index exists
|
||||
if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.html")) {
|
||||
|
||||
} else if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.htm")) {
|
||||
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
61
src/mod/webserv/templates/index.html
Normal file
@ -0,0 +1,61 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello Zoraxy</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Tahoma, sans-serif;
|
||||
background-color: #f6f6f6;
|
||||
color: #2d2e30;
|
||||
}
|
||||
.sectionHeader{
|
||||
background-color: #c4d0d9;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.sectionHeader h3{
|
||||
text-align: center;
|
||||
}
|
||||
.container{
|
||||
margin: 4em;
|
||||
margin-left: 10em;
|
||||
margin-right: 10em;
|
||||
background-color: #fefefe;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.container{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.sectionHeader{
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.textcontainer{
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="sectionHeader">
|
||||
<h3>Welcome to Zoraxy Static Web Server!</h3>
|
||||
</div>
|
||||
<div class="textcontainer">
|
||||
<p>If you see this page, that means your static web server is running.<br>
|
||||
By default, all the html files are stored under <code>./web/html/</code>
|
||||
relative to the zoraxy runtime directory.<br>
|
||||
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
||||
</p>
|
||||
<p>
|
||||
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
|
||||
Thank you for using Zoraxy!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
18
src/mod/webserv/utils.go
Normal file
@ -0,0 +1,18 @@
|
||||
package webserv
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// IsPortInUse checks if a port is in use.
|
||||
func IsPortInUse(port string) bool {
|
||||
listener, err := net.Listen("tcp", "localhost:"+port)
|
||||
if err != nil {
|
||||
// If there was an error, the port is in use.
|
||||
return true
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
// No error means the port is available.
|
||||
return false
|
||||
}
|
200
src/mod/webserv/webserv.go
Normal file
@ -0,0 +1,200 @@
|
||||
package webserv
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"imuslab.com/zoraxy/mod/webserv/filemanager"
|
||||
)
|
||||
|
||||
/*
|
||||
Static Web Server package
|
||||
|
||||
This module host a static web server
|
||||
*/
|
||||
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
type WebServerOptions struct {
|
||||
Port string //Port for listening
|
||||
EnableDirectoryListing bool //Enable listing of directory
|
||||
WebRoot string //Folder for stroing the static web folders
|
||||
EnableWebDirManager bool //Enable web file manager to handle files in web directory
|
||||
Sysdb *database.Database //Database for storing configs
|
||||
}
|
||||
|
||||
type WebServer struct {
|
||||
FileManager *filemanager.FileManager
|
||||
|
||||
mux *http.ServeMux
|
||||
server *http.Server
|
||||
option *WebServerOptions
|
||||
isRunning bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewWebServer creates a new WebServer instance. One instance only
|
||||
func NewWebServer(options *WebServerOptions) *WebServer {
|
||||
if !utils.FileExists(options.WebRoot) {
|
||||
//Web root folder not exists. Create one with default templates
|
||||
os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
|
||||
os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
|
||||
indexTemplate, err := templates.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
log.Println("Failed to read static wev server template file: ", err.Error())
|
||||
} else {
|
||||
os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Create a new file manager if it is enabled
|
||||
var newDirManager *filemanager.FileManager
|
||||
if options.EnableWebDirManager {
|
||||
fm := filemanager.NewFileManager(filepath.Join(options.WebRoot, "/html"))
|
||||
newDirManager = fm
|
||||
}
|
||||
|
||||
//Create new table to store the config
|
||||
options.Sysdb.NewTable("webserv")
|
||||
return &WebServer{
|
||||
mux: http.NewServeMux(),
|
||||
FileManager: newDirManager,
|
||||
option: options,
|
||||
isRunning: false,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the configuration to previous config
|
||||
func (ws *WebServer) RestorePreviousState() {
|
||||
//Set the port
|
||||
port := ws.option.Port
|
||||
ws.option.Sysdb.Read("webserv", "port", &port)
|
||||
ws.option.Port = port
|
||||
|
||||
//Set the enable directory list
|
||||
enableDirList := ws.option.EnableDirectoryListing
|
||||
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
|
||||
ws.option.EnableDirectoryListing = enableDirList
|
||||
|
||||
//Check the running state
|
||||
webservRunning := true
|
||||
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
||||
if webservRunning {
|
||||
ws.Start()
|
||||
} else {
|
||||
ws.Stop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ChangePort changes the server's port.
|
||||
func (ws *WebServer) ChangePort(port string) error {
|
||||
if IsPortInUse(port) {
|
||||
return errors.New("Selected port is used by another process")
|
||||
}
|
||||
|
||||
if ws.isRunning {
|
||||
if err := ws.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ws.option.Port = port
|
||||
ws.server.Addr = ":" + port
|
||||
|
||||
err := ws.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws.option.Sysdb.Write("webserv", "port", port)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get current using port in options
|
||||
func (ws *WebServer) GetListeningPort() string {
|
||||
return ws.option.Port
|
||||
}
|
||||
|
||||
// Start starts the web server.
|
||||
func (ws *WebServer) Start() error {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
//Check if server already running
|
||||
if ws.isRunning {
|
||||
return fmt.Errorf("web server is already running")
|
||||
}
|
||||
|
||||
//Check if the port is usable
|
||||
if IsPortInUse(ws.option.Port) {
|
||||
return errors.New("Port already in use or access denied by host OS")
|
||||
}
|
||||
|
||||
//Dispose the old mux and create a new one
|
||||
ws.mux = http.NewServeMux()
|
||||
|
||||
//Create a static web server
|
||||
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
|
||||
ws.mux.Handle("/", ws.fsMiddleware(fs))
|
||||
|
||||
ws.server = &http.Server{
|
||||
Addr: ":" + ws.option.Port,
|
||||
Handler: ws.mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := ws.server.ListenAndServe(); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
fmt.Printf("Web server error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
|
||||
ws.isRunning = true
|
||||
ws.option.Sysdb.Write("webserv", "enabled", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the web server.
|
||||
func (ws *WebServer) Stop() error {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
if !ws.isRunning {
|
||||
return fmt.Errorf("web server is not running")
|
||||
}
|
||||
|
||||
if err := ws.server.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws.isRunning = false
|
||||
ws.option.Sysdb.Write("webserv", "enabled", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDirectoryListing enables or disables directory listing.
|
||||
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
|
||||
ws.option.EnableDirectoryListing = enable
|
||||
ws.option.Sysdb.Write("webserv", "dirlist", enable)
|
||||
}
|
||||
|
||||
// Close stops the web server without returning an error.
|
||||
func (ws *WebServer) Close() {
|
||||
ws.Stop()
|
||||
}
|
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -22,91 +21,109 @@ var (
|
||||
|
||||
// Add user customizable reverse proxy
|
||||
func ReverseProxtInit() {
|
||||
/*
|
||||
Load Reverse Proxy Global Settings
|
||||
*/
|
||||
inboundPort := 80
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
sysdb.Read("settings", "inbound", &inboundPort)
|
||||
log.Println("Serving inbound port ", inboundPort)
|
||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||
} else {
|
||||
log.Println("Inbound port not set. Using default (80)")
|
||||
SystemWideLogger.Println("Inbound port not set. Using default (80)")
|
||||
}
|
||||
|
||||
useTls := false
|
||||
sysdb.Read("settings", "usetls", &useTls)
|
||||
if useTls {
|
||||
log.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
} else {
|
||||
log.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||
}
|
||||
|
||||
forceLatestTLSVersion := false
|
||||
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
|
||||
if forceLatestTLSVersion {
|
||||
log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
|
||||
SystemWideLogger.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
|
||||
} else {
|
||||
log.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
|
||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||
if listenOnPort80 {
|
||||
SystemWideLogger.Println("Port 80 listener enabled")
|
||||
} else {
|
||||
SystemWideLogger.Println("Port 80 listener disabled")
|
||||
}
|
||||
|
||||
forceHttpsRedirect := false
|
||||
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
||||
if forceHttpsRedirect {
|
||||
log.Println("Force HTTPS mode enabled")
|
||||
SystemWideLogger.Println("Force HTTPS mode enabled")
|
||||
//Port 80 listener must be enabled to perform http -> https redirect
|
||||
listenOnPort80 = true
|
||||
} else {
|
||||
log.Println("Force HTTPS mode disabled")
|
||||
SystemWideLogger.Println("Force HTTPS mode disabled")
|
||||
}
|
||||
|
||||
/*
|
||||
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{
|
||||
HostUUID: nodeUUID,
|
||||
HostVersion: version,
|
||||
Port: inboundPort,
|
||||
UseTls: useTls,
|
||||
ForceTLSLatest: forceLatestTLSVersion,
|
||||
NoCache: developmentMode,
|
||||
ListenOnPort80: listenOnPort80,
|
||||
ForceHttpsRedirect: forceHttpsRedirect,
|
||||
TlsManager: tlsCertManager,
|
||||
RedirectRuleTable: redirectTable,
|
||||
GeodbStore: geodbStore,
|
||||
StatisticCollector: statisticCollector,
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
|
||||
return
|
||||
}
|
||||
|
||||
dynamicProxyRouter = dprouter
|
||||
|
||||
//Load all conf from files
|
||||
/*
|
||||
|
||||
Load all conf from files
|
||||
|
||||
*/
|
||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
log.Println("Failed to load "+filepath.Base(conf), err.Error())
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
SkipCertValidations: record.SkipTlsValidation,
|
||||
RequireBasicAuth: record.RequireBasicAuth,
|
||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
||||
})
|
||||
} else if record.ProxyType == "vdir" {
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
|
||||
RootName: record.Rootname,
|
||||
Domain: record.ProxyTarget,
|
||||
RequireTLS: record.UseTLS,
|
||||
SkipCertValidations: record.SkipTlsValidation,
|
||||
RequireBasicAuth: record.RequireBasicAuth,
|
||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
||||
})
|
||||
} else {
|
||||
log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
|
||||
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
|
||||
@ -114,7 +131,7 @@ func ReverseProxtInit() {
|
||||
//reverse proxy server in front of this service
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
log.Println("Dynamic Reverse Proxy service started")
|
||||
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
||||
|
||||
//Add all proxy services to uptime monitor
|
||||
//Create a uptime monitor service
|
||||
@ -125,7 +142,7 @@ func ReverseProxtInit() {
|
||||
Interval: 300, //5 minutes
|
||||
MaxRecordsStore: 288, //1 day
|
||||
})
|
||||
log.Println("Uptime Monitor background service started")
|
||||
SystemWideLogger.Println("Uptime Monitor background service started")
|
||||
}()
|
||||
|
||||
}
|
||||
@ -158,7 +175,7 @@ func ReverseProxyHandleOnOff(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 {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
@ -177,6 +194,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
useTLS := (tls == "true")
|
||||
|
||||
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
||||
if bypassGlobalTLS == "" {
|
||||
bypassGlobalTLS = "false"
|
||||
}
|
||||
|
||||
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
||||
|
||||
stv, _ := utils.PostPara(r, "tlsval")
|
||||
if stv == "" {
|
||||
stv = "false"
|
||||
@ -219,93 +243,112 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
rootname := ""
|
||||
if eptype == "vdir" {
|
||||
vdir, 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,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
}
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
|
||||
|
||||
} else if eptype == "subd" {
|
||||
subdomain, err := utils.PostPara(r, "rootname")
|
||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||
if eptype == "host" {
|
||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "subdomain not defined")
|
||||
return
|
||||
}
|
||||
rootname = subdomain
|
||||
thisOption := dynamicproxy.SubdOptions{
|
||||
MatchingDomain: subdomain,
|
||||
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
|
||||
//I/O
|
||||
ProxyType: dynamicproxy.ProxyType_Host,
|
||||
RootOrMatchingDomain: rootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
//TLS
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
//VDir
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
//Custom headers
|
||||
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
||||
//Auth
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
DefaultSiteOption: 0,
|
||||
DefaultSiteValue: "",
|
||||
}
|
||||
|
||||
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to prepare proxy route to target endpoint: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
|
||||
proxyEndpointCreated = &thisProxyEndpoint
|
||||
} else if eptype == "root" {
|
||||
//Get the default site options and target
|
||||
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "default site action not defined")
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: false,
|
||||
|
||||
DefaultSiteOption: defaultSiteOption,
|
||||
DefaultSiteValue: dsVal,
|
||||
}
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
||||
} else if eptype == "root" {
|
||||
rootname = "root"
|
||||
thisOption := dynamicproxy.RootOptions{
|
||||
ProxyLocation: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
preparedRootProxyRoute, err := dynamicProxyRouter.PrepareProxyRoute(&rootRoutingEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to prepare root routing: "+err.Error())
|
||||
return
|
||||
}
|
||||
dynamicProxyRouter.SetRootProxy(&thisOption)
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
|
||||
proxyEndpointCreated = &rootRoutingEndpoint
|
||||
} else {
|
||||
//Invalid eptype
|
||||
utils.SendErrorResponse(w, "Invalid endpoint type")
|
||||
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||
return
|
||||
}
|
||||
|
||||
//Save it
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: eptype,
|
||||
Rootname: rootname,
|
||||
ProxyTarget: endpoint,
|
||||
UseTLS: useTLS,
|
||||
SkipTlsValidation: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
//Save the config to file
|
||||
err = SaveReverseProxyConfig(proxyEndpointCreated)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
|
||||
return
|
||||
}
|
||||
SaveReverseProxyConfig(&thisProxyConfigRecord)
|
||||
|
||||
//Update utm if exists
|
||||
if uptimeMonitor != nil {
|
||||
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
|
||||
uptimeMonitor.CleanRecords()
|
||||
}
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/*
|
||||
ReverseProxyHandleEditEndpoint handles proxy endpoint edit
|
||||
This endpoint do not handle
|
||||
basic auth credential update. The credential
|
||||
will be loaded from old config and reused
|
||||
(host only, for root use Default Site page to edit)
|
||||
This endpoint do not handle basic auth credential update.
|
||||
The credential will be loaded from old config and reused
|
||||
*/
|
||||
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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Target proxy rule not defined")
|
||||
@ -329,9 +372,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if stv == "" {
|
||||
stv = "false"
|
||||
}
|
||||
|
||||
skipTlsValidation := (stv == "true")
|
||||
|
||||
//Load bypass TLS option
|
||||
bpgtls, _ := utils.PostPara(r, "bpgtls")
|
||||
if bpgtls == "" {
|
||||
bpgtls = "false"
|
||||
}
|
||||
bypassGlobalTLS := (bpgtls == "true")
|
||||
|
||||
rba, _ := utils.PostPara(r, "bauth")
|
||||
if rba == "" {
|
||||
rba = "false"
|
||||
@ -340,48 +389,35 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
requireBasicAuth := (rba == "true")
|
||||
|
||||
//Load the previous basic auth credentials from current proxy rules
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(eptype, rootNameOrMatchingDomain)
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
thisOption := dynamicproxy.VdirOptions{
|
||||
RootName: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
dynamicProxyRouter.RemoveProxy("vdir", thisOption.RootName)
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
|
||||
//Generate a new proxyEndpoint from the new config
|
||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||
newProxyEndpoint.Domain = endpoint
|
||||
newProxyEndpoint.RequireTLS = useTLS
|
||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||
|
||||
} else if eptype == "subd" {
|
||||
thisOption := dynamicproxy.SubdOptions{
|
||||
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
dynamicProxyRouter.RemoveProxy("subd", thisOption.MatchingDomain)
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
||||
//Prepare to replace the current routing rule
|
||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
|
||||
//Save it to file
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: eptype,
|
||||
Rootname: targetProxyEntry.RootOrMatchingDomain,
|
||||
ProxyTarget: endpoint,
|
||||
UseTLS: useTLS,
|
||||
SkipTlsValidation: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
SaveReverseProxyConfig(&thisProxyConfigRecord)
|
||||
SaveReverseProxyConfig(newProxyEndpoint)
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -392,19 +428,19 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.PostPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
err = dynamicProxyRouter.RemoveProxy(ptype, ep)
|
||||
//Remove the config from runtime
|
||||
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
RemoveReverseProxyConfig(ep)
|
||||
//Remove the config from file
|
||||
err = RemoveReverseProxyConfig(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update utm if exists
|
||||
if uptimeMonitor != nil {
|
||||
@ -412,6 +448,9 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
uptimeMonitor.CleanRecords()
|
||||
}
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -431,14 +470,8 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.GetPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -460,17 +493,6 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
@ -478,7 +500,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -528,19 +550,10 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
targetProxy.BasicAuthCredentials = mergedCredentials
|
||||
|
||||
//Save it to file
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: ptype,
|
||||
Rootname: targetProxy.RootOrMatchingDomain,
|
||||
ProxyTarget: targetProxy.Domain,
|
||||
UseTLS: targetProxy.RequireTLS,
|
||||
SkipTlsValidation: targetProxy.SkipCertValidations,
|
||||
RequireBasicAuth: targetProxy.RequireBasicAuth,
|
||||
BasicAuthCredentials: targetProxy.BasicAuthCredentials,
|
||||
}
|
||||
SaveReverseProxyConfig(&thisProxyConfigRecord)
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
//Replace runtime configuration
|
||||
dynamicProxyRouter.SaveProxy(ptype, ep, targetProxy)
|
||||
targetProxy.UpdateToRuntime()
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "invalid usage", http.StatusMethodNotAllowed)
|
||||
@ -548,22 +561,157 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
// List, Update or Remove the exception paths for basic auth.
|
||||
func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
ep, err := utils.GetPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ep given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//List all the exception paths for this proxy
|
||||
results := targetProxy.BasicAuthExceptionRules
|
||||
if results == nil {
|
||||
//It is a config from a really old version of zoraxy. Overwrite it with empty array
|
||||
results = []*dynamicproxy.BasicAuthExceptionRule{}
|
||||
}
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
ep, err := utils.PostPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ep given")
|
||||
return
|
||||
}
|
||||
|
||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the prefix starts with /. If not, prepend it
|
||||
if !strings.HasPrefix(matchingPrefix, "/") {
|
||||
matchingPrefix = "/" + matchingPrefix
|
||||
}
|
||||
|
||||
//Add a new exception rule if it is not already exists
|
||||
alreadyExists := false
|
||||
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules {
|
||||
if thisExceptionRule.PathPrefix == matchingPrefix {
|
||||
alreadyExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if alreadyExists {
|
||||
utils.SendErrorResponse(w, "This matching path already exists")
|
||||
return
|
||||
}
|
||||
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
|
||||
PathPrefix: strings.TrimSpace(matchingPrefix),
|
||||
})
|
||||
|
||||
//Save configs to runtime and file
|
||||
targetProxy.UpdateToRuntime()
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
// Delete a rule
|
||||
ep, err := utils.PostPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ep given")
|
||||
return
|
||||
}
|
||||
|
||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid matching prefix given")
|
||||
return
|
||||
}
|
||||
|
||||
// Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
|
||||
matchingExists := false
|
||||
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules {
|
||||
if thisExceptionalRule.PathPrefix != matchingPrefix {
|
||||
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
|
||||
} else {
|
||||
matchingExists = true
|
||||
}
|
||||
}
|
||||
|
||||
if !matchingExists {
|
||||
utils.SendErrorResponse(w, "target matching rule not exists")
|
||||
return
|
||||
}
|
||||
|
||||
targetProxy.BasicAuthExceptionRules = newExceptionRuleList
|
||||
|
||||
// Save configs to runtime and file
|
||||
targetProxy.UpdateToRuntime()
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(dynamicProxyRouter)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
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 {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
if eptype == "host" {
|
||||
results := []*dynamicproxy.ProxyEndpoint{}
|
||||
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
|
||||
})
|
||||
|
||||
@ -571,19 +719,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if eptype == "root" {
|
||||
@ -594,6 +729,35 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle port 80 incoming traffics
|
||||
func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
|
||||
enabled, err := utils.GetPara(r, "enable")
|
||||
if err != nil {
|
||||
//Load the current status
|
||||
currentEnabled := false
|
||||
err = sysdb.Read("settings", "listenP80", ¤tEnabled)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enabled == "true" {
|
||||
sysdb.Write("settings", "listenP80", true)
|
||||
SystemWideLogger.Println("Enabling port 80 listener")
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
} else if enabled == "false" {
|
||||
sysdb.Write("settings", "listenP80", false)
|
||||
SystemWideLogger.Println("Disabling port 80 listener")
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle https redirect
|
||||
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
useRedirect, err := utils.GetPara(r, "set")
|
||||
@ -614,11 +778,11 @@ func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if useRedirect == "true" {
|
||||
sysdb.Write("settings", "redirect", true)
|
||||
log.Println("Updating force HTTPS redirection to true")
|
||||
SystemWideLogger.Println("Updating force HTTPS redirection to true")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else if useRedirect == "false" {
|
||||
sysdb.Write("settings", "redirect", false)
|
||||
log.Println("Updating force HTTPS redirection to false")
|
||||
SystemWideLogger.Println("Updating force HTTPS redirection to false")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
|
||||
@ -634,6 +798,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
newIncomingPort, err := utils.PostPara(r, "incoming")
|
||||
@ -644,11 +832,18 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
newIncomingPortInt, err := strconv.Atoi(newIncomingPort)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid incoming port given")
|
||||
utils.SendErrorResponse(w, "Invalid incoming port given")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if it is identical as proxy root (recursion!)
|
||||
if dynamicProxyRouter.Root == nil || dynamicProxyRouter.Root.Domain == "" {
|
||||
//Check if proxy root is set before checking recursive listen
|
||||
//Fixing issue #43
|
||||
utils.SendErrorResponse(w, "Set Proxy Root before changing inbound port")
|
||||
return
|
||||
}
|
||||
|
||||
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
|
||||
if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
|
||||
//Listening port is same as proxy root
|
||||
@ -671,3 +866,139 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/* Handle Custom Header Rules */
|
||||
//List all the custom header defined in this proxy rule
|
||||
|
||||
func HandleCustomHeaderList(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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
|
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,
|
||||
})
|
||||
|
||||
}
|
111
src/start.go
@ -14,6 +14,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
"imuslab.com/zoraxy/mod/tcpprox"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -76,7 +78,10 @@ func startupSequence() {
|
||||
}
|
||||
|
||||
//Create a geodb store
|
||||
geodbStore, err = geodb.NewGeoDb(sysdb)
|
||||
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
||||
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
|
||||
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -89,10 +94,29 @@ func startupSequence() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a system wide logger
|
||||
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
|
||||
if err == nil {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
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
|
||||
netstatBuffers, err = netstat.NewNetStatBuffer(300)
|
||||
if err != nil {
|
||||
log.Println("Failed to load network statistic info")
|
||||
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -114,46 +138,49 @@ func startupSequence() {
|
||||
This discover nearby ArozOS Nodes or other services
|
||||
that provide mDNS discovery with domain (e.g. Synology NAS)
|
||||
*/
|
||||
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
|
||||
if err != nil {
|
||||
portInt = 8000
|
||||
}
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: "zoraxy_" + nodeUUID,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.imuslab.com",
|
||||
Model: "Network Gateway",
|
||||
UUID: nodeUUID,
|
||||
Vendor: "imuslab.com",
|
||||
BuildVersion: version,
|
||||
}, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Start initial scanning
|
||||
go func() {
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
previousmdnsScanResults = hosts
|
||||
log.Println("mDNS Startup scan completed")
|
||||
}()
|
||||
|
||||
//Create a ticker to update mDNS results every 5 minutes
|
||||
ticker := time.NewTicker(15 * time.Minute)
|
||||
stopChan := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
case <-ticker.C:
|
||||
if *allowMdnsScanning {
|
||||
portInt, err := strconv.Atoi(strings.Split(*webUIPort, ":")[1])
|
||||
if err != nil {
|
||||
portInt = 8000
|
||||
}
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: "zoraxy_" + nodeUUID,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.arozos.com",
|
||||
Model: "Network Gateway",
|
||||
UUID: nodeUUID,
|
||||
Vendor: "imuslab.com",
|
||||
BuildVersion: version,
|
||||
}, "")
|
||||
if err != nil {
|
||||
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
|
||||
} else {
|
||||
//Start initial scanning
|
||||
go func() {
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
previousmdnsScanResults = hosts
|
||||
log.Println("mDNS scan result updated")
|
||||
}
|
||||
SystemWideLogger.Println("mDNS Startup scan completed")
|
||||
}()
|
||||
|
||||
//Create a ticker to update mDNS results every 5 minutes
|
||||
ticker := time.NewTicker(15 * time.Minute)
|
||||
stopChan := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
case <-ticker.C:
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
previousmdnsScanResults = hosts
|
||||
SystemWideLogger.Println("mDNS scan result updated")
|
||||
}
|
||||
}
|
||||
}()
|
||||
mdnsTickerStop = stopChan
|
||||
}
|
||||
}()
|
||||
mdnsTickerStop = stopChan
|
||||
}
|
||||
|
||||
/*
|
||||
Global Area Network
|
||||
@ -164,7 +191,7 @@ func startupSequence() {
|
||||
if usingZtAuthToken == "" {
|
||||
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
|
||||
if err != nil {
|
||||
log.Println("Failed to load ZeroTier controller API authtoken")
|
||||
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
|
||||
}
|
||||
}
|
||||
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
|
||||
@ -197,15 +224,21 @@ func startupSequence() {
|
||||
|
||||
Obtaining certificates from ACME Server
|
||||
*/
|
||||
//Create a table just to store acme related preferences
|
||||
sysdb.NewTable("acmepref")
|
||||
acmeHandler = initACME()
|
||||
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This sequence start after everything is initialized
|
||||
func finalSequence() {
|
||||
//Start ACME renew agent
|
||||
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)
|
||||
}
|
@ -308,7 +308,7 @@
|
||||
<div class="ui message">
|
||||
<i class="ui info circle icon"></i> IP Address support the following formats
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
|
||||
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
|
||||
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
|
||||
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
|
||||
</div>
|
||||
@ -615,8 +615,12 @@
|
||||
<p>Whitelist a certain IP or IP range</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>IP Address</label>
|
||||
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
|
||||
<label>IP Address</label>
|
||||
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
|
||||
</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">
|
||||
<i class="green add icon"></i> Whitelist IP
|
||||
@ -625,7 +629,7 @@
|
||||
<div class="ui message">
|
||||
<i class="ui info circle icon"></i> IP Address support the following formats
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
|
||||
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
|
||||
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
|
||||
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
|
||||
</div>
|
||||
@ -634,6 +638,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Remarks</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -793,11 +798,12 @@
|
||||
if (data.length === 0) {
|
||||
$('#whitelistIpTable').append(`
|
||||
<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>
|
||||
`);
|
||||
} else {
|
||||
$.each(data, function(index, ip) {
|
||||
$.each(data, function(index, ipEntry) {
|
||||
let ip = ipEntry.IP;
|
||||
let icon = "globe icon";
|
||||
if (isLAN(ip)){
|
||||
icon = "desktop icon";
|
||||
@ -807,6 +813,7 @@
|
||||
$('#whitelistIpTable').append(`
|
||||
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
|
||||
<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>
|
||||
</tr>
|
||||
`);
|
||||
@ -1003,6 +1010,7 @@
|
||||
|
||||
function addIpWhitelist(){
|
||||
let targetIp = $("#ipAddressInputWhitelist").val().trim();
|
||||
let remarks = $("#ipAddressCommentsWhitelist").val().trim();
|
||||
if (targetIp == ""){
|
||||
alert("IP address is empty")
|
||||
return
|
||||
@ -1016,7 +1024,7 @@
|
||||
$.ajax({
|
||||
url: "/api/whitelist/ip/add",
|
||||
type: "POST",
|
||||
data: {ip: targetIp.toLowerCase()},
|
||||
data: {ip: targetIp.toLowerCase(), "comment": remarks},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
@ -1025,6 +1033,7 @@
|
||||
}
|
||||
|
||||
$("#ipAddressInputWhitelist").val("");
|
||||
$("#ipAddressCommentsWhitelist").val("");
|
||||
$("#ipAddressInputWhitelist").parent().remvoeClass("error");
|
||||
},
|
||||
error: function() {
|
||||
|
@ -1,3 +1,13 @@
|
||||
<style>
|
||||
.expired.certdate{
|
||||
font-weight: bolder;
|
||||
color: #bd001c;
|
||||
}
|
||||
|
||||
.valid.certdate{
|
||||
color: #31c071;
|
||||
}
|
||||
</style>
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>TLS / SSL Certificates</h2>
|
||||
@ -5,42 +15,19 @@
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h4>Default Certificates</h4>
|
||||
<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>
|
||||
<h3>Hosts Certificates</h3>
|
||||
<p>Provide certificates for multiple domains reverse proxy</p>
|
||||
<div class="ui fluid form">
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>Server Name (Domain)</label>
|
||||
<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 class="field">
|
||||
<label>Public Key (.pem)</label>
|
||||
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
||||
<small>or .crt files in order systems</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Private Key (.key)</label>
|
||||
@ -53,33 +40,215 @@
|
||||
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<table class="ui sortable unstackable celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow refresh icon"></i> Auto Renew (ACME) Settings</button>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
||||
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
|
||||
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
||||
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
||||
<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 style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||
<table class="ui sortable unstackable basic celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th class="no-sort">Renew</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h3>Fallback Certificate</h3>
|
||||
<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>
|
||||
<table class="ui very basic unstackable celled table">
|
||||
<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 class="ui divider"></div>
|
||||
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
|
||||
<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>
|
||||
<div class="ui fluid form">
|
||||
<div class="field">
|
||||
<label>Preferred CA</label>
|
||||
<div class="ui selection dropdown" id="defaultCA">
|
||||
<input type="hidden" name="defaultCA">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Let's Encrypt</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
|
||||
<div class="item" data-value="Buypass">Buypass</div>
|
||||
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>ACME Email</label>
|
||||
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
|
||||
</div>
|
||||
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
||||
</div><br>
|
||||
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
||||
<div class="ui basic segment acmeRenewStateWrapper">
|
||||
<h4 class="ui header" id="acmeAutoRenewer">
|
||||
<i class="white remove icon"></i>
|
||||
<div class="content">
|
||||
<span id="acmeAutoRenewerStatus">Disabled</span>
|
||||
<div class="sub header">ACME Auto-Renewer</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
|
||||
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
|
||||
</div>
|
||||
<script>
|
||||
var uploadPendingPublicKey = undefined;
|
||||
var uploadPendingPrivateKey = undefined;
|
||||
|
||||
$("#defaultCA").dropdown();
|
||||
|
||||
|
||||
//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
|
||||
function deleteCertificate(domain){
|
||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||
@ -100,6 +269,77 @@
|
||||
|
||||
}
|
||||
|
||||
function initAcmeStatus(){
|
||||
//Initialize the current default CA options
|
||||
$.get("/api/acme/autoRenew/email", function(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){
|
||||
$("#defaultCA").dropdown("set value", data);
|
||||
});
|
||||
|
||||
$.get("/api/acme/autoRenew/enable", function(data){
|
||||
setACMEEnableStates(data);
|
||||
})
|
||||
}
|
||||
//Set the status of the acme enable icon
|
||||
function setACMEEnableStates(enabled){
|
||||
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
||||
if (enabled){
|
||||
$(".acmeRenewStateWrapper").addClass("enabled");
|
||||
}else{
|
||||
$(".acmeRenewStateWrapper").removeClass("enabled");
|
||||
}
|
||||
|
||||
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
|
||||
}
|
||||
initAcmeStatus();
|
||||
|
||||
function saveDefaultCA(){
|
||||
let newDefaultEmail = $("#prefACMEEmail").val().trim();
|
||||
let newDefaultCA = $("#defaultCA").dropdown("get value");
|
||||
|
||||
if (newDefaultEmail == ""){
|
||||
msgbox("Invalid acme email given", false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/acme/autoRenew/email",
|
||||
method: "POST",
|
||||
data: {"set": newDefaultEmail},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
//Update the renew button states
|
||||
$(".renewButton").removeClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "/api/acme/autoRenew/ca",
|
||||
data: {"set": newDefaultCA},
|
||||
method: "POST",
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
msgbox("Settings updated");
|
||||
|
||||
}
|
||||
|
||||
//List the stored certificates
|
||||
function initManagedDomainCertificateList(){
|
||||
$.get("/api/cert/list?date=true", function(data){
|
||||
@ -111,17 +351,20 @@
|
||||
return a.Domain > b.Domain
|
||||
});
|
||||
data.forEach(entry => {
|
||||
let isExpired = entry.RemainingDays <= 0;
|
||||
|
||||
$("#certifiedDomainList").append(`<tr>
|
||||
<td>${entry.Domain}</td>
|
||||
<td>${entry.LastModifiedDate}</td>
|
||||
<td>${entry.ExpireDate} (${entry.RemainingDays} days left)</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>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
$("#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>`);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@
|
||||
<div class="content">
|
||||
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
|
||||
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -219,8 +218,8 @@
|
||||
|
||||
}
|
||||
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
//On switch over to this page, load info
|
||||
listGANet();
|
||||
initGANetID();
|
||||
|
@ -61,7 +61,7 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="" style="overflow-x: auto;">
|
||||
@ -84,6 +84,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
<script>
|
||||
@ -355,7 +360,10 @@
|
||||
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
const tableBody = $('#networkMemeberTable');
|
||||
let tableBody = $('#networkMemeberTable');
|
||||
if (tableBody.length == 0){
|
||||
return;
|
||||
}
|
||||
data.sort((a, b) => a.address.localeCompare(b.address));
|
||||
//Check if the new object equal to the old one
|
||||
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
|
||||
function initGanetDetails(ganetId){
|
||||
currentGANetID = ganetId;
|
||||
@ -612,6 +669,11 @@
|
||||
|
||||
}
|
||||
|
||||
//Switch from other tabs back to this, exit to GAN list
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
exitToGanList();
|
||||
}
|
||||
|
||||
//Exit point
|
||||
function exitToGanList(){
|
||||
$("#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
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<div class="advancezone ui basic segment">
|
||||
<div class="ui accordion advanceSettings">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Advance Options
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
|
||||
<button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(".advanceSettings").accordion();
|
||||
|
||||
/*
|
||||
Redirection functions
|
||||
@ -125,6 +110,7 @@
|
||||
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -151,16 +137,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createAdvanceRules(){
|
||||
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
|
||||
}
|
||||
|
||||
function initRedirectionRuleList(){
|
||||
$("#redirectionRuleList").html("");
|
||||
$.get("/api/redirect/list", function(data){
|
||||
data.forEach(function(entry){
|
||||
$("#redirectionRuleList").append(`<tr>
|
||||
<td>${entry.RedirectURL} </td>
|
||||
<td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></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.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
||||
@ -169,7 +151,7 @@
|
||||
});
|
||||
|
||||
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,38 +1,186 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Set Proxy Root</h2>
|
||||
<p>For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
|
||||
<h2>Default Site</h2>
|
||||
<p>Default routing options for inbound traffic (previously called Proxy Root)</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Root</label>
|
||||
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
|
||||
<small>E.g. localhost:8080</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="rootReqTLS" >
|
||||
<label>Root require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
|
||||
<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="field">
|
||||
<label>Reverse Proxy Target</label>
|
||||
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
|
||||
<small>e.g. localhost:8080 / 192.168.0.100:80 / example.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="rootReqTLS">
|
||||
<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>
|
||||
|
||||
<!-- Redirect as default site Options-->
|
||||
<div id="defaultSiteRedirectOptions" class="ui basic segment advanceoptions defaultSiteOptionDetails" style="display:none;"">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Redirect target domain</label>
|
||||
<div class="ui input">
|
||||
<input id="redirectDomain" type="text" placeholder="http://example.com">
|
||||
</div>
|
||||
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function initRootInfo(){
|
||||
var currentDefaultSiteOption = 0; //For enum see typedef.go
|
||||
$("#advanceRootSettings").accordion();
|
||||
|
||||
//Handle toggle events of option radio boxes
|
||||
function updateAvaibleDefaultSiteOptions(){
|
||||
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();
|
||||
$("#proxyRoot").val(staticWebServerURL);
|
||||
$("#proxyRoot").parent().addClass("disabled");
|
||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||
$("#rootReqTLS").parent().addClass("disabled");
|
||||
$("#useRootProxyRouterForVdir").parent().removeClass("disabled");
|
||||
currentDefaultSiteOption = 0;
|
||||
}else if (selectedDefaultSite == "proxy"){
|
||||
$("#defaultSiteProxyOptions").show();
|
||||
$("#rootReqTLS").parent().removeClass("disabled");
|
||||
$("#proxyRoot").parent().removeClass("disabled");
|
||||
$("#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){
|
||||
$.get("/api/proxy/list?type=root", function(data){
|
||||
if (data == null){
|
||||
|
||||
}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);
|
||||
checkRootRequireTLS(data.Domain);
|
||||
}
|
||||
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
initRootInfo();
|
||||
initRootInfo(function(){
|
||||
bindDefaultSiteRadioCheckboxEvents();
|
||||
});
|
||||
|
||||
|
||||
function isUsingStaticWebServerAsRoot(callback){
|
||||
let currentProxyRoot = $("#proxyRoot").val().trim();
|
||||
$.get("/api/webserv/status", function(webservStatus){
|
||||
if (currentProxyRoot == "127.0.0.1:" + webservStatus.ListeningPort || currentProxyRoot == "localhost:" + webservStatus.ListeningPort){
|
||||
return callback(true);
|
||||
}
|
||||
return callback(false);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["setroot"] = function(){
|
||||
|
||||
}
|
||||
|
||||
//Check if the given domain will redirect to https
|
||||
function checkRootRequireTLS(targetDomain){
|
||||
//Trim off the http or https from the origin
|
||||
if (targetDomain.startsWith("http://")){
|
||||
targetDomain = targetDomain.substring(7);
|
||||
$("#proxyRoot").val(targetDomain);
|
||||
}else if (targetDomain.startsWith("https://")){
|
||||
targetDomain = targetDomain.substring(8);
|
||||
$("#proxyRoot").val(targetDomain);
|
||||
}
|
||||
$.ajax({
|
||||
url: "/api/proxy/tlscheck",
|
||||
data: {url: targetDomain},
|
||||
@ -48,32 +196,82 @@
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function setProxyRoot(){
|
||||
//Set the new proxy root option
|
||||
function setProxyRoot(btn=undefined){
|
||||
var newpr = $("#proxyRoot").val();
|
||||
if (newpr.trim() == ""){
|
||||
$("#proxyRoot").parent().addClass('error');
|
||||
return
|
||||
}else{
|
||||
$("#proxyRoot").parent().removeClass('error');
|
||||
if (newpr.trim() == "" && currentDefaultSiteOption == 0){
|
||||
//Fill in the web server info
|
||||
newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||
$("#proxyRoot").val(newpr);
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
$.ajax({
|
||||
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){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
//OK
|
||||
initRootInfo();
|
||||
msgbox("Proxy Root Updated")
|
||||
initRootInfo(function(){
|
||||
//Check if WebServ is enabled
|
||||
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
|
||||
if (isUsingWebServ){
|
||||
//Force enable static web server
|
||||
//See webserv.html for details
|
||||
setWebServerRunningState(true);
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
//Update the checkbox
|
||||
msgbox("Proxy Root Updated");
|
||||
}, 100);
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (btn != undefined){
|
||||
$(btn).removeClass("disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
@ -1,113 +1,114 @@
|
||||
<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="standardContainer">
|
||||
<div class="ui basic segment" style="margin-top: 1em;">
|
||||
<h2>New Proxy Rule</h2>
|
||||
<p>You can create a proxy endpoing by subdomain or virtual directories</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Type</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" id="ptype" value="subd">
|
||||
<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 class="field">
|
||||
<label>IP Address or Domain Name with port</label>
|
||||
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
|
||||
<small>E.g. 192.168.0.101:8000 or example.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="reqTls">
|
||||
<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="skipTLSValidation">
|
||||
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="requireBasicAuth">
|
||||
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="basicAuthCredentials" class="field">
|
||||
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
|
||||
<table class="ui very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="basicAuthCredentialTable">
|
||||
<tr>
|
||||
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="three small fields credentialEntry">
|
||||
<div class="field">
|
||||
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="blue add icon"></i> Create Endpoint</button>
|
||||
<br><br>
|
||||
<div class="ui basic segment" style="border-radius: 1em; padding: 1em !important;">
|
||||
<h2>New Proxy Rule</h2>
|
||||
<p>You can add more proxy rules to support more site via domain / subdomains</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Matching Keyword / Domain</label>
|
||||
<input type="text" id="rootname" placeholder="mydomain.com">
|
||||
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Target IP Address or Domain Name with port</label>
|
||||
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
|
||||
<small>E.g. 192.168.0.101:8000 or example.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="reqTls">
|
||||
<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="skipTLSValidation">
|
||||
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="bypassGlobalTLS">
|
||||
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="requireBasicAuth">
|
||||
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="basicAuthCredentials" class="field">
|
||||
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
|
||||
<table class="ui very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="basicAuthCredentialTable">
|
||||
<tr>
|
||||
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="three small fields credentialEntry">
|
||||
<div class="field">
|
||||
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
|
||||
<br><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<div class="ui basic segment" style="height: 100%; background-color: var(--theme_grey); color: var(--theme_lgrey);">
|
||||
<br>
|
||||
<span style="font-size: 1.2em; font-weight: 300;">Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<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 basic segment rulesInstructions">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||
Example of domain matching keyword:<br>
|
||||
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;">Virtual Directory</span><br>
|
||||
Example of virtual directory name: <br>
|
||||
<code>/s1/home/</code> <br>(Any access to {this_server}/s1/home/ will be proxy to the IP address below)<br>
|
||||
You can also ignore the tailing slash for wildcard like usage.<br>
|
||||
<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> Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<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="item"><code>/s1/room-101</code></div>
|
||||
<div class="item"><code>/s1/room-102/</code></div>
|
||||
<div class="item"><code>/s1/room-103/map.txt</code></div>
|
||||
</div><br>
|
||||
|
||||
<div class="item"><code>www.arozos.com</code></div>
|
||||
<div class="item"><code>foo.bar.arozos.com</code></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@ -116,29 +117,16 @@
|
||||
<script>
|
||||
$("#advanceProxyRules").accordion();
|
||||
|
||||
|
||||
//New Proxy Endpoint
|
||||
function newProxyEndpoint(){
|
||||
var type = $("#ptype").val();
|
||||
var rootname = $("#rootname").val();
|
||||
var proxyDomain = $("#proxyDomain").val();
|
||||
var useTLS = $("#reqTls")[0].checked;
|
||||
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
|
||||
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
||||
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
||||
|
||||
if (type === "vdir") {
|
||||
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() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
return
|
||||
@ -157,11 +145,12 @@
|
||||
$.ajax({
|
||||
url: "/api/proxy/add",
|
||||
data: {
|
||||
type: type,
|
||||
type: "host",
|
||||
rootname: rootname,
|
||||
tls: useTLS,
|
||||
ep: proxyDomain,
|
||||
tlsval: skipTLSValidation,
|
||||
bypassGlobalTLS: bypassGlobalTLS,
|
||||
bauth: requireBasicAuth,
|
||||
cred: JSON.stringify(credentials),
|
||||
},
|
||||
@ -169,16 +158,37 @@
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
//OK
|
||||
listVdirs();
|
||||
listSubd();
|
||||
msgbox("Proxy Endpoint Added");
|
||||
|
||||
//Clear old data
|
||||
$("#rootname").val("");
|
||||
$("#proxyDomain").val("");
|
||||
credentials = [];
|
||||
updateTable();
|
||||
reloadUptimeList();
|
||||
//Check if it is a new subdomain and TLS enabled
|
||||
if ($("#tls").checkbox("is checked")){
|
||||
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
|
||||
if (choice == true){
|
||||
//Load the prefer CA from TLS page
|
||||
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||
if (defaultCA.trim() == ""){
|
||||
defaultCA = "Let's Encrypt";
|
||||
}
|
||||
//Get a new cert using ACME
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
console.log("Trying to get a new certificate via ACME");
|
||||
|
||||
//Request ACME for certificate, see cert.html component
|
||||
obtainCertificate(rootname, defaultCA.trim(), function(){
|
||||
// Renew the parent certificate list
|
||||
initManagedDomainCertificateList();
|
||||
});
|
||||
}else{
|
||||
msgbox("Proxy Endpoint Added");
|
||||
}
|
||||
});
|
||||
}else{
|
||||
msgbox("Proxy Endpoint Added");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -186,14 +196,16 @@
|
||||
}
|
||||
|
||||
//Generic functions for delete rp endpoints
|
||||
function deleteEndpoint(ptype, epoint){
|
||||
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
|
||||
function deleteEndpoint(epoint){
|
||||
epoint = decodeURIComponent(epoint).hexDecode();
|
||||
if (confirm("Confirm remove proxy for :" + epoint + "?")){
|
||||
$.ajax({
|
||||
url: "/api/proxy/del",
|
||||
data: {ep: epoint, ptype: ptype},
|
||||
data: {ep: epoint, },
|
||||
success: function(){
|
||||
listVdirs();
|
||||
listSubd();
|
||||
listProxyEndpoints();
|
||||
msgbox("Proxy Rule Deleted", true);
|
||||
reloadUptimeList();
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -293,148 +305,33 @@
|
||||
updateTable();
|
||||
}
|
||||
|
||||
|
||||
//Check if a string is a valid subdomain
|
||||
function isSubdomainDomain(str) {
|
||||
const regex = /^(localhost|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}\.)$/i;
|
||||
return regex.test(str);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
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);
|
||||
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;
|
||||
let tls = payload.RequireTLS;
|
||||
if (tls){
|
||||
tls = "checked";
|
||||
}else{
|
||||
tls = "";
|
||||
}
|
||||
input = `
|
||||
<div class="ui mini fluid input">
|
||||
<input type="text" class="Domain" value="${domain}">
|
||||
</div>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="RequireTLS" ${tls}>
|
||||
<label>Require TLS</label>
|
||||
</div>
|
||||
`;
|
||||
column.empty().append(input);
|
||||
|
||||
}else if (datatype == "skipver"){
|
||||
let skipTLSValidation = payload.SkipCertValidations;
|
||||
let checkstate = "";
|
||||
if (skipTLSValidation){
|
||||
checkstate = "checked";
|
||||
}
|
||||
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||
<label>Skip Verification</label>
|
||||
<small>Check this if you are using self signed certificates</small>
|
||||
</div>`);
|
||||
}else if (datatype == "basicauth"){
|
||||
let requireBasicAuth = payload.RequireBasicAuth;
|
||||
let checkstate = "";
|
||||
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;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Credentials</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{
|
||||
//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;
|
||||
|
||||
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
||||
|
||||
$.ajax({
|
||||
url: "/api/proxy/edit",
|
||||
method: "POST",
|
||||
data: {
|
||||
"type": epttype,
|
||||
"rootname": uuid,
|
||||
"ep":newDomain,
|
||||
"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);
|
||||
|
||||
//Update v3.0.0
|
||||
//Since some proxy rules now contains wildcard characters
|
||||
//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
|
||||
}
|
||||
|
||||
String.prototype.hexDecode = function(){
|
||||
var j;
|
||||
var hexes = this.match(/.{1,4}/g) || [];
|
||||
var back = "";
|
||||
for(j = 0; j<hexes.length; j++) {
|
||||
back += String.fromCharCode(parseInt(hexes[j], 16));
|
||||
}
|
||||
|
||||
return back;
|
||||
}
|
||||
|
||||
</script>
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column statisticWrapper">
|
||||
<div class="ui greybackground statustab segment">
|
||||
<div class="ui statustab segment">
|
||||
<h5 class="ui header">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</h5>
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="ui header">
|
||||
<h5 class="ui header" style="margin-top: 0px;">
|
||||
<i class="arrows alternate horizontal icon"></i>
|
||||
<div class="content">
|
||||
<span id="forwardtype"></span>
|
||||
@ -39,32 +39,39 @@
|
||||
</div>
|
||||
</h5>
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="ui header">
|
||||
<h5 class="ui header" style="margin-top: 0px;">
|
||||
<i class="map marker alternate icon"></i>
|
||||
<div class="content">
|
||||
<span id="country"></span>
|
||||
<div class="sub header" id="countryList">
|
||||
|
||||
<i class="ui loading circle notch icon"></i> Resolving GeoIP
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkActWrapper" class="standardContainer" style="position: relative; margin-top: 1em;">
|
||||
<div class="standardContainer" style="padding-bottom: 0 !important;">
|
||||
<!-- 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>
|
||||
</div>
|
||||
<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>
|
||||
<br>
|
||||
<div class="standardContainer">
|
||||
<h4>Basic Settings</h4>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Global Settings</h4>
|
||||
<p>Inbound Port (Port to be proxied)</p>
|
||||
<div class="ui action fluid notloopbackOnly input">
|
||||
<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>
|
||||
<br>
|
||||
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
||||
@ -72,10 +79,15 @@
|
||||
<label>Use TLS to serve proxy request</label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
||||
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
|
||||
<input type="checkbox">
|
||||
<label>Force redirect HTTP request to HTTPS<br>
|
||||
<small>(Only apply when listening port is not 80)</small></label>
|
||||
<label>Enable HTTP server on port 80<br>
|
||||
<small>(Only apply when TLS enabled and not using port 80)</small></label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
|
||||
<input type="checkbox">
|
||||
<label>Force redirect HTTP request to HTTPS</label>
|
||||
</div>
|
||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
||||
<div class="ui accordion advanceSettings">
|
||||
@ -84,21 +96,22 @@
|
||||
Advance Settings
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>If you have no idea what are these, you can leave them as default :)</p>
|
||||
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
||||
<input type="checkbox">
|
||||
<label>Force TLS v1.2 or above<br>
|
||||
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
|
||||
</div>
|
||||
<br>
|
||||
<div 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>
|
||||
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
@ -109,7 +122,7 @@
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<p>Visitor Counts</p>
|
||||
<table class="ui unstackable inverted celled table">
|
||||
<table class="ui unstackable very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country ISO Code</th>
|
||||
@ -125,7 +138,7 @@
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>Proxy Request Types</p>
|
||||
<table class="ui unstackable inverted celled table">
|
||||
<table class="ui unstackable very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proxy Type</th>
|
||||
@ -170,17 +183,18 @@
|
||||
}
|
||||
$("#serverstatus").addClass("green");
|
||||
$("#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);
|
||||
}else{
|
||||
$("#startbtn").removeClass("disabled");
|
||||
$("#stopbtn").addClass("disabled");
|
||||
$("#statusTitle").text("Offline");
|
||||
$("#rpStatusIcon").attr("class", "black circle times icon")
|
||||
$("#rpStatusIcon").attr("class", "yellow moon icon")
|
||||
$("#statusText").text("Reverse proxy server is offline");
|
||||
$("#serverstatus").removeClass("green");
|
||||
}
|
||||
$("#incomingPort").val(data.Option.Port);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@ -305,6 +319,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleP80ListenerStateChange(enabled){
|
||||
$.ajax({
|
||||
url: "/api/proxy/listenPort80",
|
||||
data: {"enable": enabled},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
console.log(data.error);
|
||||
return;
|
||||
}
|
||||
if (enabled){
|
||||
$("#redirect").show();
|
||||
msgbox("Port 80 listener enabled");
|
||||
}else{
|
||||
$("#redirect").hide();
|
||||
msgbox("Port 80 listener disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function handlePortChange(){
|
||||
var newPortValue = $("#incomingPort").val();
|
||||
@ -323,6 +358,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initPort80ListenerSetting(){
|
||||
$.get("/api/proxy/listenPort80", function(data){
|
||||
if (data){
|
||||
$("#listenP80").checkbox("set checked");
|
||||
$("#redirect").show();
|
||||
}else{
|
||||
$("#listenP80").checkbox("set unchecked");
|
||||
$("#redirect").hide();
|
||||
}
|
||||
|
||||
$("#listenP80").find("input").on("change", function(){
|
||||
let enabled = $(this)[0].checked;
|
||||
handleP80ListenerStateChange(enabled);
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
initPort80ListenerSetting();
|
||||
|
||||
function initHTTPtoHTTPSRedirectSetting(){
|
||||
$.get("/api/proxy/useHttpsRedirect", function(data){
|
||||
if (data == true){
|
||||
@ -356,8 +410,6 @@
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
initHTTPtoHTTPSRedirectSetting();
|
||||
|
||||
@ -389,6 +441,30 @@
|
||||
}
|
||||
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(){
|
||||
$.get("/api/cert/tls", function(data){
|
||||
if (data == true){
|
||||
@ -517,10 +593,11 @@
|
||||
{
|
||||
type: 'line',
|
||||
responsive: true,
|
||||
resizeDelay: 100,
|
||||
resizeDelay: 300,
|
||||
options: {
|
||||
animation: false,
|
||||
maintainAspectRatio: false,
|
||||
bezierCurve: true,
|
||||
tooltips: {enabled: false},
|
||||
hover: {mode: null},
|
||||
//stepped: 'middle',
|
||||
@ -562,18 +639,18 @@
|
||||
{
|
||||
label: 'Inbound',
|
||||
data: rxValues,
|
||||
borderColor: "#4d9dd9",
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(77, 157, 217, 0.2)',
|
||||
borderColor: "#484bb8",
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'rgba(72, 75, 184, 0.2)',
|
||||
fill: true,
|
||||
pointStyle: false,
|
||||
},
|
||||
{
|
||||
label: 'Outbound',
|
||||
data: txValues,
|
||||
borderColor: '#ffe32b',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(255, 227, 43, 0.2)',
|
||||
borderColor: '#02a9c1',
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'rgba(2, 169, 193, 0.2)',
|
||||
fill: true,
|
||||
pointStyle: false,
|
||||
}
|
||||
|
@ -1,72 +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>TLS/SSL Verification</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>`;
|
||||
}
|
||||
|
||||
let tlsVerificationField = "";
|
||||
if (subd.RequireTLS){
|
||||
tlsVerificationField = !subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
|
||||
}else{
|
||||
tlsVerificationField = "N/A"
|
||||
}
|
||||
|
||||
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||
<td data-label="" editable="false"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
|
||||
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</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>
|
||||
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
|
||||
</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 basic segment" id="addproxyConfig" style="display:none;">
|
||||
<h3>TCP Proxy Config</h3>
|
||||
<div class="ui basic segment" style="margin-top: 0;">
|
||||
<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>
|
||||
<form id="tcpProxyForm" class="ui form">
|
||||
<div class="field" style="display:none;">
|
||||
@ -39,11 +61,10 @@
|
||||
<option value="starter">Starter</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
|
||||
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
|
||||
<button 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);" 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>
|
||||
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
|
||||
<h3>Proxy Mode Instructions</h3>
|
||||
<div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
|
||||
<p>TCP Proxy support the following TCP sockets proxy modes</p>
|
||||
<table class="ui celled padded inverted basic table">
|
||||
<thead>
|
||||
@ -108,28 +129,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
<script>
|
||||
@ -138,6 +137,13 @@
|
||||
$("#tcpProxyForm .dropdown").dropdown();
|
||||
$('#tcpProxyForm').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
//Check if update mode
|
||||
if ($("#editTcpProxyButton").is(":visible")){
|
||||
confirmEditTCPProxyConfig(event);
|
||||
return;
|
||||
}
|
||||
|
||||
var form = $(this);
|
||||
|
||||
var formValid = validateTCPProxyConfig(form);
|
||||
@ -165,23 +171,16 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Add proxy button pressed. Show add TCP proxy menu
|
||||
$("#addProxyConfigButton").on("click", function(){
|
||||
$('#addproxyConfig').slideToggle('fast');
|
||||
$("#addTcpProxyButton").show();
|
||||
$("#editTcpProxyButton").hide();
|
||||
});
|
||||
|
||||
|
||||
function clearTCPProxyAddEditForm(){
|
||||
$('#tcpProxyForm input, #tcpProxyForm select').val('');
|
||||
$('#tcpProxyForm select').dropdown('clear');
|
||||
}
|
||||
|
||||
function cancelTCPProxyEdit(event) {
|
||||
function cancelTCPProxyEdit(event=undefined) {
|
||||
clearTCPProxyAddEditForm();
|
||||
$('#addproxyConfig').slideUp('fast');
|
||||
$("#addTcpProxyButton").show();
|
||||
$("#editTcpProxyButton").hide();
|
||||
}
|
||||
|
||||
function validateTCPProxyConfig(form){
|
||||
@ -231,7 +230,7 @@
|
||||
proxyConfigs.forEach(function(config) {
|
||||
var runningLogo = '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){
|
||||
runningLogo = 'Running';
|
||||
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");
|
||||
}
|
||||
initProxyConfigList();
|
||||
clearTCPProxyAddEditForm();
|
||||
$("#addproxyConfig").slideUp("fast");
|
||||
cancelTCPProxyEdit();
|
||||
|
||||
},
|
||||
error: function() {
|
||||
msgbox('An error occurred while processing the request', false);
|
||||
|
@ -86,7 +86,7 @@
|
||||
|
||||
let id = value[0].ID;
|
||||
let name = value[0].Name;
|
||||
let url = value[0].URL;
|
||||
let url = value[value.length - 1].URL;
|
||||
let protocol = value[0].Protocol;
|
||||
|
||||
//Generate the status dot
|
||||
@ -112,6 +112,9 @@
|
||||
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
|
||||
//Special type of error, cause by downstream reverse proxy
|
||||
dotType = "error";
|
||||
}else if (thisStatus.StatusCode == 401){
|
||||
//Unauthorized error
|
||||
dotType = "error";
|
||||
}else{
|
||||
dotType = "offline";
|
||||
}
|
||||
@ -141,6 +144,28 @@
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
|
||||
onlineStatusCss = `color: #f38020;`;
|
||||
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
|
||||
}else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){
|
||||
switch(value[value.length - 1].StatusCode){
|
||||
case 400:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`;
|
||||
break;
|
||||
case 401:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Unauthorized`;
|
||||
break;
|
||||
case 403:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Forbidden`;
|
||||
break;
|
||||
case 404:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Not Found`;
|
||||
break;
|
||||
case 405:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`;
|
||||
break;
|
||||
}
|
||||
|
||||
onlineStatusCss = `color: #f38020;`;
|
||||
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
|
||||
|
||||
}else{
|
||||
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
|
||||
onlineStatusCss = `color: #df484a;`;
|
||||
@ -152,7 +177,7 @@
|
||||
$("#utmrender").append(`<div class="ui basic segment statusbar">
|
||||
<div class="domain">
|
||||
<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>
|
||||
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>
|
||||
|
@ -3,166 +3,179 @@
|
||||
<h2>Utilities</h2>
|
||||
<p>You might find these tools or information helpful when setting up your gateway server</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
|
||||
<div class="selfauthOnly">
|
||||
<h3>Account Management</h3>
|
||||
<p>Functions to help management the current account</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> Change Password</h5>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Current Password</label>
|
||||
<input type="password" name="oldPassword" placeholder="Current Password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>New Password</label>
|
||||
<input type="password" name="newPassword" placeholder="New Password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Confirm New Password</label>
|
||||
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
|
||||
</div>
|
||||
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
|
||||
</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 id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui circle checkmark green icon "></i> Password Updated
|
||||
<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="ui divider"></div>
|
||||
<h3>Forget Password Email</h3>
|
||||
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<label>Sender Address</label>
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
||||
<div class="fields">
|
||||
<div class="twelve wide field">
|
||||
<label>SMTP Provider Hostname</label>
|
||||
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
|
||||
<div class="selfauthOnly">
|
||||
<h3>Change Password</h3>
|
||||
<p>Update the current account credentials</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> Change Password</h5>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Current Password</label>
|
||||
<input type="password" name="oldPassword" placeholder="Current Password">
|
||||
</div>
|
||||
|
||||
<div class="four wide field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="E.g. 587" value="587">
|
||||
<div class="field">
|
||||
<label>New Password</label>
|
||||
<input type="password" name="newPassword" placeholder="New Password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Confirm New Password</label>
|
||||
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
|
||||
</div>
|
||||
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
|
||||
</div>
|
||||
|
||||
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui circle checkmark green icon "></i> Password Updated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>Sender Username</label>
|
||||
<input type="text" name="username" placeholder="E.g. admin">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Sender Domain</label>
|
||||
<div class="ui labeled input">
|
||||
<div class="ui basic label">
|
||||
@
|
||||
</div>
|
||||
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
|
||||
<div class="ui divider"></div>
|
||||
<h3>Forget Password Email</h3>
|
||||
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<label>Sender Address</label>
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
||||
<div class="fields">
|
||||
<div class="twelve wide field">
|
||||
<label>SMTP Provider Hostname</label>
|
||||
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
|
||||
</div>
|
||||
|
||||
<div class="four wide field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="E.g. 587" value="587">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sender Password</label>
|
||||
<input type="password" name="password" placeholder="Password of the email account">
|
||||
<small>Leave empty to use the old password</small>
|
||||
</div>
|
||||
|
||||
<p> <i class="caret down icon"></i> Email for sending account reset link</p>
|
||||
<div class="field">
|
||||
<label>Admin / Receiver Address</label>
|
||||
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
|
||||
</div>
|
||||
|
||||
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button>
|
||||
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<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>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
|
||||
<div class="ui message">
|
||||
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="Start IP" id="startIpInput">
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="End IP" id="endIpInput">
|
||||
</div>
|
||||
<br>
|
||||
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
|
||||
<p>Results: <div id="cidrOutput">N/A</div></p>
|
||||
</div>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
|
||||
<div class="ui action input">
|
||||
<input type="text" placeholder="CIDR" id="cidrInput">
|
||||
<button class="ui basic button" onclick="convertToIPRange()">Convert</button>
|
||||
</div>
|
||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||
</div>
|
||||
<!-- Config Tools -->
|
||||
<div class="ui divider"></div>
|
||||
<h3>System Backup & Restore</h3>
|
||||
<p>Options related to system backup, migrate and restore.</p>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
||||
<!-- System Information -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="zoraxyinfo">
|
||||
<h3 class="ui header">
|
||||
System Information
|
||||
</h3>
|
||||
<p>Basic information about this zoraxy host</p>
|
||||
<table class="ui very basic collapsing celled table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Host UUID</td>
|
||||
<td class="uuid"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td class="version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Build</td>
|
||||
<td class="development"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Running Since</td>
|
||||
<td class="boottime"></td>
|
||||
</tr>
|
||||
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>Sender Username</label>
|
||||
<input type="text" name="username" placeholder="E.g. admin">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Sender Domain</label>
|
||||
<div class="ui labeled input">
|
||||
<div class="ui basic label">
|
||||
@
|
||||
</div>
|
||||
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sender Password</label>
|
||||
<input type="password" name="password" placeholder="Password of the email account">
|
||||
<small>Leave empty to use the old password</small>
|
||||
</div>
|
||||
|
||||
<p> <i class="caret down icon"></i> Email for sending account reset link</p>
|
||||
<div class="field">
|
||||
<label>Admin / Receiver Address</label>
|
||||
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
|
||||
</div>
|
||||
|
||||
<tr>
|
||||
<td>ZeroTier Linked</td>
|
||||
<td class="zt"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enable SSH Loopback</td>
|
||||
<td class="sshlb"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button>
|
||||
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
|
||||
<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>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
|
||||
<div class="ui message">
|
||||
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="Start IP" id="startIpInput">
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="End IP" id="endIpInput">
|
||||
</div>
|
||||
<br>
|
||||
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
|
||||
<p>Results: <div id="cidrOutput">N/A</div></p>
|
||||
</div>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
|
||||
<div class="ui action input">
|
||||
<input type="text" placeholder="CIDR" id="cidrInput">
|
||||
<button class="ui basic button" onclick="convertToIPRange()">Convert</button>
|
||||
</div>
|
||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||
</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>
|
||||
<p>Options related to system backup, migrate and restore.</p>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
||||
<div class="ui divider"></div>
|
||||
<!-- System Information -->
|
||||
<div id="zoraxyinfo">
|
||||
<h3 class="ui header">
|
||||
System Information
|
||||
</h3>
|
||||
<p>Basic information about this zoraxy host</p>
|
||||
<table class="ui very basic collapsing celled table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Host UUID</td>
|
||||
<td class="uuid"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td class="version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Build</td>
|
||||
<td class="development"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Running Since</td>
|
||||
<td class="boottime"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>ZeroTier Linked</td>
|
||||
<td class="zt"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enable SSH Loopback</td>
|
||||
<td class="sshlb"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
$('.menu .nettools.item').tab();
|
||||
/*
|
||||
Account Password utilities
|
||||
*/
|
||||
@ -171,6 +184,7 @@
|
||||
if (data == 0){
|
||||
//Using external auth manager. Hide options
|
||||
$(".selfauthOnly").hide();
|
||||
$(".extAuthOnly").show();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3,72 +3,420 @@
|
||||
<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>
|
||||
</div>
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Proxy To</th>
|
||||
<th>TLS/SSL Verification</th>
|
||||
<th>Basic Auth</th>
|
||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vdirList">
|
||||
<tr>
|
||||
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Destination</th>
|
||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vdirList">
|
||||
<tr>
|
||||
<td data-label="" colspan="3">No Selected Host</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
<button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
|
||||
<br><br>
|
||||
</div>
|
||||
<script>
|
||||
//Virtual directories functions
|
||||
function listVdirs(){
|
||||
$.get("/api/proxy/list?type=vdir", function(data){
|
||||
$("#vdirList").html(``);
|
||||
if (data.error !== undefined){
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</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>`);
|
||||
|
||||
//Initialize the list of hosts that can be used to attach vdirs config
|
||||
function initVdirList(){
|
||||
//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{
|
||||
data.forEach(vdir => {
|
||||
let tlsIcon = "";
|
||||
let vdirData = encodeURIComponent(JSON.stringify(vdir));
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
}
|
||||
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>`);
|
||||
});
|
||||
|
||||
let tlsVerificationField = "";
|
||||
if (vdir.RequireTLS){
|
||||
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
|
||||
}else{
|
||||
tlsVerificationField = "N/A"
|
||||
}
|
||||
|
||||
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
|
||||
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
|
||||
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</td>
|
||||
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
||||
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
|
||||
</td>
|
||||
</tr>`);
|
||||
});
|
||||
//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>
|
||||
<td data-label="" colspan="3"><i class="green check circle icon"></i> No Virtual Directory Routing Rule</td>
|
||||
</tr>`);
|
||||
}else{
|
||||
//List the vdirs
|
||||
console.log(data);
|
||||
data.forEach(vdir => {
|
||||
var tlsIcon = "";
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
if (vdir.SkipCertValidations){
|
||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||
}
|
||||
}
|
||||
|
||||
let payload = JSON.stringify(vdir).hexEncode();
|
||||
|
||||
$("#vdirList").append(`<tr vdirid="${vdir.MatchingPath.hexEncode()}" class="vdirEntry" payload="${payload}">
|
||||
<td data-label="" editable="false" >${vdir.MatchingPath}</td>
|
||||
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
|
||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||
<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='deleteVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="trash icon"></i></button>
|
||||
</td>
|
||||
</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
|
||||
tabSwitchEventBind["vdir"] = function(){
|
||||
listVdirs();
|
||||
initVdirList();
|
||||
}
|
||||
|
||||
initVdirList();
|
||||
</script>
|
231
src/web/components/webserv.html
Normal file
@ -0,0 +1,231 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Static Web Server</h2>
|
||||
<p>A simple static web server that serve html css and js files</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment webservRunningStateWrapper">
|
||||
<h4 class="ui header" id="webservRunningState">
|
||||
<i class="green circle icon"></i>
|
||||
<div class="content">
|
||||
<span class="webserv_status">Running</span>
|
||||
<div class="sub header">Listen port :<span class="webserv_port">8081</span></div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h3>Web Server Settings</h3>
|
||||
<div class="ui form">
|
||||
<div class="inline field">
|
||||
<div class="ui toggle checkbox webservRootDisabled">
|
||||
<input id="webserv_enable" type="checkbox" class="hidden">
|
||||
<label>Enable Static Web Server</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input id="webserv_enableDirList" type="checkbox" class="hidden">
|
||||
<label>Enable Directory Listing</label>
|
||||
<small>If this folder do not contains any index files, list the directory of this folder.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Document Root Folder</label>
|
||||
<input id="webserv_docRoot" type="text" readonly="true">
|
||||
<small>
|
||||
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
|
||||
See the -webserv flag for more details.
|
||||
</small>
|
||||
</div>
|
||||
<div class="field webservRootDisabled">
|
||||
<label>Port Number</label>
|
||||
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
|
||||
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
|
||||
</div>
|
||||
</div>
|
||||
<small><i class="ui blue save icon"></i> Changes are saved automatically</small>
|
||||
<br>
|
||||
<div class="ui message">
|
||||
<div class="ui accordion webservhelp">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
How to access the static web server?
|
||||
</div>
|
||||
<div class="content">
|
||||
There are three ways to access the static web server. <br>
|
||||
<div class="ui ordered list">
|
||||
<div class="item">
|
||||
If you are using Zoraxy as your gateway reverse proxy server,
|
||||
you can add a new subdomain proxy rule that points to
|
||||
<a>http://127.0.0.1:<span class="webserv_port">8081</span></a>
|
||||
</div>
|
||||
<div class="item">
|
||||
If you are using Zoraxy under another reverse proxy server,
|
||||
add <a>http://127.0.0.1:<span class="webserv_port">8081</span></a> to the config of your upper layer reverse proxy server's config file.
|
||||
</div>
|
||||
<div class="item">
|
||||
Directly access the web server via <a>http://{zoraxy_host_ip}:<span class="webserv_port">8081</span></a> (Not recommended)
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<h2>Web Directory Manager</h2>
|
||||
<p>Manage your files inside your web directory</p>
|
||||
</div>
|
||||
<div class="ui basic segment" style="display:none;" id="webdirManDisabledNotice">
|
||||
<h4 class="ui header">
|
||||
<i class="ui red times icon"></i>
|
||||
<div class="content">
|
||||
Web Directory Manager Disabled
|
||||
<div class="sub header">Web Directory Manager has been disabled by the system administrator</div>
|
||||
</div>
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
<iframe id="webserv_dirManager" src="tools/fs.html" style="width: 100%; height: 800px; border: 0px; overflow-y: hidden;">
|
||||
|
||||
</iframe>
|
||||
<small>If you do not want to enable web access to your web directory, you can disable this feature with <code>-webfm=false</code> startup paramter</small>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$(".webservhelp").accordion();
|
||||
$(".ui.checkbox").checkbox();
|
||||
|
||||
function setWebServerRunningState(running){
|
||||
if (running){
|
||||
$("#webserv_enable").parent().checkbox("set checked");
|
||||
$("#webservRunningState").find("i").attr("class", "white circle check icon");
|
||||
$("#webservRunningState").find(".webserv_status").text("Running");
|
||||
$(".webservRunningStateWrapper").addClass("enabled")
|
||||
}else{
|
||||
$("#webserv_enable").parent().checkbox("set unchecked");
|
||||
$("#webservRunningState").find("i").attr("class", "white circle times icon");
|
||||
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
||||
$(".webservRunningStateWrapper").removeClass("enabled")
|
||||
}
|
||||
}
|
||||
|
||||
function updateWebServState(){
|
||||
$.get("/api/webserv/status", function(data){
|
||||
//Clear all event listeners
|
||||
$("#webserv_enableDirList").off("change");
|
||||
$("#webserv_enable").off("change");
|
||||
$("#webserv_listenPort").off("change");
|
||||
|
||||
setWebServerRunningState(data.Running);
|
||||
|
||||
if (data.EnableDirectoryListing){
|
||||
$("#webserv_enableDirList").parent().checkbox("set checked");
|
||||
}else{
|
||||
$("#webserv_enableDirList").parent().checkbox("set unchecked");
|
||||
}
|
||||
|
||||
$("#webserv_docRoot").val(data.WebRoot + "/html/");
|
||||
|
||||
if (!data.EnableWebDirManager){
|
||||
$("#webdirManDisabledNotice").show();
|
||||
$("#webserv_dirManager").remove();
|
||||
}
|
||||
|
||||
$("#webserv_listenPort").val(data.ListeningPort);
|
||||
updateWebServLinkExample(data.ListeningPort);
|
||||
|
||||
//Bind checkbox events
|
||||
$("#webserv_enable").off("change").on("change", function(){
|
||||
let enable = $(this)[0].checked;
|
||||
if (enable){
|
||||
$.get("/api/webserv/start", function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Static web server started");
|
||||
setWebServerRunningState(true);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
$.get("/api/webserv/stop", function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Static web server stopped");
|
||||
setWebServerRunningState(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#webserv_enableDirList").off("change").on("change", function(){
|
||||
let enable = $(this)[0].checked;
|
||||
$.ajax({
|
||||
url: "/api/webserv/setDirList",
|
||||
method: "POST",
|
||||
data: {"enable": enable},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Directory listing setting updated");
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
$("#webserv_listenPort").off("change").on("change", function(){
|
||||
let newPort = $(this).val();
|
||||
|
||||
//Check if the new value is same as listening port
|
||||
let rpListeningPort = $("#incomingPort").val();
|
||||
if (rpListeningPort == newPort){
|
||||
confirmBox("This setting might cause port conflict. Continue Anyway?", function(choice){
|
||||
if (choice == true){
|
||||
//Continue anyway
|
||||
$.ajax({
|
||||
url: "/api/webserv/setPort",
|
||||
method: "POST",
|
||||
data: {"port": newPort},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Listening port updated");
|
||||
}
|
||||
updateWebServState();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
//Cancel. Restore to previous value
|
||||
updateWebServState();
|
||||
msgbox("Setting restored");
|
||||
}
|
||||
});
|
||||
}else{
|
||||
$.ajax({
|
||||
url: "/api/webserv/setPort",
|
||||
method: "POST",
|
||||
data: {"port": newPort},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Listening port updated");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
updateWebServState();
|
||||
|
||||
function updateWebServLinkExample(newport){
|
||||
$(".webserv_port").text(newport);
|
||||
}
|
||||
</script>
|
||||
</div>
|
10
src/web/components/zgrok.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Service Expose Proxy</h2>
|
||||
<p>Expose your local test-site on the internet with single command</p>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4>Work In Progress</h4>
|
||||
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
|
||||
</div>
|
||||
</div>
|
@ -39,7 +39,7 @@
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might cause by the region limit setting of this site.</p>
|
||||
This might be caused by the region limit setting of this site.</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
@ -52,4 +52,4 @@
|
||||
$("#requrl").text(window.location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -154,4 +154,4 @@
|
||||
$("#host").text(location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 694 KiB |