mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-02 20:31:43 +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
|
||||
|
||||
+ 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
|
||||
- External permission management system for easy system integration
|
||||
- SMTP config for password reset
|
||||
- Dark Theme Mode
|
||||
|
||||
## Downloads
|
||||
|
||||
@ -51,7 +52,7 @@ If you have no background in setting up reverse proxy or web routing, you should
|
||||
|
||||
## Build from Source
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
Requires Go 1.23 or higher
|
||||
|
||||
```bash
|
||||
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)
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-earlyrenew int
|
||||
Number of days to early renew a soon expiring certificate (days) (default 30)
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-mdns
|
||||
@ -119,7 +122,7 @@ Usage of zoraxy:
|
||||
-webfm
|
||||
Enable web file manager for static web server root folder (default true)
|
||||
-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
|
||||
ZeroTier authtoken for the local node
|
||||
-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/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
@ -12,17 +12,31 @@ RUN go mod tidy &&\
|
||||
go build -o /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 &&\
|
||||
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 &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
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 --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/
|
||||
|
||||
@ -44,7 +58,7 @@ ENV WEBROOT="./www"
|
||||
ENV ZTAUTH=""
|
||||
ENV ZTPORT="9993"
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/", "/var/lib/zerotier-one/" ]
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||
|
||||
|
@ -23,7 +23,6 @@ docker run -d \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
-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 /etc/localtime:/etc/localtime \
|
||||
-e FASTGEOIP="true" \
|
||||
@ -45,7 +44,6 @@ services:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||
- /path/to/zerotier/config/:/var/lib/zerotier-one/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
environment:
|
||||
@ -66,7 +64,6 @@ services:
|
||||
| Volume | Details |
|
||||
|:-|:-|
|
||||
| `/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. |
|
||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
update-ca-certificates
|
||||
echo "CA certificates updated"
|
||||
|
||||
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
|
||||
echo "ZeroTier daemon started"
|
||||
fi
|
||||
|
||||
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">
|
||||
|
||||
<!-- 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:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<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 -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="arozos.com">
|
||||
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
|
||||
<meta property="twitter:domain" content="aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<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:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
|
@ -230,7 +230,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
@ -254,7 +264,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
@ -397,7 +417,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
p := bluemonday.StrictPolicy()
|
||||
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)
|
||||
}
|
||||
@ -420,7 +450,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
func acmeRegisterSpecialRoutingRule() {
|
||||
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
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
requireRestorePort80 := false
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
|
||||
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
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
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
|
||||
time.Sleep(3 * time.Second)
|
||||
//Add a 2 second delay to make sure everything is settle down
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
@ -117,13 +148,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
if requireRestorePort80 {
|
||||
//Restore port 80 listener
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(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
|
||||
|
281
src/api.go
281
src/api.go
@ -8,6 +8,8 @@ import (
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"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/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -17,34 +19,11 @@ import (
|
||||
API.go
|
||||
|
||||
This file contains all the API called by the web management interface
|
||||
|
||||
*/
|
||||
|
||||
var requireAuth = true
|
||||
|
||||
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 {
|
||||
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
|
||||
// Register the APIs for HTTP proxy management functions
|
||||
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
/* Reverse Proxy Settings & Status */
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
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/del", DeleteProxyEndpoint)
|
||||
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/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
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/add", ReverseProxyUpstreamAdd)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
|
||||
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
|
||||
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/add", ReverseProxyAddVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||
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/add", HandleCustomHeaderAdd)
|
||||
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/handleHostOverwrite", HandleHostOverwrite)
|
||||
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/add", AddProxyBasicAuthExceptionPaths)
|
||||
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/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||
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/checkDefault", handleDefaultCertCheck)
|
||||
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/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
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/attach", handleAttachRuleToHost)
|
||||
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||
//Blacklist APIs
|
||||
/* Blacklist */
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
//Whitelist APIs
|
||||
/* Whitelist */
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||
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/list", pathRuleHandler.HandleListBlockingPath)
|
||||
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/countries", HandleCountryDistrSummary)
|
||||
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
||||
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)
|
||||
}
|
||||
|
||||
//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/add", ganManager.HandleAddNetwork)
|
||||
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/authorize", ganManager.HandleMemberAuthorization)
|
||||
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/edit", streamProxyManager.HandleEditProxyConfigs)
|
||||
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/delete", streamProxyManager.HandleRemoveProxy)
|
||||
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/discover", HandleMdnsScanning)
|
||||
}
|
||||
|
||||
//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)
|
||||
// Register the APIs for ACME and Auto Renewer management functions
|
||||
func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
|
||||
/* ACME Core */
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
/* 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
|
||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
||||
// Register the APIs for Static Web Server management functions
|
||||
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/ping", netutils.HandlePing)
|
||||
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/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||
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
|
||||
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
||||
//Auth APIs
|
||||
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||
func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
||||
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(username)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||
uc := authAgent.GetUserCounts()
|
||||
js, _ := json.Marshal(uc)
|
||||
js, _ := json.Marshal(authAgent.GetUserCounts())
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
if authAgent.GetUserCounts() == 0 {
|
||||
//Allow register root admin
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
|
||||
|
||||
})
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
|
||||
} else {
|
||||
//This function is disabled
|
||||
utils.SendErrorResponse(w, "Root management account already exists")
|
||||
@ -315,5 +313,60 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
|
||||
authAgent.UnregisterUser(username)
|
||||
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
|
||||
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") {
|
||||
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
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
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-session/session v3.1.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/auth v0.9.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/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/azidentity v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.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/privatedns/armprivatedns v1.1.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.7.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.2.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/autorest v0.11.29 // 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/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // 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/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // 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/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // 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.2.10 // 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.7.3 // 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.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.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.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // 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/presigned-url v1.11.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
|
||||
github.com/aws/smithy-go v1.20.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // 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/v4 v4.3.0 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // 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-units v0.5.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // 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/golang-jwt/jwt/v4 v4.5.0 // 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/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/gophercloud/gophercloud v1.0.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // 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/infobloxopen/infoblox-go-client v1.1.1 // 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/labbsr0x/bindman-dns-webhook v1.0.2 // 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-go v1.6.4 // 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/mitchellh/go-homedir v1.1.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/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
||||
github.com/nrdcg/desec v0.7.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.8.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // 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/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/sacloud/api-client-go v0.2.8 // indirect
|
||||
github.com/sacloud/go-http v0.1.6 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.8 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.12.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // 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/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/spf13/cast v1.6.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/dnspod v1.0.490 // indirect
|
||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.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/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // 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.v3 v3.0.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
|
||||
|
||||
/*
|
||||
______
|
||||
|___ /
|
||||
/ / ___ _ __ __ ___ ___ _
|
||||
/ / / _ \| '__/ _` \ \/ / | | |
|
||||
/ /_| (_) | | | (_| |> <| |_| |
|
||||
/_____\___/|_| \__,_/_/\_\\__, |
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
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 (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -13,98 +42,12 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
// General flags
|
||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
var 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 (
|
||||
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.
|
||||
/* SIGTERM handler, do shutdown sequences before closing */
|
||||
func SetupCloseHandler() {
|
||||
c := make(chan os.Signal, 2)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
@ -116,9 +59,7 @@ func SetupCloseHandler() {
|
||||
}
|
||||
|
||||
func ShutdownSeq() {
|
||||
SystemWideLogger.Println("Shutting down " + name)
|
||||
//SystemWideLogger.Println("Closing GeoDB")
|
||||
//geodbStore.Close()
|
||||
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
|
||||
SystemWideLogger.Println("Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
SystemWideLogger.Println("Closing Statistic Collector")
|
||||
@ -150,7 +91,7 @@ func main() {
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@ -161,7 +102,7 @@ func main() {
|
||||
|
||||
if *enableAutoUpdate {
|
||||
fmt.Println("Checking required config update")
|
||||
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
|
||||
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
|
||||
}
|
||||
|
||||
SetupCloseHandler()
|
||||
@ -183,7 +124,7 @@ func main() {
|
||||
webminPanelMux = http.NewServeMux()
|
||||
csrfMiddleware = csrf.Protect(
|
||||
[]byte(nodeUUID),
|
||||
csrf.CookieName("zoraxy-csrf"),
|
||||
csrf.CookieName(CSRF_COOKIENAME),
|
||||
csrf.Secure(false),
|
||||
csrf.Path("/"),
|
||||
csrf.SameSite(csrf.SameSiteLaxMode),
|
||||
@ -206,11 +147,10 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
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))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,10 +30,11 @@ import (
|
||||
)
|
||||
|
||||
type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
UseDNS bool `json:"dns"`
|
||||
AcmeName string `json:"acme_name"` //ACME provider name
|
||||
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
|
||||
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
|
||||
UseDNS bool `json:"dns"` //Use DNS challenge
|
||||
PropTimeout int `json:"prop_time"` //Propagation timeout
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
|
||||
// generate private key
|
||||
@ -181,7 +189,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
return false, err
|
||||
}
|
||||
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
|
||||
if err != nil {
|
||||
a.Logf("Unable to resolve DNS challenge provider", 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
|
||||
certInfo := &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
PropTimeout: propagationTimeout,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
@ -452,12 +461,30 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
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
|
||||
cleanedDomains := []string{}
|
||||
for _, domain := range domains {
|
||||
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 {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
|
@ -1,70 +1,56 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
||||
|
||||
//Original Implementation
|
||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
||||
// Preprocessor function to get DNS challenge provider by name
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
|
||||
//Unpack the dnsCredentials (json string) to map
|
||||
var dnsCredentialsMap map[string]interface{}
|
||||
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCredentialsIntoEnvironmentVariables(credentials)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
*/
|
||||
|
||||
//New implementation using acmedns CICD pipeline generated datatype
|
||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
||||
}
|
||||
|
||||
/*
|
||||
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")
|
||||
}
|
||||
//Clear the PollingInterval and PropagationTimeout field and conert to int
|
||||
userDefinedPollingInterval := 2
|
||||
if dnsCredentialsMap["PollingInterval"] != nil {
|
||||
userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
|
||||
delete(dnsCredentialsMap, "PollingInterval")
|
||||
convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
|
||||
if err == nil {
|
||||
userDefinedPollingInterval = convertedPollingInterval
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
EarlyRenewDays: earlyRenewDays,
|
||||
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 {
|
||||
//Start the renew ticker
|
||||
thisRenewer.StartAutoRenewTicker()
|
||||
@ -103,7 +106,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -305,7 +308,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
}
|
||||
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
@ -352,6 +354,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
return a.renewExpiredDomains(expiredCertList)
|
||||
}
|
||||
|
||||
// Close the auto renewer
|
||||
func (a *AutoRenewer) Close() {
|
||||
if a.TickerstopChan != nil {
|
||||
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 {
|
||||
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
||||
} else {
|
||||
@ -431,7 +440,7 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsProvider not set")
|
||||
|
@ -3,7 +3,7 @@ package acme
|
||||
/*
|
||||
CA.go
|
||||
|
||||
This script load CA defination from embedded ca.json
|
||||
This script load CA definition from embedded ca.json
|
||||
*/
|
||||
import (
|
||||
_ "embed"
|
||||
@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CA Defination, load from embeded json when startup
|
||||
// CA definition, load from embeded json when startup
|
||||
type CaDef struct {
|
||||
Production map[string]string
|
||||
Test map[string]string
|
||||
|
@ -5,14 +5,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get the issuer name from pem file
|
||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||
// Read the PEM file
|
||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
||||
pemData, err := os.ReadFile(pemFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
session.Values["authenticated"] = false
|
||||
session.Values["username"] = nil
|
||||
session.Save(r, w)
|
||||
return nil
|
||||
session.Options.MaxAge = -1
|
||||
return session.Save(r, w)
|
||||
}
|
||||
|
||||
// Get the current session username from request
|
||||
@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
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
|
||||
- Whitelist
|
||||
- Rate Limitor
|
||||
- SSO Auth
|
||||
- Basic Auth
|
||||
- Vitrual Directory Proxy
|
||||
- Subdomain Proxy
|
||||
@ -77,7 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sep.RequireRateLimit {
|
||||
err := h.handleRateLimitRouting(w, r, sep)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -163,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
@ -188,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
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
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
parsedURL, err := url.Parse(redirectTarget)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
|
||||
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
//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.Write([]byte("500 - Internal Server Error"))
|
||||
return true
|
||||
|
@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
for _, cred := range pe.BasicAuthCredentials {
|
||||
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
||||
matchingFound = true
|
||||
|
||||
//Set the X-Remote-User header
|
||||
r.Header.Set("X-Remote-User", u)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,12 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
/*
|
||||
CustomHeader.go
|
||||
|
||||
This script handle parsing and injecting custom headers
|
||||
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
|
||||
|
||||
/*
|
||||
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 (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"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 {
|
||||
timeout := 1 * time.Second
|
||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||
@ -17,7 +32,115 @@ func DomainReachableWithError(domain string) error {
|
||||
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 {
|
||||
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).DisableCompression = true
|
||||
|
||||
//TODO: Add user adjustable timeout option here
|
||||
|
||||
if dpcOptions.IgnoreTLSVerification {
|
||||
//Ignore TLS certificate validation error
|
||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
|
@ -1,29 +1,67 @@
|
||||
package dpcore_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
func TestReplaceLocationHost(t *testing.T) {
|
||||
urlString := "http://private.com/test/newtarget/"
|
||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
||||
OriginalHost: "test.example.com",
|
||||
ProxyDomain: "private.com/test",
|
||||
UseTLS: true,
|
||||
}
|
||||
useTLS := true
|
||||
tests := []struct {
|
||||
name string
|
||||
urlString string
|
||||
rrr *dpcore.ResponseRewriteRuleSet
|
||||
useTLS bool
|
||||
expectedResult string
|
||||
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/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred: %v", err)
|
||||
{
|
||||
name: "Basic HTTPS to HTTP redirection",
|
||||
urlString: "https://proxy.example.com/resource",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
|
||||
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 {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
for _, tt := range tests {
|
||||
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
|
||||
|
||||
expectedResult := "https://test.example.com/api/"
|
||||
expectedResult := "api/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
|
@ -60,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
// Debug functions for replaceLocationHost
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
||||
|
@ -291,7 +291,7 @@ func (router *Router) Restart() error {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"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
|
||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
newHeaderList := []*UserDefinedHeader{}
|
||||
newHeaderList := []*rewrite.UserDefinedHeader{}
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if !strings.EqualFold(header.Key, key) {
|
||||
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
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
|
||||
if ep.UserDefinedHeaderExists(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"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"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)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
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{
|
||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||
OriginalHost: originalHostHeader,
|
||||
@ -226,9 +237,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
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{
|
||||
ProxyDomain: target.Domain,
|
||||
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"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/auth/sso"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
@ -44,6 +46,7 @@ type RouterOption struct {
|
||||
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
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
|
||||
}
|
||||
|
||||
@ -82,23 +85,6 @@ type BasicAuthExceptionRule struct {
|
||||
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 */
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
@ -131,7 +117,7 @@ type ProxyEndpoint struct {
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
||||
//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
|
||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||
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
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
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
|
||||
RequireRateLimit bool
|
||||
|
@ -16,7 +16,7 @@ type Sender struct {
|
||||
Port int //E.g. 587
|
||||
Username string //Username 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
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
||||
//Get controller info
|
||||
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
||||
if err != nil {
|
||||
log.Println("ZeroTier connection failed: ", err.Error())
|
||||
return &NetworkManager{
|
||||
authToken: option.AuthToken,
|
||||
apiPort: option.ApiPort,
|
||||
|
@ -28,11 +28,17 @@ type NodeInfo struct {
|
||||
Clock int64 `json:"clock"`
|
||||
Config struct {
|
||||
Settings struct {
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
||||
PrimaryPort int `json:"primaryPort"`
|
||||
SoftwareUpdate string `json:"softwareUpdate"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
|
||||
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
|
||||
HomeDir string `json:"homeDir,omitempty"`
|
||||
ListeningOn []string `json:"listeningOn,omitempty"`
|
||||
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:"config"`
|
||||
Online bool `json:"online"`
|
||||
@ -46,7 +52,6 @@ type NodeInfo struct {
|
||||
VersionMinor int `json:"versionMinor"`
|
||||
VersionRev int `json:"versionRev"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package geodb
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
@ -15,17 +16,22 @@ var geoipv4 []byte //Geodb dataset for ipv4
|
||||
var geoipv6 []byte //Geodb dataset for ipv6
|
||||
|
||||
type Store struct {
|
||||
geodb [][]string //Parsed geodb list
|
||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||
geotrie *trie
|
||||
geotrieIpv6 *trie
|
||||
sysdb *database.Database
|
||||
option *StoreOptions
|
||||
geodb [][]string //Parsed geodb list
|
||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||
geotrie *trie
|
||||
geotrieIpv6 *trie
|
||||
sysdb *database.Database
|
||||
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 {
|
||||
AllowSlowIpv4LookUp bool
|
||||
AllowSloeIpv6Lookup bool
|
||||
AllowSlowIpv4LookUp bool
|
||||
AllowSlowIpv6Lookup bool
|
||||
SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval
|
||||
}
|
||||
|
||||
type CountryInfo struct {
|
||||
@ -50,18 +56,44 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
||||
}
|
||||
|
||||
var ipv6Trie *trie
|
||||
if !option.AllowSloeIpv6Lookup {
|
||||
if !option.AllowSlowIpv6Lookup {
|
||||
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
|
||||
}
|
||||
|
||||
return &Store{
|
||||
geodb: parsedGeoData,
|
||||
geotrie: ipv4Trie,
|
||||
geodbIpv6: parsedGeoDataIpv6,
|
||||
geotrieIpv6: ipv6Trie,
|
||||
sysdb: sysdb,
|
||||
option: option,
|
||||
}, nil
|
||||
if option.SlowLookupCacheClearInterval == 0 {
|
||||
option.SlowLookupCacheClearInterval = 15 * time.Minute
|
||||
}
|
||||
|
||||
//Create a new store
|
||||
thisGeoDBStore := &Store{
|
||||
geodb: parsedGeoData,
|
||||
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) {
|
||||
@ -73,8 +105,12 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
|
||||
|
||||
}
|
||||
|
||||
// Close the store
|
||||
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 {
|
||||
|
@ -44,6 +44,7 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
|
||||
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
|
||||
false,
|
||||
true,
|
||||
0,
|
||||
})
|
||||
if err != nil {
|
||||
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) {
|
||||
return ""
|
||||
}
|
||||
|
||||
//Check if already in cache
|
||||
if cc, ok := s.slowLookupCacheIpv4[ipAddr]; ok {
|
||||
return cc
|
||||
}
|
||||
|
||||
for _, ipRange := range s.geodb {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
@ -63,6 +69,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
|
||||
|
||||
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
|
||||
if inRange {
|
||||
//Add to cache
|
||||
s.slowLookupCacheIpv4[ipAddr] = cc
|
||||
return cc
|
||||
}
|
||||
}
|
||||
@ -73,6 +81,12 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
|
||||
if isReservedIP(ipAddr) {
|
||||
return ""
|
||||
}
|
||||
|
||||
//Check if already in cache
|
||||
if cc, ok := s.slowLookupCacheIpv6[ipAddr]; ok {
|
||||
return cc
|
||||
}
|
||||
|
||||
for _, ipRange := range s.geodbIpv6 {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
@ -80,6 +94,8 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
|
||||
|
||||
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
|
||||
if inRange {
|
||||
//Add to cache
|
||||
s.slowLookupCacheIpv6[ipAddr] = 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
|
||||
}
|
||||
|
||||
//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) {
|
||||
ipStart := net.ParseIP(start)
|
||||
ipEnd := net.ParseIP(end)
|
||||
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
||||
host.CheckHostname()
|
||||
host.CheckPort("http", 80, &host.HttpPortDetected)
|
||||
host.CheckPort("https", 443, &host.HttpsPortDetected)
|
||||
fmt.Println("OK", host)
|
||||
hosts = append(hosts, host)
|
||||
|
||||
}(thisIp)
|
||||
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
|
||||
func (host *DiscoveredHost) CheckHostname() {
|
||||
// lookup the hostname for the IP address
|
||||
names, err := net.LookupAddr(host.IP)
|
||||
fmt.Println(names, err)
|
||||
//fmt.Println(names, err)
|
||||
if err == nil && len(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() {
|
||||
n.StopChan <- true
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
n.EventTicker.Stop()
|
||||
//Fixed issue #394 for stopping netstat listener on platforms not supported platforms
|
||||
if n.StopChan != nil {
|
||||
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) {
|
||||
@ -270,11 +277,11 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
||||
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
|
||||
if err != nil {
|
||||
//Permission denied
|
||||
return 0, 0, errors.New("Access denied")
|
||||
return 0, 0, errors.New("access denied")
|
||||
}
|
||||
|
||||
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)
|
||||
@ -334,5 +341,5 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
||||
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) {
|
||||
targetInstance, err := m.GetInstanceById(instanceId)
|
||||
if err != nil {
|
||||
@ -168,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp
|
||||
if username != "" {
|
||||
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")
|
||||
title := username + "@" + remoteIpAddr
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -34,6 +36,21 @@ func IsWebSSHSupported() bool {
|
||||
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
|
||||
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||
timeout := time.Second * 3
|
||||
@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||
return string(buf[:7]) == "SSH-2.0"
|
||||
}
|
||||
|
||||
// Check if the port is used by other process or application
|
||||
func isPortInUse(port int) bool {
|
||||
address := fmt.Sprintf(":%d", port)
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
// Validate the username and remote address to prevent injection
|
||||
func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
|
||||
// Validate and sanitize the username to prevent ssh injection
|
||||
validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
Database *database.Database
|
||||
DefaultTimeout int
|
||||
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 {
|
||||
@ -63,13 +67,37 @@ type Manager struct {
|
||||
|
||||
}
|
||||
|
||||
func NewStreamProxy(options *Options) *Manager {
|
||||
options.Database.NewTable("tcprox")
|
||||
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
if !utils.FileExists(options.ConfigStore) {
|
||||
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//Load relay configs from db
|
||||
previousRules := []*ProxyRelayConfig{}
|
||||
if options.Database.KeyExists("tcprox", "rules") {
|
||||
options.Database.Read("tcprox", "rules", &previousRules)
|
||||
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||
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
|
||||
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
|
||||
rule.parent = &thisManager
|
||||
if rule.Running {
|
||||
//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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
||||
}
|
||||
|
||||
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
|
||||
for i, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
return errors.New("config not found")
|
||||
}
|
||||
|
||||
// Save all configs to ConfigStore folder
|
||||
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 !c.UseTCP {
|
||||
c.Running = false
|
||||
c.udpStopChan = nil
|
||||
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)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.tcpStopChan = nil
|
||||
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() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(3000 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
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 {
|
||||
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
|
||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = 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 = nil
|
||||
}
|
||||
|
||||
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
|
||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
|
@ -1,7 +1,7 @@
|
||||
package v308
|
||||
|
||||
/*
|
||||
v307 type definations
|
||||
v307 type definitions
|
||||
|
||||
This file wrap up the self-contained data structure
|
||||
for v3.0.7 structure and allow automatic updates
|
||||
|
@ -1,7 +1,7 @@
|
||||
package v308
|
||||
|
||||
/*
|
||||
v308 type definations
|
||||
v308 type definition
|
||||
|
||||
This file wrap up the self-contained data structure
|
||||
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)
|
||||
|
||||
// Clean path to prevent path escape #274
|
||||
targetDir = filepath.ToSlash(filepath.Clean(targetDir))
|
||||
for strings.Contains(targetDir, "../") {
|
||||
targetDir = strings.ReplaceAll(targetDir, "../", "")
|
||||
isValidRequest := validatePathEscape(targetDir, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
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) {
|
||||
utils.SendErrorResponse(w, "upload target directory not exists")
|
||||
return
|
||||
@ -163,14 +172,20 @@ func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
if previewMode == "true" {
|
||||
// Serve the file using http.ServeFile
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
http.ServeFile(w, r, filePath)
|
||||
} else {
|
||||
// Trigger a download with content disposition headers
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
@ -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
|
||||
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
|
||||
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)
|
||||
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
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
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)
|
||||
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
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
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
|
||||
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
|
||||
_, 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
|
||||
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
|
||||
_, 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
|
||||
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>.
|
||||
</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!
|
||||
</p>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -26,18 +27,18 @@ func ReverseProxtInit() {
|
||||
/*
|
||||
Load Reverse Proxy Global Settings
|
||||
*/
|
||||
inboundPort := 80
|
||||
inboundPort := 443
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
sysdb.Read("settings", "inbound", &inboundPort)
|
||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||
} 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)
|
||||
if useTls {
|
||||
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
SystemWideLogger.Println("TLS mode enabled. Serving proxy request with TLS")
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
|
||||
listenOnPort80 := false
|
||||
listenOnPort80 := true
|
||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||
if listenOnPort80 {
|
||||
SystemWideLogger.Println("Port 80 listener enabled")
|
||||
@ -66,7 +67,7 @@ func ReverseProxtInit() {
|
||||
SystemWideLogger.Println("Port 80 listener disabled")
|
||||
}
|
||||
|
||||
forceHttpsRedirect := false
|
||||
forceHttpsRedirect := true
|
||||
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
||||
if forceHttpsRedirect {
|
||||
SystemWideLogger.Println("Force HTTPS mode enabled")
|
||||
@ -84,7 +85,7 @@ func ReverseProxtInit() {
|
||||
|
||||
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
||||
HostUUID: nodeUUID,
|
||||
HostVersion: version,
|
||||
HostVersion: SYSTEM_VERSION,
|
||||
Port: inboundPort,
|
||||
UseTls: useTls,
|
||||
ForceTLSLatest: forceLatestTLSVersion,
|
||||
@ -98,6 +99,7 @@ func ReverseProxtInit() {
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
AccessController: accessController,
|
||||
LoadBalancer: loadBalancer,
|
||||
SSOHandler: ssoHandler,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
@ -331,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//VDir
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
//Custom headers
|
||||
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
||||
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
|
||||
//Auth
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -1083,6 +1085,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
if dynamicProxyRouter.Running {
|
||||
dynamicProxyRouter.StopProxyService()
|
||||
dynamicProxyRouter.Option.Port = newIncomingPortInt
|
||||
time.Sleep(1 * time.Second) //Fixed start fail issue
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
} else {
|
||||
//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
|
||||
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
||||
if customHeaderList == nil {
|
||||
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
|
||||
customHeaderList = []*rewrite.UserDefinedHeader{}
|
||||
}
|
||||
js, _ := json.Marshal(customHeaderList)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
@ -1171,12 +1174,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Create a Custom Header Defination type
|
||||
var rewriteDirection dynamicproxy.HeaderDirection
|
||||
//Create a Custom Header Definition type
|
||||
var rewriteDirection rewrite.HeaderDirection
|
||||
if direction == "toOrigin" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
|
||||
} else if direction == "toClient" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
|
||||
} else {
|
||||
//Unknown direction
|
||||
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
||||
@ -1187,7 +1190,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
if rewriteType == "remove" {
|
||||
isRemove = true
|
||||
}
|
||||
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
|
||||
|
||||
headerRewriteDefinition := rewrite.UserDefinedHeader{
|
||||
Key: name,
|
||||
Value: value,
|
||||
Direction: rewriteDirection,
|
||||
@ -1195,7 +1199,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//Create a new custom header object
|
||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination)
|
||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefinition)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
|
||||
return
|
||||
|
@ -27,7 +27,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
||||
Development Mode Override
|
||||
=> 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"))
|
||||
r.URL = u
|
||||
}
|
||||
@ -36,7 +36,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
||||
Production Mode Override
|
||||
=> Web root is located in /web
|
||||
*/
|
||||
if !development && r.URL.Path == "/" {
|
||||
if !DEVELOPMENT_BUILD && r.URL.Path == "/" {
|
||||
//Redirect to web UI
|
||||
http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
@ -93,7 +93,7 @@ func FSHandler(handler http.Handler) http.Handler {
|
||||
|
||||
// Production path fix wrapper. Fix the path on production or development environment
|
||||
func ppf(relativeFilepath string) string {
|
||||
if !development {
|
||||
if !DEVELOPMENT_BUILD {
|
||||
return strings.ReplaceAll(filepath.Join("/web/", 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:] == "/" {
|
||||
relativeFilepath = relativeFilepath + "index.html"
|
||||
}
|
||||
if development {
|
||||
if DEVELOPMENT_BUILD {
|
||||
//Load from disk
|
||||
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
|
||||
content, err = os.ReadFile(targetFilePath)
|
||||
|
78
src/start.go
78
src/start.go
@ -36,7 +36,10 @@ import (
|
||||
Startup Sequence
|
||||
|
||||
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 (
|
||||
@ -49,19 +52,19 @@ var (
|
||||
|
||||
func startupSequence() {
|
||||
//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 {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
|
||||
RootFolder: "./log",
|
||||
Extension: ".log",
|
||||
RootFolder: LOG_FOLDER,
|
||||
Extension: LOG_EXTENSION,
|
||||
})
|
||||
|
||||
//Create database
|
||||
db, err := database.NewDatabase("sys.db", false)
|
||||
db, err := database.NewDatabase(DATABASE_PATH, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -70,21 +73,21 @@ func startupSequence() {
|
||||
sysdb.NewTable("settings")
|
||||
|
||||
//Create tmp folder and conf folder
|
||||
os.MkdirAll("./tmp", 0775)
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
os.MkdirAll(TMP_FOLDER, 0775)
|
||||
os.MkdirAll(CONF_HTTP_PROXY, 0775)
|
||||
|
||||
//Create an auth agent
|
||||
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
|
||||
if err != nil {
|
||||
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
|
||||
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
//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 {
|
||||
panic(err)
|
||||
}
|
||||
@ -93,15 +96,16 @@ func startupSequence() {
|
||||
db.NewTable("redirect")
|
||||
redirectAllowRegexp := false
|
||||
db.Read("redirect", "regex", &redirectAllowRegexp)
|
||||
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp, SystemWideLogger)
|
||||
redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, SystemWideLogger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a geodb store
|
||||
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
||||
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
|
||||
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
|
||||
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
|
||||
AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup,
|
||||
SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -118,12 +122,28 @@ func startupSequence() {
|
||||
accessController, err = access.NewAccessController(&access.Options{
|
||||
Database: sysdb,
|
||||
GeoDB: geodbStore,
|
||||
ConfigFolder: "./conf/access",
|
||||
ConfigFolder: CONF_ACCESS_RULE,
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
Database: sysdb,
|
||||
@ -135,7 +155,7 @@ func startupSequence() {
|
||||
//Start the static web server
|
||||
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
|
||||
Sysdb: sysdb,
|
||||
Port: "5487", //Default Port
|
||||
Port: strconv.Itoa(WEBSERV_DEFAULT_PORT), //Default Port
|
||||
WebRoot: *staticWebServerRoot,
|
||||
EnableDirectoryListing: true,
|
||||
EnableWebDirManager: *allowWebFileManager,
|
||||
@ -160,7 +180,7 @@ func startupSequence() {
|
||||
|
||||
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
|
||||
Enabled: false,
|
||||
ConfigFolder: "./conf/rules/pathrules",
|
||||
ConfigFolder: CONF_PATH_RULE,
|
||||
})
|
||||
|
||||
/*
|
||||
@ -178,7 +198,7 @@ func startupSequence() {
|
||||
|
||||
hostName := *mdnsName
|
||||
if hostName == "" {
|
||||
hostName = "zoraxy_" + nodeUUID
|
||||
hostName = MDNS_HOSTNAME_PREFIX + nodeUUID
|
||||
} else {
|
||||
//Trim off the suffix
|
||||
hostName = strings.TrimSuffix(hostName, ".local")
|
||||
@ -187,24 +207,24 @@ func startupSequence() {
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: hostName,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.arozos.com",
|
||||
Model: "Network Gateway",
|
||||
Domain: MDNS_IDENTIFY_DOMAIN,
|
||||
Model: MDNS_IDENTIFY_DEVICE_TYPE,
|
||||
UUID: nodeUUID,
|
||||
Vendor: "imuslab.com",
|
||||
BuildVersion: version,
|
||||
Vendor: MDNS_IDENTIFY_VENDOR,
|
||||
BuildVersion: SYSTEM_VERSION,
|
||||
}, "")
|
||||
if err != nil {
|
||||
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
|
||||
} else {
|
||||
//Start initial scanning
|
||||
go func() {
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
|
||||
previousmdnsScanResults = hosts
|
||||
SystemWideLogger.Println("mDNS Startup scan completed")
|
||||
}()
|
||||
|
||||
//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)
|
||||
go func() {
|
||||
for {
|
||||
@ -212,7 +232,7 @@ func startupSequence() {
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
case <-ticker.C:
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
|
||||
previousmdnsScanResults = hosts
|
||||
SystemWideLogger.Println("mDNS scan result updated")
|
||||
}
|
||||
@ -244,10 +264,14 @@ func startupSequence() {
|
||||
webSshManager = sshprox.NewSSHProxyManager()
|
||||
|
||||
//Create TCP Proxy Manager
|
||||
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
Database: sysdb,
|
||||
streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||
ConfigStore: CONF_STREAM_PROXY,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create WoL MAC storage table
|
||||
sysdb.NewTable("wolmac")
|
||||
@ -280,8 +304,8 @@ func startupSequence() {
|
||||
sysdb.NewTable("acmepref")
|
||||
acmeHandler = initACME()
|
||||
acmeAutoRenewer, err = acme.NewAutoRenewer(
|
||||
"./conf/acme_conf.json",
|
||||
"./conf/certs/",
|
||||
ACME_AUTORENEW_CONFIG_PATH,
|
||||
CONF_CERT_STORE,
|
||||
int64(*acmeAutoRenewInterval),
|
||||
*acmeCertAutoRenewDays,
|
||||
acmeHandler,
|
||||
|
@ -841,6 +841,25 @@
|
||||
function initBannedCountryList(){
|
||||
$.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
||||
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) => {
|
||||
bannedListHtml += `
|
||||
<tr>
|
||||
@ -919,18 +938,48 @@
|
||||
//Whitelist country table
|
||||
function initWhitelistCountryList(){
|
||||
$.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) => {
|
||||
let countryCode = countryWhitelistEntry.CC;
|
||||
bannedListHtml += `
|
||||
whiteListHTML += `
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#whitelistCountryList').html(bannedListHtml);
|
||||
filterCountries(data, "#countrySelectorWhitelist .menu .item");
|
||||
$('#whitelistCountryList').html(whiteListHTML);
|
||||
|
||||
//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) {
|
||||
$('#whitelistCountryList').append(`
|
||||
<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() {
|
||||
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
|
||||
let ccs = [countryCode];
|
||||
@ -1025,48 +1078,50 @@
|
||||
ccs = countryCode.split(",");
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
for(var i = 0; i < ccs.length; i++){
|
||||
let thisCountryCode = ccs[i];
|
||||
$.cjax({
|
||||
type: "POST",
|
||||
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
|
||||
}
|
||||
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
|
||||
if (ccs.includes("eu")){
|
||||
ccs = ccs.concat(getEUCCs());
|
||||
ccs = ccs.filter(function(item){
|
||||
return item != "eu";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
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');
|
||||
|
||||
}
|
||||
|
||||
function removeFromBannedList(countryCode){
|
||||
countryCode = countryCode.toLowerCase();
|
||||
let countryName = getCountryName(countryCode);
|
||||
if (countryCode == "eu"){
|
||||
let euCountries = getEUCCs();
|
||||
countryCode = euCountries.join(",");
|
||||
countryName = "European Union";
|
||||
}else{
|
||||
countryCode = countryCode.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
$.cjax({
|
||||
url: "/api/blacklist/country/remove",
|
||||
method: "POST",
|
||||
@ -1162,44 +1217,53 @@
|
||||
//Usually just a few countries a for loop will get the job done
|
||||
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)){
|
||||
setTimeout(function(){
|
||||
initWhitelistCountryList();
|
||||
if (ccs.length == 1){
|
||||
//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
|
||||
}
|
||||
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
|
||||
if (ccs.includes("eu")){
|
||||
ccs = ccs.filter(function(item){
|
||||
return item != "eu";
|
||||
});
|
||||
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');
|
||||
}
|
||||
|
||||
function removeFromWhiteList(countryCode){
|
||||
if (confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
|
||||
//Remove from whitelist, accepts a country code or "eu" for all EU countries
|
||||
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();
|
||||
}
|
||||
if (skipConfirm || confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
|
||||
$.cjax({
|
||||
url: "/api/whitelist/country/remove",
|
||||
method: "POST",
|
||||
@ -1208,6 +1272,7 @@
|
||||
if (response.error != undefined){
|
||||
msgbox(response.error, false);
|
||||
}
|
||||
msgbox(countryName + " removed from whitelist");
|
||||
initWhitelistCountryList();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@ -1276,19 +1341,27 @@
|
||||
/*
|
||||
Common Utilities
|
||||
*/
|
||||
function filterCountries(codesToShow, selector="#countrySelector .menu .item") {
|
||||
function filterCountries(alreadySelectedCCs, selector="#countrySelector .menu .item") {
|
||||
// get all items in the dropdown
|
||||
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
|
||||
items.forEach(item => {
|
||||
// get the value of the item (i.e. the country code)
|
||||
const code = item.dataset.value;
|
||||
// if the code is in the array of codes to show, show the item
|
||||
if (codesToShow.includes(code)) {
|
||||
if (alreadySelectedCCs.includes(code)) {
|
||||
//This country code already selected. Hide it
|
||||
item.style.display = 'none';
|
||||
}
|
||||
// otherwise, hide the item
|
||||
else {
|
||||
} else {
|
||||
// otherwise, show the item
|
||||
item.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
@ -61,7 +61,7 @@
|
||||
<p>Current list of loaded certificates</p>
|
||||
<div tourstep="certTable">
|
||||
<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>
|
||||
<tr><th>Domain</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>
|
||||
<div class="ui buttons">
|
||||
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
|
||||
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
|
||||
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
@ -161,7 +161,9 @@
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
|
||||
//Request ACME for certificate
|
||||
let buttonOriginalHTML = "";
|
||||
if (btn != undefined){
|
||||
buttonOriginalHTML = $(btn).html();
|
||||
$(btn).addClass('disabled');
|
||||
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
||||
}
|
||||
@ -169,11 +171,26 @@
|
||||
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
||||
if (btn != undefined){
|
||||
$(btn).removeClass('disabled');
|
||||
if (succ){
|
||||
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||
if ($(btn).hasClass("icon")){
|
||||
//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{
|
||||
$(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(){
|
||||
initManagedDomainCertificateList();
|
||||
|
@ -350,15 +350,27 @@
|
||||
let originalContent = $(column).html();
|
||||
|
||||
//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
|
||||
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||
let thisAliasName = payload.MatchingDomainAlias[i];
|
||||
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
|
||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||
|
||||
@ -371,9 +383,8 @@
|
||||
</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="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();
|
||||
}else{
|
||||
@ -536,9 +547,29 @@
|
||||
Certificate Shortcut
|
||||
*/
|
||||
|
||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
|
||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
||||
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
|
||||
|
@ -17,10 +17,21 @@
|
||||
<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>
|
||||
<div class="ui divider"></div>
|
||||
<!-- IP Scanner-->
|
||||
<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 class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<!-- IP Scanner-->
|
||||
<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>
|
||||
<!-- Traceroute-->
|
||||
<h2>Traceroute / Ping</h2>
|
||||
|
@ -30,7 +30,7 @@
|
||||
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
||||
</div>
|
||||
<!-- 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="title">
|
||||
<i class="dropdown icon"></i>
|
||||
@ -173,7 +173,7 @@
|
||||
});
|
||||
|
||||
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>
|
||||
<!-- 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 class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
@ -165,18 +165,18 @@
|
||||
<div class="ui basic segment rulesInstructions">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||
Example of domain matching keyword:<br>
|
||||
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
|
||||
<code>aroz.org</code> <br>Any acess requesting aroz.org will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
|
||||
<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>
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
|
||||
Example of wildcard matching keyword:<br>
|
||||
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br>
|
||||
<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="item"><code>www.arozos.com</code></div>
|
||||
<div class="item"><code>foo.bar.arozos.com</code></div>
|
||||
<div class="item"><code>www.aroz.org</code></div>
|
||||
<div class="item"><code>foo.bar.aroz.org</code></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
@ -295,15 +295,25 @@
|
||||
//Automatic check if the site require TLS and check the checkbox if needed
|
||||
function autoCheckTls(targetDomain){
|
||||
$.cjax({
|
||||
url: "/api/proxy/tlscheck",
|
||||
url: "/api/proxy/tlscheck?selfsignchk=true",
|
||||
data: {url: targetDomain},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else if (data == "https"){
|
||||
$("#reqTls").parent().checkbox("set checked");
|
||||
}else if (data == "http"){
|
||||
$("#reqTls").parent().checkbox("set unchecked");
|
||||
}else{
|
||||
//Check if the site require TLS
|
||||
if (data.protocol == "https"){
|
||||
$("#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="ui basic segment">
|
||||
<h2>Single-Sign-On</h2>
|
||||
<p>Create and manage accounts with Zoraxy!</p>
|
||||
<div class="ui message">
|
||||
<div class="header">
|
||||
Work in Progress
|
||||
</div>
|
||||
<p>The SSO feature is currently under development.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4>Work In Progress</h4>
|
||||
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
|
||||
</div>
|
||||
<!--
|
||||
<div 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>
|
||||
|
||||
<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>
|
||||
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
|
||||
<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>
|
||||
</div>
|
||||
<br>
|
||||
<div id="tls" class="ui toggle notloopbackOnly 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>
|
||||
<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">
|
||||
<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>
|
||||
<br>
|
||||
<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">
|
||||
<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 class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
||||
<div class="ui basic segment advanceoptions">
|
||||
<div class="ui accordion advanceSettings">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
@ -359,10 +361,10 @@
|
||||
return;
|
||||
}
|
||||
if (enabled){
|
||||
$("#redirect").show();
|
||||
//$("#redirect").show();
|
||||
msgbox("Port 80 listener enabled");
|
||||
}else{
|
||||
$("#redirect").hide();
|
||||
//$("#redirect").hide();
|
||||
msgbox("Port 80 listener disabled");
|
||||
}
|
||||
}
|
||||
@ -400,10 +402,10 @@
|
||||
$.get("/api/proxy/listenPort80", function(data){
|
||||
if (data){
|
||||
$("#listenP80").checkbox("set checked");
|
||||
$("#redirect").show();
|
||||
//$("#redirect").show();
|
||||
}else{
|
||||
$("#listenP80").checkbox("set unchecked");
|
||||
$("#redirect").hide();
|
||||
//$("#redirect").hide();
|
||||
}
|
||||
|
||||
$("#listenP80").find("input").on("change", function(){
|
||||
@ -579,7 +581,7 @@
|
||||
let timestamps = [];
|
||||
|
||||
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() {
|
||||
@ -600,10 +602,8 @@
|
||||
txValues.shift();
|
||||
}
|
||||
|
||||
|
||||
timestamps.push(parseInt(Date.now() / 1000));
|
||||
timestamps.push(new Date(Date.now()).toLocaleString().replace(',', ''));
|
||||
timestamps.shift();
|
||||
|
||||
updateChart();
|
||||
}
|
||||
})
|
||||
|
@ -74,7 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
@ -88,7 +88,7 @@
|
||||
|
||||
//Check if update mode
|
||||
if ($("#editStreamProxyButton").is(":visible")){
|
||||
confirmEditTCPProxyConfig(event);
|
||||
confirmEditTCPProxyConfig(event,$("#editStreamProxyButton")[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -274,13 +274,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
function confirmEditTCPProxyConfig(event){
|
||||
function confirmEditTCPProxyConfig(event, btn){
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
var form = $("#streamProxyForm");
|
||||
let originalButtonHTML = $(btn).html();
|
||||
$(btn).html(`<i class="ui loading spinner icon"></i> Updating`);
|
||||
$(btn).addClass("disabled");
|
||||
|
||||
var formValid = validateTCPProxyConfig(form);
|
||||
if (!formValid){
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,6 +304,8 @@
|
||||
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
||||
},
|
||||
success: function(response) {
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
if (response.error) {
|
||||
msgbox(response.error, false, 6000);
|
||||
}else{
|
||||
@ -310,6 +317,8 @@
|
||||
|
||||
},
|
||||
error: function() {
|
||||
$(btn).html(originalButtonHTML);
|
||||
$(btn).removeClass("disabled");
|
||||
msgbox('An error occurred while processing the request', false);
|
||||
}
|
||||
});
|
||||
|
@ -70,7 +70,7 @@
|
||||
initUptimeTable();
|
||||
|
||||
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 text loader">Loading</div>
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<label>Sender Address</label>
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.aroz.org">
|
||||
</div>
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
|
||||
|
@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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 class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
@ -191,14 +191,19 @@
|
||||
var targetDomain = $("#virtualDirectoryDomain").val().trim();
|
||||
if (targetDomain != ""){
|
||||
$.cjax({
|
||||
url: "/api/proxy/tlscheck",
|
||||
url: "/api/proxy/tlscheck?selfsignchk=true",
|
||||
data: {url: targetDomain},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
|
||||
}else if (data == "https"){
|
||||
}else if (data.protocol == "https"){
|
||||
$("#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");
|
||||
}
|
||||
}
|
||||
|
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/utils.js"></script>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="stylesheet" href="darktheme.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="script/darktheme.js"></script>
|
||||
<div class="menubar">
|
||||
<div class="item">
|
||||
<img class="logo" src="img/logo.svg">
|
||||
</div>
|
||||
|
||||
<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 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>
|
||||
</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>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<div class="toolbar">
|
||||
@ -160,7 +162,7 @@
|
||||
<br><br>
|
||||
<div class="ui divider"></div>
|
||||
<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 id="messageBox" class="ui green floating big compact message">
|
||||
@ -269,11 +271,18 @@
|
||||
|
||||
function toggleTheme(){
|
||||
if ($("body").hasClass("darkTheme")){
|
||||
$("body").removeClass("darkTheme")
|
||||
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
|
||||
setDarkTheme(false);
|
||||
//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{
|
||||
$("body").addClass("darkTheme");
|
||||
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
|
||||
setDarkTheme(true);
|
||||
//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="ui left icon input">
|
||||
<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 class="field loginOnly" style="text-align: left;">
|
||||
@ -175,11 +175,11 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
<script>
|
||||
AOS.init();
|
||||
|
||||
var registerMode = false;
|
||||
var redirectionAddress = "/";
|
||||
var loginAddress = "/api/auth/login";
|
||||
$(".checkbox").checkbox();
|
||||
@ -197,6 +197,7 @@
|
||||
$.get("/api/auth/userCount", function(data){
|
||||
if (data == 0){
|
||||
//Allow user creation
|
||||
registerMode = true;
|
||||
$(".loginOnly").hide();
|
||||
$(".registerOnly").show();
|
||||
}
|
||||
@ -240,13 +241,23 @@
|
||||
$("input").on("keydown",function(event){
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if ($(this).attr("id") == "magic"){
|
||||
login();
|
||||
if (registerMode){
|
||||
//Register mode
|
||||
if ($(this).attr("id") == "repeatMagic"){
|
||||
$("#regsiterbtn").click();
|
||||
}else{
|
||||
//Focus to next field
|
||||
$(this).next().focus();
|
||||
}
|
||||
}else{
|
||||
//Fuocus to password field
|
||||
$("#magic").focus();
|
||||
//Login mode
|
||||
if ($(this).attr("id") == "magic"){
|
||||
login();
|
||||
}else{
|
||||
//Fuocus to password field
|
||||
$("#magic").focus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,50 +1,8 @@
|
||||
/*
|
||||
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 */
|
||||
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 color palletes are defined in darktheme.css */
|
||||
|
||||
/* Theme Toggle CSS */
|
||||
#themeColorButton{
|
||||
@ -368,7 +326,7 @@ body{
|
||||
}
|
||||
|
||||
.basic.segment.advanceoptions{
|
||||
background-color: #f7f7f7;
|
||||
background-color: var(--theme_advance);
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@ -614,6 +572,29 @@ body{
|
||||
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
|
||||
*/
|
||||
|
@ -201,7 +201,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
<script>
|
||||
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;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<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>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<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 class="ui container">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||
<br><br><br>
|
||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||
<br><br><br>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@ -25,6 +25,8 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui header">
|
||||
@ -50,7 +52,7 @@
|
||||
</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>
|
||||
</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="title">
|
||||
<i class="dropdown icon"></i>
|
||||
@ -91,8 +93,11 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Domain(s)</label>
|
||||
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
|
||||
<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>
|
||||
<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)
|
||||
<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 class="field multiDomainOnly" style="display:none;">
|
||||
<label>Matching Rule</label>
|
||||
@ -113,7 +118,6 @@
|
||||
<div class="item" data-value="Buypass">Buypass</div>
|
||||
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
|
||||
<div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
|
||||
<!-- <div class="item" data-value="Google">Google</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,7 +140,7 @@
|
||||
</div>
|
||||
<div class="field dnsChallengeOnly" style="display:none;">
|
||||
<div class="ui divider"></div>
|
||||
<p>DNS Credentials</p>
|
||||
<p>DNS Credentials</p>
|
||||
<div id="dnsProviderAPIFields">
|
||||
<p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
|
||||
</div>
|
||||
@ -389,7 +393,7 @@
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
//On CA change in dropdown
|
||||
$("input[name=ca]").on('change', function() {
|
||||
if(this.value == "Custom ACME Server") {
|
||||
@ -432,19 +436,44 @@
|
||||
$("#dnsProviderAPIFields").html("");
|
||||
//Generate a form for this config
|
||||
let booleanFieldsHTML = "";
|
||||
let optionalFieldsHTML = "";
|
||||
for (const [key, datatype] of Object.entries(data)) {
|
||||
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;">
|
||||
${key}
|
||||
</div>
|
||||
<input type="number" value="300">
|
||||
<input type="number" value="${defaultValue}">
|
||||
</div>`);
|
||||
}else if (datatype == "bool"){
|
||||
booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
|
||||
<input type="checkbox">
|
||||
<label>${key}</label>
|
||||
</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{
|
||||
//Default to string
|
||||
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
|
||||
@ -461,6 +490,9 @@
|
||||
if (booleanFieldsHTML != ""){
|
||||
$(".dnsConfigField.checkbox").checkbox();
|
||||
}
|
||||
|
||||
//Append the optional fields at the bottom, if exists
|
||||
$("#dnsProviderAPIFields").append(optionalFieldsHTML);
|
||||
});
|
||||
});
|
||||
|
||||
@ -574,8 +606,12 @@
|
||||
//Boolean option
|
||||
let checked = $(this).find("input")[0].checked;
|
||||
dnsCredentials[thisKey] = checked;
|
||||
}else if ($(this).hasClass("typeint")){
|
||||
//Int options
|
||||
let value = $(this).find("input").val();
|
||||
dnsCredentials[thisKey] = parseInt(value);
|
||||
}else{
|
||||
//String or int options
|
||||
//String options
|
||||
let value = $(this).find("input").val().trim();
|
||||
dnsCredentials[thisKey] = value;
|
||||
}
|
||||
@ -740,6 +776,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
//Check if the entered domain contains multiple domains
|
||||
function checkIfInputDomainIsMultiple(){
|
||||
var inputDomains = $("#domainsInput").val();
|
||||
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(){
|
||||
if ( $("#useDnsChallenge")[0].checked){
|
||||
$(".dnsChallengeOnly").show();
|
||||
|
@ -10,6 +10,8 @@
|
||||
<script src="../script/utils.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<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