51 Commits
2.6.5 ... 2.6.8

Author SHA1 Message Date
20cf290d37 Merge pull request #84 from tobychui/2.6.8
Update v2.6.8
2023-11-25 22:25:22 +08:00
4ca0fcc6d1 Update main.go
Swap to use embedded web fs
2023-11-25 16:38:15 +08:00
ce4ce72820 Added optional TLS bypass mechanism
+ Added opt-out for subdomains for global TLS settings #44
+ Optimized subdomain / vdir editing interface
2023-11-25 15:54:28 +08:00
e363d55899 Fixed #77 and added systemwide logger
+ Added system wide logger (wip)
+ Fixed issue #77 uptime monitor bug
+ Added backend options for bypass global TLS (or allow per rule TLS settings, wip)
+
2023-11-25 12:50:30 +08:00
172479e4fb Added default HTTP/2 Mode
+ Added automatic HTTP/2 switch to TLS mode (Related to #40)
2023-10-20 13:44:47 +08:00
156fa5dace Bug fix
+ Added potential fix for #67
+ Update version number
+ Changed default static web port to 5487 so it is even more unlikely to be used
+
2023-10-20 11:01:28 +08:00
50f222cced Merge pull request #68 from Morethanevil/patch-6
Update CHANGELOG.md
2023-09-26 21:30:23 +08:00
640e1adf96 Update CHANGELOG.md
Updated to latest version
2023-09-26 09:28:57 +02:00
d4bb84180c Merge pull request #66 from tobychui/2.6.7
v2.6.7 Update
2023-09-26 11:24:09 +08:00
bda47fc36b Added default Ca features
+ Added default CA feature
+ Fixed RWD issue in TLS cert table
+ Optimized ACME UI in the TLS page
2023-09-25 20:54:50 +08:00
fd6ba56143 Added web directory manager
+ Added web directory manager
+ Added dummy service expose proxy page
+ Moved ACME and renew to a new section in TLS management page
2023-09-24 23:44:48 +08:00
b63a0fc246 Added static web server and black / whitelist template
- Added static web server
- Added static web server default index
- Added embeded templates to blacklist / whitelist
- Added wip Web Directory Manager

Place the templates at ./www/templates/blacklist.html or whitelist.html to replace the build in embedded template for access control 403 error
-
2023-09-14 16:15:40 +08:00
ed92cccf0e Merge pull request #65 from daluntw/2.6.7
Fix the out of range problem when certificate auto renew
2023-09-13 17:21:29 +08:00
95892802fd use issuer org as failover for json file not exist 2023-09-13 04:28:33 +00:00
8a5004e828 handle buypass issuer not match 2023-09-13 04:27:11 +00:00
c6c523e005 prevent out of range when check issuer exist 2023-09-13 00:39:29 +00:00
a692ec818d Static web server
- Fixed arm64 build bug in Make file
- Added wip static web server
2023-09-12 16:41:52 +08:00
c65f780613 Merge pull request #64 from daluntw/multidomain_fix
Fix multidomain UI handle incorrectly
2023-09-10 17:42:07 +08:00
507c2ab468 Updated 2.6.7 init 2023-09-09 12:28:24 +08:00
1180da8d11 fix multidomain missing logic 2023-09-08 23:18:47 +00:00
83f574e3ab Merge pull request #60 from Morethanevil/main
Update CHANGELOG.md
2023-08-30 22:47:42 +08:00
60837f307d Update CHANGELOG.md 2023-08-30 16:34:43 +02:00
50d5dedabe Merge pull request #59 from tobychui/2.6.6-experimental
2.6.6
2023-08-30 15:36:50 +08:00
f15c774c70 Updated go mod
+ Updated go mod to latest version
+ Fixed minor UX problem pn acme snippet
2023-08-30 12:16:53 +08:00
069f4805f6 Merge pull request #57 from daluntw/dev-custom-acme
Add Skip TLS Verify Feature For ACME Server
2023-08-29 10:06:52 +08:00
eb98624a6a fix naming convention 2023-08-28 03:35:20 +00:00
6a0c7cf499 add skipTLS for custom acme server 2023-08-28 03:31:33 +00:00
73ab9ca778 Optimized memory usage and root routing
+ Added unset subdomain custom redirection feature #46
+ Optimized memory usage by space time tradeoff in geoip lookup to fix #52
+ Replaced all stori/go.uuid to google/uuid for security reasons #55
2023-08-27 10:18:49 +08:00
9f9e0750e1 Update README.md 2023-08-26 15:05:53 +08:00
5664965491 Update README.md
Added donation link (wip)
2023-08-26 11:59:51 +08:00
db4016e79f Merge pull request #54 from PassiveLemon/Docker-Rework
Completely rework the container
2023-08-25 11:03:38 +08:00
f84c4370cf doc: update readme 2023-08-24 17:59:35 -04:00
b39cb6391b Completely rework the container 2023-08-24 17:48:57 -04:00
4f7f60188f Added basic auth exception paths
Added feature request from #25
2023-08-22 23:46:54 +08:00
dce58343db Merge pull request #48 from daluntw/dev-custom-acme
Add custom ACME server feature in backend
2023-08-22 14:05:42 +08:00
415838ad39 add frontend support 2023-08-21 13:19:33 +00:00
ce0b1a7585 fix missing space 2023-08-21 12:55:14 +00:00
352995e852 add custom acme feature in backend 2023-08-20 21:01:33 +00:00
a3d55a3274 Patching redirection bug
+ Added wip basic auth editor custom exception rules
+ Added custom logic to handle apache screw up redirect header
2023-08-20 14:50:25 +08:00
70adadf129 bug fixes for 2.6.5
+ Added potential fixes for #34 and #39
+ Added better error handling for #43
2023-08-17 14:08:48 +08:00
d42ac8a146 Merge pull request #36 from tobychui/Docker-2.1.0
Docker 2.1.0
2023-07-29 00:00:09 +08:00
f304ff8862 feature: network connectivity test 2023-07-28 10:23:20 -04:00
7d91e02dc9 feature: golang specific image
smaller size
2023-07-28 10:22:46 -04:00
dae510ae0a doc: VERSION 2023-07-28 10:22:22 -04:00
cd382a78a5 Merge pull request #35 from Morethanevil/patch-5
Update CHANGELOG.md
2023-07-27 00:22:57 +08:00
987de4a7be Update CHANGELOG.md 2023-07-26 17:21:09 +02:00
52d3b2f8c2 Fixed memory leaking
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in acme handler logic
+ Added "do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
2023-07-26 19:17:43 +08:00
5038429a70 Update README.md 2023-07-20 22:04:16 -04:00
2acbf0f3f5 Merge pull request #31 from tobychui/DockerMerge
Docker 2.0.0
2023-07-20 10:30:55 +08:00
aed703e260 Merge branch 'main' into DockerMerge 2023-07-20 10:30:07 +08:00
5ece7c0da4 2.0.0 init
Completely revamped the container to support the new external configuration files.
2023-07-19 16:39:51 -04:00
91 changed files with 19495 additions and 767 deletions

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

@ -0,0 +1,46 @@
name: Image Publisher
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Dockerhub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_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 }} \
.
docker buildx build --push \
--build-arg VERSION=${{ github.event.release.tag_name }} \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:latest \
.

