mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-04 13:21:44 +02:00
Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
0eb0696670 | |||
9fca2354c6 | |||
e56b045689 | |||
763ccb4d60 | |||
4d4492069d | |||
f3591aa171 | |||
2dcf578cbe | |||
23a5c6ceb0 | |||
015889851a | |||
093ed9c212 | |||
0af8c67346 | |||
c5170bcb94 | |||
cd48388c02 | |||
373845f8fd | |||
293a527ffc | |||
e4facbc7b6 | |||
1c79fa4e96 | |||
6515eb99e3 | |||
ec5c24b9b8 | |||
df88084375 | |||
74017baecf | |||
294d504ee6 | |||
477429900e | |||
2e9bc77a5d | |||
ed178d857a | |||
4cf5d29692 | |||
634e9c9855 | |||
e79a70b7ac | |||
779115d06b | |||
9cb315ea67 | |||
43ba00ec8d | |||
4577fb1f2f | |||
f877bf9eda | |||
363b9b6d94 | |||
c5ca68868b | |||
f927bb539a | |||
5f64b622b5 | |||
9a371f5bcb | |||
172c5afa60 | |||
f98e04a9fc | |||
99295cad86 | |||
95d0a98576 | |||
00bfa262cb | |||
528be69fe0 | |||
6923f0d200 | |||
7255b62e31 | |||
cf14d12c31 | |||
90cf26306a | |||
cab2f4e63a | |||
75d773887c | |||
a944c3ff36 | |||
465f332dfc | |||
dfda3fe94b | |||
5c56da1180 | |||
3392013a5c | |||
8b4c601d50 | |||
3a2eaf8766 | |||
a45092a449 |
43
.github/workflows/docker.yml
vendored
Normal file
43
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Build and push Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup-build-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Setup building file structure
|
||||||
|
run: |
|
||||||
|
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./docker
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
zoraxydocker/zoraxy:latest
|
||||||
|
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
40
.github/workflows/main.yml
vendored
40
.github/workflows/main.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: Image Publisher
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
setup-build-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.release.tag_name }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker & GHCR
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
|
||||||
|
|
||||||
- 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 \
|
|
||||||
--provenance=false \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
|
||||||
--tag zoraxydocker/zoraxy:latest \
|
|
||||||
.
|
|
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,3 +1,35 @@
|
|||||||
|
# v3.1.2 03 Nov 2024
|
||||||
|
|
||||||
|
+ Added auto start port 80 listener on acme certificate generator
|
||||||
|
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
|
||||||
|
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
|
||||||
|
+ Added support for X-Remote-User
|
||||||
|
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
|
||||||
|
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
|
||||||
|
+ Removed sorting on cert list
|
||||||
|
+ Fixed request certificate button bug
|
||||||
|
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
|
||||||
|
+ Fixed unable to remove new stream proxy bug
|
||||||
|
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
|
||||||
|
+ Added more code to SSO system (disabled in release)
|
||||||
|
|
||||||
|
|
||||||
|
# v3.1.1. 09 Sep 2024
|
||||||
|
|
||||||
|
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
|
||||||
|
+ Added tour for basic operations
|
||||||
|
+ Updated acme log to system wide logger implementation
|
||||||
|
+ Fixed path traversal in file manager [#274](https://github.com/tobychui/zoraxy/issues/274)
|
||||||
|
+ Removed Proxmox debug code
|
||||||
|
+ Fixed trie tree implementations
|
||||||
|
|
||||||
|
**Thanks to all contributors**
|
||||||
|
|
||||||
|
+ Fix existing containers list in docker popup [7brend7](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3A7brend7)
|
||||||
|
+ Fix network I/O chart not rendering [JokerQyou](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3AJokerQyou)
|
||||||
|
+ Fix typo remvoeClass to removeClass [Aahmadsyamim](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Aahmadsyamim)
|
||||||
|
+ Updated weighted random upstream implementation [bouroo](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Abouroo)
|
||||||
|
|
||||||
# v3.1.0 31 Jul 2024
|
# v3.1.0 31 Jul 2024
|
||||||
|
|
||||||
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
|
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
|
||||||
|
@ -33,6 +33,7 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
|||||||
- Basic single-admin management mode
|
- Basic single-admin management mode
|
||||||
- External permission management system for easy system integration
|
- External permission management system for easy system integration
|
||||||
- SMTP config for password reset
|
- SMTP config for password reset
|
||||||
|
- Dark Theme Mode
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ If you have no background in setting up reverse proxy or web routing, you should
|
|||||||
|
|
||||||
## Build from Source
|
## Build from Source
|
||||||
|
|
||||||
Requires Go 1.22 or higher
|
Requires Go 1.23 or higher
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/tobychui/zoraxy
|
git clone https://github.com/tobychui/zoraxy
|
||||||
@ -102,6 +103,8 @@ Usage of zoraxy:
|
|||||||
Enable auto config upgrade if breaking change is detected (default true)
|
Enable auto config upgrade if breaking change is detected (default true)
|
||||||
-docker
|
-docker
|
||||||
Run Zoraxy in docker compatibility mode
|
Run Zoraxy in docker compatibility mode
|
||||||
|
-earlyrenew int
|
||||||
|
Number of days to early renew a soon expiring certificate (days) (default 30)
|
||||||
-fastgeoip
|
-fastgeoip
|
||||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||||
-mdns
|
-mdns
|
||||||
@ -119,7 +122,7 @@ Usage of zoraxy:
|
|||||||
-webfm
|
-webfm
|
||||||
Enable web file manager for static web server root folder (default true)
|
Enable web file manager for static web server root folder (default true)
|
||||||
-webroot string
|
-webroot string
|
||||||
Static web server root folder. Only allow change in start parameters (default "./www")
|
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
||||||
-ztauth string
|
-ztauth string
|
||||||
ZeroTier authtoken for the local node
|
ZeroTier authtoken for the local node
|
||||||
-ztport int
|
-ztport int
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/golang:alpine AS build
|
FROM docker.io/golang:alpine AS build-zoraxy
|
||||||
|
|
||||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||||
mkdir -p /usr/local/bin/
|
mkdir -p /usr/local/bin/
|
||||||
@ -12,17 +12,31 @@ RUN go mod tidy &&\
|
|||||||
go build -o /usr/local/bin/zoraxy &&\
|
go build -o /usr/local/bin/zoraxy &&\
|
||||||
chmod 755 /usr/local/bin/zoraxy
|
chmod 755 /usr/local/bin/zoraxy
|
||||||
|
|
||||||
FROM docker.io/alpine:latest
|
FROM docker.io/ubuntu:latest AS build-zerotier
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/source/
|
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||||
|
mkdir -p /usr/local/bin/
|
||||||
|
|
||||||
RUN apk add --no-cache bash netcat-openbsd sudo &&\
|
WORKDIR /opt/zerotier/source/
|
||||||
wget https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/zerotier-one-1.10.2-r0.apk &&\
|
|
||||||
apk add --no-cache zerotier-one-1.10.2-r0.apk &&\
|
RUN apt-get update -y &&\
|
||||||
rm -r /opt/zoraxy/source/
|
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
||||||
|
|
||||||
|
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||||
|
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||||
|
cd ZeroTierOne-* &&\
|
||||||
|
make &&\
|
||||||
|
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||||
|
chmod 755 /usr/local/bin/zerotier-one
|
||||||
|
|
||||||
|
FROM docker.io/ubuntu:latest
|
||||||
|
|
||||||
|
RUN apt-get update -y &&\
|
||||||
|
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
|
||||||
|
|
||||||
COPY --from=build /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
|
||||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||||
|
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||||
|
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/config/
|
WORKDIR /opt/zoraxy/config/
|
||||||
|
|
||||||
@ -44,7 +58,7 @@ ENV WEBROOT="./www"
|
|||||||
ENV ZTAUTH=""
|
ENV ZTAUTH=""
|
||||||
ENV ZTPORT="9993"
|
ENV ZTPORT="9993"
|
||||||
|
|
||||||
VOLUME [ "/opt/zoraxy/config/", "/var/lib/zerotier-one/" ]
|
VOLUME [ "/opt/zoraxy/config/" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ docker run -d \
|
|||||||
-p 443:443 \
|
-p 443:443 \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
||||||
-v /path/to/zerotier/config/:/var/lib/zerotier-one/ \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v /etc/localtime:/etc/localtime \
|
-v /etc/localtime:/etc/localtime \
|
||||||
-e FASTGEOIP="true" \
|
-e FASTGEOIP="true" \
|
||||||
@ -45,7 +44,6 @@ services:
|
|||||||
- 8000:8000
|
- 8000:8000
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||||
- /path/to/zerotier/config/:/var/lib/zerotier-one/
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /etc/localtime:/etc/localtime
|
- /etc/localtime:/etc/localtime
|
||||||
environment:
|
environment:
|
||||||
@ -66,7 +64,6 @@ services:
|
|||||||
| Volume | Details |
|
| Volume | Details |
|
||||||
|:-|:-|
|
|:-|:-|
|
||||||
| `/opt/zoraxy/config/` | Zoraxy configuration. |
|
| `/opt/zoraxy/config/` | Zoraxy configuration. |
|
||||||
| `/var/lib/zerotier-one/` | ZeroTier configuration. Only required if you wish to use ZeroTier. |
|
|
||||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||||
|
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
update-ca-certificates
|
||||||
|
echo "CA certificates updated"
|
||||||
|
|
||||||
if [ "$ZEROTIER" = "true" ]; then
|
if [ "$ZEROTIER" = "true" ]; then
|
||||||
echo "Starting ZeroTier daemon..."
|
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
|
||||||
|
mkdir -p /opt/zoraxy/config/zerotier/
|
||||||
|
fi
|
||||||
|
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
|
||||||
zerotier-one -d
|
zerotier-one -d
|
||||||
|
echo "ZeroTier daemon started"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting Zoraxy..."
|
echo "Starting Zoraxy..."
|
||||||
|
@ -1 +1 @@
|
|||||||
zoraxy.arozos.com
|
zoraxy.aroz.org
|
@ -12,19 +12,19 @@
|
|||||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||||
|
|
||||||
<!-- Facebook Meta Tags -->
|
<!-- Facebook Meta Tags -->
|
||||||
<meta property="og:url" content="https://zoraxy.arozos.com/">
|
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||||
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png">
|
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||||
|
|
||||||
<!-- Twitter Meta Tags -->
|
<!-- Twitter Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta property="twitter:domain" content="arozos.com">
|
<meta property="twitter:domain" content="aroz.org">
|
||||||
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
|
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||||
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png">
|
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- Favicons -->
|
||||||
<link href="favicon.png" rel="icon">
|
<link href="favicon.png" rel="icon">
|
||||||
|
@ -230,7 +230,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rule.AddCountryCodeToBlackList(countryCode, comment)
|
//Check if the country code contains comma, if yes, split it
|
||||||
|
if strings.Contains(countryCode, ",") {
|
||||||
|
codes := strings.Split(countryCode, ",")
|
||||||
|
for _, code := range codes {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
rule.AddCountryCodeToBlackList(code, comment)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = strings.TrimSpace(countryCode)
|
||||||
|
rule.AddCountryCodeToBlackList(countryCode, comment)
|
||||||
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -254,7 +264,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rule.RemoveCountryCodeFromBlackList(countryCode)
|
//Check if the country code contains comma, if yes, split it
|
||||||
|
if strings.Contains(countryCode, ",") {
|
||||||
|
codes := strings.Split(countryCode, ",")
|
||||||
|
for _, code := range codes {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
rule.RemoveCountryCodeFromBlackList(code)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = strings.TrimSpace(countryCode)
|
||||||
|
rule.RemoveCountryCodeFromBlackList(countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -397,7 +417,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
p := bluemonday.StrictPolicy()
|
p := bluemonday.StrictPolicy()
|
||||||
comment = p.Sanitize(comment)
|
comment = p.Sanitize(comment)
|
||||||
|
|
||||||
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
//Check if the country code contains comma, if yes, split it
|
||||||
|
if strings.Contains(countryCode, ",") {
|
||||||
|
codes := strings.Split(countryCode, ",")
|
||||||
|
for _, code := range codes {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
rule.AddCountryCodeToWhitelist(code, comment)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = strings.TrimSpace(countryCode)
|
||||||
|
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
||||||
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -420,7 +450,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
//Check if the country code contains comma, if yes, split it
|
||||||
|
if strings.Contains(countryCode, ",") {
|
||||||
|
codes := strings.Split(countryCode, ",")
|
||||||
|
for _, code := range codes {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
rule.RemoveCountryCodeFromWhitelist(code)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countryCode = strings.TrimSpace(countryCode)
|
||||||
|
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
51
src/acme.go
51
src/acme.go
@ -41,6 +41,20 @@ func initACME() *acme.ACMEHandler {
|
|||||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
|
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart ACME handler and auto renewer
|
||||||
|
func restartACMEHandler() {
|
||||||
|
SystemWideLogger.Println("Restarting ACME handler")
|
||||||
|
//Clos the current handler and auto renewer
|
||||||
|
acmeHandler.Close()
|
||||||
|
acmeAutoRenewer.Close()
|
||||||
|
acmeDeregisterSpecialRoutingRule()
|
||||||
|
|
||||||
|
//Reinit the handler with a new random port
|
||||||
|
acmeHandler = initACME()
|
||||||
|
|
||||||
|
acmeRegisterSpecialRoutingRule()
|
||||||
|
}
|
||||||
|
|
||||||
// create the special routing rule for ACME
|
// create the special routing rule for ACME
|
||||||
func acmeRegisterSpecialRoutingRule() {
|
func acmeRegisterSpecialRoutingRule() {
|
||||||
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
|
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||||
@ -82,12 +96,29 @@ func acmeRegisterSpecialRoutingRule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove the special routing rule for ACME
|
||||||
|
func acmeDeregisterSpecialRoutingRule() {
|
||||||
|
SystemWideLogger.Println("Removing ACME routing rule")
|
||||||
|
dynamicProxyRouter.RemoveRoutingRule("acme-autorenew")
|
||||||
|
}
|
||||||
|
|
||||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
isForceHttpsRedirectEnabledOriginally := false
|
isForceHttpsRedirectEnabledOriginally := false
|
||||||
|
requireRestorePort80 := false
|
||||||
dnsPara, _ := utils.PostBool(r, "dns")
|
dnsPara, _ := utils.PostBool(r, "dns")
|
||||||
if !dnsPara {
|
if !dnsPara {
|
||||||
|
|
||||||
if dynamicProxyRouter.Option.Port == 443 {
|
if dynamicProxyRouter.Option.Port == 443 {
|
||||||
|
//Check if port 80 is enabled
|
||||||
|
if !dynamicProxyRouter.Option.ListenOnPort80 {
|
||||||
|
//Enable port 80 temporarily
|
||||||
|
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
|
||||||
|
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||||
|
requireRestorePort80 = true
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
//Enable port 80 to 443 redirect
|
//Enable port 80 to 443 redirect
|
||||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||||
SystemWideLogger.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")
|
||||||
@ -107,8 +138,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add a 3 second delay to make sure everything is settle down
|
//Add a 2 second delay to make sure everything is settle down
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Pass over to the acmeHandler to deal with the communication
|
// Pass over to the acmeHandler to deal with the communication
|
||||||
acmeHandler.HandleRenewCertificate(w, r)
|
acmeHandler.HandleRenewCertificate(w, r)
|
||||||
@ -117,13 +148,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
|||||||
tlsCertManager.UpdateLoadedCertList()
|
tlsCertManager.UpdateLoadedCertList()
|
||||||
|
|
||||||
//Restore original settings
|
//Restore original settings
|
||||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
if requireRestorePort80 {
|
||||||
if !isForceHttpsRedirectEnabledOriginally {
|
//Restore port 80 listener
|
||||||
//Default is off. Turn the redirection off
|
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
|
||||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if !isForceHttpsRedirectEnabledOriginally {
|
||||||
|
//Default is off. Turn the redirection off
|
||||||
|
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
|
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
|
||||||
|
281
src/api.go
281
src/api.go
@ -8,6 +8,8 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||||
|
"imuslab.com/zoraxy/mod/ipscan"
|
||||||
"imuslab.com/zoraxy/mod/netstat"
|
"imuslab.com/zoraxy/mod/netstat"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
@ -17,34 +19,11 @@ import (
|
|||||||
API.go
|
API.go
|
||||||
|
|
||||||
This file contains all the API called by the web management interface
|
This file contains all the API called by the web management interface
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var requireAuth = true
|
// Register the APIs for HTTP proxy management functions
|
||||||
|
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||||
func initAPIs(targetMux *http.ServeMux) {
|
/* Reverse Proxy Settings & Status */
|
||||||
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
|
||||||
AuthAgent: authAgent,
|
|
||||||
RequireAuth: requireAuth,
|
|
||||||
TargetMux: targetMux,
|
|
||||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
//Register the standard web services urls
|
|
||||||
fs := http.FileServer(http.FS(webres))
|
|
||||||
if development {
|
|
||||||
fs = http.FileServer(http.Dir("web/"))
|
|
||||||
}
|
|
||||||
//Add a layer of middleware for advance control
|
|
||||||
advHandler := FSHandler(fs)
|
|
||||||
targetMux.Handle("/", advHandler)
|
|
||||||
|
|
||||||
//Authentication APIs
|
|
||||||
registerAuthAPIs(requireAuth, targetMux)
|
|
||||||
|
|
||||||
//Reverse proxy
|
|
||||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||||
@ -55,24 +34,24 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
|
||||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||||
//Reverse proxy upstream (load balance) APIs
|
/* Reverse proxy upstream (load balance) */
|
||||||
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
|
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
|
||||||
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
|
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
|
||||||
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
|
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
|
||||||
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
|
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
|
||||||
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
|
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
|
||||||
//Reverse proxy virtual directory APIs
|
/* Reverse proxy virtual directory */
|
||||||
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||||
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||||
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||||
//Reverse proxy user define header apis
|
/* Reverse proxy user-defined header */
|
||||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||||
@ -80,12 +59,14 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
|
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
|
||||||
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
|
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
|
||||||
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
|
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
|
||||||
//Reverse proxy auth related APIs
|
/* Reverse proxy auth related */
|
||||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
|
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
|
||||||
|
}
|
||||||
|
|
||||||
//TLS / SSL config
|
// Register the APIs for TLS / SSL certificate management functions
|
||||||
|
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||||
@ -94,48 +75,84 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||||
|
}
|
||||||
|
|
||||||
//Redirection config
|
// Register the APIs for SSO and Oauth functions, WIP
|
||||||
|
func RegisterSSOAPIs(authRouter *auth.RouterDef) {
|
||||||
|
authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus)
|
||||||
|
authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable)
|
||||||
|
authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
|
||||||
|
authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
|
||||||
|
|
||||||
|
authRouter.HandleFunc("/api/sso/app/register", ssoHandler.HandleRegisterApp)
|
||||||
|
//authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp)
|
||||||
|
//authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp)
|
||||||
|
|
||||||
|
authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser)
|
||||||
|
authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser)
|
||||||
|
authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser)
|
||||||
|
authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the APIs for redirection rules management functions
|
||||||
|
func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||||
|
}
|
||||||
|
|
||||||
//Access Rules API
|
// Register the APIs for access rules management functions
|
||||||
|
func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
|
||||||
|
/* Access Rules Settings & Status */
|
||||||
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
||||||
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
||||||
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||||
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||||
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||||
//Blacklist APIs
|
/* Blacklist */
|
||||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||||
//Whitelist APIs
|
/* Whitelist */
|
||||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||||
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
||||||
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
||||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||||
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
||||||
|
}
|
||||||
|
|
||||||
//Path Blocker APIs
|
// Register the APIs for path blocking rules management functions, WIP
|
||||||
|
func RegisterPathRuleAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
|
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
|
||||||
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
|
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
|
||||||
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
|
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
|
||||||
|
}
|
||||||
|
|
||||||
//Statistic & uptime monitoring API
|
// Register the APIs statistic anlysis and uptime monitoring functions
|
||||||
|
func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
|
||||||
|
/* Traffic Summary */
|
||||||
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
||||||
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
|
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
|
||||||
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
|
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
|
||||||
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
||||||
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
|
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
|
||||||
|
/* Zoraxy Analytic */
|
||||||
|
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
||||||
|
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
||||||
|
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
||||||
|
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
||||||
|
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||||
|
/* UpTime Monitor */
|
||||||
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
||||||
|
}
|
||||||
|
|
||||||
//Global Area Network APIs
|
// Register the APIs for Global Area Network management functions, Will be moving to plugin soon
|
||||||
|
func RegisterGANAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
|
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
|
||||||
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
|
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
|
||||||
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
|
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
|
||||||
@ -150,8 +167,10 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||||
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
||||||
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||||
|
}
|
||||||
|
|
||||||
//Stream (TCP / UDP) Proxy
|
// Register the APIs for Stream (TCP / UDP) Proxy management functions
|
||||||
|
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
||||||
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
|
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
|
||||||
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
||||||
@ -159,20 +178,59 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
|
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
|
||||||
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
|
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
|
||||||
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
|
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
|
||||||
|
}
|
||||||
|
|
||||||
//mDNS APIs
|
// Register the APIs for mDNS service management functions
|
||||||
|
func RegisterMDNSAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
||||||
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
|
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
|
||||||
|
}
|
||||||
|
|
||||||
//Zoraxy Analytic
|
// Register the APIs for ACME and Auto Renewer management functions
|
||||||
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
/* ACME Core */
|
||||||
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||||
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
/* Auto Renewer */
|
||||||
|
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/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
||||||
|
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS)
|
||||||
|
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||||
|
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||||
|
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||||
|
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
|
||||||
|
/* ACME Wizard */
|
||||||
|
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck)
|
||||||
|
}
|
||||||
|
|
||||||
//Network utilities
|
// Register the APIs for Static Web Server management functions
|
||||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
|
||||||
|
/* Static Web Server Controls */
|
||||||
|
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||||
|
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||||
|
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||||
|
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||||
|
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||||
|
/* File Manager */
|
||||||
|
if *allowWebFileManager {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the APIs for Network Utilities functions
|
||||||
|
func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) {
|
||||||
|
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
|
||||||
|
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
|
||||||
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
||||||
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
||||||
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||||
@ -185,66 +243,10 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||||
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||||
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
||||||
|
|
||||||
//Account Reset
|
|
||||||
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
|
||||||
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
|
||||||
|
|
||||||
//ACME & Auto Renewer
|
|
||||||
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/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
|
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
|
||||||
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
|
|
||||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
|
||||||
|
|
||||||
//Static Web Server
|
|
||||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
|
||||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
|
||||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
|
||||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
|
||||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
|
||||||
if *allowWebFileManager {
|
|
||||||
//Web Directory Manager file operation functions
|
|
||||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
|
||||||
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
|
|
||||||
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
|
|
||||||
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
|
|
||||||
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
|
|
||||||
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
|
|
||||||
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
|
|
||||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Docker UX Optimizations
|
|
||||||
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
|
|
||||||
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
|
|
||||||
|
|
||||||
//Others
|
|
||||||
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
|
||||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
|
||||||
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
|
||||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
|
||||||
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
|
||||||
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
|
||||||
|
|
||||||
//Debug
|
|
||||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
|
||||||
|
|
||||||
//If you got APIs to add, append them here
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to renders Auth related APIs
|
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||||
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
||||||
//Auth APIs
|
|
||||||
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||||
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||||
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -260,21 +262,17 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
|||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
js, _ := json.Marshal(username)
|
js, _ := json.Marshal(username)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
})
|
})
|
||||||
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||||
uc := authAgent.GetUserCounts()
|
js, _ := json.Marshal(authAgent.GetUserCounts())
|
||||||
js, _ := json.Marshal(uc)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
})
|
})
|
||||||
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if authAgent.GetUserCounts() == 0 {
|
if authAgent.GetUserCounts() == 0 {
|
||||||
//Allow register root admin
|
//Allow register root admin
|
||||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
|
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
//This function is disabled
|
//This function is disabled
|
||||||
utils.SendErrorResponse(w, "Root management account already exists")
|
utils.SendErrorResponse(w, "Root management account already exists")
|
||||||
@ -315,5 +313,60 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
|||||||
authAgent.UnregisterUser(username)
|
authAgent.UnregisterUser(username)
|
||||||
authAgent.CreateUserAccount(username, newPassword, "")
|
authAgent.CreateUserAccount(username, newPassword, "")
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Register all the APIs */
|
||||||
|
func initAPIs(targetMux *http.ServeMux) {
|
||||||
|
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
||||||
|
AuthAgent: authAgent,
|
||||||
|
RequireAuth: requireAuth,
|
||||||
|
TargetMux: targetMux,
|
||||||
|
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//Register the standard web services urls
|
||||||
|
fs := http.FileServer(http.FS(webres))
|
||||||
|
if DEVELOPMENT_BUILD {
|
||||||
|
fs = http.FileServer(http.Dir("web/"))
|
||||||
|
}
|
||||||
|
//Add a layer of middleware for advance control
|
||||||
|
advHandler := FSHandler(fs)
|
||||||
|
targetMux.Handle("/", advHandler)
|
||||||
|
|
||||||
|
//Register the APIs
|
||||||
|
RegisterAuthAPIs(requireAuth, targetMux)
|
||||||
|
RegisterHTTPProxyAPIs(authRouter)
|
||||||
|
RegisterTLSAPIs(authRouter)
|
||||||
|
//RegisterSSOAPIs(authRouter)
|
||||||
|
RegisterRedirectionAPIs(authRouter)
|
||||||
|
RegisterAccessRuleAPIs(authRouter)
|
||||||
|
RegisterPathRuleAPIs(authRouter)
|
||||||
|
RegisterStatisticalAPIs(authRouter)
|
||||||
|
RegisterGANAPIs(authRouter)
|
||||||
|
RegisterStreamProxyAPIs(authRouter)
|
||||||
|
RegisterMDNSAPIs(authRouter)
|
||||||
|
RegisterNetworkUtilsAPIs(authRouter)
|
||||||
|
RegisterACMEAndAutoRenewerAPIs(authRouter)
|
||||||
|
RegisterStaticWebServerAPIs(authRouter)
|
||||||
|
|
||||||
|
//Account Reset
|
||||||
|
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||||
|
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
||||||
|
|
||||||
|
//Docker UX Optimizations
|
||||||
|
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
|
||||||
|
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
|
||||||
|
|
||||||
|
//Others
|
||||||
|
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||||
|
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||||
|
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||||
|
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||||
|
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
||||||
|
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
||||||
|
|
||||||
|
//Debug
|
||||||
|
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,10 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Handle front-end toggling TLS mode
|
// Handle front-end toggling TLS mode
|
||||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
currentTlsSetting := false
|
currentTlsSetting := true //Default to true
|
||||||
|
if dynamicProxyRouter.Option != nil {
|
||||||
|
currentTlsSetting = dynamicProxyRouter.Option.UseTls
|
||||||
|
}
|
||||||
if sysdb.KeyExists("settings", "usetls") {
|
if sysdb.KeyExists("settings", "usetls") {
|
||||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||||
}
|
}
|
||||||
|
138
src/def.go
Normal file
138
src/def.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
Type and flag definations
|
||||||
|
|
||||||
|
This file contains all the type and flag definations
|
||||||
|
Author: tobychui
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"flag"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/access"
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso"
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/dockerux"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
|
"imuslab.com/zoraxy/mod/email"
|
||||||
|
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||||
|
"imuslab.com/zoraxy/mod/ganserv"
|
||||||
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logviewer"
|
||||||
|
"imuslab.com/zoraxy/mod/mdns"
|
||||||
|
"imuslab.com/zoraxy/mod/netstat"
|
||||||
|
"imuslab.com/zoraxy/mod/pathrule"
|
||||||
|
"imuslab.com/zoraxy/mod/sshprox"
|
||||||
|
"imuslab.com/zoraxy/mod/statistic"
|
||||||
|
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||||
|
"imuslab.com/zoraxy/mod/streamproxy"
|
||||||
|
"imuslab.com/zoraxy/mod/tlscert"
|
||||||
|
"imuslab.com/zoraxy/mod/uptime"
|
||||||
|
"imuslab.com/zoraxy/mod/webserv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
/* Build Constants */
|
||||||
|
SYSTEM_NAME = "Zoraxy"
|
||||||
|
SYSTEM_VERSION = "3.1.4"
|
||||||
|
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||||
|
|
||||||
|
/* System Constants */
|
||||||
|
DATABASE_PATH = "sys.db"
|
||||||
|
TMP_FOLDER = "./tmp"
|
||||||
|
WEBSERV_DEFAULT_PORT = 5487
|
||||||
|
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
|
||||||
|
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
|
||||||
|
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
|
||||||
|
MDNS_IDENTIFY_VENDOR = "imuslab.com"
|
||||||
|
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
|
||||||
|
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
|
||||||
|
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
|
||||||
|
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
|
||||||
|
CSRF_COOKIENAME = "zoraxy_csrf"
|
||||||
|
LOG_PREFIX = "zr"
|
||||||
|
LOG_FOLDER = "./log"
|
||||||
|
LOG_EXTENSION = ".log"
|
||||||
|
|
||||||
|
/* Configuration Folder Storage Path Constants */
|
||||||
|
CONF_HTTP_PROXY = "./conf/proxy"
|
||||||
|
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||||
|
CONF_CERT_STORE = "./conf/certs"
|
||||||
|
CONF_REDIRECTION = "./conf/redirect"
|
||||||
|
CONF_ACCESS_RULE = "./conf/access"
|
||||||
|
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* System Startup Flags */
|
||||||
|
var (
|
||||||
|
webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||||
|
noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||||
|
showver = flag.Bool("version", false, "Show version of this server")
|
||||||
|
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||||
|
allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||||
|
mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
|
||||||
|
ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||||
|
ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||||
|
runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
|
||||||
|
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||||
|
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
|
||||||
|
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
|
||||||
|
staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
|
||||||
|
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||||
|
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Global Variables and Handlers */
|
||||||
|
var (
|
||||||
|
nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup
|
||||||
|
bootTime = time.Now().Unix()
|
||||||
|
requireAuth = true //Require authentication for webmin panel, override from flag
|
||||||
|
|
||||||
|
/*
|
||||||
|
Binary Embedding File System
|
||||||
|
*/
|
||||||
|
//go:embed web/*
|
||||||
|
webres embed.FS
|
||||||
|
|
||||||
|
/*
|
||||||
|
Handler Modules
|
||||||
|
*/
|
||||||
|
sysdb *database.Database //System database
|
||||||
|
authAgent *auth.AuthAgent //Authentication agent
|
||||||
|
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||||
|
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||||
|
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
|
||||||
|
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
|
||||||
|
|
||||||
|
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||||
|
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
||||||
|
accessController *access.Controller //Access controller, handle black list and white list
|
||||||
|
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||||
|
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||||
|
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||||
|
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
||||||
|
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||||
|
webSshManager *sshprox.Manager //Web SSH connection service
|
||||||
|
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
|
||||||
|
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||||
|
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||||
|
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||||
|
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||||
|
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||||
|
ssoHandler *sso.SSOHandler //Single Sign On handler
|
||||||
|
|
||||||
|
//Helper modules
|
||||||
|
EmailSender *email.Sender //Email sender that handle email sending
|
||||||
|
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||||
|
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
|
||||||
|
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||||
|
LogViewer *logviewer.Viewer //Log viewer HTTP handlers
|
||||||
|
)
|
204
src/go.mod
204
src/go.mod
@ -1,107 +1,127 @@
|
|||||||
module imuslab.com/zoraxy
|
module imuslab.com/zoraxy
|
||||||
|
|
||||||
go 1.21
|
go 1.22.0
|
||||||
|
|
||||||
toolchain go1.22.2
|
toolchain go1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boltdb/bolt v1.3.1
|
github.com/boltdb/bolt v1.3.1
|
||||||
github.com/docker/docker v27.0.0+incompatible
|
github.com/docker/docker v27.0.0+incompatible
|
||||||
github.com/go-acme/lego/v4 v4.16.1
|
github.com/go-acme/lego/v4 v4.19.2
|
||||||
github.com/go-ping/ping v1.1.0
|
github.com/go-ping/ping v1.1.0
|
||||||
|
github.com/go-session/session v3.1.2+incompatible
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/sessions v1.2.2
|
github.com/gorilla/sessions v1.2.2
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/likexian/whois v1.15.1
|
github.com/likexian/whois v1.15.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.29.0
|
||||||
golang.org/x/sys v0.20.0
|
golang.org/x/sys v0.25.0
|
||||||
golang.org/x/text v0.15.0
|
golang.org/x/text v0.18.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.25.1 // indirect
|
cloud.google.com/go/auth v0.9.3 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
|
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
||||||
|
github.com/tidwall/buntdb v1.1.2 // indirect
|
||||||
|
github.com/tidwall/gjson v1.12.1 // indirect
|
||||||
|
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
|
||||||
|
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute/metadata v0.5.1 // indirect
|
||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
|
||||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
github.com/aws/smithy-go v1.20.4 // indirect
|
||||||
github.com/aws/smithy-go v1.19.0 // indirect
|
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/civo/civogo v0.3.11 // indirect
|
github.com/civo/civogo v0.3.11 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
|
||||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/google/s2a-go v0.1.8 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||||
github.com/gophercloud/gophercloud v1.0.0 // indirect
|
github.com/gophercloud/gophercloud v1.14.0 // indirect
|
||||||
github.com/gorilla/csrf v1.7.2 // indirect
|
github.com/gorilla/csrf v1.7.2
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
@ -111,11 +131,11 @@ require (
|
|||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
github.com/linode/linodego v1.28.0 // indirect
|
github.com/linode/linodego v1.40.0 // indirect
|
||||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.58 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@ -126,66 +146,64 @@ require (
|
|||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||||
github.com/nrdcg/desec v0.7.0 // indirect
|
github.com/nrdcg/desec v0.8.0 // indirect
|
||||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.3.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/pquerna/otp v1.4.0 // indirect
|
github.com/pquerna/otp v1.4.0 // indirect
|
||||||
github.com/sacloud/api-client-go v0.2.8 // indirect
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
github.com/sacloud/go-http v0.1.6 // indirect
|
github.com/sacloud/go-http v0.1.8 // indirect
|
||||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
github.com/sacloud/iaas-api-go v1.12.0 // indirect
|
||||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
github.com/softlayer/softlayer-go v1.1.3 // indirect
|
github.com/softlayer/softlayer-go v1.1.5 // indirect
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
github.com/spf13/cast v1.3.1 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
github.com/stretchr/testify v1.9.0 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
github.com/xlzd/gotp v0.1.0
|
||||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
|
||||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
go.uber.org/ratelimit v0.2.0 // indirect
|
go.uber.org/ratelimit v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.27.0 // indirect
|
||||||
golang.org/x/mod v0.16.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/oauth2 v0.18.0 // indirect
|
golang.org/x/oauth2 v0.23.0 // indirect
|
||||||
golang.org/x/sync v0.6.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
golang.org/x/tools v0.19.0 // indirect
|
golang.org/x/tools v0.25.0 // indirect
|
||||||
google.golang.org/api v0.169.0 // indirect
|
google.golang.org/api v0.197.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
|
google.golang.org/grpc v1.66.1 // indirect
|
||||||
google.golang.org/grpc v1.64.0 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.12.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gotest.tools/v3 v3.5.1 // indirect
|
||||||
|
597
src/go.sum
597
src/go.sum
File diff suppressed because it is too large
Load Diff
132
src/main.go
132
src/main.go
@ -1,7 +1,36 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
______
|
||||||
|
|___ /
|
||||||
|
/ / ___ _ __ __ ___ ___ _
|
||||||
|
/ / / _ \| '__/ _` \ \/ / | | |
|
||||||
|
/ /_| (_) | | | (_| |> <| |_| |
|
||||||
|
/_____\___/|_| \__,_/_/\_\\__, |
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
Zoraxy - A general purpose HTTP reverse proxy and forwarding tool
|
||||||
|
Author: tobychui
|
||||||
|
License: AGPLv3
|
||||||
|
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, version 3 of the License or any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -13,98 +42,12 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"imuslab.com/zoraxy/mod/access"
|
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
|
||||||
"imuslab.com/zoraxy/mod/dockerux"
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
|
||||||
"imuslab.com/zoraxy/mod/email"
|
|
||||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
|
||||||
"imuslab.com/zoraxy/mod/ganserv"
|
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
|
||||||
"imuslab.com/zoraxy/mod/info/logviewer"
|
|
||||||
"imuslab.com/zoraxy/mod/mdns"
|
|
||||||
"imuslab.com/zoraxy/mod/netstat"
|
|
||||||
"imuslab.com/zoraxy/mod/pathrule"
|
|
||||||
"imuslab.com/zoraxy/mod/sshprox"
|
|
||||||
"imuslab.com/zoraxy/mod/statistic"
|
|
||||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
|
||||||
"imuslab.com/zoraxy/mod/streamproxy"
|
|
||||||
"imuslab.com/zoraxy/mod/tlscert"
|
|
||||||
"imuslab.com/zoraxy/mod/update"
|
"imuslab.com/zoraxy/mod/update"
|
||||||
"imuslab.com/zoraxy/mod/uptime"
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
"imuslab.com/zoraxy/mod/webserv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// General flags
|
|
||||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
|
||||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
|
||||||
var showver = flag.Bool("version", false, "Show version of this server")
|
|
||||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
|
||||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
|
||||||
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
|
|
||||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
|
||||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
|
||||||
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
|
|
||||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
|
||||||
var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
|
|
||||||
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 enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
|
||||||
|
|
||||||
var (
|
/* SIGTERM handler, do shutdown sequences before closing */
|
||||||
name = "Zoraxy"
|
|
||||||
version = "3.1.1"
|
|
||||||
nodeUUID = "generic" //System uuid, in uuidv4 format
|
|
||||||
development = false //Set this to false to use embedded web fs
|
|
||||||
bootTime = time.Now().Unix()
|
|
||||||
|
|
||||||
/*
|
|
||||||
Binary Embedding File System
|
|
||||||
*/
|
|
||||||
//go:embed web/*
|
|
||||||
webres embed.FS
|
|
||||||
|
|
||||||
/*
|
|
||||||
Handler Modules
|
|
||||||
*/
|
|
||||||
sysdb *database.Database //System database
|
|
||||||
authAgent *auth.AuthAgent //Authentication agent
|
|
||||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
|
||||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
|
||||||
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
|
|
||||||
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
|
|
||||||
|
|
||||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
|
||||||
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
|
||||||
accessController *access.Controller //Access controller, handle black list and white list
|
|
||||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
|
||||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
|
||||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
|
||||||
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
|
||||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
|
||||||
webSshManager *sshprox.Manager //Web SSH connection service
|
|
||||||
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
|
|
||||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
|
||||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
|
||||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
|
||||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
|
||||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
|
||||||
|
|
||||||
//Helper modules
|
|
||||||
EmailSender *email.Sender //Email sender that handle email sending
|
|
||||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
|
||||||
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
|
|
||||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
|
||||||
LogViewer *logviewer.Viewer
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kill signal handler. Do something before the system the core terminate.
|
|
||||||
func SetupCloseHandler() {
|
func SetupCloseHandler() {
|
||||||
c := make(chan os.Signal, 2)
|
c := make(chan os.Signal, 2)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
@ -116,9 +59,7 @@ func SetupCloseHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ShutdownSeq() {
|
func ShutdownSeq() {
|
||||||
SystemWideLogger.Println("Shutting down " + name)
|
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
|
||||||
//SystemWideLogger.Println("Closing GeoDB")
|
|
||||||
//geodbStore.Close()
|
|
||||||
SystemWideLogger.Println("Closing Netstats Listener")
|
SystemWideLogger.Println("Closing Netstats Listener")
|
||||||
netstatBuffers.Close()
|
netstatBuffers.Close()
|
||||||
SystemWideLogger.Println("Closing Statistic Collector")
|
SystemWideLogger.Println("Closing Statistic Collector")
|
||||||
@ -150,7 +91,7 @@ func main() {
|
|||||||
//Parse startup flags
|
//Parse startup flags
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *showver {
|
if *showver {
|
||||||
fmt.Println(name + " - Version " + version)
|
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +102,7 @@ func main() {
|
|||||||
|
|
||||||
if *enableAutoUpdate {
|
if *enableAutoUpdate {
|
||||||
fmt.Println("Checking required config update")
|
fmt.Println("Checking required config update")
|
||||||
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
|
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupCloseHandler()
|
SetupCloseHandler()
|
||||||
@ -183,7 +124,7 @@ func main() {
|
|||||||
webminPanelMux = http.NewServeMux()
|
webminPanelMux = http.NewServeMux()
|
||||||
csrfMiddleware = csrf.Protect(
|
csrfMiddleware = csrf.Protect(
|
||||||
[]byte(nodeUUID),
|
[]byte(nodeUUID),
|
||||||
csrf.CookieName("zoraxy-csrf"),
|
csrf.CookieName(CSRF_COOKIENAME),
|
||||||
csrf.Secure(false),
|
csrf.Secure(false),
|
||||||
csrf.Path("/"),
|
csrf.Path("/"),
|
||||||
csrf.SameSite(csrf.SameSiteLaxMode),
|
csrf.SameSite(csrf.SameSiteLaxMode),
|
||||||
@ -206,11 +147,10 @@ func main() {
|
|||||||
//Start the finalize sequences
|
//Start the finalize sequences
|
||||||
finalSequence()
|
finalSequence()
|
||||||
|
|
||||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
|
||||||
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CertificateInfoJSON struct {
|
type CertificateInfoJSON struct {
|
||||||
AcmeName string `json:"acme_name"`
|
AcmeName string `json:"acme_name"` //ACME provider name
|
||||||
AcmeUrl string `json:"acme_url"`
|
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
|
||||||
SkipTLS bool `json:"skip_tls"`
|
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
|
||||||
UseDNS bool `json:"dns"`
|
UseDNS bool `json:"dns"` //Use DNS challenge
|
||||||
|
PropTimeout int `json:"prop_time"` //Propagation timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACMEUser represents a user in the ACME system.
|
// ACMEUser represents a user in the ACME system.
|
||||||
@ -85,8 +86,15 @@ func (a *ACMEHandler) Logf(message string, err error) {
|
|||||||
a.Logger.PrintAndLog("ACME", message, err)
|
a.Logger.PrintAndLog("ACME", message, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the ACMEHandler.
|
||||||
|
// ACME Handler does not need to close anything
|
||||||
|
// Function defined for future compatibility
|
||||||
|
func (a *ACMEHandler) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ObtainCert obtains a certificate for the specified domains.
|
// ObtainCert obtains a certificate for the specified domains.
|
||||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) {
|
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
|
||||||
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
|
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
|
||||||
|
|
||||||
// generate private key
|
// generate private key
|
||||||
@ -181,7 +189,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
|
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.Logf("Unable to resolve DNS challenge provider", err)
|
a.Logf("Unable to resolve DNS challenge provider", err)
|
||||||
return false, err
|
return false, err
|
||||||
@ -285,10 +293,11 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
|||||||
|
|
||||||
// Save certificate's ACME info for renew usage
|
// Save certificate's ACME info for renew usage
|
||||||
certInfo := &CertificateInfoJSON{
|
certInfo := &CertificateInfoJSON{
|
||||||
AcmeName: caName,
|
AcmeName: caName,
|
||||||
AcmeUrl: caUrl,
|
AcmeUrl: caUrl,
|
||||||
SkipTLS: skipTLS,
|
SkipTLS: skipTLS,
|
||||||
UseDNS: useDNS,
|
UseDNS: useDNS,
|
||||||
|
PropTimeout: propagationTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
certInfoBytes, err := json.Marshal(certInfo)
|
certInfoBytes, err := json.Marshal(certInfo)
|
||||||
@ -452,12 +461,30 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
domains := strings.Split(domainPara, ",")
|
domains := strings.Split(domainPara, ",")
|
||||||
|
|
||||||
|
// Default propagation timeout is 300 seconds
|
||||||
|
propagationTimeout := 300
|
||||||
|
if dns {
|
||||||
|
ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
|
||||||
|
if err == nil {
|
||||||
|
propagationTimeout, err = strconv.Atoi(ppgTimeout)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid propagation timeout value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if propagationTimeout < 60 {
|
||||||
|
//Minimum propagation timeout is 60 seconds
|
||||||
|
propagationTimeout = 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Clean spaces in front or behind each domain
|
//Clean spaces in front or behind each domain
|
||||||
cleanedDomains := []string{}
|
cleanedDomains := []string{}
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
|
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
|
||||||
}
|
}
|
||||||
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns)
|
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||||
return
|
return
|
||||||
|
@ -1,70 +1,56 @@
|
|||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
// Preprocessor function to get DNS challenge provider by name
|
||||||
|
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
|
||||||
//Original Implementation
|
//Unpack the dnsCredentials (json string) to map
|
||||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
var dnsCredentialsMap map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
setCredentialsIntoEnvironmentVariables(credentials)
|
|
||||||
|
|
||||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
//Clear the PollingInterval and PropagationTimeout field and conert to int
|
||||||
*/
|
userDefinedPollingInterval := 2
|
||||||
|
if dnsCredentialsMap["PollingInterval"] != nil {
|
||||||
//New implementation using acmedns CICD pipeline generated datatype
|
userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
|
||||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
delete(dnsCredentialsMap, "PollingInterval")
|
||||||
}
|
convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
|
||||||
|
if err == nil {
|
||||||
/*
|
userDefinedPollingInterval = convertedPollingInterval
|
||||||
Original implementation of DNS ACME using OS.Env as payload
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
|
|
||||||
for key, value := range credentials {
|
|
||||||
err := os.Setenv(key, value)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
|
|
||||||
} else {
|
|
||||||
log.Println("[INFO] Environment variable %s set successfully", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func extractDnsCredentials(input string) (map[string]string, error) {
|
|
||||||
result := make(map[string]string)
|
|
||||||
|
|
||||||
// Split the input string by newline character
|
|
||||||
lines := strings.Split(input, "\n")
|
|
||||||
|
|
||||||
// Iterate over each line
|
|
||||||
for _, line := range lines {
|
|
||||||
// Split the line by "=" character
|
|
||||||
//use SpliyN to make sure not to split the value if the value is base64
|
|
||||||
parts := strings.SplitN(line, "=", 1)
|
|
||||||
|
|
||||||
// Check if the line is in the correct format
|
|
||||||
if len(parts) == 2 {
|
|
||||||
key := strings.TrimSpace(parts[0])
|
|
||||||
value := strings.TrimSpace(parts[1])
|
|
||||||
|
|
||||||
// Add the key-value pair to the map
|
|
||||||
result[key] = value
|
|
||||||
|
|
||||||
if value == "" || key == "" {
|
|
||||||
//invalid config
|
|
||||||
return result, errors.New("DNS credential extract failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
userDefinedPropagationTimeout := ppgTimeout
|
||||||
}
|
if dnsCredentialsMap["PropagationTimeout"] != nil {
|
||||||
|
userDefinedPropagationTimeoutRaw := dnsCredentialsMap["PropagationTimeout"].(string)
|
||||||
|
delete(dnsCredentialsMap, "PropagationTimeout")
|
||||||
|
convertedPropagationTimeout, err := strconv.Atoi(userDefinedPropagationTimeoutRaw)
|
||||||
|
if err == nil {
|
||||||
|
//Overwrite the default propagation timeout if it is requeted from UI
|
||||||
|
userDefinedPropagationTimeout = convertedPropagationTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
*/
|
//Restructure dnsCredentials string from map
|
||||||
|
dnsCredentialsBytes, err := json.Marshal(dnsCredentialsMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dnsCredentials = string(dnsCredentialsBytes)
|
||||||
|
|
||||||
|
//Using acmedns CICD pipeline generated datatype to optain the DNS provider
|
||||||
|
return acmedns.GetDNSProviderByJsonConfig(
|
||||||
|
dnsProvider,
|
||||||
|
dnsCredentials,
|
||||||
|
int64(userDefinedPropagationTimeout),
|
||||||
|
int64(userDefinedPollingInterval),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -88,9 +88,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
|||||||
AcmeHandler: AcmeHandler,
|
AcmeHandler: AcmeHandler,
|
||||||
RenewerConfig: &renewerConfig,
|
RenewerConfig: &renewerConfig,
|
||||||
RenewTickInterval: renewCheckInterval,
|
RenewTickInterval: renewCheckInterval,
|
||||||
|
EarlyRenewDays: earlyRenewDays,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
|
||||||
|
|
||||||
if thisRenewer.RenewerConfig.Enabled {
|
if thisRenewer.RenewerConfig.Enabled {
|
||||||
//Start the renew ticker
|
//Start the renew ticker
|
||||||
thisRenewer.StartAutoRenewTicker()
|
thisRenewer.StartAutoRenewTicker()
|
||||||
@ -103,7 +106,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AutoRenewer) Logf(message string, err error) {
|
func (a *AutoRenewer) Logf(message string, err error) {
|
||||||
a.Logger.PrintAndLog("CertRenew", message, err)
|
a.Logger.PrintAndLog("cert-renew", message, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AutoRenewer) StartAutoRenewTicker() {
|
func (a *AutoRenewer) StartAutoRenewTicker() {
|
||||||
@ -305,7 +308,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
|||||||
}
|
}
|
||||||
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
||||||
//This cert is expired
|
//This cert is expired
|
||||||
|
|
||||||
DNSName, err := ExtractDomains(certBytes)
|
DNSName, err := ExtractDomains(certBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Maybe self signed. Ignore this
|
//Maybe self signed. Ignore this
|
||||||
@ -352,6 +354,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
|||||||
return a.renewExpiredDomains(expiredCertList)
|
return a.renewExpiredDomains(expiredCertList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the auto renewer
|
||||||
func (a *AutoRenewer) Close() {
|
func (a *AutoRenewer) Close() {
|
||||||
if a.TickerstopChan != nil {
|
if a.TickerstopChan != nil {
|
||||||
a.TickerstopChan <- true
|
a.TickerstopChan <- true
|
||||||
@ -381,7 +384,13 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
|
//For upgrading config from older version of Zoraxy which don't have timeout
|
||||||
|
if certInfo.PropTimeout == 0 {
|
||||||
|
//Set default timeout
|
||||||
|
certInfo.PropTimeout = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
||||||
} else {
|
} else {
|
||||||
@ -431,7 +440,7 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle update auto renew DNS configuration
|
// Handle update auto renew DNS configuration
|
||||||
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
|
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "dnsProvider not set")
|
utils.SendErrorResponse(w, "dnsProvider not set")
|
||||||
|
@ -3,7 +3,7 @@ package acme
|
|||||||
/*
|
/*
|
||||||
CA.go
|
CA.go
|
||||||
|
|
||||||
This script load CA defination from embedded ca.json
|
This script load CA definition from embedded ca.json
|
||||||
*/
|
*/
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CA Defination, load from embeded json when startup
|
// CA definition, load from embeded json when startup
|
||||||
type CaDef struct {
|
type CaDef struct {
|
||||||
Production map[string]string
|
Production map[string]string
|
||||||
Test map[string]string
|
Test map[string]string
|
||||||
|
@ -5,14 +5,14 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get the issuer name from pem file
|
// Get the issuer name from pem file
|
||||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||||
// Read the PEM file
|
// Read the PEM file
|
||||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
pemData, err := os.ReadFile(pemFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
session.Values["authenticated"] = false
|
session.Values["authenticated"] = false
|
||||||
session.Values["username"] = nil
|
session.Values["username"] = nil
|
||||||
session.Save(r, w)
|
session.Options.MaxAge = -1
|
||||||
return nil
|
return session.Save(r, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current session username from request
|
// Get the current session username from request
|
||||||
@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||||
return false
|
return false
|
||||||
|
34
src/mod/auth/sso/app.go
Normal file
34
src/mod/auth/sso/app.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
/*
|
||||||
|
app.go
|
||||||
|
|
||||||
|
This file contains the app structure and app management
|
||||||
|
functions for the SSO module.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// RegisteredUpstreamApp is a structure that contains the information of an
|
||||||
|
// upstream app that is registered with the SSO server
|
||||||
|
type RegisteredUpstreamApp struct {
|
||||||
|
ID string
|
||||||
|
Secret string
|
||||||
|
Domain []string
|
||||||
|
Scopes []string
|
||||||
|
SessionDuration int //in seconds, default to 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterUpstreamApp registers an upstream app with the SSO server
|
||||||
|
func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp {
|
||||||
|
apps := make([]*RegisteredUpstreamApp, 0)
|
||||||
|
for _, app := range s.Apps {
|
||||||
|
apps = append(apps, &app)
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterUpstreamApp registers an upstream app with the SSO server
|
||||||
|
func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) {
|
||||||
|
app, ok := s.Apps[appID]
|
||||||
|
return &app, ok
|
||||||
|
}
|
271
src/mod/auth/sso/handlers.go
Normal file
271
src/mod/auth/sso/handlers.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
/*
|
||||||
|
handlers.go
|
||||||
|
|
||||||
|
This file contains the handlers for the SSO module.
|
||||||
|
If you are looking for handlers for SSO user management,
|
||||||
|
please refer to userHandlers.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleSSOStatus handle the request to get the status of the SSO portal server
|
||||||
|
func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type SSOStatus struct {
|
||||||
|
Enabled bool
|
||||||
|
SSOInterceptEnabled bool
|
||||||
|
ListeningPort int
|
||||||
|
AuthURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
status := SSOStatus{
|
||||||
|
Enabled: s.ssoPortalServer != nil,
|
||||||
|
//SSOInterceptEnabled: s.ssoInterceptEnabled,
|
||||||
|
ListeningPort: s.Config.PortalServerPort,
|
||||||
|
AuthURL: s.Config.AuthURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(status)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper for starting and stopping the SSO portal server
|
||||||
|
// require POST request with key "enable" and value "true" or "false"
|
||||||
|
func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
enable, err := utils.PostBool(r, "enable")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid enable value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
s.HandleStartSSOPortal(w, r)
|
||||||
|
} else {
|
||||||
|
s.HandleStopSSOPortal(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleStartSSOPortal handle the request to start the SSO portal server
|
||||||
|
func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.ssoPortalServer != nil {
|
||||||
|
//Already enabled. Do restart instead.
|
||||||
|
err := s.RestartSSOServer()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to start SSO server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the authURL is set correctly. If not, return error
|
||||||
|
if s.Config.AuthURL == "" {
|
||||||
|
utils.SendErrorResponse(w, "auth URL not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start the SSO portal server in go routine
|
||||||
|
go s.StartSSOPortal()
|
||||||
|
|
||||||
|
//Write current state to database
|
||||||
|
err := s.Config.Database.Write("sso_conf", "enabled", true)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update SSO state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleStopSSOPortal handle the request to stop the SSO portal server
|
||||||
|
func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.ssoPortalServer == nil {
|
||||||
|
//Already disabled
|
||||||
|
utils.SendOK(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.ssoPortalServer.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Failed to stop SSO portal server", err)
|
||||||
|
utils.SendErrorResponse(w, "failed to stop SSO portal server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ssoPortalServer = nil
|
||||||
|
|
||||||
|
//Write current state to database
|
||||||
|
err = s.Config.Database.Write("sso_conf", "enabled", false)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update SSO state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePortChange handle the request to change the SSO portal server port
|
||||||
|
func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
//Return the current port
|
||||||
|
js, _ := json.Marshal(s.Config.PortalServerPort)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := utils.PostInt(r, "port")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid port given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Config.PortalServerPort = port
|
||||||
|
|
||||||
|
//Write to the database
|
||||||
|
err = s.Config.Database.Write("sso_conf", "port", port)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update port")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsRunning() {
|
||||||
|
//Restart the server if it is running
|
||||||
|
err = s.RestartSSOServer()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to restart SSO server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSetAuthURL handle the request to change the SSO auth URL
|
||||||
|
// This is the URL that the SSO portal server will redirect to for authentication
|
||||||
|
// e.g. auth.yourdomain.com
|
||||||
|
func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
//Return the current auth URL
|
||||||
|
js, _ := json.Marshal(s.Config.AuthURL)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the auth URL
|
||||||
|
authURL, err := utils.PostPara(r, "auth_url")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid auth URL given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Config.AuthURL = authURL
|
||||||
|
|
||||||
|
//Write to the database
|
||||||
|
err = s.Config.Database.Write("sso_conf", "authurl", authURL)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update auth URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clear the cookie store and restart the server
|
||||||
|
err = s.RestartSSOServer()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to restart SSO server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRegisterApp handle the request to register a new app to the SSO portal
|
||||||
|
func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
|
||||||
|
appName, err := utils.PostPara(r, "app_name")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid app name given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := utils.PostPara(r, "app_id")
|
||||||
|
if err != nil {
|
||||||
|
//If id is not given, use the app name with a random UUID
|
||||||
|
newID, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to generate new app ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the given appid is already in use
|
||||||
|
if _, ok := s.Apps[id]; ok {
|
||||||
|
utils.SendErrorResponse(w, "app ID already in use")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Process the app domain
|
||||||
|
An app can have multiple domains, separated by commas
|
||||||
|
Usually the app domain is the proxy rule that points to the app
|
||||||
|
For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com
|
||||||
|
*/
|
||||||
|
appDomain, err := utils.PostPara(r, "app_domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid app URL given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appURLs := strings.Split(appDomain, ",")
|
||||||
|
//Remove padding and trailing spaces in each URL
|
||||||
|
for i := range appURLs {
|
||||||
|
appURLs[i] = strings.TrimSpace(appURLs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a new app entry
|
||||||
|
thisAppEntry := RegisteredUpstreamApp{
|
||||||
|
ID: id,
|
||||||
|
Secret: "",
|
||||||
|
Domain: appURLs,
|
||||||
|
Scopes: []string{},
|
||||||
|
SessionDuration: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(thisAppEntry)
|
||||||
|
|
||||||
|
//Create a new app in the database
|
||||||
|
err = s.Config.Database.Write("sso_apps", appName, string(js))
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to create new app")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Also add the app to runtime config
|
||||||
|
s.Apps[appName] = thisAppEntry
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAppRemove handle the request to remove an app from the SSO portal
|
||||||
|
func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
appID, err := utils.PostPara(r, "app_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid app ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the app actually exists
|
||||||
|
if _, ok := s.Apps[appID]; !ok {
|
||||||
|
utils.SendErrorResponse(w, "app not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(s.Apps, appID)
|
||||||
|
|
||||||
|
//Also remove it from the database
|
||||||
|
err = s.Config.Database.Delete("sso_apps", appID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Failed to remove app from database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
295
src/mod/auth/sso/oauth2.go
Normal file
295
src/mod/auth/sso/oauth2.go
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-oauth2/oauth2/v4/errors"
|
||||||
|
"github.com/go-oauth2/oauth2/v4/generates"
|
||||||
|
"github.com/go-oauth2/oauth2/v4/manage"
|
||||||
|
"github.com/go-oauth2/oauth2/v4/models"
|
||||||
|
"github.com/go-oauth2/oauth2/v4/server"
|
||||||
|
"github.com/go-oauth2/oauth2/v4/store"
|
||||||
|
"github.com/go-session/session"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SSO_SESSION_NAME = "ZoraxySSO"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OAuth2Server struct {
|
||||||
|
srv *server.Server //oAuth server instance
|
||||||
|
config *SSOConfig
|
||||||
|
parent *SSOHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed static/auth.html
|
||||||
|
var authHtml []byte
|
||||||
|
|
||||||
|
//go:embed static/login.html
|
||||||
|
var loginHtml []byte
|
||||||
|
|
||||||
|
// NewOAuth2Server creates a new OAuth2 server instance
|
||||||
|
func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) {
|
||||||
|
manager := manage.NewDefaultManager()
|
||||||
|
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
||||||
|
// token store
|
||||||
|
manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db"))
|
||||||
|
// generate jwt access token
|
||||||
|
manager.MapAccessGenerate(generates.NewAccessGenerate())
|
||||||
|
|
||||||
|
//Load the information of registered app within the OAuth2 server
|
||||||
|
clientStore := store.NewClientStore()
|
||||||
|
clientStore.Set("myapp", &models.Client{
|
||||||
|
ID: "myapp",
|
||||||
|
Secret: "verysecurepassword",
|
||||||
|
Domain: "localhost:9094",
|
||||||
|
})
|
||||||
|
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
|
||||||
|
manager.MapClientStorage(clientStore)
|
||||||
|
|
||||||
|
thisServer := OAuth2Server{
|
||||||
|
config: config,
|
||||||
|
parent: parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a new oauth server
|
||||||
|
srv := server.NewServer(server.NewConfig(), manager)
|
||||||
|
srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler)
|
||||||
|
srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler)
|
||||||
|
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
|
||||||
|
log.Println("Internal Error:", err.Error())
|
||||||
|
return
|
||||||
|
})
|
||||||
|
srv.SetResponseErrorHandler(func(re *errors.Response) {
|
||||||
|
log.Println("Response Error:", re.Error.Error())
|
||||||
|
})
|
||||||
|
|
||||||
|
//Set the access scope handler
|
||||||
|
srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler)
|
||||||
|
//Set the access token expiration handler based on requesting domain / hostname
|
||||||
|
srv.SetAccessTokenExpHandler(thisServer.ExpireHandler)
|
||||||
|
thisServer.srv = srv
|
||||||
|
return &thisServer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password handler, validate if the given username and password are correct
|
||||||
|
func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) {
|
||||||
|
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
|
||||||
|
if username == "test" && password == "test" {
|
||||||
|
userID = "test"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Authorization Handler, handle auth request from user
|
||||||
|
func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
|
||||||
|
store, err := session.Start(r.Context(), w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, ok := store.Get(SSO_SESSION_NAME)
|
||||||
|
if !ok {
|
||||||
|
if r.Form == nil {
|
||||||
|
r.ParseForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Set("ReturnUri", r.Form)
|
||||||
|
store.Save()
|
||||||
|
|
||||||
|
w.Header().Set("Location", "/oauth2/login")
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID = uid.(string)
|
||||||
|
store.Delete(SSO_SESSION_NAME)
|
||||||
|
store.Save()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessTokenExpHandler, set the SSO session length default value
|
||||||
|
func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) {
|
||||||
|
requestHostname := r.Host
|
||||||
|
if requestHostname == "" {
|
||||||
|
//Use default value
|
||||||
|
return time.Hour, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the Registered App Config from parent
|
||||||
|
appConfig, ok := oas.parent.Apps[requestHostname]
|
||||||
|
if !ok {
|
||||||
|
//Use default value
|
||||||
|
return time.Hour, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Use the app's session length
|
||||||
|
return time.Second * time.Duration(appConfig.SessionDuration), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationScopeHandler, handle the scope of the request
|
||||||
|
func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) {
|
||||||
|
//Get the scope from post or GEt request
|
||||||
|
if r.Form == nil {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return "none", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the hostname of the request
|
||||||
|
requestHostname := r.Host
|
||||||
|
if requestHostname == "" {
|
||||||
|
//No rule set. Use default
|
||||||
|
return "none", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the Registered App Config from parent
|
||||||
|
appConfig, ok := oas.parent.Apps[requestHostname]
|
||||||
|
if !ok {
|
||||||
|
//No rule set. Use default
|
||||||
|
return "none", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the scope is set in the request
|
||||||
|
if v, ok := r.Form["scope"]; ok {
|
||||||
|
//Check if the requested scope is in the appConfig scope
|
||||||
|
if utils.StringInArray(appConfig.Scopes, v[0]) {
|
||||||
|
return v[0], nil
|
||||||
|
}
|
||||||
|
return "none", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "none", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SSO Web Server Toggle Functions */
|
||||||
|
func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) {
|
||||||
|
primaryMux.HandleFunc("/oauth2/login", oas.loginHandler)
|
||||||
|
primaryMux.HandleFunc("/oauth2/auth", oas.authHandler)
|
||||||
|
|
||||||
|
primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
store, err := session.Start(r.Context(), w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var form url.Values
|
||||||
|
if v, ok := store.Get("ReturnUri"); ok {
|
||||||
|
form = v.(url.Values)
|
||||||
|
}
|
||||||
|
r.Form = form
|
||||||
|
|
||||||
|
store.Delete("ReturnUri")
|
||||||
|
store.Save()
|
||||||
|
|
||||||
|
err = oas.srv.HandleAuthorizeRequest(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := oas.srv.HandleTokenRequest(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token, err := oas.srv.ValidationBearerToken(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()),
|
||||||
|
"client_id": token.GetClientID(),
|
||||||
|
"user_id": token.GetUserID(),
|
||||||
|
}
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.SetIndent("", " ")
|
||||||
|
e.Encode(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
store, err := session.Start(r.Context(), w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
if r.Form == nil {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load username and password from form post
|
||||||
|
username, err := utils.PostPara(r, "username")
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte("invalid username or password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := utils.PostPara(r, "password")
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte("invalid username or password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate the user
|
||||||
|
if !oas.parent.ValidateUsernameAndPassword(username, password) {
|
||||||
|
//Wrong password
|
||||||
|
w.Write([]byte("invalid username or password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Set(SSO_SESSION_NAME, r.Form.Get("username"))
|
||||||
|
store.Save()
|
||||||
|
|
||||||
|
w.Header().Set("Location", "/oauth2/auth")
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
} else if r.Method == "GET" {
|
||||||
|
//Check if the user is logged in
|
||||||
|
if _, ok := store.Get(SSO_SESSION_NAME); ok {
|
||||||
|
w.Header().Set("Location", "/oauth2/auth")
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//User not logged in. Show login page
|
||||||
|
w.Write(loginHtml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
store, err := session.Start(context.TODO(), w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := store.Get(SSO_SESSION_NAME); !ok {
|
||||||
|
w.Header().Set("Location", "/oauth2/login")
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//User logged in. Check if this user have previously authorized the app
|
||||||
|
|
||||||
|
//TODO: Check if the user have previously authorized the app
|
||||||
|
|
||||||
|
//User have not authorized the app. Show the authorization page
|
||||||
|
w.Write(authHtml)
|
||||||
|
}
|
1
src/mod/auth/sso/oauth_test.go
Normal file
1
src/mod/auth/sso/oauth_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package sso
|
58
src/mod/auth/sso/openid.go
Normal file
58
src/mod/auth/sso/openid.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenIDConfiguration struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
JwksUri string `json:"jwks_uri"`
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||||
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
//Prepend https:// if not present
|
||||||
|
authBaseURL := h.Config.AuthURL
|
||||||
|
if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") {
|
||||||
|
authBaseURL = "https://" + authBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle the discovery request
|
||||||
|
discovery := OpenIDConfiguration{
|
||||||
|
Issuer: authBaseURL,
|
||||||
|
AuthorizationEndpoint: authBaseURL + "/oauth2/authorize",
|
||||||
|
TokenEndpoint: authBaseURL + "/oauth2/token",
|
||||||
|
JwksUri: authBaseURL + "/jwks.json",
|
||||||
|
ResponseTypesSupported: []string{"code", "token"},
|
||||||
|
SubjectTypesSupported: []string{"public"},
|
||||||
|
IDTokenSigningAlgValuesSupported: []string{
|
||||||
|
"RS256",
|
||||||
|
},
|
||||||
|
ClaimsSupported: []string{
|
||||||
|
"sub", //Subject, usually the user ID
|
||||||
|
"iss", //Issuer, usually the server URL
|
||||||
|
"aud", //Audience, usually the client ID
|
||||||
|
"exp", //Expiration Time
|
||||||
|
"iat", //Issued At
|
||||||
|
"email", //Email
|
||||||
|
"locale", //Locale
|
||||||
|
"name", //Full Name
|
||||||
|
"nickname", //Nickname
|
||||||
|
"preferred_username", //Preferred Username
|
||||||
|
"website", //Website
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//Write the response
|
||||||
|
js, _ := json.Marshal(discovery)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(js)
|
||||||
|
}
|
132
src/mod/auth/sso/server.go
Normal file
132
src/mod/auth/sso/server.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-oauth2/oauth2/v4/errors"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
server.go
|
||||||
|
|
||||||
|
This is the web server for the SSO portal. It contains the
|
||||||
|
HTTP server and the handlers for the SSO portal.
|
||||||
|
|
||||||
|
If you are looking for handlers that changes the settings
|
||||||
|
of the SSO portale or user management, please refer to
|
||||||
|
handlers.go.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
|
||||||
|
//Create a new web server for the SSO portal
|
||||||
|
pmux := http.NewServeMux()
|
||||||
|
fs := http.FileServer(http.FS(staticFiles))
|
||||||
|
pmux.Handle("/", fs)
|
||||||
|
|
||||||
|
//Register API endpoint for the SSO portal
|
||||||
|
pmux.HandleFunc("/sso/login", h.HandleLogin)
|
||||||
|
|
||||||
|
//Register API endpoint for autodiscovery
|
||||||
|
pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest)
|
||||||
|
|
||||||
|
//Register OAuth2 endpoints
|
||||||
|
h.Oauth2Server.RegisterOauthEndpoints(pmux)
|
||||||
|
h.ssoPortalMux = pmux
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSSOPortal start the SSO portal server
|
||||||
|
// This function will block the main thread, call it in a goroutine
|
||||||
|
func (h *SSOHandler) StartSSOPortal() error {
|
||||||
|
if h.ssoPortalServer != nil {
|
||||||
|
return errors.New("SSO portal server already running")
|
||||||
|
}
|
||||||
|
h.ssoPortalServer = &http.Server{
|
||||||
|
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
|
||||||
|
Handler: h.ssoPortalMux,
|
||||||
|
}
|
||||||
|
err := h.ssoPortalServer.ListenAndServe()
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
h.Log("Failed to start SSO portal server", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopSSOPortal stop the SSO portal server
|
||||||
|
func (h *SSOHandler) StopSSOPortal() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
err := h.ssoPortalServer.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
h.Log("Failed to stop SSO portal server", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.ssoPortalServer = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSSOPortal start the SSO portal server
|
||||||
|
func (h *SSOHandler) RestartSSOServer() error {
|
||||||
|
if h.ssoPortalServer != nil {
|
||||||
|
err := h.StopSSOPortal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go h.StartSSOPortal()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SSOHandler) IsRunning() bool {
|
||||||
|
return h.ssoPortalServer != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLogin handle the login request
|
||||||
|
func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//Handle the login request
|
||||||
|
username, err := utils.PostPara(r, "username")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := utils.PostPara(r, "password")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rememberMe, err := utils.PostBool(r, "remember_me")
|
||||||
|
if err != nil {
|
||||||
|
rememberMe = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the user exists
|
||||||
|
userEntry, err := h.GetSSOUser(username)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the password is correct
|
||||||
|
if !userEntry.VerifyPassword(password) {
|
||||||
|
utils.SendErrorResponse(w, "incorrect password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a new session for the user
|
||||||
|
session, _ := h.cookieStore.Get(r, "Zoraxy-SSO")
|
||||||
|
session.Values["username"] = username
|
||||||
|
if rememberMe {
|
||||||
|
session.Options.MaxAge = 86400 * 15 //15 days
|
||||||
|
} else {
|
||||||
|
session.Options.MaxAge = 3600 //1 hour
|
||||||
|
}
|
||||||
|
session.Save(r, w) //Save the session
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
158
src/mod/auth/sso/sso.go
Normal file
158
src/mod/auth/sso/sso.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
sso.go
|
||||||
|
|
||||||
|
This file contains the main SSO handler and the SSO configuration
|
||||||
|
structure. It also contains the main SSO handler functions.
|
||||||
|
|
||||||
|
SSO web interface are stored in the static folder, which is embedded
|
||||||
|
into the binary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//go:embed static/*
|
||||||
|
var staticFiles embed.FS //Static files for the SSO portal
|
||||||
|
|
||||||
|
type SSOConfig struct {
|
||||||
|
SystemUUID string //System UUID, should be passed in from main scope
|
||||||
|
AuthURL string //Authentication subdomain URL, e.g. auth.example.com
|
||||||
|
PortalServerPort int //SSO portal server port
|
||||||
|
Database *database.Database //System master key-value database
|
||||||
|
Logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSOHandler is the main SSO handler structure
|
||||||
|
type SSOHandler struct {
|
||||||
|
cookieStore *sessions.CookieStore
|
||||||
|
ssoPortalServer *http.Server
|
||||||
|
ssoPortalMux *http.ServeMux
|
||||||
|
Oauth2Server *OAuth2Server
|
||||||
|
Config *SSOConfig
|
||||||
|
Apps map[string]RegisteredUpstreamApp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Zoraxy SSO handler
|
||||||
|
func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
|
||||||
|
//Create a cookie store for the SSO handler
|
||||||
|
cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID))
|
||||||
|
cookieStore.Options = &sessions.Options{
|
||||||
|
Path: "",
|
||||||
|
Domain: "",
|
||||||
|
MaxAge: 0,
|
||||||
|
Secure: false,
|
||||||
|
HttpOnly: false,
|
||||||
|
SameSite: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Database.NewTable("sso_users") //For storing user information
|
||||||
|
config.Database.NewTable("sso_conf") //For storing SSO configuration
|
||||||
|
config.Database.NewTable("sso_apps") //For storing registered apps
|
||||||
|
|
||||||
|
//Create the SSO Handler
|
||||||
|
thisHandler := SSOHandler{
|
||||||
|
cookieStore: cookieStore,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Read the app info from database
|
||||||
|
thisHandler.Apps = make(map[string]RegisteredUpstreamApp)
|
||||||
|
|
||||||
|
//Create an oauth2 server
|
||||||
|
oauth2Server, err := NewOAuth2Server(config, &thisHandler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Register endpoints
|
||||||
|
thisHandler.Oauth2Server = oauth2Server
|
||||||
|
thisHandler.InitSSOPortal(config.PortalServerPort)
|
||||||
|
|
||||||
|
return &thisHandler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SSOHandler) RestorePreviousRunningState() {
|
||||||
|
//Load the previous SSO state
|
||||||
|
ssoEnabled := false
|
||||||
|
ssoPort := 5488
|
||||||
|
ssoAuthURL := ""
|
||||||
|
h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled)
|
||||||
|
h.Config.Database.Read("sso_conf", "port", &ssoPort)
|
||||||
|
h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL)
|
||||||
|
|
||||||
|
if ssoAuthURL == "" {
|
||||||
|
//Cannot enable SSO without auth URL
|
||||||
|
ssoEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Config.PortalServerPort = ssoPort
|
||||||
|
h.Config.AuthURL = ssoAuthURL
|
||||||
|
|
||||||
|
if ssoEnabled {
|
||||||
|
go h.StartSSOPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeForwardAuth handle the SSO request in interception mode
|
||||||
|
// Suppose to be called in dynamicproxy.
|
||||||
|
// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed
|
||||||
|
func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
//Get the current uri for appending to the auth subdomain
|
||||||
|
originalRequestURL := r.RequestURI
|
||||||
|
|
||||||
|
redirectAuthURL := h.Config.AuthURL
|
||||||
|
if redirectAuthURL == "" || !h.IsRunning() {
|
||||||
|
//Redirect not set or auth server is offlined
|
||||||
|
w.Write([]byte("SSO auth URL not set or SSO server offline."))
|
||||||
|
//TODO: Use better looking template if exists
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the user have the cookie "Zoraxy-SSO" set
|
||||||
|
session, err := h.cookieStore.Get(r, "Zoraxy-SSO")
|
||||||
|
if err != nil {
|
||||||
|
//Redirect to auth subdomain
|
||||||
|
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the user is logged in
|
||||||
|
if session.Values["username"] != true {
|
||||||
|
//Redirect to auth subdomain
|
||||||
|
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the current request subdomain is allowed
|
||||||
|
userName := session.Values["username"].(string)
|
||||||
|
user, err := h.GetSSOUser(userName)
|
||||||
|
if err != nil {
|
||||||
|
//User might have been removed from SSO. Redirect to auth subdomain
|
||||||
|
http.Redirect(w, r, redirectAuthURL, http.StatusFound)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the user have access to the current subdomain
|
||||||
|
if !user.Subdomains[r.Host].AllowAccess {
|
||||||
|
//User is not allowed to access the current subdomain. Sent 403
|
||||||
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
|
//TODO: Use better looking template if exists
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//User is logged in, continue to the next handler
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log a message with the SSO module tag
|
||||||
|
func (h *SSOHandler) Log(message string, err error) {
|
||||||
|
h.Config.Logger.PrintAndLog("SSO", message, err)
|
||||||
|
}
|
33
src/mod/auth/sso/static/auth.html
Normal file
33
src/mod/auth/sso/static/auth.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Auth</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
|
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||||
|
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<form action="/oauth2/authorize" method="POST">
|
||||||
|
<h1>Authorize</h1>
|
||||||
|
<p>The client would like to perform actions on your behalf.</p>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-lg"
|
||||||
|
style="width:200px;"
|
||||||
|
>
|
||||||
|
Allow
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
43
src/mod/auth/sso/static/index.html
Normal file
43
src/mod/auth/sso/static/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login Page</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui middle aligned center aligned grid">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="ui teal image header">
|
||||||
|
<div class="content">
|
||||||
|
Log in to your account
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<form class="ui large form">
|
||||||
|
<div class="ui stacked segment">
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<i class="user icon"></i>
|
||||||
|
<input type="text" name="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<i class="lock icon"></i>
|
||||||
|
<input type="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui fluid large teal submit button">Login</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui error message"></div>
|
||||||
|
</form>
|
||||||
|
<div class="ui message">
|
||||||
|
New to us? <a href="#">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
29
src/mod/auth/sso/static/login.html
Normal file
29
src/mod/auth/sso/static/login.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login</title>
|
||||||
|
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
|
||||||
|
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||||
|
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Login In</h1>
|
||||||
|
<form action="/oauth2/login" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">User Name</label>
|
||||||
|
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
309
src/mod/auth/sso/userHandlers.go
Normal file
309
src/mod/auth/sso/userHandlers.go
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
/*
|
||||||
|
userHandlers.go
|
||||||
|
Handlers for SSO user management
|
||||||
|
|
||||||
|
If you are looking for handlers that changes the settings
|
||||||
|
of the SSO portal (e.g. authURL or port), please refer to
|
||||||
|
handlers.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleAddUser handle the request to add a new user to the SSO system
|
||||||
|
func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username, err := utils.PostPara(r, "username")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid username given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := utils.PostPara(r, "password")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid password given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUserId, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to generate new user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a new user entry
|
||||||
|
thisUserEntry := UserEntry{
|
||||||
|
UserID: newUserId.String(),
|
||||||
|
Username: username,
|
||||||
|
PasswordHash: auth.Hash(password),
|
||||||
|
TOTPCode: "",
|
||||||
|
Enable2FA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(thisUserEntry)
|
||||||
|
|
||||||
|
//Create a new user in the database
|
||||||
|
err = s.Config.Database.Write("sso_users", newUserId.String(), string(js))
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to create new user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit user information, only accept change of username, password and enabled subdomain filed
|
||||||
|
func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userID)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load the user entry from database
|
||||||
|
userEntry, err := s.GetSSOUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update each of the fields if it is provided
|
||||||
|
username, err := utils.PostPara(r, "username")
|
||||||
|
if err == nil {
|
||||||
|
userEntry.Username = username
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := utils.PostPara(r, "password")
|
||||||
|
if err == nil {
|
||||||
|
userEntry.PasswordHash = auth.Hash(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the user entry in the database
|
||||||
|
js, _ := json.Marshal(userEntry)
|
||||||
|
err = s.Config.Database.Write("sso_users", userID, string(js))
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRemoveUser remove a user from the SSO system
|
||||||
|
func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userID)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove the user from the database
|
||||||
|
err = s.Config.Database.Delete("sso_users", userID)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to remove user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleListUser list all users in the SSO system
|
||||||
|
func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ssoUsers, err := s.ListSSOUsers()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to list users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
js, _ := json.Marshal(ssoUsers)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAddSubdomain add a subdomain to a user
|
||||||
|
func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userid, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userid)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry, err := s.GetSSOUser(userid)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subdomain, err := utils.PostPara(r, "subdomain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid subdomain given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowAccess, err := utils.PostBool(r, "allow_access")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid allow access value given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
|
||||||
|
Subdomain: subdomain,
|
||||||
|
AllowAccess: allowAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = UserEntry.Update()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRemoveSubdomain remove a subdomain from a user
|
||||||
|
func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userid, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userid)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry, err := s.GetSSOUser(userid)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subdomain, err := utils.PostPara(r, "subdomain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid subdomain given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(UserEntry.Subdomains, subdomain)
|
||||||
|
|
||||||
|
err = UserEntry.Update()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEnable2FA enable 2FA for a user
|
||||||
|
func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userid, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userid)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry, err := s.GetSSOUser(userid)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry.Enable2FA = true
|
||||||
|
provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to reset TOTP")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//As the ResetTotp function will update the user entry in the database, no need to call Update here
|
||||||
|
|
||||||
|
js, _ := json.Marshal(provisionUri)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Disable 2FA for a user
|
||||||
|
func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userid, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid user ID given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userid)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry, err := s.GetSSOUser(userid)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry.Enable2FA = false
|
||||||
|
UserEntry.TOTPCode = ""
|
||||||
|
|
||||||
|
err = UserEntry.Update()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to update user entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleVerify2FA verify the 2FA code for a user
|
||||||
|
func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||||
|
userid, err := utils.PostPara(r, "user_id")
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("invalid user ID given")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(s.SSOUserExists(userid)) {
|
||||||
|
utils.SendErrorResponse(w, "user not found")
|
||||||
|
return false, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
UserEntry, err := s.GetSSOUser(userid)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to load user entry")
|
||||||
|
return false, errors.New("failed to load user entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
totpCode, _ := utils.PostPara(r, "totp_code")
|
||||||
|
|
||||||
|
if !UserEntry.Enable2FA {
|
||||||
|
//If 2FA is not enabled, return true
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !UserEntry.VerifyTotp(totpCode) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
141
src/mod/auth/sso/users.go
Normal file
141
src/mod/auth/sso/users.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xlzd/gotp"
|
||||||
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
users.go
|
||||||
|
|
||||||
|
This file contains the user structure and user management
|
||||||
|
functions for the SSO module.
|
||||||
|
|
||||||
|
If you are looking for handlers, please refer to handlers.go.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type SubdomainAccessRule struct {
|
||||||
|
Subdomain string
|
||||||
|
AllowAccess bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEntry struct {
|
||||||
|
UserID string `json:sub` //User ID
|
||||||
|
Username string `json:"name"` //Username
|
||||||
|
Email string `json:"email"` //Email
|
||||||
|
PasswordHash string `json:"passwordhash"` //Password hash
|
||||||
|
TOTPCode string `json:"totpcode"` //TOTP code
|
||||||
|
Enable2FA bool `json:"enable2fa"` //Enable 2FA
|
||||||
|
Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules
|
||||||
|
LastLogin int64 `json:"lastlogin"` //Last login time
|
||||||
|
LastLoginIP string `json:"lastloginip"` //Last login IP
|
||||||
|
LastLoginCountry string `json:"lastlogincountry"` //Last login country
|
||||||
|
parent *SSOHandler //Parent SSO handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientResponse struct {
|
||||||
|
Sub string `json:"sub"` //User ID
|
||||||
|
Name string `json:"name"` //Username
|
||||||
|
Nickname string `json:"nickname"` //Nickname
|
||||||
|
PreferredUsername string `json:"preferred_username"` //Preferred Username
|
||||||
|
Email string `json:"email"` //Email
|
||||||
|
Locale string `json:"locale"` //Locale
|
||||||
|
Website string `json:"website"` //Website
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSOHandler) SSOUserExists(userid string) bool {
|
||||||
|
//Check if the user exists in the database
|
||||||
|
var userEntry UserEntry
|
||||||
|
err := s.Config.Database.Read("sso_users", userid, &userEntry)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) {
|
||||||
|
//Load the user entry from database
|
||||||
|
var userEntry UserEntry
|
||||||
|
err := s.Config.Database.Read("sso_users", userid, &userEntry)
|
||||||
|
if err != nil {
|
||||||
|
return UserEntry{}, err
|
||||||
|
}
|
||||||
|
userEntry.parent = s
|
||||||
|
return userEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) {
|
||||||
|
entries, err := s.Config.Database.ListTable("sso_users")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ssoUsers := []*UserEntry{}
|
||||||
|
for _, keypairs := range entries {
|
||||||
|
group := new(UserEntry)
|
||||||
|
json.Unmarshal(keypairs[1], &group)
|
||||||
|
group.parent = s
|
||||||
|
ssoUsers = append(ssoUsers, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssoUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the username and password
|
||||||
|
func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool {
|
||||||
|
//Validate the username and password
|
||||||
|
var userEntry UserEntry
|
||||||
|
err := s.Config.Database.Read("sso_users", username, &userEntry)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Remove after testing
|
||||||
|
if (username == "test") && (password == "test") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return userEntry.VerifyPassword(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserEntry) VerifyPassword(password string) bool {
|
||||||
|
return s.PasswordHash == auth.Hash(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write changes in the user entry back to the database
|
||||||
|
func (u *UserEntry) Update() error {
|
||||||
|
js, _ := json.Marshal(u)
|
||||||
|
err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset and update the TOTP code for the current user
|
||||||
|
// Return the provision uri of the new TOTP code for Google Authenticator
|
||||||
|
func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) {
|
||||||
|
u.TOTPCode = gotp.RandomSecret(16)
|
||||||
|
totp := gotp.NewDefaultTOTP(u.TOTPCode)
|
||||||
|
err := u.Update()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return totp.ProvisioningUri(accountName, issuerName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the TOTP code at current time
|
||||||
|
func (u *UserEntry) VerifyTotp(enteredCode string) bool {
|
||||||
|
totp := gotp.NewDefaultTOTP(u.TOTPCode)
|
||||||
|
return totp.Verify(enteredCode, time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserEntry) GetClientResponse() ClientResponse {
|
||||||
|
return ClientResponse{
|
||||||
|
Sub: u.UserID,
|
||||||
|
Name: u.Username,
|
||||||
|
Nickname: u.Username,
|
||||||
|
PreferredUsername: u.Username,
|
||||||
|
Email: u.Email,
|
||||||
|
Locale: "en",
|
||||||
|
Website: "",
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ import (
|
|||||||
- Blacklist
|
- Blacklist
|
||||||
- Whitelist
|
- Whitelist
|
||||||
- Rate Limitor
|
- Rate Limitor
|
||||||
|
- SSO Auth
|
||||||
- Basic Auth
|
- Basic Auth
|
||||||
- Vitrual Directory Proxy
|
- Vitrual Directory Proxy
|
||||||
- Subdomain Proxy
|
- Subdomain Proxy
|
||||||
@ -77,7 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
if sep.RequireRateLimit {
|
if sep.RequireRateLimit {
|
||||||
err := h.handleRateLimitRouting(w, r, sep)
|
err := h.handleRateLimitRouting(w, r, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429)
|
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//SSO Interception Mode
|
||||||
|
if sep.UseSSOIntercept {
|
||||||
|
allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
|
||||||
|
if !allowPass {
|
||||||
|
h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
|||||||
fallthrough
|
fallthrough
|
||||||
case DefaultSite_ReverseProxy:
|
case DefaultSite_ReverseProxy:
|
||||||
//They both share the same behavior
|
//They both share the same behavior
|
||||||
|
|
||||||
//Check if any virtual directory rules matches
|
//Check if any virtual directory rules matches
|
||||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||||
@ -188,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
|||||||
redirectTarget = "about:blank"
|
redirectTarget = "about:blank"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if the default site values start with http or https
|
||||||
|
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
|
||||||
|
redirectTarget = "http://" + redirectTarget
|
||||||
|
}
|
||||||
|
|
||||||
//Check if it is an infinite loopback redirect
|
//Check if it is an infinite loopback redirect
|
||||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
parsedURL, err := url.Parse(redirectTarget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Error when parsing target. Send to root
|
//Error when parsing target. Send to root
|
||||||
h.hostRequest(w, r, h.Parent.Root)
|
h.hostRequest(w, r, h.Parent.Root)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package dynamicproxy
|
package dynamicproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
|
|||||||
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
|
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Unable to load access rule. Target rule not found?
|
//Unable to load access rule. Target rule not found?
|
||||||
log.Println("[Proxy] Unable to load access rule: " + ruleID)
|
h.Parent.Option.Logger.PrintAndLog("proxy-access", "Unable to load access rule: "+ruleID, err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte("500 - Internal Server Error"))
|
w.Write([]byte("500 - Internal Server Error"))
|
||||||
return true
|
return true
|
||||||
|
@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
|||||||
for _, cred := range pe.BasicAuthCredentials {
|
for _, cred := range pe.BasicAuthCredentials {
|
||||||
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
||||||
matchingFound = true
|
matchingFound = true
|
||||||
|
|
||||||
|
//Set the X-Remote-User header
|
||||||
|
r.Header.Set("X-Remote-User", u)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,12 @@
|
|||||||
package dynamicproxy
|
package dynamicproxy
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
CustomHeader.go
|
CustomHeader.go
|
||||||
|
|
||||||
This script handle parsing and injecting custom headers
|
This script handle parsing and injecting custom headers
|
||||||
into the dpcore routing logic
|
into the dpcore routing logic
|
||||||
|
|
||||||
|
Updates: 2024-10-26
|
||||||
|
Contents from this file has been moved to rewrite/rewrite.go
|
||||||
|
This file is kept for contributors to understand the structure
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
|
|
||||||
// return upstream header and downstream header key-value pairs
|
|
||||||
// if the header is expected to be deleted, the value will be set to empty string
|
|
||||||
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
|
|
||||||
if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader {
|
|
||||||
//Early return if there are no defined headers
|
|
||||||
return [][]string{}, [][]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Use pre-allocation for faster performance
|
|
||||||
//Downstream +2 for Permission Policy and HSTS
|
|
||||||
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
|
|
||||||
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
|
|
||||||
upstreamHeaderCounter := 0
|
|
||||||
downstreamHeaderCounter := 0
|
|
||||||
|
|
||||||
//Sort the headers into upstream or downstream
|
|
||||||
for _, customHeader := range ept.UserDefinedHeaders {
|
|
||||||
thisHeaderSet := make([]string, 2)
|
|
||||||
thisHeaderSet[0] = customHeader.Key
|
|
||||||
thisHeaderSet[1] = customHeader.Value
|
|
||||||
if customHeader.IsRemove {
|
|
||||||
//Prevent invalid config
|
|
||||||
thisHeaderSet[1] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
//Assign to slice
|
|
||||||
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
|
|
||||||
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
|
|
||||||
upstreamHeaderCounter++
|
|
||||||
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
|
|
||||||
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
|
|
||||||
downstreamHeaderCounter++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if the endpoint require HSTS headers
|
|
||||||
if ept.HSTSMaxAge > 0 {
|
|
||||||
if ept.ContainsWildcardName(true) {
|
|
||||||
//Endpoint listening domain includes wildcards.
|
|
||||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"}
|
|
||||||
} else {
|
|
||||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
|
|
||||||
}
|
|
||||||
|
|
||||||
downstreamHeaderCounter++
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if the endpoint require Permission Policy
|
|
||||||
if ept.EnablePermissionPolicyHeader {
|
|
||||||
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
|
|
||||||
if ept.PermissionPolicy != nil {
|
|
||||||
//Custom permission policy
|
|
||||||
usingPermissionPolicy = ept.PermissionPolicy
|
|
||||||
} else {
|
|
||||||
//Permission policy is enabled but not customized. Use default
|
|
||||||
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
|
|
||||||
}
|
|
||||||
|
|
||||||
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
|
|
||||||
downstreamHeaderCounter++
|
|
||||||
}
|
|
||||||
|
|
||||||
return upstreamHeaders, downstreamHeaders
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,26 @@
|
|||||||
package domainsniff
|
package domainsniff
|
||||||
|
|
||||||
|
/*
|
||||||
|
Domainsniff
|
||||||
|
|
||||||
|
This package contain codes that perform project / domain specific behavior in Zoraxy
|
||||||
|
If you want Zoraxy to handle a particular domain or open source project in a special way,
|
||||||
|
you can add the checking logic here.
|
||||||
|
|
||||||
|
*/
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Check if the domain is reachable and return err if not reachable
|
// Check if the domain is reachable and return err if not reachable
|
||||||
func DomainReachableWithError(domain string) error {
|
func DomainReachableWithError(domain string) error {
|
||||||
timeout := 1 * time.Second
|
timeout := 1 * time.Second
|
||||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||||
@ -17,7 +32,115 @@ func DomainReachableWithError(domain string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if domain reachable
|
// Check if a domain have TLS but it is self-signed or expired
|
||||||
|
// Return false if sniff error
|
||||||
|
func DomainIsSelfSigned(domain string) bool {
|
||||||
|
//Extract the domain from URl in case the user input the full URL
|
||||||
|
host, port, err := net.SplitHostPort(domain)
|
||||||
|
if err != nil {
|
||||||
|
host = domain
|
||||||
|
} else {
|
||||||
|
domain = host + ":" + port
|
||||||
|
}
|
||||||
|
if !strings.Contains(domain, ":") {
|
||||||
|
domain = domain + ":443"
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the certificate
|
||||||
|
conn, err := net.Dial("tcp", domain)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
//Connect with TLS using secure verify
|
||||||
|
tlsConn := tls.Client(conn, nil)
|
||||||
|
err = tlsConn.Handshake()
|
||||||
|
if err == nil {
|
||||||
|
//This is a valid certificate
|
||||||
|
fmt.Println()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Connect with TLS using insecure skip verify
|
||||||
|
config := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
tlsConn = tls.Client(conn, config)
|
||||||
|
err = tlsConn.Handshake()
|
||||||
|
//If the handshake is successful, this is a self-signed certificate
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain reachable
|
||||||
func DomainReachable(domain string) bool {
|
func DomainReachable(domain string) bool {
|
||||||
return DomainReachableWithError(domain) == nil
|
return DomainReachableWithError(domain) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if domain is served by a web server using HTTPS
|
||||||
|
func DomainUsesTLS(targetURL string) bool {
|
||||||
|
//Check if the site support https
|
||||||
|
httpsUrl := fmt.Sprintf("https://%s", targetURL)
|
||||||
|
httpUrl := fmt.Sprintf("http://%s", targetURL)
|
||||||
|
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
resp, err := client.Head(httpsUrl)
|
||||||
|
if err == nil && resp.StatusCode == http.StatusOK {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Head(httpUrl)
|
||||||
|
if err == nil && resp.StatusCode == http.StatusOK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//If the site is not reachable, return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Request Handlers
|
||||||
|
*/
|
||||||
|
//Check if site support TLS
|
||||||
|
//Pass in ?selfsignchk=true to also check for self-signed certificate
|
||||||
|
func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
targetURL, err := utils.PostPara(r, "url")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid url given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//If the selfsign flag is set, also chec for self-signed certificate
|
||||||
|
_, err = utils.PostBool(r, "selfsignchk")
|
||||||
|
if err == nil {
|
||||||
|
//Return the https and selfsign status
|
||||||
|
type result struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
SelfSign bool `json:"selfsign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
scanResult := result{Protocol: "http", SelfSign: false}
|
||||||
|
|
||||||
|
if DomainUsesTLS(targetURL) {
|
||||||
|
scanResult.Protocol = "https"
|
||||||
|
if DomainIsSelfSigned(targetURL) {
|
||||||
|
scanResult.SelfSign = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(scanResult)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if DomainUsesTLS(targetURL) {
|
||||||
|
js, _ := json.Marshal("https")
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
js, _ := json.Marshal("http")
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -109,6 +109,8 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
|||||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||||
thisTransporter.(*http.Transport).DisableCompression = true
|
thisTransporter.(*http.Transport).DisableCompression = true
|
||||||
|
|
||||||
|
//TODO: Add user adjustable timeout option here
|
||||||
|
|
||||||
if dpcOptions.IgnoreTLSVerification {
|
if dpcOptions.IgnoreTLSVerification {
|
||||||
//Ignore TLS certificate validation error
|
//Ignore TLS certificate validation error
|
||||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
@ -1,29 +1,67 @@
|
|||||||
package dpcore_test
|
package dpcore_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReplaceLocationHost(t *testing.T) {
|
func TestReplaceLocationHost(t *testing.T) {
|
||||||
urlString := "http://private.com/test/newtarget/"
|
tests := []struct {
|
||||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
name string
|
||||||
OriginalHost: "test.example.com",
|
urlString string
|
||||||
ProxyDomain: "private.com/test",
|
rrr *dpcore.ResponseRewriteRuleSet
|
||||||
UseTLS: true,
|
useTLS bool
|
||||||
}
|
expectedResult string
|
||||||
useTLS := true
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic HTTP to HTTPS redirection",
|
||||||
|
urlString: "http://example.com/resource",
|
||||||
|
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "example.com", OriginalHost: "proxy.example.com", UseTLS: true},
|
||||||
|
useTLS: true,
|
||||||
|
expectedResult: "https://proxy.example.com/resource",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
|
||||||
expectedResult := "https://test.example.com/newtarget/"
|
{
|
||||||
|
name: "Basic HTTPS to HTTP redirection",
|
||||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
urlString: "https://proxy.example.com/resource",
|
||||||
if err != nil {
|
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
|
||||||
t.Errorf("Error occurred: %v", err)
|
useTLS: false,
|
||||||
|
expectedResult: "http://proxy.example.com/resource",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No rewrite on mismatched domain",
|
||||||
|
urlString: "http://anotherdomain.com/resource",
|
||||||
|
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: true},
|
||||||
|
useTLS: true,
|
||||||
|
expectedResult: "http://anotherdomain.com/resource",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Subpath trimming with HTTPS",
|
||||||
|
urlString: "https://blog.example.com/post?id=1",
|
||||||
|
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "blog.example.com", OriginalHost: "proxy.example.com/blog", UseTLS: true},
|
||||||
|
useTLS: true,
|
||||||
|
expectedResult: "https://proxy.example.com/blog/post?id=1",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != expectedResult {
|
for _, tt := range tests {
|
||||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := dpcore.ReplaceLocationHost(tt.urlString, tt.rrr, tt.useTLS)
|
||||||
|
if (err != nil) != tt.expectError {
|
||||||
|
t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
|
||||||
|
}
|
||||||
|
if result != tt.expectedResult {
|
||||||
|
result, _ = url.QueryUnescape(result)
|
||||||
|
t.Errorf("Expected result: %s, got: %s", tt.expectedResult, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +74,7 @@ func TestReplaceLocationHostRelative(t *testing.T) {
|
|||||||
}
|
}
|
||||||
useTLS := true
|
useTLS := true
|
||||||
|
|
||||||
expectedResult := "https://test.example.com/api/"
|
expectedResult := "api/"
|
||||||
|
|
||||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -60,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
|||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug functions
|
// Debug functions for replaceLocationHost
|
||||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||||
return replaceLocationHost(urlString, rrr, useTLS)
|
return replaceLocationHost(urlString, rrr, useTLS)
|
||||||
}
|
}
|
||||||
|
@ -291,7 +291,7 @@ func (router *Router) Restart() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(800 * time.Millisecond)
|
||||||
// Start the server
|
// Start the server
|
||||||
err = router.StartProxyService()
|
err = router.StartProxyService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -36,7 +37,7 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
|
|||||||
|
|
||||||
// Remvoe a user defined header from the list
|
// Remvoe a user defined header from the list
|
||||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||||
newHeaderList := []*UserDefinedHeader{}
|
newHeaderList := []*rewrite.UserDefinedHeader{}
|
||||||
for _, header := range ep.UserDefinedHeaders {
|
for _, header := range ep.UserDefinedHeaders {
|
||||||
if !strings.EqualFold(header.Key, key) {
|
if !strings.EqualFold(header.Key, key) {
|
||||||
newHeaderList = append(newHeaderList, header)
|
newHeaderList = append(newHeaderList, header)
|
||||||
@ -49,7 +50,7 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a user defined header to the list, duplicates will be automatically removed
|
// Add a user defined header to the list, duplicates will be automatically removed
|
||||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
|
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
|
||||||
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
|
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
|
||||||
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
|
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
|
||||||
}
|
}
|
||||||
|
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal file
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package loadbalance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { ... }
|
||||||
|
func TestRandomUpstreamSelection(t *testing.T) {
|
||||||
|
rand.Seed(time.Now().UnixNano()) // Seed for randomness
|
||||||
|
|
||||||
|
// Define some test upstreams
|
||||||
|
upstreams := []*Upstream{
|
||||||
|
{
|
||||||
|
OriginIpOrDomain: "192.168.1.1:8080",
|
||||||
|
RequireTLS: false,
|
||||||
|
SkipCertValidations: false,
|
||||||
|
SkipWebSocketOriginCheck: false,
|
||||||
|
Weight: 1,
|
||||||
|
MaxConn: 0, // No connection limit for now
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OriginIpOrDomain: "192.168.1.2:8080",
|
||||||
|
RequireTLS: false,
|
||||||
|
SkipCertValidations: false,
|
||||||
|
SkipWebSocketOriginCheck: false,
|
||||||
|
Weight: 1,
|
||||||
|
MaxConn: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OriginIpOrDomain: "192.168.1.3:8080",
|
||||||
|
RequireTLS: true,
|
||||||
|
SkipCertValidations: true,
|
||||||
|
SkipWebSocketOriginCheck: true,
|
||||||
|
Weight: 1,
|
||||||
|
MaxConn: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OriginIpOrDomain: "192.168.1.4:8080",
|
||||||
|
RequireTLS: true,
|
||||||
|
SkipCertValidations: true,
|
||||||
|
SkipWebSocketOriginCheck: true,
|
||||||
|
Weight: 1,
|
||||||
|
MaxConn: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track how many times each upstream is selected
|
||||||
|
selectionCount := make(map[string]int)
|
||||||
|
totalPicks := 10000 // Number of times to call getRandomUpstreamByWeight
|
||||||
|
//expectedPickCount := totalPicks / len(upstreams) // Ideal count for each upstream
|
||||||
|
|
||||||
|
// Pick upstreams and record their selection count
|
||||||
|
for i := 0; i < totalPicks; i++ {
|
||||||
|
upstream, _, err := getRandomUpstreamByWeight(upstreams)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting random upstream: %v", err)
|
||||||
|
}
|
||||||
|
selectionCount[upstream.OriginIpOrDomain]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition 1: Ensure every upstream has been picked at least once
|
||||||
|
for _, upstream := range upstreams {
|
||||||
|
if selectionCount[upstream.OriginIpOrDomain] == 0 {
|
||||||
|
t.Errorf("Upstream %s was never selected", upstream.OriginIpOrDomain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition 2: Check that the distribution is within 1-2 standard deviations
|
||||||
|
counts := make([]float64, len(upstreams))
|
||||||
|
for i, upstream := range upstreams {
|
||||||
|
counts[i] = float64(selectionCount[upstream.OriginIpOrDomain])
|
||||||
|
}
|
||||||
|
|
||||||
|
mean := float64(totalPicks) / float64(len(upstreams))
|
||||||
|
stddev := calculateStdDev(counts, mean)
|
||||||
|
|
||||||
|
tolerance := 2 * stddev // Allowing up to 2 standard deviations
|
||||||
|
for i, count := range counts {
|
||||||
|
if math.Abs(count-mean) > tolerance {
|
||||||
|
t.Errorf("Selection of upstream %s is outside acceptable range: %v picks (mean: %v, stddev: %v)", upstreams[i].OriginIpOrDomain, count, mean, stddev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Selection count:", selectionCount)
|
||||||
|
fmt.Printf("Mean: %.2f, StdDev: %.2f\n", mean, stddev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to calculate standard deviation
|
||||||
|
func calculateStdDev(data []float64, mean float64) float64 {
|
||||||
|
var sumOfSquares float64
|
||||||
|
for _, value := range data {
|
||||||
|
sumOfSquares += (value - mean) * (value - mean)
|
||||||
|
}
|
||||||
|
variance := sumOfSquares / float64(len(data))
|
||||||
|
return math.Sqrt(variance)
|
||||||
|
}
|
@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
"imuslab.com/zoraxy/mod/statistic"
|
"imuslab.com/zoraxy/mod/statistic"
|
||||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||||
@ -158,9 +159,19 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
r.URL, _ = url.Parse(originalHostHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Build downstream and upstream header rules
|
//Populate the user-defined headers with the values from the request
|
||||||
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
|
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.UserDefinedHeaders)
|
||||||
|
|
||||||
|
//Build downstream and upstream header rules
|
||||||
|
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
|
||||||
|
UserDefinedHeaders: rewrittenUserDefinedHeaders,
|
||||||
|
HSTSMaxAge: target.HSTSMaxAge,
|
||||||
|
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
|
||||||
|
EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader,
|
||||||
|
PermissionPolicy: target.PermissionPolicy,
|
||||||
|
})
|
||||||
|
|
||||||
|
//Handle the request reverse proxy
|
||||||
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
@ -226,9 +237,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
r.URL, _ = url.Parse(originalHostHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Build downstream and upstream header rules
|
//Populate the user-defined headers with the values from the request
|
||||||
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
|
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.UserDefinedHeaders)
|
||||||
|
|
||||||
|
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
|
||||||
|
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
|
||||||
|
UserDefinedHeaders: rewrittenUserDefinedHeaders,
|
||||||
|
HSTSMaxAge: target.parent.HSTSMaxAge,
|
||||||
|
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
|
||||||
|
EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader,
|
||||||
|
PermissionPolicy: target.parent.PermissionPolicy,
|
||||||
|
})
|
||||||
|
|
||||||
|
//Handle the virtual directory reverse proxy request
|
||||||
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: target.Domain,
|
ProxyDomain: target.Domain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
|
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal file
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package rewrite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHeaderVariableValuesFromRequest returns a map of header variables and their values
|
||||||
|
// note that variables behavior is not exactly identical to nginx variables
|
||||||
|
func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
|
||||||
|
vars := make(map[string]string)
|
||||||
|
|
||||||
|
// Request-specific variables
|
||||||
|
vars["$host"] = r.Host
|
||||||
|
vars["$remote_addr"] = r.RemoteAddr
|
||||||
|
vars["$request_uri"] = r.RequestURI
|
||||||
|
vars["$request_method"] = r.Method
|
||||||
|
vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
|
||||||
|
vars["$content_type"] = r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
// Parsed URI elements
|
||||||
|
vars["$uri"] = r.URL.Path
|
||||||
|
vars["$args"] = r.URL.RawQuery
|
||||||
|
vars["$scheme"] = r.URL.Scheme
|
||||||
|
vars["$query_string"] = r.URL.RawQuery
|
||||||
|
|
||||||
|
// User agent and referer
|
||||||
|
vars["$http_user_agent"] = r.UserAgent()
|
||||||
|
vars["$http_referer"] = r.Referer()
|
||||||
|
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomHeadersIncludeDynamicVariables checks if the user-defined headers contain dynamic variables
|
||||||
|
// use for early exit when processing the headers
|
||||||
|
func CustomHeadersIncludeDynamicVariables(userDefinedHeaders []*UserDefinedHeader) bool {
|
||||||
|
for _, header := range userDefinedHeaders {
|
||||||
|
if strings.Contains(header.Value, "$") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopulateRequestHeaderVariables populates the user-defined headers with the values from the request
|
||||||
|
func PopulateRequestHeaderVariables(r *http.Request, userDefinedHeaders []*UserDefinedHeader) []*UserDefinedHeader {
|
||||||
|
if !CustomHeadersIncludeDynamicVariables(userDefinedHeaders) {
|
||||||
|
// Early exit if there are no dynamic variables
|
||||||
|
return userDefinedHeaders
|
||||||
|
}
|
||||||
|
vars := GetHeaderVariableValuesFromRequest(r)
|
||||||
|
populatedHeaders := []*UserDefinedHeader{}
|
||||||
|
// Populate the user-defined headers with the values from the request
|
||||||
|
for _, header := range userDefinedHeaders {
|
||||||
|
thisHeader := header.Copy()
|
||||||
|
for key, value := range vars {
|
||||||
|
thisHeader.Value = strings.ReplaceAll(thisHeader.Value, key, value)
|
||||||
|
}
|
||||||
|
populatedHeaders = append(populatedHeaders, thisHeader)
|
||||||
|
}
|
||||||
|
return populatedHeaders
|
||||||
|
}
|
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal file
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package rewrite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHeaderVariableValuesFromRequest(t *testing.T) {
|
||||||
|
// Create a sample request
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
|
||||||
|
req.Host = "example.com"
|
||||||
|
req.RemoteAddr = "192.168.1.1:12345"
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "TestAgent")
|
||||||
|
req.Header.Set("Referer", "https://referer.com")
|
||||||
|
|
||||||
|
// Call the function
|
||||||
|
vars := GetHeaderVariableValuesFromRequest(req)
|
||||||
|
|
||||||
|
// Expected results
|
||||||
|
expected := map[string]string{
|
||||||
|
"$host": "example.com",
|
||||||
|
"$remote_addr": "192.168.1.1:12345",
|
||||||
|
"$request_uri": "https://example.com/test?foo=bar",
|
||||||
|
"$request_method": "GET",
|
||||||
|
"$content_length": "0", // ContentLength is 0 because there's no body in the request
|
||||||
|
"$content_type": "application/json",
|
||||||
|
"$uri": "/test",
|
||||||
|
"$args": "foo=bar",
|
||||||
|
"$scheme": "https",
|
||||||
|
"$query_string": "foo=bar",
|
||||||
|
"$http_user_agent": "TestAgent",
|
||||||
|
"$http_referer": "https://referer.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each expected variable
|
||||||
|
for key, expectedValue := range expected {
|
||||||
|
if vars[key] != expectedValue {
|
||||||
|
t.Errorf("Expected %s to be %s, but got %s", key, expectedValue, vars[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomHeadersIncludeDynamicVariables(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
headers []*UserDefinedHeader
|
||||||
|
expectedHasVar bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No headers",
|
||||||
|
headers: []*UserDefinedHeader{},
|
||||||
|
expectedHasVar: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Headers without dynamic variables",
|
||||||
|
headers: []*UserDefinedHeader{
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||||
|
Key: "X-Custom-Header",
|
||||||
|
Value: "staticValue",
|
||||||
|
IsRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Another-Header",
|
||||||
|
Value: "staticValue",
|
||||||
|
IsRemove: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHasVar: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Headers with one dynamic variable",
|
||||||
|
headers: []*UserDefinedHeader{
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||||
|
Key: "X-Custom-Header",
|
||||||
|
Value: "$dynamicValue",
|
||||||
|
IsRemove: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHasVar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Headers with multiple dynamic variables",
|
||||||
|
headers: []*UserDefinedHeader{
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||||
|
Key: "X-Custom-Header",
|
||||||
|
Value: "$dynamicValue1",
|
||||||
|
IsRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Another-Header",
|
||||||
|
Value: "$dynamicValue2",
|
||||||
|
IsRemove: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHasVar: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
hasVar := CustomHeadersIncludeDynamicVariables(tt.headers)
|
||||||
|
if hasVar != tt.expectedHasVar {
|
||||||
|
t.Errorf("Expected %v, but got %v", tt.expectedHasVar, hasVar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopulateRequestHeaderVariables(t *testing.T) {
|
||||||
|
// Create a sample request with specific values
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
|
||||||
|
req.Host = "example.com"
|
||||||
|
req.RemoteAddr = "192.168.1.1:12345"
|
||||||
|
req.Header.Set("User-Agent", "TestAgent")
|
||||||
|
req.Header.Set("Referer", "https://referer.com")
|
||||||
|
|
||||||
|
// Define user-defined headers with dynamic variables
|
||||||
|
userDefinedHeaders := []*UserDefinedHeader{
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||||
|
Key: "X-Forwarded-Host",
|
||||||
|
Value: "$host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Client-IP",
|
||||||
|
Value: "$remote_addr",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Custom-Header",
|
||||||
|
Value: "$request_uri",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function with the test data
|
||||||
|
resultHeaders := PopulateRequestHeaderVariables(req, userDefinedHeaders)
|
||||||
|
|
||||||
|
// Expected results after variable substitution
|
||||||
|
expectedHeaders := []*UserDefinedHeader{
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||||
|
Key: "X-Forwarded-Host",
|
||||||
|
Value: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Client-IP",
|
||||||
|
Value: "192.168.1.1:12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||||
|
Key: "X-Custom-Header",
|
||||||
|
Value: "https://example.com/test?foo=bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate results
|
||||||
|
for i, expected := range expectedHeaders {
|
||||||
|
if resultHeaders[i].Direction != expected.Direction ||
|
||||||
|
resultHeaders[i].Key != expected.Key ||
|
||||||
|
resultHeaders[i].Value != expected.Value {
|
||||||
|
t.Errorf("Expected header %v, but got %v", expected, resultHeaders[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal file
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package rewrite
|
||||||
|
|
||||||
|
/*
|
||||||
|
rewrite.go
|
||||||
|
|
||||||
|
This script handle the rewrite logic for custom headers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
|
||||||
|
// return upstream header and downstream header key-value pairs
|
||||||
|
// if the header is expected to be deleted, the value will be set to empty string
|
||||||
|
func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string, [][]string) {
|
||||||
|
if len(rewriteOptions.UserDefinedHeaders) == 0 && rewriteOptions.HSTSMaxAge == 0 && !rewriteOptions.EnablePermissionPolicyHeader {
|
||||||
|
//Early return if there are no defined headers
|
||||||
|
return [][]string{}, [][]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Use pre-allocation for faster performance
|
||||||
|
//Downstream +2 for Permission Policy and HSTS
|
||||||
|
upstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders))
|
||||||
|
downstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)+2)
|
||||||
|
upstreamHeaderCounter := 0
|
||||||
|
downstreamHeaderCounter := 0
|
||||||
|
|
||||||
|
//Sort the headers into upstream or downstream
|
||||||
|
for _, customHeader := range rewriteOptions.UserDefinedHeaders {
|
||||||
|
thisHeaderSet := make([]string, 2)
|
||||||
|
thisHeaderSet[0] = customHeader.Key
|
||||||
|
thisHeaderSet[1] = customHeader.Value
|
||||||
|
if customHeader.IsRemove {
|
||||||
|
//Prevent invalid config
|
||||||
|
thisHeaderSet[1] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
//Assign to slice
|
||||||
|
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
|
||||||
|
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
|
||||||
|
upstreamHeaderCounter++
|
||||||
|
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
|
||||||
|
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
|
||||||
|
downstreamHeaderCounter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the endpoint require HSTS headers
|
||||||
|
if rewriteOptions.HSTSMaxAge > 0 {
|
||||||
|
if rewriteOptions.HSTSIncludeSubdomains {
|
||||||
|
//Endpoint listening domain includes wildcards.
|
||||||
|
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge)) + "; includeSubdomains"}
|
||||||
|
} else {
|
||||||
|
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge))}
|
||||||
|
}
|
||||||
|
|
||||||
|
downstreamHeaderCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the endpoint require Permission Policy
|
||||||
|
if rewriteOptions.EnablePermissionPolicyHeader {
|
||||||
|
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
|
||||||
|
if rewriteOptions.PermissionPolicy != nil {
|
||||||
|
//Custom permission policy
|
||||||
|
usingPermissionPolicy = rewriteOptions.PermissionPolicy
|
||||||
|
} else {
|
||||||
|
//Permission policy is enabled but not customized. Use default
|
||||||
|
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
|
||||||
|
downstreamHeaderCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreamHeaders, downstreamHeaders
|
||||||
|
}
|
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal file
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package rewrite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
typdef.go
|
||||||
|
|
||||||
|
This script handle the type definition for custom headers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Custom Header Related Data structure */
|
||||||
|
// Header injection direction type
|
||||||
|
type HeaderDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
|
||||||
|
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
|
||||||
|
)
|
||||||
|
|
||||||
|
// User defined headers to add into a proxy endpoint
|
||||||
|
type UserDefinedHeader struct {
|
||||||
|
Direction HeaderDirection
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
IsRemove bool //Instead of set, remove this key instead
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderRewriteOptions struct {
|
||||||
|
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||||
|
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||||
|
HSTSIncludeSubdomains bool //Include subdomains in HSTS header
|
||||||
|
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||||
|
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities for header rewrite
|
||||||
|
func (h *UserDefinedHeader) GetDirection() HeaderDirection {
|
||||||
|
return h.Direction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy eturns a deep copy of the UserDefinedHeader
|
||||||
|
func (h *UserDefinedHeader) Copy() *UserDefinedHeader {
|
||||||
|
result := UserDefinedHeader{}
|
||||||
|
js, _ := json.Marshal(h)
|
||||||
|
json.Unmarshal(js, &result)
|
||||||
|
return &result
|
||||||
|
}
|
@ -7,10 +7,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
"imuslab.com/zoraxy/mod/statistic"
|
"imuslab.com/zoraxy/mod/statistic"
|
||||||
@ -44,6 +46,7 @@ type RouterOption struct {
|
|||||||
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
||||||
WebDirectory string //The static web server directory containing the templates folder
|
WebDirectory string //The static web server directory containing the templates folder
|
||||||
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
|
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
|
||||||
|
SSOHandler *sso.SSOHandler //SSO handler for handling SSO requests, interception mode only
|
||||||
Logger *logger.Logger //Logger for reverse proxy requets
|
Logger *logger.Logger //Logger for reverse proxy requets
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,23 +85,6 @@ type BasicAuthExceptionRule struct {
|
|||||||
PathPrefix string
|
PathPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Header Related Data structure */
|
|
||||||
// Header injection direction type
|
|
||||||
type HeaderDirection int
|
|
||||||
|
|
||||||
const (
|
|
||||||
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
|
|
||||||
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
|
|
||||||
)
|
|
||||||
|
|
||||||
// User defined headers to add into a proxy endpoint
|
|
||||||
type UserDefinedHeader struct {
|
|
||||||
Direction HeaderDirection
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
IsRemove bool //Instead of set, remove this key instead
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Routing Rule Data Structures */
|
/* Routing Rule Data Structures */
|
||||||
|
|
||||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||||
@ -131,7 +117,7 @@ type ProxyEndpoint struct {
|
|||||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||||
|
|
||||||
//Custom Headers
|
//Custom Headers
|
||||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||||
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
|
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
|
||||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||||
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||||
@ -142,6 +128,7 @@ type ProxyEndpoint struct {
|
|||||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||||
|
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
|
||||||
|
|
||||||
// Rate Limiting
|
// Rate Limiting
|
||||||
RequireRateLimit bool
|
RequireRateLimit bool
|
||||||
|
@ -16,7 +16,7 @@ type Sender struct {
|
|||||||
Port int //E.g. 587
|
Port int //E.g. 587
|
||||||
Username string //Username of the email account
|
Username string //Username of the email account
|
||||||
Password string //Password of the email account
|
Password string //Password of the email account
|
||||||
SenderAddr string //e.g. admin@arozos.com
|
SenderAddr string //e.g. admin@aroz.org
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new email sender object
|
// Create a new email sender object
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package ganserv
|
package ganserv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
|||||||
//Get controller info
|
//Get controller info
|
||||||
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Println("ZeroTier connection failed: ", err.Error())
|
||||||
return &NetworkManager{
|
return &NetworkManager{
|
||||||
authToken: option.AuthToken,
|
authToken: option.AuthToken,
|
||||||
apiPort: option.ApiPort,
|
apiPort: option.ApiPort,
|
||||||
|
@ -28,11 +28,17 @@ type NodeInfo struct {
|
|||||||
Clock int64 `json:"clock"`
|
Clock int64 `json:"clock"`
|
||||||
Config struct {
|
Config struct {
|
||||||
Settings struct {
|
Settings struct {
|
||||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
|
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
|
||||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
|
||||||
PrimaryPort int `json:"primaryPort"`
|
HomeDir string `json:"homeDir,omitempty"`
|
||||||
SoftwareUpdate string `json:"softwareUpdate"`
|
ListeningOn []string `json:"listeningOn,omitempty"`
|
||||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
|
||||||
|
PrimaryPort int `json:"primaryPort,omitempty"`
|
||||||
|
SecondaryPort int `json:"secondaryPort,omitempty"`
|
||||||
|
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
|
||||||
|
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
|
||||||
|
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
|
||||||
|
TertiaryPort int `json:"tertiaryPort,omitempty"`
|
||||||
} `json:"settings"`
|
} `json:"settings"`
|
||||||
} `json:"config"`
|
} `json:"config"`
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
@ -46,7 +52,6 @@ type NodeInfo struct {
|
|||||||
VersionMinor int `json:"versionMinor"`
|
VersionMinor int `json:"versionMinor"`
|
||||||
VersionRev int `json:"versionRev"`
|
VersionRev int `json:"versionRev"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrResp struct {
|
type ErrResp struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package geodb
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
@ -15,17 +16,22 @@ var geoipv4 []byte //Geodb dataset for ipv4
|
|||||||
var geoipv6 []byte //Geodb dataset for ipv6
|
var geoipv6 []byte //Geodb dataset for ipv6
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
geodb [][]string //Parsed geodb list
|
geodb [][]string //Parsed geodb list
|
||||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||||
geotrie *trie
|
geotrie *trie
|
||||||
geotrieIpv6 *trie
|
geotrieIpv6 *trie
|
||||||
sysdb *database.Database
|
sysdb *database.Database
|
||||||
option *StoreOptions
|
slowLookupCacheIpv4 map[string]string //Cache for slow lookup
|
||||||
|
slowLookupCacheIpv6 map[string]string //Cache for slow lookup
|
||||||
|
cacheClearTicker *time.Ticker //Ticker for clearing cache
|
||||||
|
cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker
|
||||||
|
option *StoreOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreOptions struct {
|
type StoreOptions struct {
|
||||||
AllowSlowIpv4LookUp bool
|
AllowSlowIpv4LookUp bool
|
||||||
AllowSloeIpv6Lookup bool
|
AllowSlowIpv6Lookup bool
|
||||||
|
SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval
|
||||||
}
|
}
|
||||||
|
|
||||||
type CountryInfo struct {
|
type CountryInfo struct {
|
||||||
@ -50,18 +56,44 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipv6Trie *trie
|
var ipv6Trie *trie
|
||||||
if !option.AllowSloeIpv6Lookup {
|
if !option.AllowSlowIpv6Lookup {
|
||||||
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
|
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Store{
|
if option.SlowLookupCacheClearInterval == 0 {
|
||||||
geodb: parsedGeoData,
|
option.SlowLookupCacheClearInterval = 15 * time.Minute
|
||||||
geotrie: ipv4Trie,
|
}
|
||||||
geodbIpv6: parsedGeoDataIpv6,
|
|
||||||
geotrieIpv6: ipv6Trie,
|
//Create a new store
|
||||||
sysdb: sysdb,
|
thisGeoDBStore := &Store{
|
||||||
option: option,
|
geodb: parsedGeoData,
|
||||||
}, nil
|
geotrie: ipv4Trie,
|
||||||
|
geodbIpv6: parsedGeoDataIpv6,
|
||||||
|
geotrieIpv6: ipv6Trie,
|
||||||
|
sysdb: sysdb,
|
||||||
|
slowLookupCacheIpv4: make(map[string]string),
|
||||||
|
slowLookupCacheIpv6: make(map[string]string),
|
||||||
|
cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval),
|
||||||
|
cacheClearTickerStopChan: make(chan bool),
|
||||||
|
option: option,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start cache clear ticker
|
||||||
|
if option.AllowSlowIpv4LookUp || option.AllowSlowIpv6Lookup {
|
||||||
|
go func(store *Store) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-store.cacheClearTickerStopChan:
|
||||||
|
return
|
||||||
|
case <-thisGeoDBStore.cacheClearTicker.C:
|
||||||
|
thisGeoDBStore.slowLookupCacheIpv4 = make(map[string]string)
|
||||||
|
thisGeoDBStore.slowLookupCacheIpv6 = make(map[string]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(thisGeoDBStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
return thisGeoDBStore, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
||||||
@ -73,8 +105,12 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the store
|
||||||
func (s *Store) Close() {
|
func (s *Store) Close() {
|
||||||
|
if s.option.AllowSlowIpv4LookUp || s.option.AllowSlowIpv6Lookup {
|
||||||
|
//Stop cache clear ticker
|
||||||
|
s.cacheClearTickerStopChan <- true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
|
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
|
||||||
|
@ -44,6 +44,7 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
|
|||||||
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
|
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
0,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error creating store: %v", err)
|
t.Errorf("error creating store: %v", err)
|
||||||
|
118657
src/mod/geodb/geoipv4.csv
118657
src/mod/geodb/geoipv4.csv
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
67
src/mod/geodb/locale.go
Normal file
67
src/mod/geodb/locale.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package geodb
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// GetRequesterCountryISOCode get the locale of the requester
|
||||||
|
func (s *Store) GetLocaleFromRequest(r *http.Request) (string, error) {
|
||||||
|
cc := s.GetRequesterCountryISOCode(r)
|
||||||
|
return GetLocaleFromCountryCode(cc), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocaleFromCountryCode get the locale given the country code
|
||||||
|
func GetLocaleFromCountryCode(cc string) string {
|
||||||
|
//If you find your country is not in the list, please add it here
|
||||||
|
mapCountryToLocale := map[string]string{
|
||||||
|
"aa": "ar_AA",
|
||||||
|
"by": "be_BY",
|
||||||
|
"bg": "bg_BG",
|
||||||
|
"es": "ca_ES",
|
||||||
|
"cz": "cs_CZ",
|
||||||
|
"dk": "da_DK",
|
||||||
|
"ch": "de_CH",
|
||||||
|
"de": "de_DE",
|
||||||
|
"gr": "el_GR",
|
||||||
|
"au": "en_AU",
|
||||||
|
"be": "en_BE",
|
||||||
|
"gb": "en_GB",
|
||||||
|
"jp": "en_JP",
|
||||||
|
"us": "en_US",
|
||||||
|
"za": "en_ZA",
|
||||||
|
"fi": "fi_FI",
|
||||||
|
"ca": "fr_CA",
|
||||||
|
"fr": "fr_FR",
|
||||||
|
"hr": "hr_HR",
|
||||||
|
"hu": "hu_HU",
|
||||||
|
"is": "is_IS",
|
||||||
|
"it": "it_IT",
|
||||||
|
"il": "iw_IL",
|
||||||
|
"kr": "ko_KR",
|
||||||
|
"lt": "lt_LT",
|
||||||
|
"lv": "lv_LV",
|
||||||
|
"mk": "mk_MK",
|
||||||
|
"nl": "nl_NL",
|
||||||
|
"no": "no_NO",
|
||||||
|
"pl": "pl_PL",
|
||||||
|
"br": "pt_BR",
|
||||||
|
"pt": "pt_PT",
|
||||||
|
"ro": "ro_RO",
|
||||||
|
"ru": "ru_RU",
|
||||||
|
"sp": "sh_SP",
|
||||||
|
"sk": "sk_SK",
|
||||||
|
"sl": "sl_SL",
|
||||||
|
"al": "sq_AL",
|
||||||
|
"se": "sv_SE",
|
||||||
|
"th": "th_TH",
|
||||||
|
"tr": "tr_TR",
|
||||||
|
"ua": "uk_UA",
|
||||||
|
"cn": "zh_CN",
|
||||||
|
"tw": "zh_TW",
|
||||||
|
"hk": "zh_HK",
|
||||||
|
}
|
||||||
|
locale, ok := mapCountryToLocale[cc]
|
||||||
|
if !ok {
|
||||||
|
return "en-US"
|
||||||
|
}
|
||||||
|
|
||||||
|
return locale
|
||||||
|
}
|
@ -56,6 +56,12 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
|
|||||||
if isReservedIP(ipAddr) {
|
if isReservedIP(ipAddr) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if already in cache
|
||||||
|
if cc, ok := s.slowLookupCacheIpv4[ipAddr]; ok {
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
for _, ipRange := range s.geodb {
|
for _, ipRange := range s.geodb {
|
||||||
startIp := ipRange[0]
|
startIp := ipRange[0]
|
||||||
endIp := ipRange[1]
|
endIp := ipRange[1]
|
||||||
@ -63,6 +69,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
|
|||||||
|
|
||||||
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
|
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
|
||||||
if inRange {
|
if inRange {
|
||||||
|
//Add to cache
|
||||||
|
s.slowLookupCacheIpv4[ipAddr] = cc
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,6 +81,12 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
|
|||||||
if isReservedIP(ipAddr) {
|
if isReservedIP(ipAddr) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if already in cache
|
||||||
|
if cc, ok := s.slowLookupCacheIpv6[ipAddr]; ok {
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
for _, ipRange := range s.geodbIpv6 {
|
for _, ipRange := range s.geodbIpv6 {
|
||||||
startIp := ipRange[0]
|
startIp := ipRange[0]
|
||||||
endIp := ipRange[1]
|
endIp := ipRange[1]
|
||||||
@ -80,6 +94,8 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
|
|||||||
|
|
||||||
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
|
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
|
||||||
if inRange {
|
if inRange {
|
||||||
|
//Add to cache
|
||||||
|
s.slowLookupCacheIpv6[ipAddr] = cc
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
src/mod/ipscan/handlers.go
Normal file
79
src/mod/ipscan/handlers.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package ipscan
|
||||||
|
|
||||||
|
/*
|
||||||
|
ipscan http handlers
|
||||||
|
|
||||||
|
This script provide http handlers for ipscan module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleScanPort is the HTTP handler for scanning opened ports on a given IP address
|
||||||
|
func HandleScanPort(w http.ResponseWriter, r *http.Request) {
|
||||||
|
targetIp, err := utils.GetPara(r, "ip")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "target IP address not given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IP is a valid IP address
|
||||||
|
ip := net.ParseIP(targetIp)
|
||||||
|
if ip == nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid IP address")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan the ports
|
||||||
|
openPorts := ScanPorts(targetIp)
|
||||||
|
jsonData, err := json.Marshal(openPorts)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to marshal JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendJSONResponse(w, string(jsonData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleIpScan is the HTTP handler for scanning IP addresses in a given range or CIDR
|
||||||
|
func HandleIpScan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cidr, err := utils.PostPara(r, "cidr")
|
||||||
|
if err != nil {
|
||||||
|
//Ip range mode
|
||||||
|
start, err := utils.PostPara(r, "start")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "missing start ip")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := utils.PostPara(r, "end")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "missing end ip")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discoveredHosts, err := ScanIpRange(start, end)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(discoveredHosts)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
} else {
|
||||||
|
//CIDR mode
|
||||||
|
discoveredHosts, err := ScanCIDRRange(cidr)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(discoveredHosts)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ type DiscoveredHost struct {
|
|||||||
HttpsPortDetected bool
|
HttpsPortDetected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//Scan an IP range given the start and ending ip address
|
// Scan an IP range given the start and ending ip address
|
||||||
func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
||||||
ipStart := net.ParseIP(start)
|
ipStart := net.ParseIP(start)
|
||||||
ipEnd := net.ParseIP(end)
|
ipEnd := net.ParseIP(end)
|
||||||
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
|||||||
host.CheckHostname()
|
host.CheckHostname()
|
||||||
host.CheckPort("http", 80, &host.HttpPortDetected)
|
host.CheckPort("http", 80, &host.HttpPortDetected)
|
||||||
host.CheckPort("https", 443, &host.HttpsPortDetected)
|
host.CheckPort("https", 443, &host.HttpsPortDetected)
|
||||||
fmt.Println("OK", host)
|
|
||||||
hosts = append(hosts, host)
|
hosts = append(hosts, host)
|
||||||
|
|
||||||
}(thisIp)
|
}(thisIp)
|
||||||
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
|
|||||||
func (host *DiscoveredHost) CheckHostname() {
|
func (host *DiscoveredHost) CheckHostname() {
|
||||||
// lookup the hostname for the IP address
|
// lookup the hostname for the IP address
|
||||||
names, err := net.LookupAddr(host.IP)
|
names, err := net.LookupAddr(host.IP)
|
||||||
fmt.Println(names, err)
|
//fmt.Println(names, err)
|
||||||
if err == nil && len(names) > 0 {
|
if err == nil && len(names) > 0 {
|
||||||
host.Hostname = names[0]
|
host.Hostname = names[0]
|
||||||
}
|
}
|
||||||
|
48
src/mod/ipscan/portscan.go
Normal file
48
src/mod/ipscan/portscan.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package ipscan
|
||||||
|
|
||||||
|
/*
|
||||||
|
Port Scanner
|
||||||
|
|
||||||
|
This module scan the given IP address and scan all the opened port
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenedPort holds information about an open port and its service type
|
||||||
|
type OpenedPort struct {
|
||||||
|
Port int
|
||||||
|
IsTCP bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanPorts scans all the opened ports on a given host IP (both IPv4 and IPv6)
|
||||||
|
func ScanPorts(host string) []*OpenedPort {
|
||||||
|
var openPorts []*OpenedPort
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for port := 1; port <= 65535; port++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(port int) {
|
||||||
|
defer wg.Done()
|
||||||
|
address := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
|
||||||
|
// Check TCP
|
||||||
|
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||||
|
if err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
openPorts = append(openPorts, &OpenedPort{Port: port, IsTCP: true})
|
||||||
|
mu.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return openPorts
|
||||||
|
}
|
@ -169,9 +169,16 @@ func (n *NetStatBuffers) HandleGetBufferedNetworkInterfaceStats(w http.ResponseW
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *NetStatBuffers) Close() {
|
func (n *NetStatBuffers) Close() {
|
||||||
n.StopChan <- true
|
//Fixed issue #394 for stopping netstat listener on platforms not supported platforms
|
||||||
time.Sleep(300 * time.Millisecond)
|
if n.StopChan != nil {
|
||||||
n.EventTicker.Stop()
|
n.StopChan <- true
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.EventTicker != nil {
|
||||||
|
n.EventTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
|
func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -270,11 +277,11 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
|||||||
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
|
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Permission denied
|
//Permission denied
|
||||||
return 0, 0, errors.New("Access denied")
|
return 0, 0, errors.New("access denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allIfaceRxByteFiles) == 0 {
|
if len(allIfaceRxByteFiles) == 0 {
|
||||||
return 0, 0, errors.New("No valid iface found")
|
return 0, 0, errors.New("no valid iface found")
|
||||||
}
|
}
|
||||||
|
|
||||||
rxSum := int64(0)
|
rxSum := int64(0)
|
||||||
@ -334,5 +341,5 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
|||||||
return 0, 0, nil //no ethernet adapters with en*/<Link#*>
|
return 0, 0, nil //no ethernet adapters with en*/<Link#*>
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, 0, errors.New("Platform not supported")
|
return 0, 0, errors.New("platform not supported")
|
||||||
}
|
}
|
||||||
|
@ -50,21 +50,6 @@ func NewSSHProxyManager() *Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the next free port in the list
|
|
||||||
func (m *Manager) GetNextPort() int {
|
|
||||||
nextPort := m.StartingPort
|
|
||||||
occupiedPort := make(map[int]bool)
|
|
||||||
for _, instance := range m.Instances {
|
|
||||||
occupiedPort[instance.AssignedPort] = true
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if !occupiedPort[nextPort] {
|
|
||||||
return nextPort
|
|
||||||
}
|
|
||||||
nextPort++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
|
func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
targetInstance, err := m.GetInstanceById(instanceId)
|
targetInstance, err := m.GetInstanceById(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -168,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp
|
|||||||
if username != "" {
|
if username != "" {
|
||||||
connAddr = username + "@" + remoteIpAddr
|
connAddr = username + "@" + remoteIpAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Trim the space in the username and remote address
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
remoteIpAddr = strings.TrimSpace(remoteIpAddr)
|
||||||
|
|
||||||
|
//Validate the username and remote address
|
||||||
|
err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
|
configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
|
||||||
title := username + "@" + remoteIpAddr
|
title := username + "@" + remoteIpAddr
|
||||||
if remotePort != 22 {
|
if remotePort != 22 {
|
||||||
|
66
src/mod/sshprox/sshprox_test.go
Normal file
66
src/mod/sshprox/sshprox_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package sshprox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstance_Destroy(t *testing.T) {
|
||||||
|
manager := NewSSHProxyManager()
|
||||||
|
instance, err := manager.NewSSHProxy("/tmp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create new SSH proxy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Destroy()
|
||||||
|
|
||||||
|
if len(manager.Instances) != 0 {
|
||||||
|
t.Errorf("Expected Instances to be empty, got %d", len(manager.Instances))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstance_ValidateUsernameAndRemoteAddr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
username string
|
||||||
|
remoteAddr string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"validuser", "127.0.0.1", false},
|
||||||
|
{"valid.user", "example.com", false},
|
||||||
|
{"; bash ;", "example.com", true},
|
||||||
|
{"valid-user", "example.com", false},
|
||||||
|
{"invalid user", "127.0.0.1", true},
|
||||||
|
{"validuser", "invalid address", true},
|
||||||
|
{"invalid@user", "127.0.0.1", true},
|
||||||
|
{"validuser", "invalid@address", true},
|
||||||
|
{"injection; rm -rf /", "127.0.0.1", true},
|
||||||
|
{"validuser", "127.0.0.1; rm -rf /", true},
|
||||||
|
{"$(reboot)", "127.0.0.1", true},
|
||||||
|
{"validuser", "$(reboot)", true},
|
||||||
|
{"validuser", "127.0.0.1; $(reboot)", true},
|
||||||
|
{"validuser", "127.0.0.1 | ls", true},
|
||||||
|
{"validuser", "127.0.0.1 & ls", true},
|
||||||
|
{"validuser", "127.0.0.1 && ls", true},
|
||||||
|
{"validuser", "127.0.0.1 |& ls", true},
|
||||||
|
{"validuser", "127.0.0.1 ; ls", true},
|
||||||
|
{"validuser", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false},
|
||||||
|
{"validuser", "2001:db8::ff00:42:8329", false},
|
||||||
|
{"validuser", "2001:db8:0:1234:0:567:8:1", false},
|
||||||
|
{"validuser", "2001:db8::1234:0:567:8:1", false},
|
||||||
|
{"validuser", "2001:db8:0:0:0:0:2:1", false},
|
||||||
|
{"validuser", "2001:db8::2:1", false},
|
||||||
|
{"validuser", "2001:db8:0:0:8:800:200c:417a", false},
|
||||||
|
{"validuser", "2001:db8::8:800:200c:417a", false},
|
||||||
|
{"validuser", "2001:db8:0:0:8:800:200c:417a; rm -rf /", true},
|
||||||
|
{"validuser", "2001:db8::8:800:200c:417a; rm -rf /", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := ValidateUsernameAndRemoteAddr(test.username, test.remoteAddr)
|
||||||
|
if test.expectError && err == nil {
|
||||||
|
t.Errorf("Expected error for username %s and remoteAddr %s, but got none", test.username, test.remoteAddr)
|
||||||
|
}
|
||||||
|
if !test.expectError && err != nil {
|
||||||
|
t.Errorf("Did not expect error for username %s and remoteAddr %s, but got %v", test.username, test.remoteAddr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package sshprox
|
package sshprox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -34,6 +36,21 @@ func IsWebSSHSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the next free port in the list
|
||||||
|
func (m *Manager) GetNextPort() int {
|
||||||
|
nextPort := m.StartingPort
|
||||||
|
occupiedPort := make(map[int]bool)
|
||||||
|
for _, instance := range m.Instances {
|
||||||
|
occupiedPort[instance.AssignedPort] = true
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if !occupiedPort[nextPort] {
|
||||||
|
return nextPort
|
||||||
|
}
|
||||||
|
nextPort++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if a given domain and port is a valid ssh server
|
// Check if a given domain and port is a valid ssh server
|
||||||
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||||
timeout := time.Second * 3
|
timeout := time.Second * 3
|
||||||
@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
|
|||||||
return string(buf[:7]) == "SSH-2.0"
|
return string(buf[:7]) == "SSH-2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the port is used by other process or application
|
// Validate the username and remote address to prevent injection
|
||||||
func isPortInUse(port int) bool {
|
func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
|
||||||
address := fmt.Sprintf(":%d", port)
|
// Validate and sanitize the username to prevent ssh injection
|
||||||
listener, err := net.Listen("tcp", address)
|
validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
if err != nil {
|
if !validUsername.MatchString(username) {
|
||||||
|
return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the remoteIpAddr is a valid ipv4 or ipv6 address
|
||||||
|
if net.ParseIP(remoteIpAddr) != nil {
|
||||||
|
//A valid IP address do not need further validation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize the remote domain to prevent injection
|
||||||
|
validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
|
if !validRemoteAddr.MatchString(remoteIpAddr) {
|
||||||
|
return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the given ip or domain is a loopback address
|
||||||
|
// or resolves to a loopback address
|
||||||
|
func IsLoopbackIPOrDomain(ipOrDomain string) bool {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(ipOrDomain), "localhost") || strings.TrimSpace(ipOrDomain) == "127.0.0.1" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
listener.Close()
|
|
||||||
|
//Check if the ipOrDomain resolves to a loopback address
|
||||||
|
ips, err := net.LookupIP(ipOrDomain)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package streamproxy
|
package streamproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -48,9 +51,10 @@ type ProxyRelayConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Database *database.Database
|
|
||||||
DefaultTimeout int
|
DefaultTimeout int
|
||||||
AccessControlHandler func(net.Conn) bool
|
AccessControlHandler func(net.Conn) bool
|
||||||
|
ConfigStore string //Folder to store the config files, will be created if not exists
|
||||||
|
Logger *logger.Logger //Logger for the stream proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -63,13 +67,37 @@ type Manager struct {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStreamProxy(options *Options) *Manager {
|
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||||
options.Database.NewTable("tcprox")
|
if !utils.FileExists(options.ConfigStore) {
|
||||||
|
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Load relay configs from db
|
//Load relay configs from db
|
||||||
previousRules := []*ProxyRelayConfig{}
|
previousRules := []*ProxyRelayConfig{}
|
||||||
if options.Database.KeyExists("tcprox", "rules") {
|
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||||
options.Database.Read("tcprox", "rules", &previousRules)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, configFile := range streamProxyConfigFiles {
|
||||||
|
//Read file into bytes
|
||||||
|
configBytes, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
thisRelayConfig := &ProxyRelayConfig{}
|
||||||
|
err = json.Unmarshal(configBytes, thisRelayConfig)
|
||||||
|
if err != nil {
|
||||||
|
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//Append the config to the list
|
||||||
|
previousRules = append(previousRules, thisRelayConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
|
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
|
||||||
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
|
|||||||
rule.parent = &thisManager
|
rule.parent = &thisManager
|
||||||
if rule.Running {
|
if rule.Running {
|
||||||
//This was previously running. Start it again
|
//This was previously running. Start it again
|
||||||
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name)
|
thisManager.logf("Resuming stream proxy rule "+rule.Name, nil)
|
||||||
rule.Start()
|
rule.Start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thisManager.Configs = previousRules
|
thisManager.Configs = previousRules
|
||||||
|
|
||||||
return &thisManager
|
return &thisManager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper function to log error
|
||||||
|
func (m *Manager) logf(message string, originalError error) {
|
||||||
|
if m.Options.Logger == nil {
|
||||||
|
//Print to fmt
|
||||||
|
if originalError != nil {
|
||||||
|
message += ": " + originalError.Error()
|
||||||
|
}
|
||||||
|
println(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||||
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||||
|
//Remove the config from file
|
||||||
|
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// Find and remove the config with the specified UUID
|
// Find and remove the config with the specified UUID
|
||||||
for i, config := range m.Configs {
|
for i, config := range m.Configs {
|
||||||
if config.UUID == configUUID {
|
if config.UUID == configUUID {
|
||||||
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
|
|||||||
return errors.New("config not found")
|
return errors.New("config not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save all configs to ConfigStore folder
|
||||||
func (m *Manager) SaveConfigToDatabase() {
|
func (m *Manager) SaveConfigToDatabase() {
|
||||||
m.Options.Database.Write("tcprox", "rules", m.Configs)
|
for _, config := range m.Configs {
|
||||||
|
configBytes, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
m.logf("Failed to marshal stream proxy config", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = os.WriteFile(m.Options.ConfigStore+"/"+config.UUID+".config", configBytes, 0775)
|
||||||
|
if err != nil {
|
||||||
|
m.logf("Failed to save stream proxy config", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -217,9 +274,10 @@ func (c *ProxyRelayConfig) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if !c.UseTCP {
|
if !c.UseTCP {
|
||||||
c.Running = false
|
c.Running = false
|
||||||
|
c.udpStopChan = nil
|
||||||
c.parent.SaveConfigToDatabase()
|
c.parent.SaveConfigToDatabase()
|
||||||
}
|
}
|
||||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -231,8 +289,9 @@ func (c *ProxyRelayConfig) Start() error {
|
|||||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Running = false
|
c.Running = false
|
||||||
|
c.tcpStopChan = nil
|
||||||
c.parent.SaveConfigToDatabase()
|
c.parent.SaveConfigToDatabase()
|
||||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -253,27 +312,27 @@ func (c *ProxyRelayConfig) Restart() {
|
|||||||
if c.IsRunning() {
|
if c.IsRunning() {
|
||||||
c.Stop()
|
c.Stop()
|
||||||
}
|
}
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(3000 * time.Millisecond)
|
||||||
c.Start()
|
c.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop a running proxy if running
|
// Stop a running proxy if running
|
||||||
func (c *ProxyRelayConfig) Stop() {
|
func (c *ProxyRelayConfig) Stop() {
|
||||||
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name)
|
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
||||||
|
|
||||||
if c.udpStopChan != nil {
|
if c.udpStopChan != nil {
|
||||||
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
|
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||||
c.udpStopChan <- true
|
c.udpStopChan <- true
|
||||||
c.udpStopChan = nil
|
c.udpStopChan = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.tcpStopChan != nil {
|
if c.tcpStopChan != nil {
|
||||||
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name)
|
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
||||||
c.tcpStopChan <- true
|
c.tcpStopChan <- true
|
||||||
c.tcpStopChan = nil
|
c.tcpStopChan = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
|
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||||
c.Running = false
|
c.Running = false
|
||||||
|
|
||||||
//Update the running status
|
//Update the running status
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package v308
|
package v308
|
||||||
|
|
||||||
/*
|
/*
|
||||||
v307 type definations
|
v307 type definitions
|
||||||
|
|
||||||
This file wrap up the self-contained data structure
|
This file wrap up the self-contained data structure
|
||||||
for v3.0.7 structure and allow automatic updates
|
for v3.0.7 structure and allow automatic updates
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package v308
|
package v308
|
||||||
|
|
||||||
/*
|
/*
|
||||||
v308 type definations
|
v308 type definition
|
||||||
|
|
||||||
This file wrap up the self-contained data structure
|
This file wrap up the self-contained data structure
|
||||||
for v3.0.8 structure and allow automatic updates
|
for v3.0.8 structure and allow automatic updates
|
||||||
|
@ -43,9 +43,10 @@ func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
|
|||||||
targetDir := filepath.Join(fm.Directory, directory)
|
targetDir := filepath.Join(fm.Directory, directory)
|
||||||
|
|
||||||
// Clean path to prevent path escape #274
|
// Clean path to prevent path escape #274
|
||||||
targetDir = filepath.ToSlash(filepath.Clean(targetDir))
|
isValidRequest := validatePathEscape(targetDir, fm.Directory)
|
||||||
for strings.Contains(targetDir, "../") {
|
if !isValidRequest {
|
||||||
targetDir = strings.ReplaceAll(targetDir, "../", "")
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the target directory
|
// Open the target directory
|
||||||
@ -124,6 +125,14 @@ func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Specify the directory where you want to save the uploaded file
|
// Specify the directory where you want to save the uploaded file
|
||||||
uploadDir := filepath.Join(fm.Directory, dir)
|
uploadDir := filepath.Join(fm.Directory, dir)
|
||||||
|
|
||||||
|
// Clean path to prevent path escape #274
|
||||||
|
isValidRequest := validatePathEscape(uploadDir, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !utils.FileExists(uploadDir) {
|
if !utils.FileExists(uploadDir) {
|
||||||
utils.SendErrorResponse(w, "upload target directory not exists")
|
utils.SendErrorResponse(w, "upload target directory not exists")
|
||||||
return
|
return
|
||||||
@ -163,14 +172,20 @@ func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(fm.Directory, filename)
|
||||||
|
// Clean path to prevent path escape #274
|
||||||
|
isValidRequest := validatePathEscape(filePath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
previewMode, _ := utils.GetPara(r, "preview")
|
previewMode, _ := utils.GetPara(r, "preview")
|
||||||
if previewMode == "true" {
|
if previewMode == "true" {
|
||||||
// Serve the file using http.ServeFile
|
// Serve the file using http.ServeFile
|
||||||
filePath := filepath.Join(fm.Directory, filename)
|
|
||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filePath)
|
||||||
} else {
|
} else {
|
||||||
// Trigger a download with content disposition headers
|
// Trigger a download with content disposition headers
|
||||||
filePath := filepath.Join(fm.Directory, filename)
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filePath)
|
||||||
}
|
}
|
||||||
@ -191,6 +206,11 @@ func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Specify the directory where you want to create the new folder
|
// Specify the directory where you want to create the new folder
|
||||||
newFolderPath := filepath.Join(fm.Directory, dirName)
|
newFolderPath := filepath.Join(fm.Directory, dirName)
|
||||||
|
isValidRequest := validatePathEscape(newFolderPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the folder already exists
|
// Check if the folder already exists
|
||||||
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
|
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
|
||||||
@ -232,6 +252,18 @@ func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
|
|||||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||||
|
|
||||||
|
//Make sure the copy source and dest are within web directory folder
|
||||||
|
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the source path exists
|
// Check if the source path exists
|
||||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||||
utils.SendErrorResponse(w, "source path does not exist")
|
utils.SendErrorResponse(w, "source path does not exist")
|
||||||
@ -294,6 +326,18 @@ func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
|
|||||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||||
|
|
||||||
|
//Make sure move source and target are within web server directory
|
||||||
|
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the source path exists
|
// Check if the source path exists
|
||||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||||
utils.SendErrorResponse(w, "source path does not exist")
|
utils.SendErrorResponse(w, "source path does not exist")
|
||||||
@ -325,6 +369,11 @@ func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
// Construct the absolute path to the target file or directory
|
// Construct the absolute path to the target file or directory
|
||||||
absPath := filepath.Join(fm.Directory, filePath)
|
absPath := filepath.Join(fm.Directory, filePath)
|
||||||
|
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the target path exists
|
// Check if the target path exists
|
||||||
_, err = os.Stat(absPath)
|
_, err = os.Stat(absPath)
|
||||||
@ -392,6 +441,11 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Construct the absolute path to the target file or directory
|
// Construct the absolute path to the target file or directory
|
||||||
absPath := filepath.Join(fm.Directory, filePath)
|
absPath := filepath.Join(fm.Directory, filePath)
|
||||||
|
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
||||||
|
if !isValidRequest {
|
||||||
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the target path exists
|
// Check if the target path exists
|
||||||
_, err = os.Stat(absPath)
|
_, err = os.Stat(absPath)
|
||||||
@ -410,3 +464,25 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
|
|||||||
// Respond with a success message or appropriate response
|
// Respond with a success message or appropriate response
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return true if the path is within the root path
|
||||||
|
func validatePathEscape(reqestPath string, rootPath string) bool {
|
||||||
|
reqestPath = filepath.ToSlash(filepath.Clean(reqestPath))
|
||||||
|
rootPath = filepath.ToSlash(filepath.Clean(rootPath))
|
||||||
|
|
||||||
|
requestPathAbs, err := filepath.Abs(reqestPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPathAbs, err := filepath.Abs(rootPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(requestPathAbs, rootPathAbs) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
||||||
</p>
|
</p>
|
||||||
<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>
|
For online documentation, please refer to <a href="//zoraxy.aroz.org">zoraxy.aroz.org</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
|
||||||
Thank you for using Zoraxy!
|
Thank you for using Zoraxy!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
"imuslab.com/zoraxy/mod/uptime"
|
"imuslab.com/zoraxy/mod/uptime"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@ -26,18 +27,18 @@ func ReverseProxtInit() {
|
|||||||
/*
|
/*
|
||||||
Load Reverse Proxy Global Settings
|
Load Reverse Proxy Global Settings
|
||||||
*/
|
*/
|
||||||
inboundPort := 80
|
inboundPort := 443
|
||||||
if sysdb.KeyExists("settings", "inbound") {
|
if sysdb.KeyExists("settings", "inbound") {
|
||||||
sysdb.Read("settings", "inbound", &inboundPort)
|
sysdb.Read("settings", "inbound", &inboundPort)
|
||||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||||
} else {
|
} else {
|
||||||
SystemWideLogger.Println("Inbound port not set. Using default (80)")
|
SystemWideLogger.Println("Inbound port not set. Using default (443)")
|
||||||
}
|
}
|
||||||
|
|
||||||
useTls := false
|
useTls := true
|
||||||
sysdb.Read("settings", "usetls", &useTls)
|
sysdb.Read("settings", "usetls", &useTls)
|
||||||
if useTls {
|
if useTls {
|
||||||
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
|
SystemWideLogger.Println("TLS mode enabled. Serving proxy request with TLS")
|
||||||
} else {
|
} else {
|
||||||
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
|
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||||
}
|
}
|
||||||
@ -58,7 +59,7 @@ func ReverseProxtInit() {
|
|||||||
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
|
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
listenOnPort80 := false
|
listenOnPort80 := true
|
||||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||||
if listenOnPort80 {
|
if listenOnPort80 {
|
||||||
SystemWideLogger.Println("Port 80 listener enabled")
|
SystemWideLogger.Println("Port 80 listener enabled")
|
||||||
@ -66,7 +67,7 @@ func ReverseProxtInit() {
|
|||||||
SystemWideLogger.Println("Port 80 listener disabled")
|
SystemWideLogger.Println("Port 80 listener disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
forceHttpsRedirect := false
|
forceHttpsRedirect := true
|
||||||
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
||||||
if forceHttpsRedirect {
|
if forceHttpsRedirect {
|
||||||
SystemWideLogger.Println("Force HTTPS mode enabled")
|
SystemWideLogger.Println("Force HTTPS mode enabled")
|
||||||
@ -84,7 +85,7 @@ func ReverseProxtInit() {
|
|||||||
|
|
||||||
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
||||||
HostUUID: nodeUUID,
|
HostUUID: nodeUUID,
|
||||||
HostVersion: version,
|
HostVersion: SYSTEM_VERSION,
|
||||||
Port: inboundPort,
|
Port: inboundPort,
|
||||||
UseTls: useTls,
|
UseTls: useTls,
|
||||||
ForceTLSLatest: forceLatestTLSVersion,
|
ForceTLSLatest: forceLatestTLSVersion,
|
||||||
@ -98,6 +99,7 @@ func ReverseProxtInit() {
|
|||||||
WebDirectory: *staticWebServerRoot,
|
WebDirectory: *staticWebServerRoot,
|
||||||
AccessController: accessController,
|
AccessController: accessController,
|
||||||
LoadBalancer: loadBalancer,
|
LoadBalancer: loadBalancer,
|
||||||
|
SSOHandler: ssoHandler,
|
||||||
Logger: SystemWideLogger,
|
Logger: SystemWideLogger,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -331,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
//VDir
|
//VDir
|
||||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
//Custom headers
|
//Custom headers
|
||||||
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
|
||||||
//Auth
|
//Auth
|
||||||
RequireBasicAuth: requireBasicAuth,
|
RequireBasicAuth: requireBasicAuth,
|
||||||
BasicAuthCredentials: basicAuthCredentials,
|
BasicAuthCredentials: basicAuthCredentials,
|
||||||
@ -1083,6 +1085,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
|||||||
if dynamicProxyRouter.Running {
|
if dynamicProxyRouter.Running {
|
||||||
dynamicProxyRouter.StopProxyService()
|
dynamicProxyRouter.StopProxyService()
|
||||||
dynamicProxyRouter.Option.Port = newIncomingPortInt
|
dynamicProxyRouter.Option.Port = newIncomingPortInt
|
||||||
|
time.Sleep(1 * time.Second) //Fixed start fail issue
|
||||||
dynamicProxyRouter.StartProxyService()
|
dynamicProxyRouter.StartProxyService()
|
||||||
} else {
|
} else {
|
||||||
//Only change setting but not starting the proxy service
|
//Only change setting but not starting the proxy service
|
||||||
@ -1126,7 +1129,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
|||||||
//List all custom headers
|
//List all custom headers
|
||||||
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
||||||
if customHeaderList == nil {
|
if customHeaderList == nil {
|
||||||
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
|
customHeaderList = []*rewrite.UserDefinedHeader{}
|
||||||
}
|
}
|
||||||
js, _ := json.Marshal(customHeaderList)
|
js, _ := json.Marshal(customHeaderList)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
@ -1171,12 +1174,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Create a Custom Header Defination type
|
//Create a Custom Header Definition type
|
||||||
var rewriteDirection dynamicproxy.HeaderDirection
|
var rewriteDirection rewrite.HeaderDirection
|
||||||
if direction == "toOrigin" {
|
if direction == "toOrigin" {
|
||||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
|
rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
|
||||||
} else if direction == "toClient" {
|
} else if direction == "toClient" {
|
||||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
|
rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
|
||||||
} else {
|
} else {
|
||||||
//Unknown direction
|
//Unknown direction
|
||||||
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
||||||
@ -1187,7 +1190,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
if rewriteType == "remove" {
|
if rewriteType == "remove" {
|
||||||
isRemove = true
|
isRemove = true
|
||||||
}
|
}
|
||||||
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
|
|
||||||
|
headerRewriteDefinition := rewrite.UserDefinedHeader{
|
||||||
Key: name,
|
Key: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
Direction: rewriteDirection,
|
Direction: rewriteDirection,
|
||||||
@ -1195,7 +1199,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create a new custom header object
|
//Create a new custom header object
|
||||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination)
|
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefinition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
|
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
|
||||||
return
|
return
|
||||||
|
@ -27,7 +27,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
|||||||
Development Mode Override
|
Development Mode Override
|
||||||
=> Web root is located in /
|
=> Web root is located in /
|
||||||
*/
|
*/
|
||||||
if development && strings.HasPrefix(r.URL.Path, "/web/") {
|
if DEVELOPMENT_BUILD && strings.HasPrefix(r.URL.Path, "/web/") {
|
||||||
u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web"))
|
u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web"))
|
||||||
r.URL = u
|
r.URL = u
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
|||||||
Production Mode Override
|
Production Mode Override
|
||||||
=> Web root is located in /web
|
=> Web root is located in /web
|
||||||
*/
|
*/
|
||||||
if !development && r.URL.Path == "/" {
|
if !DEVELOPMENT_BUILD && r.URL.Path == "/" {
|
||||||
//Redirect to web UI
|
//Redirect to web UI
|
||||||
http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
@ -93,7 +93,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
|||||||
|
|
||||||
// Production path fix wrapper. Fix the path on production or development environment
|
// Production path fix wrapper. Fix the path on production or development environment
|
||||||
func ppf(relativeFilepath string) string {
|
func ppf(relativeFilepath string) string {
|
||||||
if !development {
|
if !DEVELOPMENT_BUILD {
|
||||||
return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/")
|
return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/")
|
||||||
}
|
}
|
||||||
return relativeFilepath
|
return relativeFilepath
|
||||||
@ -111,7 +111,7 @@ func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath s
|
|||||||
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
|
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
|
||||||
relativeFilepath = relativeFilepath + "index.html"
|
relativeFilepath = relativeFilepath + "index.html"
|
||||||
}
|
}
|
||||||
if development {
|
if DEVELOPMENT_BUILD {
|
||||||
//Load from disk
|
//Load from disk
|
||||||
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
|
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
|
||||||
content, err = os.ReadFile(targetFilePath)
|
content, err = os.ReadFile(targetFilePath)
|
||||||
|
78
src/start.go
78
src/start.go
@ -36,7 +36,10 @@ import (
|
|||||||
Startup Sequence
|
Startup Sequence
|
||||||
|
|
||||||
This function starts the startup sequence of all
|
This function starts the startup sequence of all
|
||||||
required modules
|
required modules. Their startup sequences are inter-dependent
|
||||||
|
and must be started in a specific order.
|
||||||
|
|
||||||
|
Don't touch this function unless you know what you are doing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -49,19 +52,19 @@ var (
|
|||||||
|
|
||||||
func startupSequence() {
|
func startupSequence() {
|
||||||
//Start a system wide logger and log viewer
|
//Start a system wide logger and log viewer
|
||||||
l, err := logger.NewLogger("zr", "./log")
|
l, err := logger.NewLogger(LOG_PREFIX, LOG_FOLDER)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
SystemWideLogger = l
|
SystemWideLogger = l
|
||||||
} else {
|
} else {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
|
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
|
||||||
RootFolder: "./log",
|
RootFolder: LOG_FOLDER,
|
||||||
Extension: ".log",
|
Extension: LOG_EXTENSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
//Create database
|
//Create database
|
||||||
db, err := database.NewDatabase("sys.db", false)
|
db, err := database.NewDatabase(DATABASE_PATH, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -70,21 +73,21 @@ func startupSequence() {
|
|||||||
sysdb.NewTable("settings")
|
sysdb.NewTable("settings")
|
||||||
|
|
||||||
//Create tmp folder and conf folder
|
//Create tmp folder and conf folder
|
||||||
os.MkdirAll("./tmp", 0775)
|
os.MkdirAll(TMP_FOLDER, 0775)
|
||||||
os.MkdirAll("./conf/proxy/", 0775)
|
os.MkdirAll(CONF_HTTP_PROXY, 0775)
|
||||||
|
|
||||||
//Create an auth agent
|
//Create an auth agent
|
||||||
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
|
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
|
authAgent = auth.NewAuthenticationAgent(SYSTEM_NAME, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
|
||||||
//Not logged in. Redirecting to login page
|
//Not logged in. Redirecting to login page
|
||||||
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
|
|
||||||
//Create a TLS certificate manager
|
//Create a TLS certificate manager
|
||||||
tlsCertManager, err = tlscert.NewManager("./conf/certs", development, SystemWideLogger)
|
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, DEVELOPMENT_BUILD, SystemWideLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -93,15 +96,16 @@ func startupSequence() {
|
|||||||
db.NewTable("redirect")
|
db.NewTable("redirect")
|
||||||
redirectAllowRegexp := false
|
redirectAllowRegexp := false
|
||||||
db.Read("redirect", "regex", &redirectAllowRegexp)
|
db.Read("redirect", "regex", &redirectAllowRegexp)
|
||||||
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp, SystemWideLogger)
|
redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, SystemWideLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Create a geodb store
|
//Create a geodb store
|
||||||
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
||||||
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
|
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
|
||||||
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
|
AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup,
|
||||||
|
SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -118,12 +122,28 @@ func startupSequence() {
|
|||||||
accessController, err = access.NewAccessController(&access.Options{
|
accessController, err = access.NewAccessController(&access.Options{
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
GeoDB: geodbStore,
|
GeoDB: geodbStore,
|
||||||
ConfigFolder: "./conf/access",
|
ConfigFolder: CONF_ACCESS_RULE,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
//Create an SSO handler
|
||||||
|
ssoHandler, err = sso.NewSSOHandler(&sso.SSOConfig{
|
||||||
|
SystemUUID: nodeUUID,
|
||||||
|
PortalServerPort: 5488,
|
||||||
|
AuthURL: "http://auth.localhost",
|
||||||
|
Database: sysdb,
|
||||||
|
Logger: SystemWideLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
//Restore the SSO handler to previous state before shutdown
|
||||||
|
ssoHandler.RestorePreviousRunningState()
|
||||||
|
*/
|
||||||
|
|
||||||
//Create a statistic collector
|
//Create a statistic collector
|
||||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
@ -135,7 +155,7 @@ func startupSequence() {
|
|||||||
//Start the static web server
|
//Start the static web server
|
||||||
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
|
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
|
||||||
Sysdb: sysdb,
|
Sysdb: sysdb,
|
||||||
Port: "5487", //Default Port
|
Port: strconv.Itoa(WEBSERV_DEFAULT_PORT), //Default Port
|
||||||
WebRoot: *staticWebServerRoot,
|
WebRoot: *staticWebServerRoot,
|
||||||
EnableDirectoryListing: true,
|
EnableDirectoryListing: true,
|
||||||
EnableWebDirManager: *allowWebFileManager,
|
EnableWebDirManager: *allowWebFileManager,
|
||||||
@ -160,7 +180,7 @@ func startupSequence() {
|
|||||||
|
|
||||||
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
|
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
ConfigFolder: "./conf/rules/pathrules",
|
ConfigFolder: CONF_PATH_RULE,
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -178,7 +198,7 @@ func startupSequence() {
|
|||||||
|
|
||||||
hostName := *mdnsName
|
hostName := *mdnsName
|
||||||
if hostName == "" {
|
if hostName == "" {
|
||||||
hostName = "zoraxy_" + nodeUUID
|
hostName = MDNS_HOSTNAME_PREFIX + nodeUUID
|
||||||
} else {
|
} else {
|
||||||
//Trim off the suffix
|
//Trim off the suffix
|
||||||
hostName = strings.TrimSuffix(hostName, ".local")
|
hostName = strings.TrimSuffix(hostName, ".local")
|
||||||
@ -187,24 +207,24 @@ func startupSequence() {
|
|||||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||||
HostName: hostName,
|
HostName: hostName,
|
||||||
Port: portInt,
|
Port: portInt,
|
||||||
Domain: "zoraxy.arozos.com",
|
Domain: MDNS_IDENTIFY_DOMAIN,
|
||||||
Model: "Network Gateway",
|
Model: MDNS_IDENTIFY_DEVICE_TYPE,
|
||||||
UUID: nodeUUID,
|
UUID: nodeUUID,
|
||||||
Vendor: "imuslab.com",
|
Vendor: MDNS_IDENTIFY_VENDOR,
|
||||||
BuildVersion: version,
|
BuildVersion: SYSTEM_VERSION,
|
||||||
}, "")
|
}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
|
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
|
||||||
} else {
|
} else {
|
||||||
//Start initial scanning
|
//Start initial scanning
|
||||||
go func() {
|
go func() {
|
||||||
hosts := mdnsScanner.Scan(30, "")
|
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
|
||||||
previousmdnsScanResults = hosts
|
previousmdnsScanResults = hosts
|
||||||
SystemWideLogger.Println("mDNS Startup scan completed")
|
SystemWideLogger.Println("mDNS Startup scan completed")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//Create a ticker to update mDNS results every 5 minutes
|
//Create a ticker to update mDNS results every 5 minutes
|
||||||
ticker := time.NewTicker(15 * time.Minute)
|
ticker := time.NewTicker(MDNS_SCAN_UPDATE_INTERVAL * time.Minute)
|
||||||
stopChan := make(chan bool)
|
stopChan := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@ -212,7 +232,7 @@ func startupSequence() {
|
|||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
hosts := mdnsScanner.Scan(30, "")
|
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
|
||||||
previousmdnsScanResults = hosts
|
previousmdnsScanResults = hosts
|
||||||
SystemWideLogger.Println("mDNS scan result updated")
|
SystemWideLogger.Println("mDNS scan result updated")
|
||||||
}
|
}
|
||||||
@ -244,10 +264,14 @@ func startupSequence() {
|
|||||||
webSshManager = sshprox.NewSSHProxyManager()
|
webSshManager = sshprox.NewSSHProxyManager()
|
||||||
|
|
||||||
//Create TCP Proxy Manager
|
//Create TCP Proxy Manager
|
||||||
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
|
streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||||
Database: sysdb,
|
|
||||||
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||||
|
ConfigStore: CONF_STREAM_PROXY,
|
||||||
|
Logger: SystemWideLogger,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
//Create WoL MAC storage table
|
//Create WoL MAC storage table
|
||||||
sysdb.NewTable("wolmac")
|
sysdb.NewTable("wolmac")
|
||||||
@ -280,8 +304,8 @@ func startupSequence() {
|
|||||||
sysdb.NewTable("acmepref")
|
sysdb.NewTable("acmepref")
|
||||||
acmeHandler = initACME()
|
acmeHandler = initACME()
|
||||||
acmeAutoRenewer, err = acme.NewAutoRenewer(
|
acmeAutoRenewer, err = acme.NewAutoRenewer(
|
||||||
"./conf/acme_conf.json",
|
ACME_AUTORENEW_CONFIG_PATH,
|
||||||
"./conf/certs/",
|
CONF_CERT_STORE,
|
||||||
int64(*acmeAutoRenewInterval),
|
int64(*acmeAutoRenewInterval),
|
||||||
*acmeCertAutoRenewDays,
|
*acmeCertAutoRenewDays,
|
||||||
acmeHandler,
|
acmeHandler,
|
||||||
|
@ -841,6 +841,25 @@
|
|||||||
function initBannedCountryList(){
|
function initBannedCountryList(){
|
||||||
$.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
$.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
||||||
let bannedListHtml = '';
|
let bannedListHtml = '';
|
||||||
|
|
||||||
|
//Check if the country code list contains all eu countries. If yes, replace it with "EU"
|
||||||
|
let allEu = true;
|
||||||
|
let euCountries = getEUCCs();
|
||||||
|
for (var i = 0; i < euCountries.length; i++){
|
||||||
|
if (!data.includes(euCountries[i])){
|
||||||
|
allEu = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allEu){
|
||||||
|
//Remove EU countries from the list and replace it with EU
|
||||||
|
data = data.filter(function(value, index, arr){
|
||||||
|
return !euCountries.includes(value);
|
||||||
|
});
|
||||||
|
data.push("eu");
|
||||||
|
}
|
||||||
|
|
||||||
data.forEach((countryCode) => {
|
data.forEach((countryCode) => {
|
||||||
bannedListHtml += `
|
bannedListHtml += `
|
||||||
<tr>
|
<tr>
|
||||||
@ -919,18 +938,48 @@
|
|||||||
//Whitelist country table
|
//Whitelist country table
|
||||||
function initWhitelistCountryList(){
|
function initWhitelistCountryList(){
|
||||||
$.get("/api/whitelist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
$.get("/api/whitelist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
||||||
let bannedListHtml = '';
|
let whiteListHTML = '';
|
||||||
|
|
||||||
|
//Check if the country code list contains all eu countries. If yes, replace it with "EU"
|
||||||
|
let allEu = true;
|
||||||
|
let euCountries = getEUCCs();
|
||||||
|
let countryCodesIndata = data.map(function(item){
|
||||||
|
//data[n].CC is the country code
|
||||||
|
return item.CC;
|
||||||
|
});
|
||||||
|
for (var i = 0; i < euCountries.length; i++){
|
||||||
|
if (!countryCodesIndata.includes(euCountries[i])){
|
||||||
|
allEu = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allEu){
|
||||||
|
//Remove EU countries from the list and replace it with EU
|
||||||
|
data = data.filter(function(value, index, arr){
|
||||||
|
return !euCountries.includes(value.CC);
|
||||||
|
});
|
||||||
|
data.push({
|
||||||
|
CC: "eu"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
data.forEach((countryWhitelistEntry) => {
|
data.forEach((countryWhitelistEntry) => {
|
||||||
let countryCode = countryWhitelistEntry.CC;
|
let countryCode = countryWhitelistEntry.CC;
|
||||||
bannedListHtml += `
|
whiteListHTML += `
|
||||||
<tr>
|
<tr>
|
||||||
<td><i class="${countryCode} flag"></i> ${getCountryName(countryCode)} (${countryCode.toUpperCase()})</td>
|
<td><i class="${countryCode} flag"></i> ${getCountryName(countryCode)} (${countryCode.toUpperCase()})</td>
|
||||||
<td><button class="ui red basic mini icon button" onclick="removeFromWhiteList('${countryCode}')"><i class="trash icon"></i></button></td>
|
<td><button class="ui red basic mini icon button" onclick="removeFromWhiteList('${countryCode}')"><i class="trash icon"></i></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
$('#whitelistCountryList').html(bannedListHtml);
|
$('#whitelistCountryList').html(whiteListHTML);
|
||||||
filterCountries(data, "#countrySelectorWhitelist .menu .item");
|
|
||||||
|
//Map the data.CC to the country code
|
||||||
|
let countryCodes = data.map(function(item){
|
||||||
|
return item.CC;
|
||||||
|
});
|
||||||
|
filterCountries(countryCodes, "#countrySelectorWhitelist .menu .item");
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
$('#whitelistCountryList').append(`
|
$('#whitelistCountryList').append(`
|
||||||
<tr>
|
<tr>
|
||||||
@ -1016,6 +1065,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEUCCs(){
|
||||||
|
return ["at","be","bg","cy","cz","de","dk","ee","es","fi","fr","gr","hr","hu","ie","it","lt","lu","lv","mt","nl","pl","pt","se","si","sk"];
|
||||||
|
}
|
||||||
|
|
||||||
function addCountryToBlacklist() {
|
function addCountryToBlacklist() {
|
||||||
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
|
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
|
||||||
let ccs = [countryCode];
|
let ccs = [countryCode];
|
||||||
@ -1025,48 +1078,50 @@
|
|||||||
ccs = countryCode.split(",");
|
ccs = countryCode.split(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 0;
|
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
|
||||||
for(var i = 0; i < ccs.length; i++){
|
if (ccs.includes("eu")){
|
||||||
let thisCountryCode = ccs[i];
|
ccs = ccs.concat(getEUCCs());
|
||||||
$.cjax({
|
ccs = ccs.filter(function(item){
|
||||||
type: "POST",
|
return item != "eu";
|
||||||
url: "/api/blacklist/country/add",
|
|
||||||
method: "POST",
|
|
||||||
data: { cc: thisCountryCode, id: currentEditingAccessRule},
|
|
||||||
success: function(response) {
|
|
||||||
if (response.error != undefined){
|
|
||||||
msgbox(response.error, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter == (ccs.length - 1)){
|
|
||||||
//Last item
|
|
||||||
setTimeout(function(){
|
|
||||||
initBannedCountryList();
|
|
||||||
if (ccs.length == 1){
|
|
||||||
//Single country
|
|
||||||
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
|
|
||||||
}else{
|
|
||||||
msgbox(ccs.length + " countries added to blacklist");
|
|
||||||
}
|
|
||||||
|
|
||||||
}, (ccs.length==1)?0:100);
|
|
||||||
}
|
|
||||||
counter++;
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
// handle error response
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let counter = ccs.length;
|
||||||
|
$.cjax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/api/blacklist/country/add",
|
||||||
|
method: "POST",
|
||||||
|
data: { cc: ccs.join(","), id: currentEditingAccessRule},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.error != undefined){
|
||||||
|
msgbox(response.error, false);
|
||||||
|
}
|
||||||
|
initBannedCountryList();
|
||||||
|
if (ccs.length == 1){
|
||||||
|
//Single country
|
||||||
|
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
|
||||||
|
}else{
|
||||||
|
msgbox(ccs.length + " countries added to blacklist");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
// handle error response
|
||||||
|
}
|
||||||
|
});
|
||||||
$('#countrySelector').dropdown('clear');
|
$('#countrySelector').dropdown('clear');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromBannedList(countryCode){
|
function removeFromBannedList(countryCode){
|
||||||
countryCode = countryCode.toLowerCase();
|
|
||||||
let countryName = getCountryName(countryCode);
|
let countryName = getCountryName(countryCode);
|
||||||
|
if (countryCode == "eu"){
|
||||||
|
let euCountries = getEUCCs();
|
||||||
|
countryCode = euCountries.join(",");
|
||||||
|
countryName = "European Union";
|
||||||
|
}else{
|
||||||
|
countryCode = countryCode.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/blacklist/country/remove",
|
url: "/api/blacklist/country/remove",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1162,44 +1217,53 @@
|
|||||||
//Usually just a few countries a for loop will get the job done
|
//Usually just a few countries a for loop will get the job done
|
||||||
ccs = countryCode.split(",");
|
ccs = countryCode.split(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
for(var i = 0; i < ccs.length; i++){
|
|
||||||
let thisCountryCode = ccs[i];
|
|
||||||
$.cjax({
|
|
||||||
type: "POST",
|
|
||||||
url: "/api/whitelist/country/add",
|
|
||||||
data: { cc: thisCountryCode , id: currentEditingAccessRule},
|
|
||||||
success: function(response) {
|
|
||||||
if (response.error != undefined){
|
|
||||||
msgbox(response.error, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter == (ccs.length - 1)){
|
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
|
||||||
setTimeout(function(){
|
if (ccs.includes("eu")){
|
||||||
initWhitelistCountryList();
|
ccs = ccs.filter(function(item){
|
||||||
if (ccs.length == 1){
|
return item != "eu";
|
||||||
//Single country
|
|
||||||
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
|
|
||||||
}else{
|
|
||||||
msgbox(ccs.length + " countries added to whitelist");
|
|
||||||
}
|
|
||||||
}, (ccs.length==1)?0:100);
|
|
||||||
}
|
|
||||||
counter++;
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
// handle error response
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
ccs = ccs.concat(getEUCCs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let counter = ccs.length;
|
||||||
|
$.cjax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/api/whitelist/country/add",
|
||||||
|
data: { cc: ccs.join(",") , id: currentEditingAccessRule},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.error != undefined){
|
||||||
|
msgbox(response.error, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initWhitelistCountryList();
|
||||||
|
if (ccs.length == 1){
|
||||||
|
//Single country
|
||||||
|
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
|
||||||
|
}else{
|
||||||
|
msgbox(ccs.length + " countries added to whitelist");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
// handle error response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$('#countrySelectorWhitelist').dropdown('clear');
|
$('#countrySelectorWhitelist').dropdown('clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromWhiteList(countryCode){
|
//Remove from whitelist, accepts a country code or "eu" for all EU countries
|
||||||
if (confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
|
function removeFromWhiteList(countryCode, skipConfirm = true){
|
||||||
|
let countryName = getCountryName(countryCode);
|
||||||
|
if (countryCode == "eu"){
|
||||||
|
let euCountries = getEUCCs();
|
||||||
|
countryCode = euCountries.join(",");
|
||||||
|
countryName = "European Union";
|
||||||
|
}else{
|
||||||
countryCode = countryCode.toLowerCase();
|
countryCode = countryCode.toLowerCase();
|
||||||
|
}
|
||||||
|
if (skipConfirm || confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/whitelist/country/remove",
|
url: "/api/whitelist/country/remove",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1208,6 +1272,7 @@
|
|||||||
if (response.error != undefined){
|
if (response.error != undefined){
|
||||||
msgbox(response.error, false);
|
msgbox(response.error, false);
|
||||||
}
|
}
|
||||||
|
msgbox(countryName + " removed from whitelist");
|
||||||
initWhitelistCountryList();
|
initWhitelistCountryList();
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
@ -1276,19 +1341,27 @@
|
|||||||
/*
|
/*
|
||||||
Common Utilities
|
Common Utilities
|
||||||
*/
|
*/
|
||||||
function filterCountries(codesToShow, selector="#countrySelector .menu .item") {
|
function filterCountries(alreadySelectedCCs, selector="#countrySelector .menu .item") {
|
||||||
// get all items in the dropdown
|
// get all items in the dropdown
|
||||||
const items = document.querySelectorAll(selector);
|
const items = document.querySelectorAll(selector);
|
||||||
|
const euCountries = getEUCCs();
|
||||||
|
//Replce "eu" in alreadySelectedCCs with all EU countries
|
||||||
|
if (alreadySelectedCCs.includes("eu")){
|
||||||
|
alreadySelectedCCs = alreadySelectedCCs.filter(function(item){
|
||||||
|
return item != "eu";
|
||||||
|
});
|
||||||
|
alreadySelectedCCs = alreadySelectedCCs.concat(euCountries);
|
||||||
|
}
|
||||||
|
|
||||||
// loop through all items
|
// loop through all items
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
// get the value of the item (i.e. the country code)
|
// get the value of the item (i.e. the country code)
|
||||||
const code = item.dataset.value;
|
const code = item.dataset.value;
|
||||||
// if the code is in the array of codes to show, show the item
|
if (alreadySelectedCCs.includes(code)) {
|
||||||
if (codesToShow.includes(code)) {
|
//This country code already selected. Hide it
|
||||||
item.style.display = 'none';
|
item.style.display = 'none';
|
||||||
}
|
} else {
|
||||||
// otherwise, hide the item
|
// otherwise, show the item
|
||||||
else {
|
|
||||||
item.style.display = 'block';
|
item.style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<p>Current list of loaded certificates</p>
|
<p>Current list of loaded certificates</p>
|
||||||
<div tourstep="certTable">
|
<div tourstep="certTable">
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||||
<table class="ui sortable unstackable basic celled table">
|
<table class="ui unstackable basic celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Domain</th>
|
<tr><th>Domain</th>
|
||||||
<th>Last Update</th>
|
<th>Last Update</th>
|
||||||
@ -101,7 +101,7 @@
|
|||||||
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
|
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
|
||||||
<div class="ui buttons">
|
<div class="ui buttons">
|
||||||
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
||||||
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
|
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
@ -161,7 +161,9 @@
|
|||||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||||
|
|
||||||
//Request ACME for certificate
|
//Request ACME for certificate
|
||||||
|
let buttonOriginalHTML = "";
|
||||||
if (btn != undefined){
|
if (btn != undefined){
|
||||||
|
buttonOriginalHTML = $(btn).html();
|
||||||
$(btn).addClass('disabled');
|
$(btn).addClass('disabled');
|
||||||
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
||||||
}
|
}
|
||||||
@ -169,11 +171,26 @@
|
|||||||
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
||||||
if (btn != undefined){
|
if (btn != undefined){
|
||||||
$(btn).removeClass('disabled');
|
$(btn).removeClass('disabled');
|
||||||
if (succ){
|
if ($(btn).hasClass("icon")){
|
||||||
$(btn).html(`<i class="ui green check icon"></i>`);
|
//Only change the button icon
|
||||||
|
if (succ){
|
||||||
|
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||||
|
}else{
|
||||||
|
$(btn).html(`<i class="ui red times icon"></i>`);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
$(btn).html(`<i class="ui red times icon"></i>`);
|
//Show error or success icon with text
|
||||||
|
if (succ){
|
||||||
|
$(btn).html(`<i class="ui green check icon"></i> Requested`);
|
||||||
|
}else{
|
||||||
|
$(btn).html(`<i class="ui red times icon"></i> Error`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Restore the button after 3 seconds
|
||||||
|
setTimeout(function(){
|
||||||
|
$(btn).html(buttonOriginalHTML);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
initManagedDomainCertificateList();
|
initManagedDomainCertificateList();
|
||||||
|
@ -350,15 +350,27 @@
|
|||||||
let originalContent = $(column).html();
|
let originalContent = $(column).html();
|
||||||
|
|
||||||
//Check if this host is covered within one of the certificates. If not, show the icon
|
//Check if this host is covered within one of the certificates. If not, show the icon
|
||||||
let domainIsCovered = true;
|
let enableQuickRequestButton = true;
|
||||||
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||||
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||||
let thisAliasName = payload.MatchingDomainAlias[i];
|
let thisAliasName = payload.MatchingDomainAlias[i];
|
||||||
domains.push(thisAliasName);
|
domains.push(thisAliasName);
|
||||||
}
|
}
|
||||||
if (true){
|
|
||||||
domainIsCovered = false;
|
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||||
|
if (payload.RootOrMatchingDomain.indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.MatchingDomainAlias != undefined){
|
||||||
|
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||||
|
if (payload.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//encode the domain to DOM
|
//encode the domain to DOM
|
||||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||||
|
|
||||||
@ -371,9 +383,8 @@
|
|||||||
</div><br>
|
</div><br>
|
||||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
|
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
|
||||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
|
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
|
||||||
<button class="ui basic compact tiny ${domainIsCovered?"disabled":""} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}');"><i class="green lock icon"></i> Get Certificate</button>
|
<button class="ui basic compact tiny ${enableQuickRequestButton?"":"disabled"} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}', this);"><i class="green lock icon"></i> Get Certificate</button>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
||||||
$(".hostAccessRuleSelector").dropdown();
|
$(".hostAccessRuleSelector").dropdown();
|
||||||
}else{
|
}else{
|
||||||
@ -536,9 +547,29 @@
|
|||||||
Certificate Shortcut
|
Certificate Shortcut
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
|
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
||||||
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
|
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
|
||||||
alert(RootAndAliasDomains.join(", "))
|
let renewDomainKey = RootAndAliasDomains.join(",");
|
||||||
|
let preferedACMEEmail = $("#prefACMEEmail").val();
|
||||||
|
if (preferedACMEEmail == ""){
|
||||||
|
msgbox("Preferred email for ACME registration not set", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||||
|
if (defaultCA == ""){
|
||||||
|
defaultCA = "Let's Encrypt";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the root or the alias domain contain wildcard character, if yes, return error
|
||||||
|
for (var i = 0; i < RootAndAliasDomains.length; i++){
|
||||||
|
if (RootAndAliasDomains[i].indexOf("*") != -1){
|
||||||
|
msgbox("Wildcard domain can only be setup via ACME tool", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Renew the certificate
|
||||||
|
renewCertificate(renewDomainKey, false, btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Bind on tab switch events
|
//Bind on tab switch events
|
||||||
|
@ -17,10 +17,21 @@
|
|||||||
<p>Discover mDNS enabled service in this gateway forwarded network</p>
|
<p>Discover mDNS enabled service in this gateway forwarded network</p>
|
||||||
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
|
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<!-- IP Scanner-->
|
<div class="ui stackable grid">
|
||||||
<h2>IP Scanner</h2>
|
<div class="eight wide column">
|
||||||
<p>Discover local area network devices by pinging them one by one</p>
|
<!-- IP Scanner-->
|
||||||
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
|
<h2>IP Scanner</h2>
|
||||||
|
<p>Discover local area network devices by pinging them one by one</p>
|
||||||
|
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
|
||||||
|
</div>
|
||||||
|
<div class="eight wide column">
|
||||||
|
<!-- Port Scanner-->
|
||||||
|
<h2>Port Scanner</h2>
|
||||||
|
<p>Scan for open ports on a given IP address</p>
|
||||||
|
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/portscan.html',1000, 640);">Start Scanner</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<!-- Traceroute-->
|
<!-- Traceroute-->
|
||||||
<h2>Traceroute / Ping</h2>
|
<h2>Traceroute / Ping</h2>
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
||||||
</div>
|
</div>
|
||||||
<!-- Options -->
|
<!-- Options -->
|
||||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@ -173,7 +173,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (data.length == 0){
|
if (data.length == 0){
|
||||||
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
|
$("#redirectionRuleList").append(`<tr><td colspan="5"><i class="green check circle icon"></i> No redirection rule</td></tr>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Advance configs -->
|
<!-- Advance configs -->
|
||||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div id="advanceProxyRules" class="ui fluid accordion">
|
<div id="advanceProxyRules" class="ui fluid accordion">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@ -165,18 +165,18 @@
|
|||||||
<div class="ui basic segment rulesInstructions">
|
<div class="ui basic segment rulesInstructions">
|
||||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||||
Example of domain matching keyword:<br>
|
Example of domain matching keyword:<br>
|
||||||
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
|
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
||||||
Example of subdomain matching keyword:<br>
|
Example of subdomain matching keyword:<br>
|
||||||
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
|
<code>s1.aroz.org</code> <br>Any request starting with s1.aroz.org will be proxy to the IP address below<br>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
||||||
Example of wildcard matching keyword:<br>
|
Example of wildcard matching keyword:<br>
|
||||||
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br>
|
<code>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
|
||||||
<div class="ui list">
|
<div class="ui list">
|
||||||
<div class="item"><code>www.arozos.com</code></div>
|
<div class="item"><code>www.aroz.org</code></div>
|
||||||
<div class="item"><code>foo.bar.arozos.com</code></div>
|
<div class="item"><code>foo.bar.aroz.org</code></div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
@ -295,15 +295,25 @@
|
|||||||
//Automatic check if the site require TLS and check the checkbox if needed
|
//Automatic check if the site require TLS and check the checkbox if needed
|
||||||
function autoCheckTls(targetDomain){
|
function autoCheckTls(targetDomain){
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/tlscheck",
|
url: "/api/proxy/tlscheck?selfsignchk=true",
|
||||||
data: {url: targetDomain},
|
data: {url: targetDomain},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
msgbox(data.error, false);
|
msgbox(data.error, false);
|
||||||
}else if (data == "https"){
|
}else{
|
||||||
$("#reqTls").parent().checkbox("set checked");
|
//Check if the site require TLS
|
||||||
}else if (data == "http"){
|
if (data.protocol == "https"){
|
||||||
$("#reqTls").parent().checkbox("set unchecked");
|
$("#reqTls").parent().checkbox("set checked");
|
||||||
|
}else if (data.protocol == "http"){
|
||||||
|
$("#reqTls").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
//Check if the site is using self-signed cert
|
||||||
|
if (data.selfsign){
|
||||||
|
$("#skipTLSValidation").parent().checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$("#skipTLSValidation").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,381 @@
|
|||||||
<div class="standardContainer">
|
<div class="standardContainer">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h2>Single-Sign-On</h2>
|
<div class="ui message">
|
||||||
<p>Create and manage accounts with Zoraxy!</p>
|
<div class="header">
|
||||||
|
Work in Progress
|
||||||
|
</div>
|
||||||
|
<p>The SSO feature is currently under development.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui message">
|
</div>
|
||||||
<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 class="standardContainer">
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h2>Zoraxy SSO / Oauth</h2>
|
||||||
|
<p>A centralized authentication system for all your subdomains</p>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui basic segment enabled ssoRunningState">
|
||||||
|
<h4 class="ui header" id="ssoRunningState">
|
||||||
|
<i class="circle check icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<span class="webserv_status">Running</span>
|
||||||
|
<div class="sub header">Listen port :<span class="oauthserv_port">8081</span></div>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="ui form">
|
||||||
|
<h3 class="ui dividing header">Oauth2 Server Settings</h3>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<input type="checkbox" name="enableOauth2">
|
||||||
|
<label>Enable Oauth2 Server<br>
|
||||||
|
<small>Oauth2 server for handling external authentication requests</small></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Oauth2 Server Port</label>
|
||||||
|
<div class="ui action input">
|
||||||
|
<input type="number" name="oauth2Port" placeholder="Port" value="5488">
|
||||||
|
<button id="saveOauthServerPortBtn" class="ui basic green button"><i class="ui green circle check icon"></i> Update</button>
|
||||||
|
</div>
|
||||||
|
<small>Listening port of the Zoraxy internal Oauth2 Server.You can create a subdomain proxy rule to <code>127.0.0.1:<span class="ssoPort">5488</span></code></small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Auth URL</label>
|
||||||
|
<div class="ui action input">
|
||||||
|
<input type="text" name="authURL" placeholder="https://auth.yourdomain.com">
|
||||||
|
<button id="saveAuthURLBtn" class="ui basic blue button"><i class="ui blue save icon"></i> Save</button>
|
||||||
|
</div>
|
||||||
|
<small>The exposed authentication URL of the Oauth2 server, usually <code>https://auth.example.com</code> or <code>https://sso.yourdomain.com</code>. <b>Remember to include the http:// or https:// in your URL.</b></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="ui form">
|
||||||
|
<h3 class="ui dividing header">Zoraxy SSO Settings</h3>
|
||||||
|
<div class="field">
|
||||||
|
<label>Default Redirection URL </label>
|
||||||
|
<div class="ui fluid input">
|
||||||
|
<input type="text" name="defaultSiteURL" placeholder="https://yourdomain.com">
|
||||||
|
</div>
|
||||||
|
<small>The default URL to redirect to after login if redirection target is not set</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="ui basic button"> <i class="ui green check icon"></i> Apply Changes </button>
|
||||||
|
</div>
|
||||||
|
<div class="ui basic message">
|
||||||
|
<div class="header">
|
||||||
|
<i class="ui yellow exclamation triangle icon"></i> Important Notes about Zoraxy SSO
|
||||||
|
</div>
|
||||||
|
<p>Zoraxy SSO, if enabled in HTTP Proxy rule, will automatically intercept the proxy request and provide an SSO interface on upstreams that do not support OAuth natively.
|
||||||
|
It is basically like basic auth with a login page. <b> The same user credential can be used in OAuth sign-in and Zoraxy SSO sign-in.</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="ui blue user circle icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
Registered Users
|
||||||
|
<div class="sub header">A list of users that are registered with the SSO server</div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Registered On</th>
|
||||||
|
<th>Reset Password</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="registeredSsoUsers">
|
||||||
|
<tr>
|
||||||
|
<td>admin</td>
|
||||||
|
<td>2020-01-01</td>
|
||||||
|
<td><button class="ui blue basic small icon button"><i class="ui blue key icon"></i></button></td>
|
||||||
|
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button onclick="handleUserListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
|
||||||
|
<button onclick="openRegisteredUserManager();" class="ui basic button"><i class="ui blue users icon"></i> Manage Registered Users</button>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="ui green th icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
Registered Apps
|
||||||
|
<div class="sub header">A list of apps that are registered with the SSO server</div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>App Name</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>App ID</th>
|
||||||
|
<th>Registered On</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="registeredSsoApps">
|
||||||
|
<tr>
|
||||||
|
<td>My App</td>
|
||||||
|
<td><a href="//example.com" target="_blank">example.com</a></td>
|
||||||
|
<td>123456</td>
|
||||||
|
<td>2020-01-01</td>
|
||||||
|
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button onclick="handleRegisterAppListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
|
||||||
|
<button onclick="openRegisterAppManagementSnippet();" class="ui basic button"><i style="font-size: 1em; margin-top: -0.2em;" class="ui green th large icon"></i> Manage Registered App</button>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("input[name=oauth2Port]").on("change", function() {
|
||||||
|
$(".ssoPort").text($(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSSOStatus(){
|
||||||
|
$.get("/api/sso/status", function(data){
|
||||||
|
if(data.error != undefined){
|
||||||
|
//Show error message
|
||||||
|
$(".ssoRunningState").removeClass("enabled").addClass("disabled");
|
||||||
|
$("#ssoRunningState .webserv_status").html('Error: '+data.error);
|
||||||
|
}else{
|
||||||
|
if (data.Enabled){
|
||||||
|
$(".ssoRunningState").addClass("enabled");
|
||||||
|
$("#ssoRunningState .webserv_status").html('Running');
|
||||||
|
$(".ssoRunningState i").attr("class", "circle check icon");
|
||||||
|
$("input[name=enableOauth2]").parent().checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$(".ssoRunningState").removeClass("enabled");
|
||||||
|
$("#ssoRunningState .webserv_status").html('Stopped');
|
||||||
|
$(".ssoRunningState i").attr("class", "circle times icon");
|
||||||
|
$("input[name=enableOauth2]").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
$("input[name=oauth2Port]").val(data.ListeningPort);
|
||||||
|
$(".oauthserv_port").text(data.ListeningPort);
|
||||||
|
$("input[name=authURL]").val(data.AuthURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initSSOStatus(){
|
||||||
|
$.get("/api/sso/status", function(data){
|
||||||
|
//Update the SSO status from the server
|
||||||
|
updateSSOStatus();
|
||||||
|
|
||||||
|
//Bind events to the enable checkbox
|
||||||
|
$("input[name=enableOauth2]").off("change").on("change", function(){
|
||||||
|
var checked = $(this).prop("checked");
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/sso/enable",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
enable: checked
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if(data.error != undefined){
|
||||||
|
msgbox("Failed to toggle SSO: " + data.error, false);
|
||||||
|
//Unbind the event to prevent infinite loop
|
||||||
|
$("input[name=enableOauth2]").off("change");
|
||||||
|
}else{
|
||||||
|
initSSOStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initSSOStatus();
|
||||||
|
|
||||||
|
/* Save the Oauth server port */
|
||||||
|
function saveOauthServerPort(){
|
||||||
|
var port = $("input[name=oauth2Port]").val();
|
||||||
|
//Check if the port is valid
|
||||||
|
if (port < 1 || port > 65535){
|
||||||
|
msgbox("Invalid port number", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Use cjax to send the port to the server with csrf token
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/sso/setPort",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
port: port
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox("Failed to update Oauth server port: " + data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox("Oauth server port updated", true);
|
||||||
|
|
||||||
|
}
|
||||||
|
updateSSOStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Bind the save button to the saveOauthServerPort function
|
||||||
|
$("#saveOauthServerPortBtn").on("click", function() {
|
||||||
|
saveOauthServerPort();
|
||||||
|
});
|
||||||
|
$("input[name=oauth2Port]").on("keypress", function(e) {
|
||||||
|
if (e.which == 13) {
|
||||||
|
saveOauthServerPort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Save the Oauth server URL (aka AuthURL) */
|
||||||
|
function saveAuthURL(){
|
||||||
|
var url = $("input[name=authURL]").val();
|
||||||
|
//Make sure the url contains http:// or https://
|
||||||
|
if (!url.startsWith("http://") && !url.startsWith("https://")){
|
||||||
|
msgbox("Invalid URL. Make sure to include http:// or https://", false);
|
||||||
|
$("input[name=authURL]").parent().parent().addClass("error");
|
||||||
|
return;
|
||||||
|
}else{
|
||||||
|
$("input[name=authURL]").parent().parent().removeClass("error");
|
||||||
|
}
|
||||||
|
//Use cjax to send the port to the server with csrf token
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/sso/setAuthURL",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"auth_url": url
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox("Failed to update Oauth server port: " + data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox("Oauth server port updated", true);
|
||||||
|
|
||||||
|
}
|
||||||
|
updateSSOStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bind the save button to the saveAuthURL function
|
||||||
|
$("#saveAuthURLBtn").on("click", function() {
|
||||||
|
saveAuthURL();
|
||||||
|
});
|
||||||
|
$("input[name=authURL]").on("keypress", function(e) {
|
||||||
|
if (e.which == 13) {
|
||||||
|
saveAuthURL();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Registered Apps Event Handlers */
|
||||||
|
|
||||||
|
//Function to initialize the registered app table
|
||||||
|
function initRegisteredAppTable(){
|
||||||
|
$.get("/api/sso/app/list", function(data){
|
||||||
|
if(data.error != undefined){
|
||||||
|
msgbox("Failed to get registered apps: " + data.error, false);
|
||||||
|
}else{
|
||||||
|
var tbody = $("#registeredSsoApps");
|
||||||
|
tbody.empty();
|
||||||
|
for(var i = 0; i < data.length; i++){
|
||||||
|
var app = data[i];
|
||||||
|
var tr = $("<tr>");
|
||||||
|
tr.append($("<td>").text(app.AppName));
|
||||||
|
tr.append($("<td>").html('<a href="//'+app.Domain+'" target="_blank">'+app.Domain+'</a>'));
|
||||||
|
tr.append($("<td>").text(app.AppID));
|
||||||
|
tr.append($("<td>").text(app.RegisteredOn));
|
||||||
|
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
|
||||||
|
removeBtn.on("click", function(){
|
||||||
|
removeApp(app.AppID);
|
||||||
|
});
|
||||||
|
tr.append($("<td>").append(removeBtn));
|
||||||
|
tbody.append(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length == 0){
|
||||||
|
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initRegisteredAppTable();
|
||||||
|
|
||||||
|
//Also bind the refresh button to the initRegisteredAppTable function
|
||||||
|
function handleRegisterAppListRefresh(){
|
||||||
|
initRegisteredAppTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegisterAppManagementSnippet(){
|
||||||
|
//Open the register app management snippet
|
||||||
|
showSideWrapper("snippet/sso_app.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Bind the remove button to the removeApp function
|
||||||
|
function removeApp(appID){
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/sso/removeApp",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
appID: appID
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if(data.error != undefined){
|
||||||
|
msgbox("Failed to remove app: " + data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("App removed", true);
|
||||||
|
updateSSOStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Registered Users Event Handlers */
|
||||||
|
function initUserList(){
|
||||||
|
$.get("/api/sso/user/list", function(data){
|
||||||
|
if(data.error != undefined){
|
||||||
|
msgbox("Failed to get registered users: " + data.error, false);
|
||||||
|
}else{
|
||||||
|
var tbody = $("#registeredSsoUsers");
|
||||||
|
tbody.empty();
|
||||||
|
for(var i = 0; i < data.length; i++){
|
||||||
|
var user = data[i];
|
||||||
|
var tr = $("<tr>");
|
||||||
|
tr.append($("<td>").text(user.Username));
|
||||||
|
tr.append($("<td>").text(user.RegisteredOn));
|
||||||
|
var resetBtn = $("<button>").addClass("ui blue basic small icon button").html('<i class="ui blue key icon"></i>');
|
||||||
|
resetBtn.on("click", function(){
|
||||||
|
resetPassword(user.Username);
|
||||||
|
});
|
||||||
|
tr.append($("<td>").append(resetBtn));
|
||||||
|
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
|
||||||
|
removeBtn.on("click", function(){
|
||||||
|
removeUser(user.Username);
|
||||||
|
});
|
||||||
|
tr.append($("<td>").append(removeBtn));
|
||||||
|
tbody.append(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length == 0){
|
||||||
|
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bind the refresh button to the initUserList function
|
||||||
|
function handleUserListRefresh(){
|
||||||
|
initUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegisteredUserManager(){
|
||||||
|
//Open the registered user management snippet
|
||||||
|
showSideWrapper("snippet/sso_user.html");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
-->
|
@ -73,28 +73,30 @@
|
|||||||
<p>Inbound Port (Reverse Proxy Listening Port)</p>
|
<p>Inbound Port (Reverse Proxy Listening Port)</p>
|
||||||
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
|
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
|
||||||
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
|
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
|
||||||
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
|
<input type="text" id="incomingPort" placeholder="Incoming Port" value="443">
|
||||||
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
|
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<label>Use TLS to serve proxy request</label>
|
<label>Use TLS to serve proxy request<br>
|
||||||
|
<small>Also known as HTTPS mode</small></label>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
|
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;" >
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<label>Enable HTTP server on port 80<br>
|
<label>Enable HTTP server on port 80<br>
|
||||||
<small>(Only apply when TLS enabled and not using port 80)</small></label>
|
<small>Accept HTTP requests even if you are using HTTPS mode</small></label>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div tourstep="forceHttpsRedirect" style="display: inline-block;">
|
<div tourstep="forceHttpsRedirect" style="display: inline-block;">
|
||||||
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
|
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<label>Force redirect HTTP request to HTTPS</label>
|
<label>Force redirect HTTP request to HTTPS<br>
|
||||||
|
<small>Redirect web traffic from port 80 to 443, require enabling HTTP server on port 80</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@ -359,10 +361,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (enabled){
|
if (enabled){
|
||||||
$("#redirect").show();
|
//$("#redirect").show();
|
||||||
msgbox("Port 80 listener enabled");
|
msgbox("Port 80 listener enabled");
|
||||||
}else{
|
}else{
|
||||||
$("#redirect").hide();
|
//$("#redirect").hide();
|
||||||
msgbox("Port 80 listener disabled");
|
msgbox("Port 80 listener disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,10 +402,10 @@
|
|||||||
$.get("/api/proxy/listenPort80", function(data){
|
$.get("/api/proxy/listenPort80", function(data){
|
||||||
if (data){
|
if (data){
|
||||||
$("#listenP80").checkbox("set checked");
|
$("#listenP80").checkbox("set checked");
|
||||||
$("#redirect").show();
|
//$("#redirect").show();
|
||||||
}else{
|
}else{
|
||||||
$("#listenP80").checkbox("set unchecked");
|
$("#listenP80").checkbox("set unchecked");
|
||||||
$("#redirect").hide();
|
//$("#redirect").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#listenP80").find("input").on("change", function(){
|
$("#listenP80").find("input").on("change", function(){
|
||||||
@ -579,7 +581,7 @@
|
|||||||
let timestamps = [];
|
let timestamps = [];
|
||||||
|
|
||||||
for(var i = 0; i < dataCount; i++){
|
for(var i = 0; i < dataCount; i++){
|
||||||
timestamps.push(parseInt(Date.now() / 1000) + i);
|
timestamps.push(new Date(Date.now() + i * 1000).toLocaleString().replace(',', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchData() {
|
function fetchData() {
|
||||||
@ -600,10 +602,8 @@
|
|||||||
txValues.shift();
|
txValues.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamps.push(new Date(Date.now()).toLocaleString().replace(',', ''));
|
||||||
timestamps.push(parseInt(Date.now() / 1000));
|
|
||||||
timestamps.shift();
|
timestamps.shift();
|
||||||
|
|
||||||
updateChart();
|
updateChart();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
||||||
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
|
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
|
||||||
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
//Check if update mode
|
//Check if update mode
|
||||||
if ($("#editStreamProxyButton").is(":visible")){
|
if ($("#editStreamProxyButton").is(":visible")){
|
||||||
confirmEditTCPProxyConfig(event);
|
confirmEditTCPProxyConfig(event,$("#editStreamProxyButton")[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,13 +274,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmEditTCPProxyConfig(event){
|
function confirmEditTCPProxyConfig(event, btn){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
var form = $("#streamProxyForm");
|
var form = $("#streamProxyForm");
|
||||||
|
let originalButtonHTML = $(btn).html();
|
||||||
|
$(btn).html(`<i class="ui loading spinner icon"></i> Updating`);
|
||||||
|
$(btn).addClass("disabled");
|
||||||
|
|
||||||
var formValid = validateTCPProxyConfig(form);
|
var formValid = validateTCPProxyConfig(form);
|
||||||
if (!formValid){
|
if (!formValid){
|
||||||
|
$(btn).html(originalButtonHTML);
|
||||||
|
$(btn).removeClass("disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +304,8 @@
|
|||||||
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
$(btn).html(originalButtonHTML);
|
||||||
|
$(btn).removeClass("disabled");
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
msgbox(response.error, false, 6000);
|
msgbox(response.error, false, 6000);
|
||||||
}else{
|
}else{
|
||||||
@ -310,6 +317,8 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function() {
|
||||||
|
$(btn).html(originalButtonHTML);
|
||||||
|
$(btn).removeClass("disabled");
|
||||||
msgbox('An error occurred while processing the request', false);
|
msgbox('An error occurred while processing the request', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
initUptimeTable();
|
initUptimeTable();
|
||||||
|
|
||||||
function reloadUptimeList(){
|
function reloadUptimeList(){
|
||||||
$("#utmrender").html(`<div class="ui segment">
|
$("#utmrender").html(`<div class="ui utmloading segment">
|
||||||
<div class="ui active inverted dimmer" style="z-index: 2;">
|
<div class="ui active inverted dimmer" style="z-index: 2;">
|
||||||
<div class="ui text loader">Loading</div>
|
<div class="ui text loader">Loading</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
<form id="email-form" class="ui form">
|
<form id="email-form" class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Sender Address</label>
|
<label>Sender Address</label>
|
||||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.aroz.org">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Advance configs -->
|
<!-- Advance configs -->
|
||||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div id="advanceProxyRules" class="ui fluid accordion">
|
<div id="advanceProxyRules" class="ui fluid accordion">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@ -191,14 +191,19 @@
|
|||||||
var targetDomain = $("#virtualDirectoryDomain").val().trim();
|
var targetDomain = $("#virtualDirectoryDomain").val().trim();
|
||||||
if (targetDomain != ""){
|
if (targetDomain != ""){
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/tlscheck",
|
url: "/api/proxy/tlscheck?selfsignchk=true",
|
||||||
data: {url: targetDomain},
|
data: {url: targetDomain},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
|
|
||||||
}else if (data == "https"){
|
}else if (data.protocol == "https"){
|
||||||
$("#vdReqTls").parent().checkbox("set checked");
|
$("#vdReqTls").parent().checkbox("set checked");
|
||||||
}else if (data == "http"){
|
if (data.selfsign){
|
||||||
|
$("#vdSkipTLSValidation").parent().checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$("#vdSkipTLSValidation").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
}else if (data.protocol == "http"){
|
||||||
$("#vdReqTls").parent().checkbox("set unchecked");
|
$("#vdReqTls").parent().checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1133
src/web/darktheme.css
Normal file
1133
src/web/darktheme.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,22 +16,24 @@
|
|||||||
<script src="script/chart.js"></script>
|
<script src="script/chart.js"></script>
|
||||||
<script src="script/utils.js"></script>
|
<script src="script/utils.js"></script>
|
||||||
<link rel="stylesheet" href="main.css">
|
<link rel="stylesheet" href="main.css">
|
||||||
|
<link rel="stylesheet" href="darktheme.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script src="script/darktheme.js"></script>
|
||||||
<div class="menubar">
|
<div class="menubar">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<img class="logo" src="img/logo.svg">
|
<img class="logo" src="img/logo.svg">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
|
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
|
||||||
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
|
<button id="sidemenuBtn" class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
|
<div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
|
||||||
<button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
<button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="ui right floated buttons" style="padding-top: 2px;">
|
<div class="ui right floated buttons" style="padding-top: 2px; margin-right: 0.4em;">
|
||||||
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
|
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -160,7 +162,7 @@
|
|||||||
<br><br>
|
<br><br>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui container" style="color: grey; font-size: 90%">
|
<div class="ui container" style="color: grey; font-size: 90%">
|
||||||
<p><a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a> <span class="zrversion"></span> © 2021 - <span class="year"></span> tobychui. Licensed under AGPL</p>
|
<p><a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a> <span class="zrversion"></span> © 2021 - <span class="year"></span> tobychui. Licensed under AGPL</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messageBox" class="ui green floating big compact message">
|
<div id="messageBox" class="ui green floating big compact message">
|
||||||
@ -269,11 +271,18 @@
|
|||||||
|
|
||||||
function toggleTheme(){
|
function toggleTheme(){
|
||||||
if ($("body").hasClass("darkTheme")){
|
if ($("body").hasClass("darkTheme")){
|
||||||
$("body").removeClass("darkTheme")
|
setDarkTheme(false);
|
||||||
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
|
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
||||||
|
if ($(".sideWrapper").is(":visible")){
|
||||||
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
$("body").addClass("darkTheme");
|
setDarkTheme(true);
|
||||||
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
|
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
||||||
|
if ($(".sideWrapper").is(":visible")){
|
||||||
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@
|
|||||||
<div class="field registerOnly">
|
<div class="field registerOnly">
|
||||||
<div class="ui left icon input">
|
<div class="ui left icon input">
|
||||||
<i class="lock icon"></i>
|
<i class="lock icon"></i>
|
||||||
<input id="repeatMagic" type="password" name="passwordconfirm" placeholder="Confirm Password">
|
<input id="repeatMagic" type="password" name="passwordconfirm" placeholder="Confirm Password" >
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field loginOnly" style="text-align: left;">
|
<div class="field loginOnly" style="text-align: left;">
|
||||||
@ -175,11 +175,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="wavebase">
|
<div class="wavebase">
|
||||||
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
|
<p>Proudly powered by <a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a></p>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
AOS.init();
|
AOS.init();
|
||||||
|
var registerMode = false;
|
||||||
var redirectionAddress = "/";
|
var redirectionAddress = "/";
|
||||||
var loginAddress = "/api/auth/login";
|
var loginAddress = "/api/auth/login";
|
||||||
$(".checkbox").checkbox();
|
$(".checkbox").checkbox();
|
||||||
@ -197,6 +197,7 @@
|
|||||||
$.get("/api/auth/userCount", function(data){
|
$.get("/api/auth/userCount", function(data){
|
||||||
if (data == 0){
|
if (data == 0){
|
||||||
//Allow user creation
|
//Allow user creation
|
||||||
|
registerMode = true;
|
||||||
$(".loginOnly").hide();
|
$(".loginOnly").hide();
|
||||||
$(".registerOnly").show();
|
$(".registerOnly").show();
|
||||||
}
|
}
|
||||||
@ -240,13 +241,23 @@
|
|||||||
$("input").on("keydown",function(event){
|
$("input").on("keydown",function(event){
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ($(this).attr("id") == "magic"){
|
if (registerMode){
|
||||||
login();
|
//Register mode
|
||||||
|
if ($(this).attr("id") == "repeatMagic"){
|
||||||
|
$("#regsiterbtn").click();
|
||||||
|
}else{
|
||||||
|
//Focus to next field
|
||||||
|
$(this).next().focus();
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
//Fuocus to password field
|
//Login mode
|
||||||
$("#magic").focus();
|
if ($(this).attr("id") == "magic"){
|
||||||
|
login();
|
||||||
|
}else{
|
||||||
|
//Fuocus to password field
|
||||||
|
$("#magic").focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,50 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
index.html style overwrite
|
index.html style overwrite
|
||||||
*/
|
*/
|
||||||
:root{
|
|
||||||
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
|
|
||||||
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
|
||||||
--theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
|
|
||||||
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme Color Definations */
|
/* Theme color palletes are defined in darktheme.css */
|
||||||
body:not(.darkTheme){
|
|
||||||
--theme_bg: #f6f6f6;
|
|
||||||
--theme_bg_primary: #ffffff;
|
|
||||||
--theme_bg_secondary: #ffffff;
|
|
||||||
--theme_bg_active: #ececec;
|
|
||||||
--theme_highlight: #a9d1f3;
|
|
||||||
--theme_bg_inverted: #27292d;
|
|
||||||
--theme_advance: #f8f8f9;
|
|
||||||
--item_color: #5e5d5d;
|
|
||||||
--item_color_select: rgba(0,0,0,.87);
|
|
||||||
--text_color: #414141;
|
|
||||||
--input_color: white;
|
|
||||||
--divider_color: #cacaca;
|
|
||||||
--text_color_inverted: #fcfcfc;
|
|
||||||
--button_text_color: #878787;
|
|
||||||
--button_border_color: #dedede;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme{
|
|
||||||
--theme_bg: #27292d;
|
|
||||||
--theme_bg_primary: #3d3f47;
|
|
||||||
--theme_bg_secondary: #373a42;
|
|
||||||
--theme_highlight: #6682c4;
|
|
||||||
--theme_bg_active: #292929;
|
|
||||||
--theme_bg_inverted: #f8f8f9;
|
|
||||||
--theme_advance: #333333;
|
|
||||||
--item_color: #cacaca;
|
|
||||||
--text_color: #fcfcfc;
|
|
||||||
--text_color_secondary: #dfdfdf;
|
|
||||||
--input_color: black;
|
|
||||||
--divider_color: #3b3b3b;
|
|
||||||
--item_color_select: rgba(255, 255, 255, 0.87);
|
|
||||||
--text_color_inverted: #414141;
|
|
||||||
--button_text_color: #e9e9e9;
|
|
||||||
--button_border_color: #646464;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme Toggle CSS */
|
/* Theme Toggle CSS */
|
||||||
#themeColorButton{
|
#themeColorButton{
|
||||||
@ -368,7 +326,7 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.basic.segment.advanceoptions{
|
.basic.segment.advanceoptions{
|
||||||
background-color: #f7f7f7;
|
background-color: var(--theme_advance);
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,6 +572,29 @@ body{
|
|||||||
background: var(--theme_green) !important;
|
background: var(--theme_green) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SSO Panel
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ssoRunningState{
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ssoRunningState .ui.header, .ssoRunningState .sub.header{
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssoRunningState:not(.enabled){
|
||||||
|
background: var(--theme_red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssoRunningState.enabled{
|
||||||
|
background: var(--theme_green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Static Web Server
|
Static Web Server
|
||||||
*/
|
*/
|
||||||
|
@ -201,7 +201,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="wavebase">
|
<div class="wavebase">
|
||||||
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
|
<p>Proudly powered by <a href="https://zoraxy.aroz.org" target="_blank">Zoraxy</a></p>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
AOS.init();
|
AOS.init();
|
||||||
|
6
src/web/robots.txt
Normal file
6
src/web/robots.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# robots.txt for Zoraxy project
|
||||||
|
# In general, you should not expose the management interface to the internet.
|
||||||
|
# In case you do, this file (hopefully) protects you from web crawlers.
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
51
src/web/script/darktheme.js
Normal file
51
src/web/script/darktheme.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Dark Theme Toggle Manager
|
||||||
|
|
||||||
|
This script is used to manage the dark theme toggle button in the header of the website.
|
||||||
|
It will change the theme of the website to dark mode when the toggle is clicked and back to light mode when clicked again.
|
||||||
|
|
||||||
|
Must be included just after the start of body tag in the HTML file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _whiteThemeHandleApplyChange(){
|
||||||
|
$(".menubar .logo").attr("src", "img/logo.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _darkThemeHandleApplyChange(){
|
||||||
|
$(".menubar .logo").attr("src", "img/logo_white.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Check if the theme is dark, must be done before the body is loaded to prevent flickering
|
||||||
|
function setDarkTheme(isDarkTheme = false){
|
||||||
|
if (isDarkTheme){
|
||||||
|
$("body").addClass("darkTheme");
|
||||||
|
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
|
||||||
|
//Check if the page is still loading, if not change the logo
|
||||||
|
if (document.readyState == "complete"){
|
||||||
|
_darkThemeHandleApplyChange();
|
||||||
|
}else{
|
||||||
|
//Wait for the page to load and then change the logo
|
||||||
|
$(document).ready(function(){
|
||||||
|
_darkThemeHandleApplyChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$("body").removeClass("darkTheme")
|
||||||
|
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
//By default the page is white theme. So no need to change the logo if page is still loading
|
||||||
|
if (document.readyState == "complete"){
|
||||||
|
//Switching back to light theme
|
||||||
|
_whiteThemeHandleApplyChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem("theme") == "dark"){
|
||||||
|
setDarkTheme(true);
|
||||||
|
}else{
|
||||||
|
setDarkTheme(false);
|
||||||
|
}
|
@ -14,69 +14,72 @@
|
|||||||
top: 0.4em;
|
top: 0.4em;
|
||||||
right: 1em;
|
right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<br>
|
<link rel="stylesheet" href="../darktheme.css">
|
||||||
<div class="ui container">
|
<script src="../script/darktheme.js"></script>
|
||||||
<div class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
Access Rule Editor
|
|
||||||
<div class="sub header">Create, Edit or Remove Access Rules</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui top attached tabular menu">
|
|
||||||
<a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
|
|
||||||
<a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
|
|
||||||
</div>
|
|
||||||
<div class="ui bottom attached active tab segment" data-tab="new">
|
|
||||||
<p>Create a new Access Rule</p>
|
|
||||||
<form class="ui form" id="accessRuleForm">
|
|
||||||
<div class="field">
|
|
||||||
<label>Rule Name</label>
|
|
||||||
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea name="description" placeholder="Description" required></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
|
||||||
</form>
|
|
||||||
<br>
|
<br>
|
||||||
</div>
|
<div class="ui container">
|
||||||
<div class="ui bottom attached tab segment" data-tab="edit">
|
<div class="ui header">
|
||||||
<p>Select an Access Rule to edit</p>
|
<div class="content">
|
||||||
<button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
|
Access Rule Editor
|
||||||
<div class="ui selection fluid dropdown" id="accessRuleSelector">
|
<div class="sub header">Create, Edit or Remove Access Rules</div>
|
||||||
<input type="hidden" name="targetAccessRule" value="default">
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
<div class="default text"></div>
|
|
||||||
<div class="menu" id="accessRuleList">
|
|
||||||
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui top attached tabular menu">
|
||||||
|
<a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
|
||||||
|
<a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
|
||||||
|
</div>
|
||||||
|
<div class="ui bottom attached active tab segment" data-tab="new">
|
||||||
|
<p>Create a new Access Rule</p>
|
||||||
|
<form class="ui form" id="accessRuleForm">
|
||||||
|
<div class="field">
|
||||||
|
<label>Rule Name</label>
|
||||||
|
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea name="description" placeholder="Description" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<div class="ui bottom attached tab segment" data-tab="edit">
|
||||||
|
<p>Select an Access Rule to edit</p>
|
||||||
|
<button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
|
||||||
|
<div class="ui selection fluid dropdown" id="accessRuleSelector">
|
||||||
|
<input type="hidden" name="targetAccessRule" value="default">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="default text"></div>
|
||||||
|
<div class="menu" id="accessRuleList">
|
||||||
|
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<form class="ui form" id="modifyRuleInfo">
|
||||||
|
<div class="disabled field">
|
||||||
|
<label>Rule ID</label>
|
||||||
|
<input type="text" name="accessRuleUUID">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Rule Name</label>
|
||||||
|
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea name="description" placeholder="Description" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
|
||||||
|
<button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<form class="ui form" id="modifyRuleInfo">
|
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||||
<div class="disabled field">
|
<br><br><br>
|
||||||
<label>Rule ID</label>
|
|
||||||
<input type="text" name="accessRuleUUID">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Rule Name</label>
|
|
||||||
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea name="description" placeholder="Description" required></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
|
|
||||||
<button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
|
||||||
<br><br><br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<link rel="stylesheet" href="../darktheme.css">
|
||||||
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
@ -50,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
|
<small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@ -91,8 +93,11 @@
|
|||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Domain(s)</label>
|
<label>Domain(s)</label>
|
||||||
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
|
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="handlePostInputAutomation();">
|
||||||
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
|
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)
|
||||||
|
<span id="caNoDNSSupportWarning" style="color: #ffaf2e; display:none;"><br> <i class="exclamation triangle icon"></i> Current selected CA do not support DNS challenge</span>
|
||||||
|
</small>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field multiDomainOnly" style="display:none;">
|
<div class="field multiDomainOnly" style="display:none;">
|
||||||
<label>Matching Rule</label>
|
<label>Matching Rule</label>
|
||||||
@ -113,7 +118,6 @@
|
|||||||
<div class="item" data-value="Buypass">Buypass</div>
|
<div class="item" data-value="Buypass">Buypass</div>
|
||||||
<div class="item" data-value="ZeroSSL">ZeroSSL</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="Custom ACME Server">Custom ACME Server</div>
|
||||||
<!-- <div class="item" data-value="Google">Google</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field dnsChallengeOnly" style="display:none;">
|
<div class="field dnsChallengeOnly" style="display:none;">
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<p>DNS Credentials</p>
|
<p>DNS Credentials</p>
|
||||||
<div id="dnsProviderAPIFields">
|
<div id="dnsProviderAPIFields">
|
||||||
<p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
|
<p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
|
||||||
</div>
|
</div>
|
||||||
@ -389,7 +393,7 @@
|
|||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//On CA change in dropdown
|
//On CA change in dropdown
|
||||||
$("input[name=ca]").on('change', function() {
|
$("input[name=ca]").on('change', function() {
|
||||||
if(this.value == "Custom ACME Server") {
|
if(this.value == "Custom ACME Server") {
|
||||||
@ -432,19 +436,44 @@
|
|||||||
$("#dnsProviderAPIFields").html("");
|
$("#dnsProviderAPIFields").html("");
|
||||||
//Generate a form for this config
|
//Generate a form for this config
|
||||||
let booleanFieldsHTML = "";
|
let booleanFieldsHTML = "";
|
||||||
|
let optionalFieldsHTML = "";
|
||||||
for (const [key, datatype] of Object.entries(data)) {
|
for (const [key, datatype] of Object.entries(data)) {
|
||||||
if (datatype == "int"){
|
if (datatype == "int"){
|
||||||
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input" key="${key}" style="margin-top: 0.2em;">
|
let defaultValue = 10;
|
||||||
|
if (key == "HTTPTimeout"){
|
||||||
|
defaultValue = 300;
|
||||||
|
}
|
||||||
|
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input typeint" key="${key}" style="margin-top: 0.2em;">
|
||||||
<div class="ui basic blue label" style="font-weight: 300;">
|
<div class="ui basic blue label" style="font-weight: 300;">
|
||||||
${key}
|
${key}
|
||||||
</div>
|
</div>
|
||||||
<input type="number" value="300">
|
<input type="number" value="${defaultValue}">
|
||||||
</div>`);
|
</div>`);
|
||||||
}else if (datatype == "bool"){
|
}else if (datatype == "bool"){
|
||||||
booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
|
booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<label>${key}</label>
|
<label>${key}</label>
|
||||||
</div>`);
|
</div>`);
|
||||||
|
}else if (datatype == "time.Duration"){
|
||||||
|
let defaultIntValue = 120;
|
||||||
|
let defaultMinValue = 30;
|
||||||
|
if (key == "PollingInterval"){
|
||||||
|
defaultIntValue = 2;
|
||||||
|
defaultMinValue = 1;
|
||||||
|
}else if (key == "PropagationTimeout"){
|
||||||
|
defaultIntValue = 120;
|
||||||
|
defaultMinValue = 30;
|
||||||
|
}
|
||||||
|
optionalFieldsHTML += (`<div class="ui fluid labeled dnsConfigField small input" key="${key}" style="margin-top: 0.2em;">
|
||||||
|
<div class="ui basic blue label" style="font-weight: 300;">
|
||||||
|
${key}
|
||||||
|
</div>
|
||||||
|
<input type="number" min="${defaultMinValue}" value="${defaultIntValue}">
|
||||||
|
<div class="ui basic label" style="font-weight: 300;">
|
||||||
|
secs
|
||||||
|
</div>
|
||||||
|
</div>`);
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
//Default to string
|
//Default to string
|
||||||
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
|
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
|
||||||
@ -461,6 +490,9 @@
|
|||||||
if (booleanFieldsHTML != ""){
|
if (booleanFieldsHTML != ""){
|
||||||
$(".dnsConfigField.checkbox").checkbox();
|
$(".dnsConfigField.checkbox").checkbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Append the optional fields at the bottom, if exists
|
||||||
|
$("#dnsProviderAPIFields").append(optionalFieldsHTML);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -574,8 +606,12 @@
|
|||||||
//Boolean option
|
//Boolean option
|
||||||
let checked = $(this).find("input")[0].checked;
|
let checked = $(this).find("input")[0].checked;
|
||||||
dnsCredentials[thisKey] = checked;
|
dnsCredentials[thisKey] = checked;
|
||||||
|
}else if ($(this).hasClass("typeint")){
|
||||||
|
//Int options
|
||||||
|
let value = $(this).find("input").val();
|
||||||
|
dnsCredentials[thisKey] = parseInt(value);
|
||||||
}else{
|
}else{
|
||||||
//String or int options
|
//String options
|
||||||
let value = $(this).find("input").val().trim();
|
let value = $(this).find("input").val().trim();
|
||||||
dnsCredentials[thisKey] = value;
|
dnsCredentials[thisKey] = value;
|
||||||
}
|
}
|
||||||
@ -740,6 +776,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if the entered domain contains multiple domains
|
||||||
function checkIfInputDomainIsMultiple(){
|
function checkIfInputDomainIsMultiple(){
|
||||||
var inputDomains = $("#domainsInput").val();
|
var inputDomains = $("#domainsInput").val();
|
||||||
if (inputDomains.includes(",")){
|
if (inputDomains.includes(",")){
|
||||||
@ -749,6 +786,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Validate if the current combinations of domain and CA supports DNS challenge
|
||||||
|
function validateDNSChallengeSupport(){
|
||||||
|
if ($("#domainsInput").val().includes("*")){
|
||||||
|
var ca = $("#ca").dropdown("get value");
|
||||||
|
if (ca == "Let's Encrypt" || ca == ""){
|
||||||
|
$("#caNoDNSSupportWarning").hide();
|
||||||
|
}else{
|
||||||
|
$("#caNoDNSSupportWarning").show();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$("#caNoDNSSupportWarning").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//call to validateDNSChallengeSupport() on #ca value change
|
||||||
|
$("#ca").dropdown({
|
||||||
|
onChange: function(value, text, $selectedItem) {
|
||||||
|
validateDNSChallengeSupport();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Handle the input change event on domain input
|
||||||
|
function handlePostInputAutomation(){
|
||||||
|
checkIfInputDomainIsMultiple();
|
||||||
|
validateDNSChallengeSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function toggleDnsChallenge(){
|
function toggleDnsChallenge(){
|
||||||
if ( $("#useDnsChallenge")[0].checked){
|
if ( $("#useDnsChallenge")[0].checked){
|
||||||
$(".dnsChallengeOnly").show();
|
$(".dnsChallengeOnly").show();
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
<script src="../script/utils.js"></script>
|
<script src="../script/utils.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<link rel="stylesheet" href="../darktheme.css">
|
||||||
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user