View File

@ -1,4 +1,30 @@
v2.6.5 Jul 19 2023
# 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 back-end, thx [@daluntw](https://github.com/daluntw)
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
+ Introduce new startparameter `-fastgeoip=true`, see [Releases](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
# v2.6.5.1 Jul 26 2023
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in acme handler logic
+ Added "Do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
# v2.6.5 Jul 19 2023
+ Added Import / Export-Feature
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)

View File

@ -157,6 +157,12 @@ Loopback web ssh connection, by default, is disabled. This means that if you are
./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.**

View File

@ -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"]

View File

@ -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.

View File

@ -1,7 +1,9 @@
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Version](https://img.shields.io/docker/v/passivelemon/zoraxy-docker/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Size](https://img.shields.io/docker/image-size/passivelemon/zoraxy-docker/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Pulls](https://img.shields.io/docker/pulls/passivelemon/zoraxy-docker?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
## Setup: </br>
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
@ -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 I have 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>

View File

@ -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}

View File

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

View File

@ -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
@ -108,8 +109,28 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
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)
}
}

View File

@ -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,15 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
//Reverse proxy root related APIs
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
//Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
//TLS / SSL config
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
@ -154,6 +163,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 +171,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", staticWebServer.HandlePortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager {
//Web Directory Manager file operation functions
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
//Others
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
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
}

View File

@ -6,7 +6,6 @@ import (
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
@ -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")

View File

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

View File

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

View File

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

View File

@ -4,16 +4,15 @@ 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/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0
golang.org/x/tools v0.12.0 // indirect
)

View File

@ -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,8 +735,9 @@ 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/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -695,38 +760,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 +815,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 +829,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 +845,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 +875,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 +903,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 +942,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 +961,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 +989,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 +1035,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 +1044,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 +1088,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 +1106,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 +1116,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 +1183,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 +1225,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 +1241,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 +1252,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 +1263,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 +1293,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 +1310,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 +1365,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 +1380,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 +1388,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 +1404,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 +1433,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 +1450,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 +1525,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 +1773,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 +1783,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 +1801,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 +1812,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=

View File

@ -20,6 +20,7 @@ import (
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
@ -30,18 +31,25 @@ import (
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
)
// General flags
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var (
name = "Zoraxy"
version = "2.6.5"
version = "2.6.8"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -71,10 +79,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 +106,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,6 +119,9 @@ 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()
@ -141,7 +157,7 @@ func main() {
}
uuidBytes, err := os.ReadFile(uuidRecord)
if err != nil {
log.Println("Unable to read system uuid from file system")
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
panic(err)
}
nodeUUID = string(uuidBytes)
@ -163,7 +179,7 @@ func main() {
//Start the finalize sequences
finalSequence()
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
err = http.ListenAndServe(handler.Port, nil)
if err != nil {

View File

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

View File

@ -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 {

View File

@ -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
}

View File

@ -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]

View File

@ -1,8 +1,13 @@
package dynamicproxy
import (
_ "embed"
"errors"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
@ -21,6 +26,11 @@ import (
- Vitrual Directory Routing
*/
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
@ -108,10 +118,69 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
} else {
//Passthrough the request to root
h.proxyRequest(w, r, h.Parent.Root)
h.handleRootRouting(w, r)
}
} else {
//No routing rules found. Route to root.
//No routing rules found.
h.handleRootRouting(w, r)
}
}
/*
handleRootRouting
This function handle root routing situations where there are no subdomain
, vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
//Route to custom domain
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
//Not set. Redirect to first level of domain redirectable
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
if err != nil {
//Redirect to proxy root
h.proxyRequest(w, r, h.Parent.Root)
} else {
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
}
return
} else if h.isTopLevelRedirectableDomain(domainOnly) {
//This is requesting a top level private domain that should be serving root
h.proxyRequest(w, r, h.Parent.Root)
} else {
//Validate the redirection target URL
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
if err != nil {
//Error when parsing target. Send to root
h.proxyRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if domainOnly != hostname {
//Redirect to target
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
return
} else {
//Loopback request due to bad settings (Shd leave it empty)
//Forward it to root proxy
h.proxyRequest(w, r, h.Parent.Root)
}
}
} else {
//Route to root
h.proxyRequest(w, r, h.Parent.Root)
}
}
@ -124,9 +193,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 +207,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)
}
@ -150,3 +219,44 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
return false
}
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@ -3,6 +3,7 @@ package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
@ -15,6 +16,16 @@ import (
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"

View File

@ -14,10 +14,6 @@ import (
var onExitFlushLoop func()
const (
defaultTimeout = time.Minute * 5
)
// ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the
// client, support http, also support https tunnel using http.hijacker

View File

@ -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, "/") {

View File

@ -3,6 +3,7 @@ package dynamicproxy
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log"
"net/http"
@ -29,12 +30,19 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
tldMap: map[string]int{},
}
thisRouter.mux = &ProxyHandler{
Parent: &thisRouter,
}
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil
}
@ -52,6 +60,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
@ -65,10 +79,18 @@ func (router *Router) StartProxyService() error {
return errors.New("Reverse proxy server already running")
}
//Check if root route is set
if router.Root == nil {
return errors.New("Reverse proxy router root not set")
}
//Load root options from file
loadedRootOption, err := loadRootRoutingOptionsFromFile()
if err != nil {
return err
}
router.RootRoutingOptions = loadedRootOption
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
@ -79,27 +101,73 @@ 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
/*
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
}
router.tlsListener = ln
*/
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
TLSConfig: config,
}
router.tlsListener = ln
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
//Add a 80 to 443 redirector
httpServer := &http.Server{
Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
//Check if the domain requesting allow non TLS mode
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS,
PathPrefix: "",
})
return
}
if router.Option.ForceHttpsRedirect {
//Redirect to https is enabled
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
} else {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
}
} else {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
//Do not do redirection
if sep != nil {
//Sub-domain exists but not allow non-TLS access
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("400 - Bad Request"))
} else {
//No defined sub-domain
http.NotFound(w, r)
}
}
}),
@ -127,7 +195,7 @@ func (router *Router) StartProxyService() error {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
//Unable to startup port 80 listener. Handle shutdown process gracefully
stopChan <- true
log.Fatalf("Could not start server: %v\n", err)
log.Fatalf("Could not start redirection server: %v\n", err)
}
}()
router.tlsRedirectStop = stopChan
@ -136,8 +204,8 @@ func (router *Router) StartProxyService() error {
//Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)")
go func() {
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start server: %v\n", err)
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start proxy server: %v\n", err)
}
}()
} else {
@ -246,14 +314,15 @@ func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) erro
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
endpointObject := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
@ -271,46 +340,24 @@ func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
} else if ptype == "subd" {
proxy, ok := router.SubdomainEndpoint.Load(key)
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
}
return nil, errors.New("unsupported ptype")
}
/*
Save routing from RP
*/
func (router *Router) SaveProxy(ptype string, key string, newConfig *ProxyEndpoint) {
if ptype == "vdir" {
router.ProxyEndpoints.Store(key, newConfig)
} else if ptype == "subd" {
router.SubdomainEndpoint.Store(key, newConfig)
}
}
/*
Remove routing from RP
*/
func (router *Router) RemoveProxy(ptype string, key string) error {
//fmt.Println(ptype, key)
if ptype == "vdir" {
router.ProxyEndpoints.Delete(key)
return nil
} else if ptype == "subd" {
router.SubdomainEndpoint.Delete(key)
return nil
}
return errors.New("invalid ptype")
}
/*
Add an default router for the proxy server
*/
@ -335,14 +382,15 @@ func (router *Router) SetRootProxy(options *RootOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint

View File

@ -0,0 +1,68 @@
package dynamicproxy
import "errors"
/*
ProxyEndpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
//Get the string version of proxy type
func (ep *ProxyEndpoint) GetProxyTypeString() string {
if ep.ProxyType == ProxyType_Subdomain {
return "subd"
} else if ep.ProxyType == ProxyType_Vdir {
return "vdir"
}
return "unknown"
}
//Update change in the current running proxy endpoint config
func (ep *ProxyEndpoint) UpdateToRuntime() {
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Store(ep.RootOrMatchingDomain, ep)
}
}
//Return true if the endpoint type is virtual directory
func (ep *ProxyEndpoint) IsVdir() bool {
return ep.ProxyType == ProxyType_Vdir
}
//Return true if the endpoint type is subdomain
func (ep *ProxyEndpoint) IsSubDomain() bool {
return ep.ProxyType == ProxyType_Subdomain
}
//Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
//fmt.Println(ptype, key)
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Delete(ep.RootOrMatchingDomain)
return nil
}
return errors.New("invalid or unsupported type")
}
//ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(proxyType string, rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(proxyType, rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -183,6 +183,5 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
}
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
}()
}
}

View File

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

View File

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

View File

@ -0,0 +1,55 @@
<html>
<head>
<!-- Zoraxy Forbidden Template -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
<title>Forbidden</title>
<style>
#msg{
position: absolute;
top: calc(50% - 150px);
left: calc(50% - 250px);
width: 500px;
height: 300px;
text-align: center;
}
#footer{
position: fixed;
padding: 2em;
padding-left: 5em;
padding-right: 5em;
bottom: 0px;
left: 0px;
width: 100%;
}
small{
word-break: break-word;
}
</style>
</head>
<body>
<div id="msg">
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
<div>
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
<div class="ui divider"></div>
<p>You do not have permission to view this directory or page. <br>
This might cause by the region limit setting of this site.</p>
<div class="ui divider"></div>
<div style="text-align: left;">
<small>Request time: <span id="reqtime"></span></small><br>
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
</div>
</div>
</div>
<script>
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
$("#requrl").text(window.location.href);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package dynamicproxy
import (
_ "embed"
"net"
"net/http"
"sync"
@ -26,25 +27,29 @@ type RouterOption struct {
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
RootRoutingOptions *RootRoutingOptions
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json
}
// Auth credential for basic auth on certain endpoints
@ -59,56 +64,73 @@ type BasicAuthUnhashedCredentials struct {
Password string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
Proxy *dpcore.ReverseProxy `json:"-"`
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
Proxy *dpcore.ReverseProxy `json:"-"`
parent *Router
}
// Root options are those that are required for reverse proxy handler to work
type RootOptions struct {
ProxyLocation string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
RequireTLS bool //Proxy root target require TLS connection (not recommended)
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
//Basic Auth Related
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
// Additional options are here for letting router knows how to route exception cases for root
type RootRoutingOptions struct {
//Root only configs
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
RootName string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
/*
type ProxyEndpoint struct {
Root string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
type SubdomainEndpoint struct {
MatchingDomain string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
Web Templates
*/
var (
//go:embed templates/forbidden.html
page_forbidden []byte
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@ -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,17 @@ func startupSequence() {
panic(err)
}
//Create a system wide logger
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
//Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300)
if err != nil {
log.Println("Failed to load network statistic info")
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
panic(err)
}
@ -114,46 +126,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(handler.Port, ":")[1])
if err != nil {
portInt = 8000
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID,
Port: portInt,
Domain: "zoraxy.arozos.com",
Model: "Network Gateway",
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
}, "")
if err != nil {
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
} else {
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
log.Println("mDNS scan result updated")
}
SystemWideLogger.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS scan result updated")
}
}
}()
mdnsTickerStop = stopChan
}
}()
mdnsTickerStop = stopChan
}
/*
Global Area Network
@ -164,7 +179,7 @@ func startupSequence() {
if usingZtAuthToken == "" {
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
if err != nil {
log.Println("Failed to load ZeroTier controller API authtoken")
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
}
}
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
@ -197,11 +212,29 @@ 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)
}
/*
Static Web Server
Start the static web server
*/
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb,
Port: "5487", //Default Port
WebRoot: *staticWebServerRoot,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
})
//Restore the web server to previous shutdown state
staticWebServer.RestorePreviousState()
}
// This sequence start after everything is initialized

View File

@ -308,7 +308,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
@ -625,7 +625,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>

View File

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

View File

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

View File

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

View File

@ -72,10 +72,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">
@ -181,6 +186,7 @@
$("#serverstatus").removeClass("green");
}
$("#incomingPort").val(data.Option.Port);
});
}
@ -305,6 +311,27 @@
});
}
function handleP80ListenerStateChange(enabled){
$.ajax({
url: "/api/proxy/listenPort80",
data: {"enable": enabled},
success: function(data){
if (data.error != undefined){
console.log(data.error);
return;
}
if (enabled){
$("#redirect").show();
msgbox("Port 80 listener enabled");
}else{
$("#redirect").hide();
msgbox("Port 80 listener disabled");
}
}
});
}
function handlePortChange(){
var newPortValue = $("#incomingPort").val();
@ -323,6 +350,25 @@
});
}
function initPort80ListenerSetting(){
$.get("/api/proxy/listenPort80", function(data){
if (data){
$("#listenP80").checkbox("set checked");
$("#redirect").show();
}else{
$("#listenP80").checkbox("set unchecked");
$("#redirect").hide();
}
$("#listenP80").find("input").on("change", function(){
let enabled = $(this)[0].checked;
handleP80ListenerStateChange(enabled);
})
});
}
initPort80ListenerSetting();
function initHTTPtoHTTPSRedirectSetting(){
$.get("/api/proxy/useHttpsRedirect", function(data){
if (data == true){
@ -356,8 +402,6 @@
})
});
});
}
initHTTPtoHTTPSRedirectSetting();

View File

@ -9,7 +9,6 @@
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -41,19 +40,14 @@
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"
if (subd.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="false"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${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>

View File

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

View File

@ -9,7 +9,6 @@
<tr>
<th>Virtual Directory</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -43,6 +42,9 @@
let vdirData = encodeURIComponent(JSON.stringify(vdir));
if (vdir.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (vdir.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
let tlsVerificationField = "";
@ -55,7 +57,6 @@
$("#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>

View File

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

View File

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

View File

@ -62,13 +62,16 @@
<a class="item" tag="gan">
<i class="simplistic globe icon"></i> Global Area Network
</a>
<a class="item" tag="">
<a class="item" tag="zgrok">
<i class="simplistic podcast icon"></i> Service Expose Proxy
</a>
<a class="item" tag="tcpprox">
<i class="simplistic exchange icon"></i> TCP Proxy
</a>
<div class="ui divider menudivider">Others</div>
<a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server
</a>
<a class="item" tag="utm">
<i class="simplistic time icon"></i> Uptime Monitor
</a>
@ -114,9 +117,15 @@
<!-- Global Area Networking -->
<div id="gan" class="functiontab" target="gan.html"></div>
<!-- Service Expose Proxy -->
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
<!-- TCP Proxy -->
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
<!-- Web Server -->
<div id="webserv" class="functiontab" target="webserv.html"></div>
<!-- Up Time Monitor -->
<div id="utm" class="functiontab" target="uptime.html"></div>

View File

@ -116,7 +116,7 @@ body{
}
#confirmBox .ui.progress .bar{
background: #ffe32b !important;
background: #a9d1f3 !important;
}
#confirmBox .confirmBoxBody .button{

View File

@ -83,8 +83,8 @@
</div>
</div>
<div class="ui divider"></div>
<h3>Manual Renew</h3>
<p>Pick a certificate below to force renew</p>
<h3>Generate New Certificate</h3>
<p>Enter a new / existing domain(s) to request new certificate(s)</p>
<div class="ui form">
<div class="field">
<label>Domain(s)</label>
@ -109,11 +109,22 @@
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div>
</div>
</div>
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
<div class="field" id="caInput" style="display:none;">
<label>ACME Server URL</label>
<input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
</div>
<div class="field" id="skipTLS" style="display:none;">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSCheckbox">
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Get Certificate</button>
</div>
<div class="ui divider"></div>
<small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
@ -126,6 +137,7 @@
let enableTrigerOnChangeEvent = true;
$(".accordion").accordion();
$(".dropdown").dropdown();
$(".checkbox").checkbox();
function setAutoRenewIfCASupportMode(useAutoMode = true){
if (useAutoMode){
@ -206,6 +218,11 @@
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
}
});
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
}
//Render the domains table that exists in this zoraxy host
@ -295,13 +312,24 @@
obtainCertificate();
});
$("input[name=ca]").on('change', function() {
if(this.value == "Custom ACME Server") {
$("#caInput").show();
$("#skipTLS").show();
} else {
$("#caInput").hide();
$("#skipTLS").hide();
}
})
// Obtain certificate from API
function obtainCertificate() {
var domains = $("#domainsInput").val();
var filename = $("#filenameInput").val();
var email = $("#caRegisterEmail").val();
if (email == ""){
parent.msgbox("ACME renew email is not set")
parent.msgbox("ACME renew email is not set", false)
$("#obtainButton").removeClass("loading").removeClass("disabled");
return;
}
if (filename.trim() == "" && !domains.includes(",")){
@ -312,11 +340,21 @@
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
}else if (filename == "" && domains.includes(",")){
parent.msgbox("Filename cannot be empty for certs containing multiple domains.", false, 5000);
$("#obtainButton").removeClass("loading").removeClass("disabled");
return;
}
var ca = $("#ca").dropdown("get value");
var caURL = "";
if (ca == "Custom ACME Server") {
ca = "custom";
caURL = $("#caURL").val();
}
var skipTLSValue = $("#skipTLSCheckbox")[0].checked;
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
@ -325,6 +363,8 @@
filename: filename,
email: email,
ca: ca,
caURL: caURL,
skipTLS: skipTLSValue,
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");

View File

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

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

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

After

Width:  |  Height:  |  Size: 618 B

View File

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

After

Width:  |  Height:  |  Size: 616 B

8
src/web/tools/img/eq.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<path fill="#4f4f4f" d="M13.95,36.15v-24.3h3.3v24.3H13.95z M22.35,44.15V3.85h3.3v40.3H22.35z M5.6,28.15v-8.3h3.25v8.3H5.6z
M30.75,36.15v-24.3h3.3v24.3H30.75z M39.15,28.15v-8.3h3.25v8.3H39.15z"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

3442
src/web/tools/img/file.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 251 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<g id="圖層_2">
<polygon fill="#DBAC50" points="104.03,38.089 104.03,101.074 22.388,101.074 22.388,27.94 49.702,27.75 55.666,38.345 "/>
</g>
<g id="圖層_3">
<polygon fill="#E5BD64" points="104.328,101.97 22.836,101.97 38.209,52.716 118.806,52.716 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 728 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="200px" height="50px" viewBox="0 0 200 50" enable-background="new 0 0 200 50" xml:space="preserve">
<polygon fill="#DBDCDC" points="18.325,42.875 37.912,6.593 57.5,42.875 "/>
<polygon fill="#EEEEEF" points="66.771,6.594 94.125,24.744 66.771,42.895 "/>
<polygon fill="#9E9E9F" points="180.347,6.594 165.384,25 150.421,6.594 "/>
<polygon fill="#9E9E9F" points="150.422,42.875 165.384,24.469 180.347,42.875 "/>
<circle fill="#DBDCDC" cx="122.75" cy="25.156" r="17.875"/>
</svg>

After

Width:  |  Height:  |  Size: 843 B

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<g id="圖層_2">
<polygon fill="#95D5F4" points="110.998,23.424 110.998,84.064 17.001,84.064 17.001,13.652 48.449,13.469 55.315,23.669 "/>
</g>
<g id="圖層_3">
<polygon fill="#6BC2EC" points="110.998,84.064 17.001,84.064 17.087,31.401 110.57,31.401 "/>
</g>
<g id="圖層_4">
<rect x="17.001" y="103.51" fill="#B5B5B6" width="93.997" height="4.691"/>
<rect x="60.985" y="84.064" fill="#B5B5B6" width="6.029" height="19.445"/>
<path fill="#C9CACA" d="M72.935,110.512c0,2.221-1.8,4.02-4.021,4.02h-9.827c-2.221,0-4.021-1.799-4.021-4.02v-9.828
c0-2.221,1.8-4.021,4.021-4.021h9.827c2.221,0,4.021,1.801,4.021,4.021V110.512z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="78.125,32.875 78.125,83.25
30.125,83.25 30.125,22 68.625,22 "/>
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="78.125,35.75 65.125,35.75 65.125,22 "/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="38.667" x2="60.417" y2="38.667"/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="45.167" x2="73.25" y2="45.167"/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="51.25" x2="73.25" y2="51.25"/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="57.833" x2="73.25" y2="57.833"/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.417" y1="64" x2="73.25" y2="64"/>
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.417" y1="70.75" x2="73.25" y2="70.75"/>
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="104.5,57.916 104.5,108.291
56.5,108.291 56.5,47.041 95,47.041 "/>
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="104.5,60.791 91.5,60.791 91.5,47.041 "/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="63.708" x2="86.791" y2="63.708"/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="70.207" x2="99.625" y2="70.207"/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="76.291" x2="99.625" y2="76.291"/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="82.875" x2="99.625" y2="82.875"/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="61.792" y1="89.041" x2="99.625" y2="89.041"/>
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="61.792" y1="95.791" x2="99.625" y2="95.791"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<polygon fill="#EC0013" stroke="#C30D23" stroke-miterlimit="10" points="95.338,37.37 88.63,30.662 64,55.292 39.37,30.662
32.662,37.37 57.292,62 32.662,86.63 39.369,93.338 64,68.707 88.63,93.338 95.338,86.631 70.707,62 "/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<image display="none" overflow="visible" width="347" height="333" xlink:href="data:image/jpeg;base64,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" transform="matrix(0.3272 0 0 0.3272 5.9824 3.0469)">
</image>
<rect x="19.833" y="28.5" fill="#322912" width="92.667" height="52.5"/>
<rect x="19.833" y="81" fill="#DCDDDD" width="92.667" height="10.667"/>
<rect x="50.167" y="101.167" fill="#DCDDDD" width="32.5" height="3.666"/>
<rect x="59.833" y="91.667" fill="#C9CACA" width="13.5" height="9.5"/>
<rect x="23.333" y="31.667" fill="#00A0E9" width="85.833" height="46"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="55.591,51.929 66.042,63.104
77.577,51.929 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" x1="66.042" y1="63.104" x2="66.043" y2="28.5"/>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<rect x="26.2" y="17.403" fill="#F5D370" width="71.08" height="88.553"/>
<g>
<g>
<polygon fill="#FFE79C" points="59.98,72.606 59.98,22.099 26.2,17.403 26.2,105.956 49.486,109.192 49.486,76.212 "/>
</g>
</g>
<polygon fill="#FFFFFF" stroke="#727171" stroke-miterlimit="10" points="109.001,90.244 94.813,90.244 94.813,76.057
87.313,76.057 87.313,90.244 73.126,90.244 73.126,97.744 87.313,97.744 87.313,111.932 94.813,111.932 94.813,97.744
109.001,97.744 "/>
</svg>

After

Width:  |  Height:  |  Size: 943 B

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<g id="圖層_2">
<g>
<polygon fill="#FADA79" points="104.029,38.089 104.029,101.074 22.388,101.074 22.388,27.94 49.702,27.75 55.666,38.345 "/>
</g>
</g>
<g id="圖層_3">
<g>
<polygon fill="#FFE79E" points="104.328,101.971 22.836,101.971 38.209,52.716 118.807,52.716 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<image display="none" overflow="visible" width="256" height="256" xlink:href="data:image/jpeg;base64,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
z/ad/wAVfOxia5zSMNU0ziHoY7S5SKa2yDOPCLi1YTH3VqwmFwsa2TrSykOUrv1ISHKuFcP9l3/F
XzsImuc0jDVNM4h6GO0uUim+BuiJafQSkfpqOTsMilIysImuc0jDVNM4h6GO0uUim+CiTO8TS5rC
JrnNIw1TTOIehjtLlIpunJUZkV+4M5WEvIZysJeQzlYS8hnKwl5DOVhLyGcrCXkM5WEvIZysJeQz
lYS8hnKwl5DOVhLyBqSZmZlJdk8umLqlzWETXOaRhqmmcQ9DHaXKRTfBRdUuawia5zSMNU0ziHoY
7S5SxdOUlBrPsLqY4dXjqjh1eOqOHV46o4dXjqjh1eOqOHV46o4dXjqjh1eOqOHV46o4dXjqjh1e
OqDJTRtkRSynL5F0xdUuawia5zSMNU0ziHoY7S5SxfBRdUuawia5zSMNU0ziGoY7S5SxfBRdUuaw
ia5zSMNU0ziHoY7S5SxdOQZmRX5SGeoZ6hnqGeoZ6hnqGeoZ6hnqGeoZ6hlEozlKS70xdUuawia5
zSMNU0ziGoY7S5SxfBRdUuawia5zSMNU0ziGoY7S5SxdOUSTUfYQ2KuvcNirr3DYq69w2KuvcNir
r3DYq69w2KuvcNirr3DYq69w2KuvcNirr3A5UGiTt6YuqXNYRNc5pGGqaZxDUMdpcpYvgouqXNYR
Nc5pGGqaZxD0MdpcpYvgouqXNYRNc5pGELv5KiPAcoZUh1KFoKT7xyENuzvCHEM7whxDO8IcQzvC
HEM7whxDO8IcQzvCHEM7wg4RKSssq+g8or3b05BmZfMhnqGeoZ6hnqGeoZ6hnqGeoZ6hnqGeoHIo
zl7emLqlzWETXOaR9B/pOKRLfyTkF19fqMbZeExtl4TG2XhMbZeExtl4TG2XhMbZeEwpTijUr9VV
07p/BRdUuawia5zSO1nXK6cokmr5ENirr3DYq69w2KuvcNirr3DYq69w2KuvcNirr3DYq69w2Kuv
cNirr3DYq69wOVBok7emLqlzWETXOaR2s65XwUXVLmsImuc0jtZ1yvgouqXMdhE1zmkdrOuX8FF1
K5rCJrnNI7WdcoXheF4XheF4XheF4XheF4XheF4XheEXUrmsImuc0jtf6TEQ403LLkpUZFKf0HGP
esxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96
zHGPesxxj3rMGRxbpkdwyNZ2ETXOaR/8D//Z" transform="matrix(0.459 0 0 0.459 5.25 4.5)">
</image>
<path fill="#D0B065" stroke="#B28247" stroke-width="2" stroke-miterlimit="10" d="M81.61,90.593c0,0.923-0.748,1.671-1.671,1.671
H24.671c-0.923,0-1.672-0.748-1.672-1.671V20.698c0-0.924,0.749-1.672,1.672-1.672h55.268c0.923,0,1.671,0.749,1.671,1.672V90.593z"
/>
<rect x="41.492" y="18.085" fill="#DCDDDD" stroke="#9FA0A0" stroke-width="2" stroke-miterlimit="10" width="22.985" height="10.552"/>
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="105.714,53.116 105.714,106.334
55.004,106.334 55.004,41.627 95.679,41.627 "/>
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="105.714,56.153 91.98,56.153 91.98,41.627
"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="59.234" x2="87.006" y2="59.234"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="66.101" x2="100.563" y2="66.101"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="72.527" x2="100.563" y2="72.527"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="79.484" x2="100.563" y2="79.484"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.595" y1="85.998" x2="100.563" y2="85.998"/>
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.595" y1="93.129" x2="100.563" y2="93.129"/>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<circle fill="#24CC29" cx="84.5" cy="42.833" r="13.833"/>
<circle fill="#24CC29" cx="36.5" cy="67.334" r="13.833"/>
<circle fill="#24CC29" cx="84.5" cy="92.167" r="13.833"/>
<line fill="none" stroke="#24CC29" stroke-width="9" stroke-miterlimit="10" x1="36.5" y1="67.334" x2="84.5" y2="42.833"/>
<line fill="none" stroke="#24CC29" stroke-width="9" stroke-miterlimit="10" x1="36.5" y1="67.334" x2="84.5" y2="92.166"/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<g>
<circle fill="#86D1EF" cx="37.667" cy="71.896" r="25.083"/>
<circle fill="#86D1EF" cx="64.917" cy="58.895" r="36.249"/>
<circle fill="#86D1EF" cx="94.999" cy="76.729" r="20.417"/>
<path fill="#86D1EF" d="M96.416,95.146c0,1.104-0.777,2-1.738,2H37.156c-0.961,0-1.739-0.896-1.739-2v-31c0-1.106,0.778-2,1.739-2
h57.521c0.961,0,1.738,0.895,1.738,2V95.146z"/>
</g>
<image display="none" overflow="visible" width="512" height="512" xlink:href="data:image/jpeg;base64,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
AeWG6LhvhCHNj7MT6vAf2QcwhzTaIs+jPyO7US5DbZJVDz2IQ+WGLXX08xw9ran8dujZzdMCIQPo
NluBBs3CLT9bLIwIfajNJPhJoOAqkWfQ1LiGjpNhEfc+68eFln32kWQB9hhuiy7Ci5xLnG2TZPM8
Pa2p/Hbo2c9+KM9o6KSRgKsxA/GaPhQu9Dhm9SF5LMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwl
eQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5
DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV3YcNt8ErzQwfxAH60qmNFc/GJI5zh7W1P47
dGz1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dGz1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dG
z1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dGz1A4e1tRti2x/l+faHmLd5bvLd5bvLd5bvLd5bv
Ld5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvL
d5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5f/AJX+H/t8zQf+
S//Z" transform="matrix(0.2174 0 0 0.2174 8.354 3.207)">
</image>
<polyline fill="none" stroke="#FFFFFF" stroke-width="10" stroke-miterlimit="10" points="78.459,66.957 64.917,52.479
49.973,66.957 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="10" stroke-miterlimit="10" x1="64.917" y1="52.479" x2="64.917" y2="97.313"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
<circle fill="#231815" stroke="#231815" stroke-miterlimit="10" cx="64" cy="98.551" r="8.801"/>
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M10.296,49.66
c29.139-29.141,76.297-29.141,105.436,0"/>
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M28.269,65.352
c19.917-19.917,52.152-19.917,72.068,0"/>
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M45.548,81.838
c10.367-10.367,27.145-10.367,37.509,0"/>
</svg>

After

Width:  |  Height:  |  Size: 981 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path stroke="#231815" stroke-width="0.4px" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zm.5-7H9v2H7v1h2v2h1v-2h2V9h-2z"/></svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path stroke="#231815" stroke-width="0.4px" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"/></svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -96,6 +96,13 @@
$('<th>').text('Vendor')
)
);
if (data.error != undefined){
$('#mdns-hosts').html(`<table class="ui celled unstackable table"><tbody></tbody></table>`);
$('#mdns-hosts').find("tbody").append(`<tr><td colspan="5"><i class="ui red circle times icon"></i> ${data.error}</td></tr>`);
$("#discover").addClass("disabled");
return;
}
// Create table body
var tableBody = $('<tbody>');

View File

@ -19,7 +19,6 @@ package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
@ -104,7 +103,21 @@ func HandleCountryDistrSummary(w http.ResponseWriter, r *http.Request) {
/*
Up Time Monitor
*/
//Generate uptime monitor targets from reverse proxy rules
// Update uptime monitor targets after rules updated
// See https://github.com/tobychui/zoraxy/issues/77
func UpdateUptimeMonitorTargets() {
if uptimeMonitor != nil {
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
go func() {
uptimeMonitor.ExecuteUptimeCheck()
}()
SystemWideLogger.PrintAndLog("Uptime", "Uptime monitor config updated", nil)
}
}
// Generate uptime monitor targets from reverse proxy rules
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
subds := dp.GetSDProxyEndpointsAsMap()
vdirs := dp.GetVDProxyEndpointsAsMap()
@ -156,11 +169,19 @@ func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) {
// Handle listing current registered mdns nodes
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
if mdnsScanner == nil {
utils.SendErrorResponse(w, "mDNS scanner is disabled on this host")
return
}
js, _ := json.Marshal(previousmdnsScanResults)
utils.SendJSONResponse(w, string(js))
}
func HandleMdnsScanning(w http.ResponseWriter, r *http.Request) {
if mdnsScanner == nil {
utils.SendErrorResponse(w, "mDNS scanner is disabled on this host")
return
}
domain, err := utils.PostPara(r, "domain")
var hosts []*mdns.NetworkHost
if err != nil {
@ -255,7 +276,7 @@ func HandleWakeOnLan(w http.ResponseWriter, r *http.Request) {
return
}
log.Println("[WoL] Sending Wake on LAN magic packet to " + wake)
SystemWideLogger.PrintAndLog("WoL", "Sending Wake on LAN magic packet to "+wake, nil)
err := wakeonlan.WakeTarget(wake)
if err != nil {
utils.SendErrorResponse(w, err.Error())
@ -327,3 +348,20 @@ func HandleZoraxyInfo(w http.ResponseWriter, r *http.Request) {
js, _ := json.MarshalIndent(info, "", " ")
utils.SendJSONResponse(w, string(js))
}
func HandleGeoIpLookup(w http.ResponseWriter, r *http.Request) {
ip, err := utils.GetPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "ip not given")
return
}
cc, err := geodbStore.ResolveCountryCodeFromIP(ip)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(cc)
utils.SendJSONResponse(w, string(js))
}