mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 06:08:30 +02:00
Compare commits
49 Commits
31ba4f20ae
...
v3.2.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
223ae9e112 | ||
![]() |
aff1975c5a | ||
![]() |
5c6950ca56 | ||
![]() |
70b1ccfa6e | ||
![]() |
100c1e9c04 | ||
![]() |
a33600d3e2 | ||
![]() |
c4c10d2130 | ||
![]() |
4d3d1b25cb | ||
![]() |
118b5e5114 | ||
![]() |
ad53b894c0 | ||
![]() |
a0a394885c | ||
![]() |
51334a3a75 | ||
![]() |
6f5fadc085 | ||
![]() |
45506c8772 | ||
![]() |
c091b9d1ca | ||
![]() |
691cb603ce | ||
![]() |
e53724d6e5 | ||
![]() |
e225407b03 | ||
![]() |
273cae2a98 | ||
![]() |
6b3b89f7bf | ||
![]() |
2d611a559a | ||
![]() |
6c5eba01c2 | ||
![]() |
f641797d10 | ||
![]() |
f92ff068f3 | ||
![]() |
b59ac47c8c | ||
![]() |
8030f3d62a | ||
![]() |
f8f623e3e4 | ||
![]() |
061839756c | ||
![]() |
1dcaa0c257 | ||
![]() |
ffd3909964 | ||
![]() |
3ddccdffce | ||
![]() |
929d4cc82a | ||
![]() |
4f1cd8a571 | ||
![]() |
f6b3656bb1 | ||
![]() |
74a816216e | ||
![]() |
4a093cf096 | ||
![]() |
68f9fccf3a | ||
![]() |
f276040ad0 | ||
![]() |
2f40593daf | ||
![]() |
0b6dbd49bb | ||
![]() |
eb07917c14 | ||
![]() |
217bc48001 | ||
![]() |
38cfab4a09 | ||
![]() |
217e5e90ff | ||
![]() |
4a37a989a0 | ||
![]() |
eb540b774d | ||
![]() |
26d03f9ad4 | ||
![]() |
650d61ba24 | ||
![]() |
366a44a992 |
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -2,7 +2,7 @@ name: Build and push Docker image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ released, prereleased ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-build-push:
|
setup-build-push:
|
||||||
@@ -33,7 +33,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image (Release)
|
||||||
|
if: "!github.event.release.prerelease"
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./docker
|
context: ./docker
|
||||||
@@ -45,3 +46,15 @@ jobs:
|
|||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Build and push Docker image (Prerelease)
|
||||||
|
if: "github.event.release.prerelease"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./docker
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -29,8 +29,6 @@ src/Zoraxy_*_*
|
|||||||
src/certs/*
|
src/certs/*
|
||||||
src/rules/*
|
src/rules/*
|
||||||
src/README.md
|
src/README.md
|
||||||
docker/ContainerTester.sh
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
src/mod/acme/test/stackoverflow.pem
|
src/mod/acme/test/stackoverflow.pem
|
||||||
/tools/dns_challenge_update/code-gen/acmedns
|
/tools/dns_challenge_update/code-gen/acmedns
|
||||||
/tools/dns_challenge_update/code-gen/lego
|
/tools/dns_challenge_update/code-gen/lego
|
||||||
@@ -41,11 +39,15 @@ src/sys.uuid
|
|||||||
src/zoraxy
|
src/zoraxy
|
||||||
src/log/
|
src/log/
|
||||||
|
|
||||||
|
|
||||||
# dev-tags
|
# dev-tags
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
/Entrypoint.sh
|
/Entrypoint.sh
|
||||||
|
|
||||||
|
# docker testing stuff
|
||||||
|
docker/test/
|
||||||
|
docker/container-builder.sh
|
||||||
|
docker/docker-compose.yaml
|
||||||
|
|
||||||
# plugins
|
# plugins
|
||||||
example/plugins/ztnc/ztnc.db
|
example/plugins/ztnc/ztnc.db
|
||||||
example/plugins/ztnc/authtoken.secret
|
example/plugins/ztnc/authtoken.secret
|
||||||
@@ -57,3 +59,5 @@ tmp
|
|||||||
sys.*
|
sys.*
|
||||||
www/html/index.html
|
www/html/index.html
|
||||||
*.exe
|
*.exe
|
||||||
|
/src/dist
|
||||||
|
|
||||||
|
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,3 +1,36 @@
|
|||||||
|
# v3.2.4 28 Jun 2025
|
||||||
|
|
||||||
|
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
|
||||||
|
|
||||||
|
|
||||||
|
+ Added Authentik support by [JokerQyou](https://github.com/tobychui/zoraxy/commits?author=JokerQyou)
|
||||||
|
+ Added pluginsystem and moved GAN and Zerotier to plugins
|
||||||
|
+ Add loopback detection [#573](https://github.com/tobychui/zoraxy/issues/573)
|
||||||
|
+ Fixed Dark theme not working with Advanced Option accordion [#591](https://github.com/tobychui/zoraxy/issues/591)
|
||||||
|
+ Update logger to include UserAgent by [Raithmir](https://github.com/Raithmir)
|
||||||
|
+ Fixed memory usage in UI [#600](https://github.com/tobychui/zoraxy/issues/600)
|
||||||
|
+ Added docker-compose.yml by [SamuelPalubaCZ](https://github.com/tobychui/zoraxy/commits?author=SamuelPalubaCZ)
|
||||||
|
+ Added more statistics for proxy hosts [#201](https://github.com/tobychui/zoraxy/issues/201) and [#608](https://github.com/tobychui/zoraxy/issues/608)
|
||||||
|
+ Fixed origin field in logs [#618](https://github.com/tobychui/zoraxy/issues/618)
|
||||||
|
+ Added FreeBSD support by Andreas Burri
|
||||||
|
+ Fixed HTTP proxy redirect [#626](https://github.com/tobychui/zoraxy/issues/626)
|
||||||
|
+ Fixed proxy handling #629](https://github.com/tobychui/zoraxy/issues/629)
|
||||||
|
+ Move Scope ID handling into CIDR check by [Nirostar](https://github.com/tobychui/zoraxy/commits?author=Nirostar)
|
||||||
|
+ Prevent the browser from filling the saved Zoraxy login account by [WHFo](https://github.com/tobychui/zoraxy/commits?author=WHFo)
|
||||||
|
+ Added port number and http proto to http proxy list link
|
||||||
|
+ Fixed headers for authelia by [james-d-elliott](https://github.com/tobychui/zoraxy/commits?author=james-d-elliott)
|
||||||
|
+ Refactored docker container list and UI improvements by [eyerrock](https://github.com/tobychui/zoraxy/commits?author=eyerrock)
|
||||||
|
+ Refactored Dockerfile by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||||
|
+ Added new HTTP proxy UI
|
||||||
|
+ Added inbound host name edit function
|
||||||
|
+ Added static web server option to disable listen to all interface
|
||||||
|
+ Merged SSO implementations (Oauth2) [#649](https://github.com/tobychui/zoraxy/pull/649)
|
||||||
|
+ Merged forward-auth optimization [#692(https://github.com/tobychui/zoraxy/pull/692)
|
||||||
|
+ Optimized SSO UI
|
||||||
|
+ Refactored docker image workflows by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||||
|
+ Added disable chunked transfer encoding checkbox (for upstreams that uses legacy HTTP implementations)
|
||||||
|
+ Bug fixes [#694](https://github.com/tobychui/zoraxy/issues/694), [#659](https://github.com/tobychui/zoraxy/issues/659) by [jemmy1794](https://github.com/tobychui/zoraxy/commits?author=jemmy1794), [#695](https://github.com/tobychui/zoraxy/issues/695)
|
||||||
|
|
||||||
# v3.1.9 1 Mar 2025
|
# v3.1.9 1 Mar 2025
|
||||||
|
|
||||||
+ Fixed netstat underflow bug
|
+ Fixed netstat underflow bug
|
||||||
|
@@ -200,6 +200,10 @@ Some section of Zoraxy are contributed by our amazing community and if you have
|
|||||||
|
|
||||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||||
|
|
||||||
|
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)
|
||||||
|
|
||||||
|
- Change Log [@Morethanevil](https://github.com/Morethanevil)
|
||||||
|
|
||||||
### Looking for Maintainer
|
### Looking for Maintainer
|
||||||
|
|
||||||
- ACME DNS Challenge Module
|
- ACME DNS Challenge Module
|
||||||
|
@@ -34,34 +34,18 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
|||||||
chmod 755 /usr/local/bin/zerotier-one
|
chmod 755 /usr/local/bin/zerotier-one
|
||||||
|
|
||||||
|
|
||||||
## Fetch plugin
|
|
||||||
FROM docker.io/golang:alpine AS fetch-plugin
|
|
||||||
|
|
||||||
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
RUN apk add --update --no-cache git
|
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/
|
|
||||||
|
|
||||||
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
|
|
||||||
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
|
|
||||||
## Main
|
## Main
|
||||||
FROM docker.io/golang:alpine
|
FROM docker.io/alpine:latest
|
||||||
|
|
||||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
RUN apk add --update --no-cache python3 sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||||
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
COPY --chmod=700 ./entrypoint.py /opt/zoraxy/
|
||||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
|
||||||
|
|
||||||
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||||
|
|
||||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
RUN mkdir -p /opt/zoraxy/plugin/ &&\
|
||||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
|
||||||
echo "tun" | tee -a /etc/modules
|
echo "tun" | tee -a /etc/modules
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/config/
|
WORKDIR /opt/zoraxy/config/
|
||||||
@@ -89,7 +73,7 @@ VOLUME [ "/opt/zoraxy/config/" ]
|
|||||||
|
|
||||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
ENTRYPOINT [ "python3", "-u", "/opt/zoraxy/entrypoint.py" ]
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
||||||
|
|
||||||
|
@@ -119,18 +119,14 @@ Or for Docker Compose:
|
|||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
Zoraxy includes a (experimental) store to download and use official plugins right from inside Zoraxy, no preparation required.
|
||||||
|
For those looking to use custom plugins, build your plugins and place them inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location).
|
||||||
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Plugins are currently experimental.
|
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
To build the Docker image:
|
To build the Docker image:
|
||||||
- Check out the repository/branch.
|
- Check out the repository/branch.
|
||||||
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
|
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
|
||||||
- Run the build command with `docker build -t zoraxy_build .`
|
- Run the build command with `docker build -t zoraxy_build .`
|
||||||
- You can now use the image `zoraxy_build`
|
- You can now use the image `zoraxy_build`
|
||||||
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
echo "Copying zoraxy_plugin to all mods..."
|
|
||||||
for dir in "$1"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Running go mod tidy and go build for all directories..."
|
|
||||||
for dir in "$1"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
cd "$dir" || exit 1
|
|
||||||
go mod tidy
|
|
||||||
go build
|
|
||||||
cd "$1" || exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
128
docker/entrypoint.py
Normal file
128
docker/entrypoint.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
zoraxy_proc = None
|
||||||
|
zerotier_proc = None
|
||||||
|
|
||||||
|
def getenv(key, default=None):
|
||||||
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
|
def run(command):
|
||||||
|
try:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Command failed: {command} - {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def popen(command):
|
||||||
|
proc = subprocess.Popen(command)
|
||||||
|
time.sleep(1)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
print(f"{command} exited early with code {proc.returncode}")
|
||||||
|
raise RuntimeError(f"Failed to start {command}")
|
||||||
|
return proc
|
||||||
|
|
||||||
|
def cleanup(_signum, _frame):
|
||||||
|
print("Shutdown signal received. Cleaning up...")
|
||||||
|
|
||||||
|
global zoraxy_proc, zerotier_proc
|
||||||
|
|
||||||
|
if zoraxy_proc and zoraxy_proc.poll() is None:
|
||||||
|
print("Terminating Zoraxy...")
|
||||||
|
zoraxy_proc.terminate()
|
||||||
|
|
||||||
|
if zerotier_proc and zerotier_proc.poll() is None:
|
||||||
|
print("Terminating ZeroTier-One...")
|
||||||
|
zerotier_proc.terminate()
|
||||||
|
|
||||||
|
if zoraxy_proc:
|
||||||
|
try:
|
||||||
|
zoraxy_proc.wait(timeout=8)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
zoraxy_proc.kill()
|
||||||
|
zoraxy_proc.wait()
|
||||||
|
|
||||||
|
if zerotier_proc:
|
||||||
|
try:
|
||||||
|
zerotier_proc.wait(timeout=8)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
zerotier_proc.kill()
|
||||||
|
zerotier_proc.wait()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink("/var/lib/zerotier-one")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to unlink ZeroTier socket: {e}")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def start_zerotier():
|
||||||
|
print("Starting ZeroTier...")
|
||||||
|
|
||||||
|
global zerotier_proc
|
||||||
|
|
||||||
|
config_dir = "/opt/zoraxy/config/zerotier/"
|
||||||
|
zt_path = "/var/lib/zerotier-one"
|
||||||
|
|
||||||
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
|
|
||||||
|
os.symlink(config_dir, zt_path, target_is_directory=True)
|
||||||
|
|
||||||
|
zerotier_proc = popen(["zerotier-one"])
|
||||||
|
|
||||||
|
def start_zoraxy():
|
||||||
|
print("Starting Zoraxy...")
|
||||||
|
|
||||||
|
global zoraxy_proc
|
||||||
|
|
||||||
|
zoraxy_args = [
|
||||||
|
"zoraxy",
|
||||||
|
f"-autorenew={getenv('AUTORENEW', '86400')}",
|
||||||
|
f"-cfgupgrade={getenv('CFGUPGRADE', 'true')}",
|
||||||
|
f"-db={getenv('DB', 'auto')}",
|
||||||
|
f"-docker={getenv('DOCKER', 'true')}",
|
||||||
|
f"-earlyrenew={getenv('EARLYRENEW', '30')}",
|
||||||
|
f"-fastgeoip={getenv('FASTGEOIP', 'false')}",
|
||||||
|
f"-mdns={getenv('MDNS', 'true')}",
|
||||||
|
f"-mdnsname={getenv('MDNSNAME', "''")}",
|
||||||
|
f"-noauth={getenv('NOAUTH', 'false')}",
|
||||||
|
f"-plugin={getenv('PLUGIN', '/opt/zoraxy/plugin/')}",
|
||||||
|
f"-port=:{getenv('PORT', '8000')}",
|
||||||
|
f"-sshlb={getenv('SSHLB', 'false')}",
|
||||||
|
f"-update_geoip={getenv('UPDATE_GEOIP', 'false')}",
|
||||||
|
f"-version={getenv('VERSION', 'false')}",
|
||||||
|
f"-webfm={getenv('WEBFM', 'true')}",
|
||||||
|
f"-webroot={getenv('WEBROOT', './www')}",
|
||||||
|
]
|
||||||
|
|
||||||
|
zoraxy_proc = popen(zoraxy_args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
signal.signal(signal.SIGTERM, cleanup)
|
||||||
|
signal.signal(signal.SIGINT, cleanup)
|
||||||
|
|
||||||
|
print("Updating CA certificates...")
|
||||||
|
run(["update-ca-certificates"])
|
||||||
|
|
||||||
|
print("Updating GeoIP data...")
|
||||||
|
run(["zoraxy", "-update_geoip=true"])
|
||||||
|
|
||||||
|
os.chdir("/opt/zoraxy/config/")
|
||||||
|
|
||||||
|
if getenv("ZEROTIER", "false") == "true":
|
||||||
|
start_zerotier()
|
||||||
|
|
||||||
|
start_zoraxy()
|
||||||
|
|
||||||
|
signal.pause()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
echo "Stop signal received. Shutting down..."
|
|
||||||
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
|
||||||
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
|
||||||
unlink /var/lib/zerotier-one/zerotier/
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup SIGTERM SIGINT TERM INT
|
|
||||||
|
|
||||||
update-ca-certificates && echo "CA certificates updated."
|
|
||||||
zoraxy -update_geoip=true && echo "GeoIP data updated ."
|
|
||||||
|
|
||||||
echo "Building plugins..."
|
|
||||||
cd /opt/zoraxy/plugin/ || exit 1
|
|
||||||
build_plugins "$PWD"
|
|
||||||
echo "Plugins built."
|
|
||||||
cd /opt/zoraxy/config/ || exit 1
|
|
||||||
|
|
||||||
if [ "$ZEROTIER" = "true" ]; then
|
|
||||||
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 &
|
|
||||||
zerotierpid=$!
|
|
||||||
echo "ZeroTier daemon started."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting Zoraxy..."
|
|
||||||
zoraxy \
|
|
||||||
-autorenew="$AUTORENEW" \
|
|
||||||
-cfgupgrade="$CFGUPGRADE" \
|
|
||||||
-db="$DB" \
|
|
||||||
-docker="$DOCKER" \
|
|
||||||
-earlyrenew="$EARLYRENEW" \
|
|
||||||
-fastgeoip="$FASTGEOIP" \
|
|
||||||
-mdns="$MDNS" \
|
|
||||||
-mdnsname="$MDNSNAME" \
|
|
||||||
-noauth="$NOAUTH" \
|
|
||||||
-plugin="$PLUGIN" \
|
|
||||||
-port=:"$PORT" \
|
|
||||||
-sshlb="$SSHLB" \
|
|
||||||
-update_geoip="$UPDATE_GEOIP" \
|
|
||||||
-version="$VERSION" \
|
|
||||||
-webfm="$WEBFM" \
|
|
||||||
-webroot="$WEBROOT" \
|
|
||||||
&
|
|
||||||
|
|
||||||
zoraxypid=$!
|
|
||||||
wait "$zoraxypid"
|
|
||||||
wait "$zerotierpid"
|
|
||||||
|
|
@@ -519,8 +519,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
|
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
|
||||||
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
|
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
|
||||||
<div class="item"><a href="" target="_blank">Offical Plugin List</a></div>
|
<div class="item"><a href="https://github.com/aroz-online/zoraxy-official-plugins" target="_blank">Offical Plugin List</a></div>
|
||||||
<div class="item"><a href="" target="_blank">Plugin Development Guide</a></div>
|
<div class="item"><a href="https://zoraxy.aroz.org/plugins/html/" target="_blank">Plugin Development Guide</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="three wide column">
|
<div class="three wide column">
|
||||||
|
@@ -318,7 +318,7 @@ If everything is correctly setup, you should see the following page when request
|
|||||||
|
|
||||||
Example terminal output for requesting `/foobar/*`:
|
Example terminal output for requesting `/foobar/*`:
|
||||||
|
|
||||||
```
|
```html
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
|
||||||
|
@@ -615,7 +615,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
|
|||||||
:
|
:
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
<pre><span class="ts-text is-code">[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
|
<pre><code class="language-html">[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
|
||||||
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
|
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
|
||||||
@@ -641,7 +641,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
|
|||||||
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
|
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
|
||||||
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
|
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
|
||||||
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
|
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
|
||||||
</span></pre>
|
</code></pre>
|
||||||
<div class="ts-divider has-top-spaced-large"></div>
|
<div class="ts-divider has-top-spaced-large"></div>
|
||||||
<p>
|
<p>
|
||||||
<p class="ts-text">
|
<p class="ts-text">
|
||||||
|
20
src/api.go
20
src/api.go
@@ -34,6 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
|||||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||||
|
authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig)
|
||||||
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
|
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
|
||||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||||
@@ -71,14 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
|||||||
|
|
||||||
// Register the APIs for TLS / SSL certificate management functions
|
// Register the APIs for TLS / SSL certificate management functions
|
||||||
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||||
|
//Global certificate settings
|
||||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
|
||||||
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
|
authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
|
||||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
|
||||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
//Certificate store functions
|
||||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
|
||||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload)
|
||||||
|
authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate)
|
||||||
|
authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains)
|
||||||
|
authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck)
|
||||||
|
authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove)
|
||||||
|
authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||||
@@ -193,6 +200,7 @@ func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
|
|||||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||||
|
authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface)
|
||||||
/* File Manager */
|
/* File Manager */
|
||||||
if *allowWebFileManager {
|
if *allowWebFileManager {
|
||||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||||
|
379
src/cert.go
379
src/cert.go
@@ -1,180 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if the default certificates is correctly setup
|
|
||||||
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
type CheckResult struct {
|
|
||||||
DefaultPubExists bool
|
|
||||||
DefaultPriExists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, pri := tlsCertManager.DefaultCertExistsSep()
|
|
||||||
js, _ := json.Marshal(CheckResult{
|
|
||||||
pub,
|
|
||||||
pri,
|
|
||||||
})
|
|
||||||
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a list of domains where the certificates covers
|
|
||||||
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
filenames, err := tlsCertManager.ListCertDomains()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showDate, _ := utils.GetPara(r, "date")
|
|
||||||
if showDate == "true" {
|
|
||||||
type CertInfo struct {
|
|
||||||
Domain string
|
|
||||||
LastModifiedDate string
|
|
||||||
ExpireDate string
|
|
||||||
RemainingDays int
|
|
||||||
UseDNS bool
|
|
||||||
}
|
|
||||||
|
|
||||||
results := []*CertInfo{}
|
|
||||||
|
|
||||||
for _, filename := range filenames {
|
|
||||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
|
||||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
|
||||||
fileInfo, err := os.Stat(certFilepath)
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
certExpireTime := "Unknown"
|
|
||||||
certBtyes, err := os.ReadFile(certFilepath)
|
|
||||||
expiredIn := 0
|
|
||||||
if err != nil {
|
|
||||||
//Unable to load this file
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
//Cert loaded. Check its expire time
|
|
||||||
block, _ := pem.Decode(certBtyes)
|
|
||||||
if block != nil {
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err == nil {
|
|
||||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
duration := cert.NotAfter.Sub(time.Now())
|
|
||||||
|
|
||||||
// Convert the duration to days
|
|
||||||
expiredIn = int(duration.Hours() / 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
|
||||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
|
||||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
|
||||||
if err == nil {
|
|
||||||
useDNSValidation = certInfo.UseDNS
|
|
||||||
}
|
|
||||||
|
|
||||||
thisCertInfo := CertInfo{
|
|
||||||
Domain: filename,
|
|
||||||
LastModifiedDate: modifiedTime,
|
|
||||||
ExpireDate: certExpireTime,
|
|
||||||
RemainingDays: expiredIn,
|
|
||||||
UseDNS: useDNSValidation,
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, &thisCertInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(results)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(js)
|
|
||||||
} else {
|
|
||||||
response, err := json.Marshal(filenames)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all certificates and map all their domains to the cert filename
|
|
||||||
func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
|
||||||
filenames, err := os.ReadDir("./conf/certs/")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
certnameToDomainMap := map[string]string{}
|
|
||||||
for _, filename := range filenames {
|
|
||||||
if filename.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
|
||||||
|
|
||||||
certBtyes, err := os.ReadFile(certFilepath)
|
|
||||||
if err != nil {
|
|
||||||
// Unable to load this file
|
|
||||||
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// Cert loaded. Check its expiry time
|
|
||||||
block, _ := pem.Decode(certBtyes)
|
|
||||||
if block != nil {
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err == nil {
|
|
||||||
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
|
||||||
for _, dnsName := range cert.DNSNames {
|
|
||||||
certnameToDomainMap[dnsName] = certname
|
|
||||||
}
|
|
||||||
certnameToDomainMap[cert.Subject.CommonName] = certname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requireCompact, _ := utils.GetPara(r, "compact")
|
|
||||||
if requireCompact == "true" {
|
|
||||||
result := make(map[string][]string)
|
|
||||||
|
|
||||||
for key, value := range certnameToDomainMap {
|
|
||||||
if _, ok := result[value]; !ok {
|
|
||||||
result[value] = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
result[value] = append(result[value], key)
|
|
||||||
}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(result)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(certnameToDomainMap)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle front-end toggling TLS mode
|
// Handle front-end toggling TLS mode
|
||||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
currentTlsSetting := true //Default to true
|
currentTlsSetting := true //Default to true
|
||||||
@@ -185,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodGet {
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
//Get the current status
|
//Get the current status
|
||||||
js, _ := json.Marshal(currentTlsSetting)
|
js, _ := json.Marshal(currentTlsSetting)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else if r.Method == http.MethodPost {
|
case http.MethodPost:
|
||||||
newState, err := utils.PostBool(r, "set")
|
newState, err := utils.PostBool(r, "set")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "new state not set or invalid")
|
utils.SendErrorResponse(w, "new state not set or invalid")
|
||||||
@@ -205,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||||
}
|
}
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
} else {
|
default:
|
||||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,144 +58,136 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
|||||||
js, _ := json.Marshal(reqLatestTLS)
|
js, _ := json.Marshal(reqLatestTLS)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else {
|
} else {
|
||||||
if newState == "true" {
|
switch newState {
|
||||||
|
case "true":
|
||||||
sysdb.Write("settings", "forceLatestTLS", true)
|
sysdb.Write("settings", "forceLatestTLS", true)
|
||||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||||
} else if newState == "false" {
|
case "false":
|
||||||
sysdb.Write("settings", "forceLatestTLS", false)
|
sysdb.Write("settings", "forceLatestTLS", false)
|
||||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||||
} else {
|
default:
|
||||||
utils.SendErrorResponse(w, "invalid state given")
|
utils.SendErrorResponse(w, "invalid state given")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle download of the selected certificate
|
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
||||||
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// get the certificate name
|
|
||||||
certname, err := utils.GetPara(r, "certname")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "invalid certname given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certname = filepath.Base(certname) //prevent path escape
|
|
||||||
|
|
||||||
// check if the cert exists
|
|
||||||
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
|
|
||||||
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
|
|
||||||
|
|
||||||
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
|
||||||
//Zip them and serve them via http download
|
|
||||||
seeking, _ := utils.GetBool(r, "seek")
|
|
||||||
if seeking {
|
|
||||||
//This request only check if the key exists. Do not provide download
|
|
||||||
utils.SendOK(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Serve both file in zip
|
|
||||||
zipTmpFolder := "./tmp/download"
|
|
||||||
os.MkdirAll(zipTmpFolder, 0775)
|
|
||||||
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
|
||||||
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
|
||||||
|
|
||||||
// Serve the zip file
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
|
||||||
http.ServeFile(w, r, zipFileName)
|
|
||||||
} else {
|
|
||||||
//Not both key exists
|
|
||||||
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle upload of the certificate
|
|
||||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// check if request method is POST
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the key type
|
|
||||||
keytype, err := utils.GetPara(r, "ktype")
|
|
||||||
overWriteFilename := ""
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the domain
|
// get the domain
|
||||||
domain, err := utils.GetPara(r, "domain")
|
domain, err := utils.GetPara(r, "domain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Assume localhost
|
utils.SendErrorResponse(w, "invalid domain given")
|
||||||
domain = "default"
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if keytype == "pub" {
|
// get the proxy rule, the pass in domain value must be root or matching domain
|
||||||
overWriteFilename = domain + ".pem"
|
proxyRule, err := dynamicProxyRouter.GetProxyEndpointById(domain, false)
|
||||||
} else if keytype == "pri" {
|
if err != nil {
|
||||||
overWriteFilename = domain + ".key"
|
//Try to resolve the domain via alias
|
||||||
|
proxyRule, err = dynamicProxyRouter.GetProxyEndpointByAlias(domain)
|
||||||
|
if err != nil {
|
||||||
|
//No matching rule found
|
||||||
|
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// list all the alias domains for this rule
|
||||||
|
allDomains := []string{proxyRule.RootOrMatchingDomain}
|
||||||
|
aliasDomains := []string{}
|
||||||
|
for _, alias := range proxyRule.MatchingDomainAlias {
|
||||||
|
if alias != "" {
|
||||||
|
aliasDomains = append(aliasDomains, alias)
|
||||||
|
allDomains = append(allDomains, alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve the domain
|
||||||
|
domainKeyPairs := map[string]string{}
|
||||||
|
for _, thisDomain := range allDomains {
|
||||||
|
pubkey, prikey, err := tlsCertManager.GetCertificateByHostname(thisDomain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Make sure pubkey and private key are not empty
|
||||||
|
if pubkey == "" || prikey == "" {
|
||||||
|
domainKeyPairs[thisDomain] = ""
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
//Store the key pair
|
||||||
return
|
keyname := strings.TrimSuffix(filepath.Base(pubkey), filepath.Ext(pubkey))
|
||||||
|
if keyname == "localhost" {
|
||||||
|
//Internal certs like localhost should not be used
|
||||||
|
//report as "fallback" key
|
||||||
|
keyname = "fallback certificate"
|
||||||
|
}
|
||||||
|
domainKeyPairs[thisDomain] = keyname
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse multipart form data
|
|
||||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get file from form data
|
//A domain must be UseDNSValidation if it is a wildcard domain or its alias is a wildcard domain
|
||||||
file, _, err := r.FormFile("file")
|
useDNSValidation := strings.HasPrefix(proxyRule.RootOrMatchingDomain, "*")
|
||||||
if err != nil {
|
for _, alias := range aliasDomains {
|
||||||
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
if strings.HasPrefix(alias, "*") || strings.HasPrefix(domain, "*") {
|
||||||
return
|
useDNSValidation = true
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// create file in upload directory
|
|
||||||
os.MkdirAll("./conf/certs", 0775)
|
|
||||||
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// copy file contents to destination file
|
|
||||||
_, err = io.Copy(f, file)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Update cert list
|
type CertInfo struct {
|
||||||
tlsCertManager.UpdateLoadedCertList()
|
Domain string `json:"domain"`
|
||||||
|
AliasDomains []string `json:"alias_domains"`
|
||||||
// send response
|
DomainKeyPair map[string]string `json:"domain_key_pair"`
|
||||||
fmt.Fprintln(w, "File upload successful!")
|
UseDNSValidation bool `json:"use_dns_validation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle cert remove
|
result := &CertInfo{
|
||||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
Domain: proxyRule.RootOrMatchingDomain,
|
||||||
|
AliasDomains: aliasDomains,
|
||||||
|
DomainKeyPair: domainKeyPairs,
|
||||||
|
UseDNSValidation: useDNSValidation,
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(result)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//Get the domain
|
||||||
domain, err := utils.PostPara(r, "domain")
|
domain, err := utils.PostPara(r, "domain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "invalid domain given")
|
utils.SendErrorResponse(w, "invalid domain given")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = tlsCertManager.RemoveCert(domain)
|
|
||||||
|
//Get the certificate name
|
||||||
|
certName, err := utils.PostPara(r, "certname")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, "invalid certificate name given")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Load the target endpoint
|
||||||
|
ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set the preferred certificate for the domain
|
||||||
|
err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SaveReverseProxyConfig(ept)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
|
"imuslab.com/zoraxy/mod/tlscert"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,12 +60,18 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
|||||||
thisConfigEndpoint.Tags = []string{}
|
thisConfigEndpoint.Tags = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Make sure the TLS options are not nil
|
||||||
|
if thisConfigEndpoint.TlsOptions == nil {
|
||||||
|
thisConfigEndpoint.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
|
||||||
|
}
|
||||||
|
|
||||||
//Matching domain not set. Assume root
|
//Matching domain not set. Assume root
|
||||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
switch thisConfigEndpoint.ProxyType {
|
||||||
|
case dynamicproxy.ProxyTypeRoot:
|
||||||
//This is a root config file
|
//This is a root config file
|
||||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
|||||||
|
|
||||||
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||||
|
|
||||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
|
case dynamicproxy.ProxyTypeHost:
|
||||||
//This is a host config file
|
//This is a host config file
|
||||||
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,7 +88,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
||||||
} else {
|
default:
|
||||||
return errors.New("not supported proxy type")
|
return errors.New("not supported proxy type")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +108,9 @@ func filterProxyConfigFilename(filename string) string {
|
|||||||
|
|
||||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||||
//Get filename for saving
|
//Get filename for saving
|
||||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
filename := filepath.Join(CONF_HTTP_PROXY, endpoint.RootOrMatchingDomain+".config")
|
||||||
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||||
filename = "./conf/proxy/root.config"
|
filename = filepath.Join(CONF_HTTP_PROXY, "root.config")
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = filterProxyConfigFilename(filename)
|
filename = filterProxyConfigFilename(filename)
|
||||||
@@ -118,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RemoveReverseProxyConfig(endpoint string) error {
|
func RemoveReverseProxyConfig(endpoint string) error {
|
||||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config")
|
||||||
if endpoint == "/" {
|
if endpoint == "/" {
|
||||||
filename = "./conf/proxy/root.config"
|
filename = filepath.Join(CONF_HTTP_PROXY, "/root.config")
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = filterProxyConfigFilename(filename)
|
filename = filterProxyConfigFilename(filename)
|
||||||
@@ -172,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Specify the folder path to be zipped
|
// Specify the folder path to be zipped
|
||||||
if !utils.FileExists("./conf") {
|
if !utils.FileExists(CONF_FOLDER) {
|
||||||
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
folderPath := "./conf"
|
folderPath := CONF_FOLDER
|
||||||
|
|
||||||
// Set the Content-Type header to indicate it's a zip file
|
// Set the Content-Type header to indicate it's a zip file
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
@@ -277,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Create the target directory to unzip the files
|
// Create the target directory to unzip the files
|
||||||
targetDir := "./conf"
|
targetDir := CONF_FOLDER
|
||||||
if utils.FileExists(targetDir) {
|
if utils.FileExists(targetDir) {
|
||||||
//Backup the old config to old
|
//Backup the old config to old
|
||||||
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
||||||
//os.Rename(*path_conf, backupPath)
|
//os.Rename(*path_conf, backupPath)
|
||||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
os.Rename(CONF_FOLDER, CONF_FOLDER+".old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(targetDir, os.ModePerm)
|
err = os.MkdirAll(targetDir, os.ModePerm)
|
||||||
|
23
src/def.go
23
src/def.go
@@ -44,7 +44,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.2.3"
|
SYSTEM_VERSION = "3.2.5"
|
||||||
DEVELOPMENT_BUILD = false
|
DEVELOPMENT_BUILD = false
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
@@ -63,14 +63,19 @@ const (
|
|||||||
LOG_EXTENSION = ".log"
|
LOG_EXTENSION = ".log"
|
||||||
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
||||||
|
|
||||||
/* Configuration Folder Storage Path Constants */
|
/*
|
||||||
CONF_HTTP_PROXY = "./conf/proxy"
|
Configuration Folder Storage Path Constants
|
||||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
Note: No tailing slash in the path
|
||||||
CONF_CERT_STORE = "./conf/certs"
|
*/
|
||||||
CONF_REDIRECTION = "./conf/redirect"
|
CONF_FOLDER = "./conf"
|
||||||
CONF_ACCESS_RULE = "./conf/access"
|
CONF_HTTP_PROXY = CONF_FOLDER + "/proxy"
|
||||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
CONF_STREAM_PROXY = CONF_FOLDER + "/streamproxy"
|
||||||
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
|
CONF_CERT_STORE = CONF_FOLDER + "/certs"
|
||||||
|
CONF_REDIRECTION = CONF_FOLDER + "/redirect"
|
||||||
|
CONF_ACCESS_RULE = CONF_FOLDER + "/access"
|
||||||
|
CONF_PATH_RULE = CONF_FOLDER + "/rules/pathrules"
|
||||||
|
CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json"
|
||||||
|
CONF_GEODB_PATH = CONF_FOLDER + "/geodb"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* System Startup Flags */
|
/* System Startup Flags */
|
||||||
|
39
src/go.mod
39
src/go.mod
@@ -18,33 +18,16 @@ require (
|
|||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/monperrus/crawler-user-agents v1.1.0
|
github.com/monperrus/crawler-user-agents v1.1.0
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1
|
github.com/shirou/gopsutil/v4 v4.25.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/syndtr/goleveldb v1.0.0
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
golang.org/x/net v0.33.0
|
golang.org/x/net v0.33.0
|
||||||
|
golang.org/x/oauth2 v0.24.0
|
||||||
golang.org/x/text v0.21.0
|
golang.org/x/text v0.21.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.13.0 // indirect
|
cloud.google.com/go/auth v0.13.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.6 // 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/ebitengine/purego v0.8.2 // indirect
|
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
|
||||||
github.com/peterhellberg/link v1.2.0 // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
|
||||||
github.com/shopspring/decimal v1.3.1 // indirect
|
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
|
||||||
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||||
@@ -53,6 +36,7 @@ require (
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.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/dns/armdns v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||||
@@ -82,6 +66,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
|
||||||
github.com/aws/smithy-go v1.22.1 // indirect
|
github.com/aws/smithy-go v1.22.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
@@ -94,6 +79,7 @@ require (
|
|||||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
@@ -102,11 +88,14 @@ require (
|
|||||||
github.com/go-jose/go-jose/v4 v4.0.4 // 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/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.16.2 // indirect
|
github.com/go-resty/resty/v2 v2.16.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.8 // indirect
|
github.com/google/s2a-go v0.1.8 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||||
@@ -119,6 +108,7 @@ require (
|
|||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
@@ -155,29 +145,36 @@ require (
|
|||||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||||
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/pquerna/otp v1.4.0 // indirect
|
github.com/pquerna/otp v1.4.0 // indirect
|
||||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
github.com/sacloud/go-http v0.1.8 // indirect
|
github.com/sacloud/go-http v0.1.8 // indirect
|
||||||
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||||
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
|
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
|
||||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
|
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.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 v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||||
@@ -187,8 +184,8 @@ require (
|
|||||||
go.uber.org/ratelimit v0.3.0 // indirect
|
go.uber.org/ratelimit v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/oauth2 v0.24.0 // indirect
|
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.28.0 // indirect
|
golang.org/x/tools v0.28.0 // indirect
|
||||||
google.golang.org/api v0.214.0 // indirect
|
google.golang.org/api v0.214.0 // indirect
|
||||||
|
@@ -37,6 +37,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
if *geoDbUpdate {
|
if *geoDbUpdate {
|
||||||
geodb.DownloadGeoDBUpdate("./conf/geodb")
|
geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,13 @@ func main() {
|
|||||||
//Start the finalize sequences
|
//Start the finalize sequences
|
||||||
finalSequence()
|
finalSequence()
|
||||||
|
|
||||||
|
if strings.HasPrefix(*webUIPort, ":") {
|
||||||
|
//Bind to all interfaces, issue #672
|
||||||
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
|
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
|
||||||
|
} else {
|
||||||
|
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://" + *webUIPort)
|
||||||
|
}
|
||||||
|
|
||||||
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -11,6 +11,7 @@ const (
|
|||||||
DatabaseKeyResponseHeaders = "responseHeaders"
|
DatabaseKeyResponseHeaders = "responseHeaders"
|
||||||
DatabaseKeyResponseClientHeaders = "responseClientHeaders"
|
DatabaseKeyResponseClientHeaders = "responseClientHeaders"
|
||||||
DatabaseKeyRequestHeaders = "requestHeaders"
|
DatabaseKeyRequestHeaders = "requestHeaders"
|
||||||
|
DatabaseKeyRequestIncludedCookies = "requestIncludedCookies"
|
||||||
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
|
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
|
||||||
|
|
||||||
HeaderXForwardedProto = "X-Forwarded-Proto"
|
HeaderXForwardedProto = "X-Forwarded-Proto"
|
||||||
|
@@ -3,7 +3,6 @@ package forward
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -28,6 +27,10 @@ type AuthRouterOptions struct {
|
|||||||
// headers are copied.
|
// headers are copied.
|
||||||
RequestHeaders []string
|
RequestHeaders []string
|
||||||
|
|
||||||
|
// RequestIncludedCookies is a list of cookie keys that if defined will be the only cookies sent in the request to
|
||||||
|
// the authorization server.
|
||||||
|
RequestIncludedCookies []string
|
||||||
|
|
||||||
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
|
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
|
||||||
RequestExcludedCookies []string
|
RequestExcludedCookies []string
|
||||||
|
|
||||||
@@ -47,17 +50,28 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
|||||||
//Read settings from database if available.
|
//Read settings from database if available.
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
|
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
|
||||||
|
|
||||||
responseHeaders, responseClientHeaders, requestHeaders, requestExcludedCookies := "", "", "", ""
|
responseHeaders, responseClientHeaders, requestHeaders, requestIncludedCookies, requestExcludedCookies := "", "", "", "", ""
|
||||||
|
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, &responseHeaders)
|
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, &responseHeaders)
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyResponseClientHeaders, &responseClientHeaders)
|
options.Database.Read(DatabaseTable, DatabaseKeyResponseClientHeaders, &responseClientHeaders)
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
||||||
|
|
||||||
options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
// Helper function to clean empty strings from split results
|
||||||
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
cleanSplit := func(s string) []string {
|
||||||
options.RequestHeaders = strings.Split(requestHeaders, ",")
|
if s == "" {
|
||||||
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Split(s, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
options.ResponseHeaders = cleanSplit(responseHeaders)
|
||||||
|
options.ResponseClientHeaders = cleanSplit(responseClientHeaders)
|
||||||
|
options.RequestHeaders = cleanSplit(requestHeaders)
|
||||||
|
options.RequestIncludedCookies = cleanSplit(requestIncludedCookies)
|
||||||
|
options.RequestExcludedCookies = cleanSplit(requestExcludedCookies)
|
||||||
|
|
||||||
return &AuthRouter{
|
return &AuthRouter{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
@@ -82,11 +96,12 @@ func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
|
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
|
||||||
js, _ := json.Marshal(map[string]interface{}{
|
js, _ := json.Marshal(map[string]any{
|
||||||
DatabaseKeyAddress: ar.options.Address,
|
DatabaseKeyAddress: ar.options.Address,
|
||||||
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
|
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
|
||||||
DatabaseKeyResponseClientHeaders: ar.options.ResponseClientHeaders,
|
DatabaseKeyResponseClientHeaders: ar.options.ResponseClientHeaders,
|
||||||
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
|
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
|
||||||
|
DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies,
|
||||||
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
|
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,6 +123,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
|||||||
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
|
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
|
||||||
responseClientHeaders, _ := utils.PostPara(r, DatabaseKeyResponseClientHeaders)
|
responseClientHeaders, _ := utils.PostPara(r, DatabaseKeyResponseClientHeaders)
|
||||||
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
|
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
|
||||||
|
requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies)
|
||||||
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
|
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
|
||||||
|
|
||||||
// Write changes to runtime
|
// Write changes to runtime
|
||||||
@@ -115,6 +131,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
|||||||
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||||
ar.options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
ar.options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
||||||
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
|
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
|
||||||
|
ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
|
||||||
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
||||||
|
|
||||||
// Write changes to database
|
// Write changes to database
|
||||||
@@ -122,6 +139,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
|
|||||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
|
||||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseClientHeaders, responseClientHeaders)
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseClientHeaders, responseClientHeaders)
|
||||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies)
|
||||||
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
|
||||||
|
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
@@ -136,60 +154,40 @@ func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *ht
|
|||||||
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
|
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
|
||||||
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
|
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
|
||||||
if ar.options.Address == "" {
|
if ar.options.Address == "" {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
return ar.handle500Error(w, nil, "Address not set")
|
||||||
|
|
||||||
ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil)
|
|
||||||
|
|
||||||
return ErrInternalServerError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a request to Authz Server to verify the request
|
// Make a request to Authz Server to verify the request
|
||||||
|
// TODO: Add opt-in support for copying the request body to the forward auth request. Currently it's just an
|
||||||
|
// empty body which is usually fine in most instances. It's likely best to see if anyone wants this feature
|
||||||
|
// as I'm unaware of any specific forward auth implementation that needs it.
|
||||||
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
|
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
return ar.handle500Error(w, err, "Unable to create request")
|
||||||
|
|
||||||
ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err)
|
|
||||||
|
|
||||||
return ErrInternalServerError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add opt-in support for copying the request body to the forward auth request.
|
|
||||||
headerCopyIncluded(r.Header, req.Header, ar.options.RequestHeaders, true)
|
headerCopyIncluded(r.Header, req.Header, ar.options.RequestHeaders, true)
|
||||||
|
headerCookieRedact(r, ar.options.RequestIncludedCookies, false)
|
||||||
|
|
||||||
// TODO: Add support for upstream headers.
|
// TODO: Add support for headers from upstream proxies. This will likely involve implementing some form of
|
||||||
|
// proxy specific trust system within Zoraxy.
|
||||||
rSetForwardedHeaders(r, req)
|
rSetForwardedHeaders(r, req)
|
||||||
|
|
||||||
// Make the Authz Request.
|
// Make the Authz Request.
|
||||||
respForwarded, err := ar.client.Do(req)
|
respForwarded, err := ar.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
return ar.handle500Error(w, err, "Unable to perform forwarded auth due to a request error")
|
||||||
|
|
||||||
ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err)
|
|
||||||
|
|
||||||
return ErrInternalServerError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer respForwarded.Body.Close()
|
defer respForwarded.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(respForwarded.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err)
|
|
||||||
|
|
||||||
return ErrInternalServerError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
|
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
|
||||||
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
|
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
|
||||||
if len(ar.options.ResponseClientHeaders) != 0 {
|
if len(ar.options.ResponseClientHeaders) != 0 {
|
||||||
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseClientHeaders, false)
|
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseClientHeaders, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ar.options.RequestExcludedCookies) != 0 {
|
headerCookieRedact(r, ar.options.RequestExcludedCookies, true)
|
||||||
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
|
|
||||||
headerCookieRedact(r, ar.options.RequestExcludedCookies)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ar.options.ResponseHeaders) != 0 {
|
if len(ar.options.ResponseHeaders) != 0 {
|
||||||
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
|
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
|
||||||
@@ -197,138 +195,32 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
|
|||||||
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders, false)
|
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the request to the proxy for forwarding to the backend.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the response.
|
// Copy the unsuccessful response.
|
||||||
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
|
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
|
||||||
|
|
||||||
w.WriteHeader(respForwarded.StatusCode)
|
w.WriteHeader(respForwarded.StatusCode)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(respForwarded.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ar.handle500Error(w, err, "Unable to read response to forward auth request")
|
||||||
|
}
|
||||||
|
|
||||||
if _, err = w.Write(body); err != nil {
|
if _, err = w.Write(body); err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
return ar.handle500Error(w, err, "Unable to write response")
|
||||||
|
|
||||||
ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err)
|
|
||||||
|
|
||||||
return ErrInternalServerError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
func scheme(r *http.Request) string {
|
// handle500Error is func intended on factorizing a commonly repeated functional flow within this provider.
|
||||||
if r.TLS != nil {
|
func (ar *AuthRouter) handle500Error(w http.ResponseWriter, err error, message string) error {
|
||||||
return "https"
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
|
||||||
|
|
||||||
return "http"
|
ar.options.Logger.PrintAndLog(LogTitle, message, err)
|
||||||
}
|
|
||||||
|
|
||||||
func headerCookieRedact(r *http.Request, excluded []string) {
|
return ErrInternalServerError
|
||||||
original := r.Cookies()
|
|
||||||
|
|
||||||
if len(original) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookies []string
|
|
||||||
|
|
||||||
for _, cookie := range original {
|
|
||||||
if stringInSlice(cookie.Name, excluded) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies = append(cookies, cookie.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Header.Set(HeaderCookie, strings.Join(cookies, "; "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
|
|
||||||
for key, values := range original {
|
|
||||||
// We should never copy the headers in the below list.
|
|
||||||
if stringInSliceFold(key, doNotCopyHeaders) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if stringInSliceFold(key, excludedHeaders) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
destination[key] = append(destination[key], values...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
|
|
||||||
if allIfEmpty && len(includedHeaders) == 0 {
|
|
||||||
headerCopyAll(original, destination)
|
|
||||||
} else {
|
|
||||||
headerCopyIncludedExact(original, destination, includedHeaders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerCopyAll(original, destination http.Header) {
|
|
||||||
for key, values := range original {
|
|
||||||
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
|
||||||
if stringInSliceFold(key, doNotCopyHeaders) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
destination[key] = append(destination[key], values...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
|
|
||||||
for _, key := range keys {
|
|
||||||
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
|
||||||
if stringInSliceFold(key, doNotCopyHeaders) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if values, ok := original[key]; ok {
|
|
||||||
destination[key] = append(destination[key], values...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringInSlice(needle string, haystack []string) bool {
|
|
||||||
if len(haystack) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range haystack {
|
|
||||||
if needle == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringInSliceFold(needle string, haystack []string) bool {
|
|
||||||
if len(haystack) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range haystack {
|
|
||||||
if strings.EqualFold(needle, v) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func rSetForwardedHeaders(r, req *http.Request) {
|
|
||||||
if r.RemoteAddr != "" {
|
|
||||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
|
||||||
|
|
||||||
if ip := net.ParseIP(before); ip != nil {
|
|
||||||
req.Header.Set(HeaderXForwardedFor, ip.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set(HeaderXForwardedMethod, r.Method)
|
|
||||||
req.Header.Set(HeaderXForwardedProto, scheme(r))
|
|
||||||
req.Header.Set(HeaderXForwardedHost, r.Host)
|
|
||||||
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
|
|
||||||
}
|
}
|
||||||
|
137
src/mod/auth/sso/forward/util.go
Normal file
137
src/mod/auth/sso/forward/util.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package forward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func scheme(r *http.Request) string {
|
||||||
|
if r.TLS != nil {
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCookieRedact(r *http.Request, names []string, exclude bool) {
|
||||||
|
if len(names) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
original := r.Cookies()
|
||||||
|
|
||||||
|
if len(original) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []string
|
||||||
|
|
||||||
|
for _, cookie := range original {
|
||||||
|
if exclude && stringInSlice(cookie.Name, names) {
|
||||||
|
continue
|
||||||
|
} else if !exclude && !stringInSlice(cookie.Name, names) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies = append(cookies, cookie.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.Join(cookies, "; ")
|
||||||
|
|
||||||
|
r.Header.Set(HeaderCookie, value)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
|
||||||
|
for key, values := range original {
|
||||||
|
// We should never copy the headers in the below list.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if stringInSliceFold(key, excludedHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
|
||||||
|
if allIfEmpty && len(includedHeaders) == 0 {
|
||||||
|
headerCopyAll(original, destination)
|
||||||
|
} else {
|
||||||
|
headerCopyIncludedExact(original, destination, includedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyAll(original, destination http.Header) {
|
||||||
|
for key, values := range original {
|
||||||
|
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
|
||||||
|
for key, values := range original {
|
||||||
|
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringInSliceFold(key, keys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringInSlice(needle string, haystack []string) bool {
|
||||||
|
if len(haystack) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range haystack {
|
||||||
|
if needle == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringInSliceFold(needle string, haystack []string) bool {
|
||||||
|
if len(haystack) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range haystack {
|
||||||
|
if strings.EqualFold(needle, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func rSetForwardedHeaders(r, req *http.Request) {
|
||||||
|
if r.RemoteAddr != "" {
|
||||||
|
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||||
|
|
||||||
|
if ip := net.ParseIP(before); ip != nil {
|
||||||
|
req.Header.Set(HeaderXForwardedFor, ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(HeaderXForwardedMethod, r.Method)
|
||||||
|
req.Header.Set(HeaderXForwardedProto, scheme(r))
|
||||||
|
req.Header.Set(HeaderXForwardedHost, r.Host)
|
||||||
|
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
|
||||||
|
}
|
217
src/mod/auth/sso/forward/util_test.go
Normal file
217
src/mod/auth/sso/forward/util_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package forward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScheme(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have *http.Request
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldHandleDefault",
|
||||||
|
&http.Request{},
|
||||||
|
"http",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleExplicit",
|
||||||
|
&http.Request{
|
||||||
|
TLS: nil,
|
||||||
|
},
|
||||||
|
"http",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleHTTPS",
|
||||||
|
&http.Request{
|
||||||
|
TLS: &tls.ConnectionState{},
|
||||||
|
},
|
||||||
|
"https",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected, scheme(tc.have))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderCookieRedact(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have string
|
||||||
|
names []string
|
||||||
|
expectedInclude string
|
||||||
|
expectedExclude string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldHandleIncludeEmptyWithoutSettings",
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleIncludeEmptyWithSettings",
|
||||||
|
"",
|
||||||
|
[]string{"include"},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleValueWithoutSettings",
|
||||||
|
"include=value; exclude=value",
|
||||||
|
nil,
|
||||||
|
"include=value; exclude=value",
|
||||||
|
"include=value; exclude=value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleValueWithSettings",
|
||||||
|
"include=value; exclude=value",
|
||||||
|
[]string{"include"},
|
||||||
|
"include=value",
|
||||||
|
"exclude=value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var include, exclude *http.Request
|
||||||
|
|
||||||
|
include, exclude = &http.Request{Header: http.Header{}}, &http.Request{Header: http.Header{}}
|
||||||
|
|
||||||
|
if tc.have != "" {
|
||||||
|
include.Header.Set(HeaderCookie, tc.have)
|
||||||
|
exclude.Header.Set(HeaderCookie, tc.have)
|
||||||
|
}
|
||||||
|
|
||||||
|
headerCookieRedact(include, tc.names, false)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedInclude, include.Header.Get(HeaderCookie))
|
||||||
|
|
||||||
|
headerCookieRedact(exclude, tc.names, true)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedExclude, exclude.Header.Get(HeaderCookie))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderCopyExcluded(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
original http.Header
|
||||||
|
excluded []string
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldHandleNoSettingsNoHeaders",
|
||||||
|
http.Header{},
|
||||||
|
nil,
|
||||||
|
http.Header{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleNoSettingsWithHeaders",
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Exclude": []string{"value", "other"},
|
||||||
|
HeaderUpgrade: []string{"do", "not", "copy"},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Exclude": []string{"value", "other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleSettingsWithHeaders",
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Exclude": []string{"value", "other"},
|
||||||
|
HeaderUpgrade: []string{"do", "not", "copy"},
|
||||||
|
},
|
||||||
|
[]string{"exclude"},
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
headers := http.Header{}
|
||||||
|
|
||||||
|
headerCopyExcluded(tc.original, headers, tc.excluded)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, headers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderCopyIncluded(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
original http.Header
|
||||||
|
included []string
|
||||||
|
expected http.Header
|
||||||
|
expectedAll http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldHandleNoSettingsNoHeaders",
|
||||||
|
http.Header{},
|
||||||
|
nil,
|
||||||
|
http.Header{},
|
||||||
|
http.Header{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleNoSettingsWithHeaders",
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Include": []string{"value", "other"},
|
||||||
|
HeaderUpgrade: []string{"do", "not", "copy"},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
http.Header{},
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Include": []string{"value", "other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldHandleSettingsWithHeaders",
|
||||||
|
http.Header{
|
||||||
|
"Example": []string{"value", "other"},
|
||||||
|
"Include": []string{"value", "other"},
|
||||||
|
HeaderUpgrade: []string{"do", "not", "copy"},
|
||||||
|
},
|
||||||
|
[]string{"include"},
|
||||||
|
http.Header{
|
||||||
|
"Include": []string{"value", "other"},
|
||||||
|
},
|
||||||
|
http.Header{
|
||||||
|
"Include": []string{"value", "other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
headers := http.Header{}
|
||||||
|
|
||||||
|
headerCopyIncluded(tc.original, headers, tc.included, false)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, headers)
|
||||||
|
|
||||||
|
headers = http.Header{}
|
||||||
|
|
||||||
|
headerCopyIncluded(tc.original, headers, tc.included, true)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedAll, headers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -4,13 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuth2RouterOptions struct {
|
type OAuth2RouterOptions struct {
|
||||||
@@ -250,7 +251,19 @@ func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request)
|
|||||||
cookie.SameSite = http.SameSiteLaxMode
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
}
|
}
|
||||||
w.Header().Add("Set-Cookie", cookie.String())
|
w.Header().Add("Set-Cookie", cookie.String())
|
||||||
|
|
||||||
|
//Fix for #695
|
||||||
|
location := strings.TrimPrefix(state, "/internal/")
|
||||||
|
//Check if the location starts with http:// or https://. if yes, this is full URL
|
||||||
|
decodedLocation, err := url.PathUnescape(location)
|
||||||
|
if err == nil && (strings.HasPrefix(decodedLocation, "http://") || strings.HasPrefix(decodedLocation, "https://")) {
|
||||||
|
//Redirect to the full URL
|
||||||
|
http.Redirect(w, r, decodedLocation, http.StatusTemporaryRedirect)
|
||||||
|
} else {
|
||||||
|
//Redirect to a relative path
|
||||||
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
return errors.New("authorized")
|
return errors.New("authorized")
|
||||||
}
|
}
|
||||||
unauthorized := false
|
unauthorized := false
|
||||||
|
@@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
sep := h.Parent.GetProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && !sep.Disabled {
|
if sep != nil && !sep.Disabled {
|
||||||
//Matching proxy rule found
|
//Matching proxy rule found
|
||||||
//Access Check (blacklist / whitelist)
|
//Access Check (blacklist / whitelist)
|
||||||
|
59
src/mod/dynamicproxy/certificate.go
Normal file
59
src/mod/dynamicproxy/certificate.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package dynamicproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/tlscert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (router *Router) ResolveHostSpecificTlsBehaviorForHostname(hostname string) (*tlscert.HostSpecificTlsBehavior, error) {
|
||||||
|
if hostname == "" {
|
||||||
|
return nil, errors.New("hostname cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
ept := router.GetProxyEndpointFromHostname(hostname)
|
||||||
|
if ept == nil {
|
||||||
|
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the endpoint has a specific TLS behavior
|
||||||
|
if ept.TlsOptions != nil {
|
||||||
|
imported := &tlscert.HostSpecificTlsBehavior{}
|
||||||
|
router.tlsBehaviorMutex.RLock()
|
||||||
|
// Deep copy the TlsOptions using JSON marshal/unmarshal
|
||||||
|
data, err := json.Marshal(ept.TlsOptions)
|
||||||
|
if err != nil {
|
||||||
|
router.tlsBehaviorMutex.RUnlock()
|
||||||
|
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||||
|
}
|
||||||
|
router.tlsBehaviorMutex.RUnlock()
|
||||||
|
if err := json.Unmarshal(data, imported); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||||
|
}
|
||||||
|
return imported, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router *Router) SetPreferredCertificateForDomain(ept *ProxyEndpoint, domain string, certName string) error {
|
||||||
|
if ept == nil || certName == "" {
|
||||||
|
return errors.New("endpoint and certificate name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the preferred certificate for the endpoint
|
||||||
|
if ept.TlsOptions == nil {
|
||||||
|
ept.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
|
||||||
|
}
|
||||||
|
|
||||||
|
router.tlsBehaviorMutex.Lock()
|
||||||
|
if ept.TlsOptions.PreferredCertificate == nil {
|
||||||
|
ept.TlsOptions.PreferredCertificate = make(map[string]string)
|
||||||
|
}
|
||||||
|
ept.TlsOptions.PreferredCertificate[domain] = certName
|
||||||
|
router.tlsBehaviorMutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -72,6 +72,7 @@ type ResponseRewriteRuleSet struct {
|
|||||||
/* Advance Usecase Options */
|
/* Advance Usecase Options */
|
||||||
HostHeaderOverwrite string //Force overwrite of request "Host" header (advanced usecase)
|
HostHeaderOverwrite string //Force overwrite of request "Host" header (advanced usecase)
|
||||||
NoRemoveHopByHop bool //Do not remove hop-by-hop headers (advanced usecase)
|
NoRemoveHopByHop bool //Do not remove hop-by-hop headers (advanced usecase)
|
||||||
|
DisableChunkedTransferEncoding bool //Disable chunked transfer encoding
|
||||||
|
|
||||||
/* System Information Payload */
|
/* System Information Payload */
|
||||||
Version string //Version number of Zoraxy, use for X-Proxy-By
|
Version string //Version number of Zoraxy, use for X-Proxy-By
|
||||||
@@ -287,7 +288,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
|
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
|
||||||
|
|
||||||
//Fix proxmox transfer encoding bug if detected Proxmox Cookie
|
//Fix proxmox transfer encoding bug if detected Proxmox Cookie
|
||||||
if domainsniff.IsProxmox(req) {
|
if rrr.DisableChunkedTransferEncoding || domainsniff.IsProxmox(req) {
|
||||||
outreq.TransferEncoding = []string{"identity"}
|
outreq.TransferEncoding = []string{"identity"}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +330,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
locationRewrite := res.Header.Get("Location")
|
locationRewrite := res.Header.Get("Location")
|
||||||
originLocation := res.Header.Get("Location")
|
originLocation := res.Header.Get("Location")
|
||||||
res.Header.Set("zr-origin-location", originLocation)
|
res.Header.Set("zr-origin-location", originLocation)
|
||||||
|
decodedOriginLocation, err := url.PathUnescape(originLocation)
|
||||||
|
if err == nil {
|
||||||
|
originLocation = decodedOriginLocation
|
||||||
|
}
|
||||||
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
|
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
|
||||||
//Full path
|
//Full path
|
||||||
//Replace the forwarded target with expected Host
|
//Replace the forwarded target with expected Host
|
||||||
|
@@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error {
|
|||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
sep := router.GetProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && sep.BypassGlobalTLS {
|
if sep != nil && sep.BypassGlobalTLS {
|
||||||
//Allow routing via non-TLS handler
|
//Allow routing via non-TLS handler
|
||||||
originalHostHeader := r.Host
|
originalHostHeader := r.Host
|
||||||
@@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
|||||||
hostname = r.Host
|
hostname = r.Host
|
||||||
}
|
}
|
||||||
hostname = strings.Split(hostname, ":")[0]
|
hostname = strings.Split(hostname, ":")[0]
|
||||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
|
||||||
return subdEndpoint != nil
|
return subdEndpoint != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
123
src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
Normal file
123
src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package permissionpolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Content Security Policy
|
||||||
|
|
||||||
|
This is a content security policy header modifier that changes
|
||||||
|
the request content security policy fields
|
||||||
|
|
||||||
|
author: tobychui
|
||||||
|
|
||||||
|
//TODO: intergrate this with the dynamic proxy module
|
||||||
|
*/
|
||||||
|
|
||||||
|
type ContentSecurityPolicy struct {
|
||||||
|
DefaultSrc []string `json:"default_src"`
|
||||||
|
ScriptSrc []string `json:"script_src"`
|
||||||
|
StyleSrc []string `json:"style_src"`
|
||||||
|
ImgSrc []string `json:"img_src"`
|
||||||
|
ConnectSrc []string `json:"connect_src"`
|
||||||
|
FontSrc []string `json:"font_src"`
|
||||||
|
ObjectSrc []string `json:"object_src"`
|
||||||
|
MediaSrc []string `json:"media_src"`
|
||||||
|
FrameSrc []string `json:"frame_src"`
|
||||||
|
WorkerSrc []string `json:"worker_src"`
|
||||||
|
ChildSrc []string `json:"child_src"`
|
||||||
|
ManifestSrc []string `json:"manifest_src"`
|
||||||
|
PrefetchSrc []string `json:"prefetch_src"`
|
||||||
|
FormAction []string `json:"form_action"`
|
||||||
|
FrameAncestors []string `json:"frame_ancestors"`
|
||||||
|
BaseURI []string `json:"base_uri"`
|
||||||
|
Sandbox []string `json:"sandbox"`
|
||||||
|
ReportURI []string `json:"report_uri"`
|
||||||
|
ReportTo []string `json:"report_to"`
|
||||||
|
UpgradeInsecureRequests bool `json:"upgrade_insecure_requests"`
|
||||||
|
BlockAllMixedContent bool `json:"block_all_mixed_content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultContentSecurityPolicy returns a ContentSecurityPolicy struct with default permissive settings
|
||||||
|
func GetDefaultContentSecurityPolicy() *ContentSecurityPolicy {
|
||||||
|
return &ContentSecurityPolicy{
|
||||||
|
DefaultSrc: []string{"*"},
|
||||||
|
ScriptSrc: []string{"*"},
|
||||||
|
StyleSrc: []string{"*"},
|
||||||
|
ImgSrc: []string{"*"},
|
||||||
|
ConnectSrc: []string{"*"},
|
||||||
|
FontSrc: []string{"*"},
|
||||||
|
ObjectSrc: []string{"*"},
|
||||||
|
MediaSrc: []string{"*"},
|
||||||
|
FrameSrc: []string{"*"},
|
||||||
|
WorkerSrc: []string{"*"},
|
||||||
|
ChildSrc: []string{"*"},
|
||||||
|
ManifestSrc: []string{"*"},
|
||||||
|
PrefetchSrc: []string{"*"},
|
||||||
|
FormAction: []string{"*"},
|
||||||
|
FrameAncestors: []string{"*"},
|
||||||
|
BaseURI: []string{"*"},
|
||||||
|
Sandbox: []string{},
|
||||||
|
ReportURI: []string{},
|
||||||
|
ReportTo: []string{},
|
||||||
|
UpgradeInsecureRequests: false,
|
||||||
|
BlockAllMixedContent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHeader converts a ContentSecurityPolicy struct into a CSP header key-value pair
|
||||||
|
func (csp *ContentSecurityPolicy) ToHeader() []string {
|
||||||
|
directives := []string{}
|
||||||
|
|
||||||
|
addDirective := func(name string, sources []string) {
|
||||||
|
if len(sources) > 0 {
|
||||||
|
directives = append(directives, name+" "+strings.Join(sources, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDirective("default-src", csp.DefaultSrc)
|
||||||
|
addDirective("script-src", csp.ScriptSrc)
|
||||||
|
addDirective("style-src", csp.StyleSrc)
|
||||||
|
addDirective("img-src", csp.ImgSrc)
|
||||||
|
addDirective("connect-src", csp.ConnectSrc)
|
||||||
|
addDirective("font-src", csp.FontSrc)
|
||||||
|
addDirective("object-src", csp.ObjectSrc)
|
||||||
|
addDirective("media-src", csp.MediaSrc)
|
||||||
|
addDirective("frame-src", csp.FrameSrc)
|
||||||
|
addDirective("worker-src", csp.WorkerSrc)
|
||||||
|
addDirective("child-src", csp.ChildSrc)
|
||||||
|
addDirective("manifest-src", csp.ManifestSrc)
|
||||||
|
addDirective("prefetch-src", csp.PrefetchSrc)
|
||||||
|
addDirective("form-action", csp.FormAction)
|
||||||
|
addDirective("frame-ancestors", csp.FrameAncestors)
|
||||||
|
addDirective("base-uri", csp.BaseURI)
|
||||||
|
if len(csp.Sandbox) > 0 {
|
||||||
|
directives = append(directives, "sandbox "+strings.Join(csp.Sandbox, " "))
|
||||||
|
}
|
||||||
|
if len(csp.ReportURI) > 0 {
|
||||||
|
addDirective("report-uri", csp.ReportURI)
|
||||||
|
}
|
||||||
|
if len(csp.ReportTo) > 0 {
|
||||||
|
addDirective("report-to", csp.ReportTo)
|
||||||
|
}
|
||||||
|
if csp.UpgradeInsecureRequests {
|
||||||
|
directives = append(directives, "upgrade-insecure-requests")
|
||||||
|
}
|
||||||
|
if csp.BlockAllMixedContent {
|
||||||
|
directives = append(directives, "block-all-mixed-content")
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue := strings.Join(directives, "; ")
|
||||||
|
return []string{"Content-Security-Policy", headerValue}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InjectContentSecurityPolicyHeader injects the CSP header into the response
|
||||||
|
func InjectContentSecurityPolicyHeader(w http.ResponseWriter, csp *ContentSecurityPolicy) {
|
||||||
|
if csp == nil || w.Header().Get("Content-Security-Policy") != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headerKV := csp.ToHeader()
|
||||||
|
w.Header().Set(headerKV[0], headerKV[1])
|
||||||
|
}
|
@@ -11,7 +11,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
@@ -35,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
|
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
|
||||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||||
hostname = strings.ToLower(hostname)
|
hostname = strings.ToLower(hostname)
|
||||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||||
@@ -64,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Wildcard not match. Check for alias
|
//Wildcard not match. Check for alias
|
||||||
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
|
if len(ep.MatchingDomainAlias) > 0 {
|
||||||
for _, aliasDomain := range ep.MatchingDomainAlias {
|
for _, aliasDomain := range ep.MatchingDomainAlias {
|
||||||
match, err := filepath.Match(aliasDomain, hostname)
|
match, err := filepath.Match(aliasDomain, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,6 +192,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
PathPrefix: "",
|
PathPrefix: "",
|
||||||
UpstreamHeaders: upstreamHeaders,
|
UpstreamHeaders: upstreamHeaders,
|
||||||
DownstreamHeaders: downstreamHeaders,
|
DownstreamHeaders: downstreamHeaders,
|
||||||
|
DisableChunkedTransferEncoding: target.DisableChunkedTransferEncoding,
|
||||||
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
|
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
|
||||||
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
|
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
|
||||||
Version: target.parent.Option.HostVersion,
|
Version: target.parent.Option.HostVersion,
|
||||||
@@ -244,8 +244,8 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain)
|
h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain)
|
||||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||||
SkipTLSValidation: target.SkipCertValidations,
|
SkipTLSValidation: target.SkipCertValidations,
|
||||||
SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
|
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
|
||||||
CopyAllHeaders: domainsniff.RequireWebsocketHeaderCopy(r), //Left this as default to prevent nginx user setting / as vdir
|
CopyAllHeaders: target.parent.EnableWebsocketCustomHeaders, //Left this as default to prevent nginx user setting / as vdir
|
||||||
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
|
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
|
||||||
Logger: h.Parent.Option.Logger,
|
Logger: h.Parent.Option.Logger,
|
||||||
})
|
})
|
||||||
@@ -286,6 +286,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
PathPrefix: target.MatchingPath,
|
PathPrefix: target.MatchingPath,
|
||||||
UpstreamHeaders: upstreamHeaders,
|
UpstreamHeaders: upstreamHeaders,
|
||||||
DownstreamHeaders: downstreamHeaders,
|
DownstreamHeaders: downstreamHeaders,
|
||||||
|
DisableChunkedTransferEncoding: target.parent.DisableChunkedTransferEncoding,
|
||||||
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
|
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
|
||||||
Version: target.parent.parent.Option.HostVersion,
|
Version: target.parent.parent.Option.HostVersion,
|
||||||
})
|
})
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -105,3 +106,49 @@ func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain str
|
|||||||
|
|
||||||
return targetEpt.Remove()
|
return targetEpt.Remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProxyEndpointById retrieves a proxy endpoint by its ID from the Router's ProxyEndpoints map.
|
||||||
|
// It returns the ProxyEndpoint if found, or an error if not found.
|
||||||
|
func (h *Router) GetProxyEndpointById(searchingDomain string, includeAlias bool) (*ProxyEndpoint, error) {
|
||||||
|
var found *ProxyEndpoint
|
||||||
|
h.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||||
|
proxy, ok := value.(*ProxyEndpoint)
|
||||||
|
if ok && (proxy.RootOrMatchingDomain == searchingDomain || (includeAlias && utils.StringInArray(proxy.MatchingDomainAlias, searchingDomain))) {
|
||||||
|
found = proxy
|
||||||
|
return false // stop iteration
|
||||||
|
}
|
||||||
|
return true // continue iteration
|
||||||
|
})
|
||||||
|
if found != nil {
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("proxy rule with given id not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Router) GetProxyEndpointByAlias(alias string) (*ProxyEndpoint, error) {
|
||||||
|
var found *ProxyEndpoint
|
||||||
|
h.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||||
|
proxy, ok := value.(*ProxyEndpoint)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
//Also check for wildcard aliases that matches the alias
|
||||||
|
for _, thisAlias := range proxy.MatchingDomainAlias {
|
||||||
|
if ok && thisAlias == alias {
|
||||||
|
found = proxy
|
||||||
|
return false // stop iteration
|
||||||
|
} else if ok && strings.HasPrefix(thisAlias, "*") {
|
||||||
|
//Check if the alias matches a wildcard alias
|
||||||
|
if strings.HasSuffix(alias, thisAlias[1:]) {
|
||||||
|
found = proxy
|
||||||
|
return false // stop iteration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true // continue iteration
|
||||||
|
})
|
||||||
|
if found != nil {
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("proxy rule with given alias not found")
|
||||||
|
}
|
||||||
|
@@ -78,13 +78,17 @@ type Router struct {
|
|||||||
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
|
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
|
||||||
Running bool //If the router is running
|
Running bool //If the router is running
|
||||||
Root *ProxyEndpoint //Root proxy endpoint, default site
|
Root *ProxyEndpoint //Root proxy endpoint, default site
|
||||||
|
|
||||||
|
/* Internals */
|
||||||
mux http.Handler //HTTP handler
|
mux http.Handler //HTTP handler
|
||||||
server *http.Server //HTTP server
|
server *http.Server //HTTP server
|
||||||
tlsListener net.Listener //TLS listener, handle SNI routing
|
|
||||||
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
||||||
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
|
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
|
||||||
|
|
||||||
|
tlsListener net.Listener //TLS listener, handle SNI routing
|
||||||
|
tlsBehaviorMutex sync.RWMutex //Mutex for tlsBehavior map
|
||||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||||
|
|
||||||
rateLimterStop chan bool //Stop channel for rate limiter
|
rateLimterStop chan bool //Stop channel for rate limiter
|
||||||
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
||||||
}
|
}
|
||||||
@@ -176,6 +180,7 @@ type ProxyEndpoint struct {
|
|||||||
|
|
||||||
//Inbound TLS/SSL Related
|
//Inbound TLS/SSL Related
|
||||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||||
|
TlsOptions *tlscert.HostSpecificTlsBehavior //TLS options for this endpoint, if nil, use global TLS options
|
||||||
|
|
||||||
//Virtual Directories
|
//Virtual Directories
|
||||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||||
@@ -194,6 +199,9 @@ type ProxyEndpoint struct {
|
|||||||
//Uptime Monitor
|
//Uptime Monitor
|
||||||
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
|
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
|
||||||
|
|
||||||
|
// Chunked Transfer Encoding
|
||||||
|
DisableChunkedTransferEncoding bool //Disable chunked transfer encoding for this endpoint
|
||||||
|
|
||||||
//Access Control
|
//Access Control
|
||||||
AccessFilterUUID string //Access filter ID
|
AccessFilterUUID string //Access filter ID
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ import (
|
|||||||
type SniffResult int
|
type SniffResult int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
|||||||
payload.rawRequest = r
|
payload.rawRequest = r
|
||||||
|
|
||||||
sniffResult := handler(&payload)
|
sniffResult := handler(&payload)
|
||||||
if sniffResult == SniffResultAccpet {
|
if sniffResult == SniffResultAccept {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK"))
|
w.Write([]byte("OK"))
|
||||||
} else {
|
} else {
|
||||||
|
@@ -47,6 +47,8 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||||
|
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
|
||||||
|
enableLogging, _ := utils.PostBool(r, "enableLogging")
|
||||||
|
|
||||||
//Create the target config
|
//Create the target config
|
||||||
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
|
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
|
||||||
@@ -56,6 +58,8 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
UseTCP: useTCP,
|
UseTCP: useTCP,
|
||||||
UseUDP: useUDP,
|
UseUDP: useUDP,
|
||||||
|
UseProxyProtocol: useProxyProtocol,
|
||||||
|
EnableLogging: enableLogging,
|
||||||
})
|
})
|
||||||
|
|
||||||
js, _ := json.Marshal(newConfigUUID)
|
js, _ := json.Marshal(newConfigUUID)
|
||||||
@@ -75,6 +79,8 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
|
|||||||
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
|
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
|
||||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||||
|
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
|
||||||
|
enableLogging, _ := utils.PostBool(r, "enableLogging")
|
||||||
|
|
||||||
newTimeoutStr, _ := utils.PostPara(r, "timeout")
|
newTimeoutStr, _ := utils.PostPara(r, "timeout")
|
||||||
newTimeout := -1
|
newTimeout := -1
|
||||||
@@ -86,8 +92,21 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new ProxyRuleUpdateConfig with the extracted parameters
|
||||||
|
newConfig := &ProxyRuleUpdateConfig{
|
||||||
|
InstanceUUID: configUUID,
|
||||||
|
NewName: newName,
|
||||||
|
NewListeningAddr: listenAddr,
|
||||||
|
NewProxyAddr: proxyAddr,
|
||||||
|
UseTCP: useTCP,
|
||||||
|
UseUDP: useUDP,
|
||||||
|
UseProxyProtocol: useProxyProtocol,
|
||||||
|
EnableLogging: enableLogging,
|
||||||
|
NewTimeout: newTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
// Call the EditConfig method to modify the configuration
|
// Call the EditConfig method to modify the configuration
|
||||||
err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
|
err = m.EditConfig(newConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
return
|
return
|
||||||
|
110
src/mod/streamproxy/instances.go
Normal file
110
src/mod/streamproxy/instances.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package streamproxy
|
||||||
|
|
||||||
|
/*
|
||||||
|
Instances.go
|
||||||
|
|
||||||
|
This file contains the methods to start, stop, and manage the proxy relay instances.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *ProxyRelayInstance) LogMsg(message string, originalError error) {
|
||||||
|
if !c.EnableLogging {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if originalError != nil {
|
||||||
|
log.Println(message, "error:", originalError)
|
||||||
|
} else {
|
||||||
|
log.Println(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a proxy if stopped
|
||||||
|
func (c *ProxyRelayInstance) Start() error {
|
||||||
|
if c.IsRunning() {
|
||||||
|
c.Running = true
|
||||||
|
return errors.New("proxy already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a stopChan to control the loop
|
||||||
|
tcpStopChan := make(chan bool)
|
||||||
|
udpStopChan := make(chan bool)
|
||||||
|
|
||||||
|
//Start the proxy service
|
||||||
|
if c.UseUDP {
|
||||||
|
c.udpStopChan = udpStopChan
|
||||||
|
go func() {
|
||||||
|
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
|
||||||
|
if err != nil {
|
||||||
|
if !c.UseTCP {
|
||||||
|
c.Running = false
|
||||||
|
c.udpStopChan = nil
|
||||||
|
c.parent.SaveConfigToDatabase()
|
||||||
|
}
|
||||||
|
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseTCP {
|
||||||
|
c.tcpStopChan = tcpStopChan
|
||||||
|
go func() {
|
||||||
|
//Default to transport mode
|
||||||
|
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||||
|
if err != nil {
|
||||||
|
c.Running = false
|
||||||
|
c.tcpStopChan = nil
|
||||||
|
c.parent.SaveConfigToDatabase()
|
||||||
|
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Successfully spawned off the proxy routine
|
||||||
|
c.Running = true
|
||||||
|
c.parent.SaveConfigToDatabase()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if a proxy config is running
|
||||||
|
func (c *ProxyRelayInstance) IsRunning() bool {
|
||||||
|
return c.tcpStopChan != nil || c.udpStopChan != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart a proxy config
|
||||||
|
func (c *ProxyRelayInstance) Restart() {
|
||||||
|
if c.IsRunning() {
|
||||||
|
c.Stop()
|
||||||
|
}
|
||||||
|
time.Sleep(3000 * time.Millisecond)
|
||||||
|
c.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop a running proxy if running
|
||||||
|
func (c *ProxyRelayInstance) Stop() {
|
||||||
|
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
||||||
|
|
||||||
|
if c.udpStopChan != nil {
|
||||||
|
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||||
|
c.udpStopChan <- true
|
||||||
|
c.udpStopChan = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.tcpStopChan != nil {
|
||||||
|
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
||||||
|
c.tcpStopChan <- true
|
||||||
|
c.tcpStopChan = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||||
|
c.Running = false
|
||||||
|
|
||||||
|
//Update the running status
|
||||||
|
c.parent.SaveConfigToDatabase()
|
||||||
|
}
|
@@ -8,7 +8,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
@@ -30,9 +29,25 @@ type ProxyRelayOptions struct {
|
|||||||
Timeout int
|
Timeout int
|
||||||
UseTCP bool
|
UseTCP bool
|
||||||
UseUDP bool
|
UseUDP bool
|
||||||
|
UseProxyProtocol bool
|
||||||
|
EnableLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyRelayConfig struct {
|
// ProxyRuleUpdateConfig is used to update the proxy rule config
|
||||||
|
type ProxyRuleUpdateConfig struct {
|
||||||
|
InstanceUUID string //The target instance UUID to update
|
||||||
|
NewName string //New name for the instance, leave empty for no change
|
||||||
|
NewListeningAddr string //New listening address, leave empty for no change
|
||||||
|
NewProxyAddr string //New proxy target address, leave empty for no change
|
||||||
|
UseTCP bool //Enable TCP proxy, default to false
|
||||||
|
UseUDP bool //Enable UDP proxy, default to false
|
||||||
|
UseProxyProtocol bool //Enable Proxy Protocol, default to false
|
||||||
|
EnableLogging bool //Enable Logging TCP/UDP Message, default to true
|
||||||
|
NewTimeout int //New timeout for the connection, leave -1 for no change
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyRelayInstance struct {
|
||||||
|
/* Runtime Config */
|
||||||
UUID string //A UUIDv4 representing this config
|
UUID string //A UUIDv4 representing this config
|
||||||
Name string //Name of the config
|
Name string //Name of the config
|
||||||
Running bool //Status, read only
|
Running bool //Status, read only
|
||||||
@@ -41,7 +56,11 @@ type ProxyRelayConfig struct {
|
|||||||
ProxyTargetAddr string //Proxy target address
|
ProxyTargetAddr string //Proxy target address
|
||||||
UseTCP bool //Enable TCP proxy
|
UseTCP bool //Enable TCP proxy
|
||||||
UseUDP bool //Enable UDP proxy
|
UseUDP bool //Enable UDP proxy
|
||||||
|
UseProxyProtocol bool //Enable Proxy Protocol
|
||||||
|
EnableLogging bool //Enable logging for ProxyInstance
|
||||||
Timeout int //Timeout for connection in sec
|
Timeout int //Timeout for connection in sec
|
||||||
|
|
||||||
|
/* Internal */
|
||||||
tcpStopChan chan bool //Stop channel for TCP listener
|
tcpStopChan chan bool //Stop channel for TCP listener
|
||||||
udpStopChan chan bool //Stop channel for UDP listener
|
udpStopChan chan bool //Stop channel for UDP listener
|
||||||
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
|
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
|
||||||
@@ -60,13 +79,14 @@ type Options struct {
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
//Config and stores
|
//Config and stores
|
||||||
Options *Options
|
Options *Options
|
||||||
Configs []*ProxyRelayConfig
|
Configs []*ProxyRelayInstance
|
||||||
|
|
||||||
//Realtime Statistics
|
//Realtime Statistics
|
||||||
Connections int //currently connected connect counts
|
Connections int //currently connected connect counts
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStreamProxy creates a new stream proxy manager with the given options
|
||||||
func NewStreamProxy(options *Options) (*Manager, error) {
|
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||||
if !utils.FileExists(options.ConfigStore) {
|
if !utils.FileExists(options.ConfigStore) {
|
||||||
err := os.MkdirAll(options.ConfigStore, 0775)
|
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||||
@@ -76,7 +96,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Load relay configs from db
|
//Load relay configs from db
|
||||||
previousRules := []*ProxyRelayConfig{}
|
previousRules := []*ProxyRelayInstance{}
|
||||||
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -89,7 +109,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
|
|||||||
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
|
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
thisRelayConfig := &ProxyRelayConfig{}
|
thisRelayConfig := &ProxyRelayInstance{}
|
||||||
err = json.Unmarshal(configBytes, thisRelayConfig)
|
err = json.Unmarshal(configBytes, thisRelayConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
|
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
|
||||||
@@ -142,6 +162,7 @@ func (m *Manager) logf(message string, originalError error) {
|
|||||||
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
|
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new proxy relay config with the given options
|
||||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||||
//Generate two zero value for atomic int64
|
//Generate two zero value for atomic int64
|
||||||
aAcc := atomic.Int64{}
|
aAcc := atomic.Int64{}
|
||||||
@@ -150,13 +171,15 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
|||||||
bAcc.Store(0)
|
bAcc.Store(0)
|
||||||
//Generate a new config from options
|
//Generate a new config from options
|
||||||
configUUID := uuid.New().String()
|
configUUID := uuid.New().String()
|
||||||
thisConfig := ProxyRelayConfig{
|
thisConfig := ProxyRelayInstance{
|
||||||
UUID: configUUID,
|
UUID: configUUID,
|
||||||
Name: config.Name,
|
Name: config.Name,
|
||||||
ListeningAddress: config.ListeningAddr,
|
ListeningAddress: config.ListeningAddr,
|
||||||
ProxyTargetAddr: config.ProxyAddr,
|
ProxyTargetAddr: config.ProxyAddr,
|
||||||
UseTCP: config.UseTCP,
|
UseTCP: config.UseTCP,
|
||||||
UseUDP: config.UseUDP,
|
UseUDP: config.UseUDP,
|
||||||
|
UseProxyProtocol: config.UseProxyProtocol,
|
||||||
|
EnableLogging: config.EnableLogging,
|
||||||
Timeout: config.Timeout,
|
Timeout: config.Timeout,
|
||||||
tcpStopChan: nil,
|
tcpStopChan: nil,
|
||||||
udpStopChan: nil,
|
udpStopChan: nil,
|
||||||
@@ -170,7 +193,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
|||||||
return configUUID
|
return configUUID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) {
|
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayInstance, error) {
|
||||||
// Find and return the config with the specified UUID
|
// Find and return the config with the specified UUID
|
||||||
for _, config := range m.Configs {
|
for _, config := range m.Configs {
|
||||||
if config.UUID == configUUID {
|
if config.UUID == configUUID {
|
||||||
@@ -181,32 +204,34 @@ func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Edit the config based on config UUID, leave empty for unchange fields
|
// Edit the config based on config UUID, leave empty for unchange fields
|
||||||
func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr string, newProxyAddr string, useTCP bool, useUDP bool, newTimeout int) error {
|
func (m *Manager) EditConfig(newConfig *ProxyRuleUpdateConfig) error {
|
||||||
// Find the config with the specified UUID
|
// Find the config with the specified UUID
|
||||||
foundConfig, err := m.GetConfigByUUID(configUUID)
|
foundConfig, err := m.GetConfigByUUID(newConfig.InstanceUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and update the fields
|
// Validate and update the fields
|
||||||
if newName != "" {
|
if newConfig.NewName != "" {
|
||||||
foundConfig.Name = newName
|
foundConfig.Name = newConfig.NewName
|
||||||
}
|
}
|
||||||
if newListeningAddr != "" {
|
if newConfig.NewListeningAddr != "" {
|
||||||
foundConfig.ListeningAddress = newListeningAddr
|
foundConfig.ListeningAddress = newConfig.NewListeningAddr
|
||||||
}
|
}
|
||||||
if newProxyAddr != "" {
|
if newConfig.NewProxyAddr != "" {
|
||||||
foundConfig.ProxyTargetAddr = newProxyAddr
|
foundConfig.ProxyTargetAddr = newConfig.NewProxyAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
foundConfig.UseTCP = useTCP
|
foundConfig.UseTCP = newConfig.UseTCP
|
||||||
foundConfig.UseUDP = useUDP
|
foundConfig.UseUDP = newConfig.UseUDP
|
||||||
|
foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol
|
||||||
|
foundConfig.EnableLogging = newConfig.EnableLogging
|
||||||
|
|
||||||
if newTimeout != -1 {
|
if newConfig.NewTimeout != -1 {
|
||||||
if newTimeout < 0 {
|
if newConfig.NewTimeout < 0 {
|
||||||
return errors.New("invalid timeout value given")
|
return errors.New("invalid timeout value given")
|
||||||
}
|
}
|
||||||
foundConfig.Timeout = newTimeout
|
foundConfig.Timeout = newConfig.NewTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SaveConfigToDatabase()
|
m.SaveConfigToDatabase()
|
||||||
@@ -215,12 +240,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
|||||||
if foundConfig.IsRunning() {
|
if foundConfig.IsRunning() {
|
||||||
foundConfig.Restart()
|
foundConfig.Restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the config from file by UUID
|
||||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||||
//Remove the config from file
|
|
||||||
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
|
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -250,91 +274,3 @@ func (m *Manager) SaveConfigToDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Config Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Start a proxy if stopped
|
|
||||||
func (c *ProxyRelayConfig) Start() error {
|
|
||||||
if c.IsRunning() {
|
|
||||||
c.Running = true
|
|
||||||
return errors.New("proxy already running")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a stopChan to control the loop
|
|
||||||
tcpStopChan := make(chan bool)
|
|
||||||
udpStopChan := make(chan bool)
|
|
||||||
|
|
||||||
//Start the proxy service
|
|
||||||
if c.UseUDP {
|
|
||||||
c.udpStopChan = udpStopChan
|
|
||||||
go func() {
|
|
||||||
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
|
|
||||||
if err != nil {
|
|
||||||
if !c.UseTCP {
|
|
||||||
c.Running = false
|
|
||||||
c.udpStopChan = nil
|
|
||||||
c.parent.SaveConfigToDatabase()
|
|
||||||
}
|
|
||||||
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.UseTCP {
|
|
||||||
c.tcpStopChan = tcpStopChan
|
|
||||||
go func() {
|
|
||||||
//Default to transport mode
|
|
||||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
|
||||||
if err != nil {
|
|
||||||
c.Running = false
|
|
||||||
c.tcpStopChan = nil
|
|
||||||
c.parent.SaveConfigToDatabase()
|
|
||||||
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Successfully spawned off the proxy routine
|
|
||||||
c.Running = true
|
|
||||||
c.parent.SaveConfigToDatabase()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if a proxy config is running
|
|
||||||
func (c *ProxyRelayConfig) IsRunning() bool {
|
|
||||||
return c.tcpStopChan != nil || c.udpStopChan != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart a proxy config
|
|
||||||
func (c *ProxyRelayConfig) Restart() {
|
|
||||||
if c.IsRunning() {
|
|
||||||
c.Stop()
|
|
||||||
}
|
|
||||||
time.Sleep(3000 * time.Millisecond)
|
|
||||||
c.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop a running proxy if running
|
|
||||||
func (c *ProxyRelayConfig) Stop() {
|
|
||||||
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
|
||||||
|
|
||||||
if c.udpStopChan != nil {
|
|
||||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
|
||||||
c.udpStopChan <- true
|
|
||||||
c.udpStopChan = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.tcpStopChan != nil {
|
|
||||||
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
|
||||||
c.tcpStopChan <- true
|
|
||||||
c.tcpStopChan = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
|
||||||
c.Running = false
|
|
||||||
|
|
||||||
//Update the running status
|
|
||||||
c.parent.SaveConfigToDatabase()
|
|
||||||
}
|
|
||||||
|
@@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
|
|||||||
stopChan := make(chan bool)
|
stopChan := make(chan bool)
|
||||||
|
|
||||||
// Create a ProxyRelayConfig with dummy values
|
// Create a ProxyRelayConfig with dummy values
|
||||||
config := &streamproxy.ProxyRelayConfig{
|
config := &streamproxy.ProxyRelayInstance{
|
||||||
Timeout: 1,
|
Timeout: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ package streamproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -30,31 +31,50 @@ func isValidPort(port string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
|
func (c *ProxyRelayInstance) connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
|
||||||
n, err := io.Copy(conn1, conn2)
|
n, err := io.Copy(conn1, conn2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accumulator.Add(n) //Add to accumulator
|
accumulator.Add(n) //Add to accumulator
|
||||||
conn1.Close()
|
conn1.Close()
|
||||||
log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
|
c.LogMsg("[←] close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]", nil)
|
||||||
//conn2.Close()
|
//conn2.Close()
|
||||||
//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
|
//c.LogMsg("[←] close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]", nil)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
|
func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error {
|
||||||
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
clientAddr, ok1 := src.RemoteAddr().(*net.TCPAddr)
|
||||||
|
proxyAddr, ok2 := src.LocalAddr().(*net.TCPAddr)
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return errors.New("invalid TCP address for proxy protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n",
|
||||||
|
clientAddr.IP.String(),
|
||||||
|
proxyAddr.IP.String(),
|
||||||
|
clientAddr.Port,
|
||||||
|
proxyAddr.Port)
|
||||||
|
|
||||||
|
_, err := dst.Write([]byte(header))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProxyRelayInstance) forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
|
||||||
|
msg := fmt.Sprintf("[+] start transmit. [%s],[%s] <-> [%s],[%s]",
|
||||||
|
conn1.LocalAddr().String(), conn1.RemoteAddr().String(),
|
||||||
|
conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||||
|
c.LogMsg(msg, nil)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
// wait tow goroutines
|
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go connCopy(conn1, conn2, &wg, aTob)
|
go c.connCopy(conn1, conn2, &wg, aTob)
|
||||||
go connCopy(conn2, conn1, &wg, bToa)
|
go c.connCopy(conn2, conn1, &wg, bToa)
|
||||||
//blocking when the wg is locked
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
|
func (c *ProxyRelayInstance) accept(listener net.Listener) (net.Conn, error) {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -65,13 +85,13 @@ func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
|
|||||||
if !c.parent.Options.AccessControlHandler(conn) {
|
if !c.parent.Options.AccessControlHandler(conn) {
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
|
c.LogMsg("[x] Connection from "+addr.IP.String()+" rejected by access control policy", nil)
|
||||||
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
|
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
|
c.LogMsg("[√] accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]", nil)
|
||||||
return conn, err
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startListener(address string) (net.Listener, error) {
|
func startListener(address string) (net.Listener, error) {
|
||||||
@@ -92,7 +112,7 @@ func startListener(address string) (net.Listener, error) {
|
|||||||
portA -> server
|
portA -> server
|
||||||
server -> portB
|
server -> portB
|
||||||
*/
|
*/
|
||||||
func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
|
func (c *ProxyRelayInstance) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
|
||||||
listenerStartingAddr := allowPort
|
listenerStartingAddr := allowPort
|
||||||
if isValidPort(allowPort) {
|
if isValidPort(allowPort) {
|
||||||
//number only, e.g. 8080
|
//number only, e.g. 8080
|
||||||
@@ -112,7 +132,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
|
|||||||
//Start stop handler
|
//Start stop handler
|
||||||
go func() {
|
go func() {
|
||||||
<-stopChan
|
<-stopChan
|
||||||
log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
|
c.LogMsg("[x] Received stop signal. Exiting Port to Host forwarder", nil)
|
||||||
server.Close()
|
server.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -129,18 +149,32 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func(targetAddress string) {
|
go func(targetAddress string) {
|
||||||
log.Println("[+]", "start connect host:["+targetAddress+"]")
|
c.LogMsg("[+] start connect host:["+targetAddress+"]", nil)
|
||||||
target, err := net.Dial("tcp", targetAddress)
|
target, err := net.Dial("tcp", targetAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// temporarily unavailable, don't use fatal.
|
// temporarily unavailable, don't use fatal.
|
||||||
log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ")
|
c.LogMsg("[x] connect target address ["+targetAddress+"] failed. retry in "+strconv.Itoa(c.Timeout)+" seconds.", nil)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
|
c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil)
|
||||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
|
c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil)
|
||||||
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
|
||||||
|
if c.UseProxyProtocol {
|
||||||
|
c.LogMsg("[+] write proxy protocol header to target address ["+targetAddress+"]", nil)
|
||||||
|
err = writeProxyProtocolHeaderV1(target, conn)
|
||||||
|
if err != nil {
|
||||||
|
c.LogMsg("[x] Write proxy protocol header failed: "+err.Error(), nil)
|
||||||
|
target.Close()
|
||||||
|
conn.Close()
|
||||||
|
c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil)
|
||||||
|
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||||
}(targetAddress)
|
}(targetAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,7 @@ func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Go routine which manages connection from server to single client
|
// Go routine which manages connection from server to single client
|
||||||
func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
|
func (c *ProxyRelayInstance) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
|
||||||
var buffer [1500]byte
|
var buffer [1500]byte
|
||||||
for {
|
for {
|
||||||
// Read from server
|
// Read from server
|
||||||
@@ -74,7 +74,7 @@ func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lise
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close all connections that waiting for read from server
|
// Close all connections that waiting for read from server
|
||||||
func (c *ProxyRelayConfig) CloseAllUDPConnections() {
|
func (c *ProxyRelayInstance) CloseAllUDPConnections() {
|
||||||
c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
|
c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
|
||||||
conn := clientServerConn.(*udpClientServerConn)
|
conn := clientServerConn.(*udpClientServerConn)
|
||||||
conn.ServerConn.Close()
|
conn.ServerConn.Close()
|
||||||
@@ -82,7 +82,7 @@ func (c *ProxyRelayConfig) CloseAllUDPConnections() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error {
|
func (c *ProxyRelayInstance) ForwardUDP(address1, address2 string, stopChan chan bool) error {
|
||||||
//By default the incoming listen Address is int
|
//By default the incoming listen Address is int
|
||||||
//We need to add the loopback address into it
|
//We need to add the loopback address into it
|
||||||
if isValidPort(address1) {
|
if isValidPort(address1) {
|
||||||
@@ -90,8 +90,8 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
|
|||||||
address1 = ":" + address1
|
address1 = ":" + address1
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(address1, ":") {
|
if strings.HasPrefix(address1, ":") {
|
||||||
//Prepend 127.0.0.1 to the address
|
//Prepend 0.0.0.0 to the address
|
||||||
address1 = "127.0.0.1" + address1
|
address1 = "0.0.0.0" + address1
|
||||||
}
|
}
|
||||||
|
|
||||||
lisener, targetAddr, err := initUDPConnections(address1, address2)
|
lisener, targetAddr, err := initUDPConnections(address1, address2)
|
||||||
@@ -138,12 +138,12 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.udpClientMap.Store(saddr, conn)
|
c.udpClientMap.Store(saddr, conn)
|
||||||
log.Println("[UDP] Created new connection for client " + saddr)
|
c.LogMsg("[UDP] Created new connection for client "+saddr, nil)
|
||||||
// Fire up routine to manage new connection
|
// Fire up routine to manage new connection
|
||||||
go c.RunUDPConnectionRelay(conn, lisener)
|
go c.RunUDPConnectionRelay(conn, lisener)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.Println("[UDP] Found connection for client " + saddr)
|
c.LogMsg("[UDP] Found connection for client "+saddr, nil)
|
||||||
conn = rawConn.(*udpClientServerConn)
|
conn = rawConn.(*udpClientServerConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
93
src/mod/tlscert/certgen.go
Normal file
93
src/mod/tlscert/certgen.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package tlscert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateSelfSignedCertificate generates a self-signed ECDSA certificate and saves it to the specified files.
|
||||||
|
func (m *Manager) GenerateSelfSignedCertificate(cn string, sans []string, certFile string, keyFile string) error {
|
||||||
|
// Generate private key (ECDSA P-256)
|
||||||
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to generate private key", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate template
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cn, // Common Name for the certificate
|
||||||
|
Organization: []string{"aroz.org"}, // Organization name
|
||||||
|
OrganizationalUnit: []string{"Zoraxy"}, // Organizational Unit
|
||||||
|
Country: []string{"US"}, // Country code
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour), // valid for 1 year
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: sans, // Subject Alternative Names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create self-signed certificate
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to create certificate", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old certificate file if it exists
|
||||||
|
certPath := filepath.Join(m.CertStore, certFile)
|
||||||
|
if _, err := os.Stat(certPath); err == nil {
|
||||||
|
os.Remove(certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old key file if it exists
|
||||||
|
keyPath := filepath.Join(m.CertStore, keyFile)
|
||||||
|
if _, err := os.Stat(keyPath); err == nil {
|
||||||
|
os.Remove(keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write certificate to file
|
||||||
|
certOut, err := os.Create(filepath.Join(m.CertStore, certFile))
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to open cert file for writing: "+certFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer certOut.Close()
|
||||||
|
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to write certificate to file: "+certFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode private key to PEM
|
||||||
|
privBytes, err := x509.MarshalECPrivateKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Unable to marshal ECDSA private key", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyOut, err := os.Create(filepath.Join(m.CertStore, keyFile))
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to open key file for writing: "+keyFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer keyOut.Close()
|
||||||
|
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to write private key to file: "+keyFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Certificate and key generated: "+certFile+", "+keyFile, nil)
|
||||||
|
return nil
|
||||||
|
}
|
352
src/mod/tlscert/handler.go
Normal file
352
src/mod/tlscert/handler.go
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
package tlscert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle cert remove
|
||||||
|
func (m *Manager) HandleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid domain given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = m.RemoveCert(domain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle download of the selected certificate
|
||||||
|
func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// get the certificate name
|
||||||
|
certname, err := utils.GetPara(r, "certname")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid certname given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
certname = filepath.Base(certname) //prevent path escape
|
||||||
|
|
||||||
|
// check if the cert exists
|
||||||
|
pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key")
|
||||||
|
priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem")
|
||||||
|
|
||||||
|
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
||||||
|
//Zip them and serve them via http download
|
||||||
|
seeking, _ := utils.GetBool(r, "seek")
|
||||||
|
if seeking {
|
||||||
|
//This request only check if the key exists. Do not provide download
|
||||||
|
utils.SendOK(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serve both file in zip
|
||||||
|
zipTmpFolder := "./tmp/download"
|
||||||
|
os.MkdirAll(zipTmpFolder, 0775)
|
||||||
|
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
||||||
|
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
||||||
|
|
||||||
|
// Serve the zip file
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
http.ServeFile(w, r, zipFileName)
|
||||||
|
} else {
|
||||||
|
//Not both key exists
|
||||||
|
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle upload of the certificate
|
||||||
|
func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// check if request method is POST
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the key type
|
||||||
|
keytype, err := utils.GetPara(r, "ktype")
|
||||||
|
overWriteFilename := ""
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the domain
|
||||||
|
domain, err := utils.GetPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
//Assume localhost
|
||||||
|
domain = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keytype {
|
||||||
|
case "pub":
|
||||||
|
overWriteFilename = domain + ".pem"
|
||||||
|
case "pri":
|
||||||
|
overWriteFilename = domain + ".key"
|
||||||
|
default:
|
||||||
|
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse multipart form data
|
||||||
|
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get file from form data
|
||||||
|
file, _, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// create file in upload directory
|
||||||
|
os.MkdirAll(m.CertStore, 0775)
|
||||||
|
f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// copy file contents to destination file
|
||||||
|
_, err = io.Copy(f, file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update cert list
|
||||||
|
m.UpdateLoadedCertList()
|
||||||
|
|
||||||
|
// send response
|
||||||
|
fmt.Fprintln(w, "File upload successful!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all certificates and map all their domains to the cert filename
|
||||||
|
func (m *Manager) HandleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filenames, err := os.ReadDir(m.CertStore)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certnameToDomainMap := map[string]string{}
|
||||||
|
for _, filename := range filenames {
|
||||||
|
if filename.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certFilepath := filepath.Join(m.CertStore, filename.Name())
|
||||||
|
|
||||||
|
certBtyes, err := os.ReadFile(certFilepath)
|
||||||
|
if err != nil {
|
||||||
|
// Unable to load this file
|
||||||
|
m.Logger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Cert loaded. Check its expiry time
|
||||||
|
block, _ := pem.Decode(certBtyes)
|
||||||
|
if block != nil {
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
||||||
|
for _, dnsName := range cert.DNSNames {
|
||||||
|
certnameToDomainMap[dnsName] = certname
|
||||||
|
}
|
||||||
|
certnameToDomainMap[cert.Subject.CommonName] = certname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requireCompact, _ := utils.GetPara(r, "compact")
|
||||||
|
if requireCompact == "true" {
|
||||||
|
result := make(map[string][]string)
|
||||||
|
|
||||||
|
for key, value := range certnameToDomainMap {
|
||||||
|
if _, ok := result[value]; !ok {
|
||||||
|
result[value] = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
result[value] = append(result[value], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(result)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(certnameToDomainMap)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a list of domains where the certificates covers
|
||||||
|
func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filenames, err := m.ListCertDomains()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showDate, _ := utils.GetBool(r, "date")
|
||||||
|
if showDate {
|
||||||
|
type CertInfo struct {
|
||||||
|
Domain string
|
||||||
|
LastModifiedDate string
|
||||||
|
ExpireDate string
|
||||||
|
RemainingDays int
|
||||||
|
UseDNS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []*CertInfo{}
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
certFilepath := filepath.Join(m.CertStore, filename+".pem")
|
||||||
|
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||||
|
fileInfo, err := os.Stat(certFilepath)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
certExpireTime := "Unknown"
|
||||||
|
certBtyes, err := os.ReadFile(certFilepath)
|
||||||
|
expiredIn := 0
|
||||||
|
if err != nil {
|
||||||
|
//Unable to load this file
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
//Cert loaded. Check its expire time
|
||||||
|
block, _ := pem.Decode(certBtyes)
|
||||||
|
if block != nil {
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
duration := cert.NotAfter.Sub(time.Now())
|
||||||
|
|
||||||
|
// Convert the duration to days
|
||||||
|
expiredIn = int(duration.Hours() / 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
certInfoFilename := filepath.Join(m.CertStore, filename+".json")
|
||||||
|
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||||
|
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||||
|
if err == nil {
|
||||||
|
useDNSValidation = certInfo.UseDNS
|
||||||
|
}
|
||||||
|
|
||||||
|
thisCertInfo := CertInfo{
|
||||||
|
Domain: filename,
|
||||||
|
LastModifiedDate: modifiedTime,
|
||||||
|
ExpireDate: certExpireTime,
|
||||||
|
RemainingDays: expiredIn,
|
||||||
|
UseDNS: useDNSValidation,
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, &thisCertInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert ExpireDate to date object and sort asc
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
|
||||||
|
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
|
||||||
|
return date1.Before(date2)
|
||||||
|
})
|
||||||
|
|
||||||
|
js, _ := json.Marshal(results)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(js)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := json.Marshal(filenames)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the default certificates is correctly setup
|
||||||
|
func (m *Manager) HandleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type CheckResult struct {
|
||||||
|
DefaultPubExists bool
|
||||||
|
DefaultPriExists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, pri := m.DefaultCertExistsSep()
|
||||||
|
js, _ := json.Marshal(CheckResult{
|
||||||
|
pub,
|
||||||
|
pri,
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the common name from the request
|
||||||
|
cn, err := utils.GetPara(r, "cn")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Common name not provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domains, err := utils.PostPara(r, "domains")
|
||||||
|
if err != nil {
|
||||||
|
//No alias domains provided, use the common name as the only domain
|
||||||
|
domains = "[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
SANs := []string{}
|
||||||
|
if err := json.Unmarshal([]byte(domains), &SANs); err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid domains format: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//SANs = append([]string{cn}, SANs...)
|
||||||
|
priKeyFilename := domainToFilename(cn, ".key")
|
||||||
|
pubKeyFilename := domainToFilename(cn, ".pem")
|
||||||
|
|
||||||
|
// Generate self-signed certificate
|
||||||
|
err = m.GenerateSelfSignedCertificate(cn, SANs, pubKeyFilename, priKeyFilename)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to generate self-signed certificate: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the certificate store
|
||||||
|
err = m.UpdateLoadedCertList()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to update certificate store: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
@@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
|||||||
|
|
||||||
return matchingDomain
|
return matchingDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert a domain name to a filename format
|
||||||
|
func domainToFilename(domain string, ext string) string {
|
||||||
|
// Replace wildcard '*' with '_'
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
|
if strings.HasPrefix(domain, "*") {
|
||||||
|
domain = "_" + strings.TrimPrefix(domain, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add .pem extension
|
||||||
|
ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot
|
||||||
|
return domain + "." + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenameToDomain(filename string) string {
|
||||||
|
// Remove the extension
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
if ext != "" {
|
||||||
|
filename = strings.TrimSuffix(filename, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(filename, "_") {
|
||||||
|
filename = "*" + filename[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
@@ -20,17 +20,26 @@ type CertCache struct {
|
|||||||
PriKey string
|
PriKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HostSpecificTlsBehavior struct {
|
||||||
|
DisableSNI bool //If SNI is enabled for this server name
|
||||||
|
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
|
||||||
|
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
|
||||||
|
PreferredCertificate map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate
|
||||||
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
CertStore string //Path where all the certs are stored
|
CertStore string //Path where all the certs are stored
|
||||||
LoadedCerts []*CertCache //A list of loaded certs
|
LoadedCerts []*CertCache //A list of loaded certs
|
||||||
Logger *logger.Logger //System wide logger for debug mesage
|
Logger *logger.Logger //System wide logger for debug mesage
|
||||||
verbal bool
|
|
||||||
|
/* External handlers */
|
||||||
|
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed localhost.pem localhost.key
|
//go:embed localhost.pem localhost.key
|
||||||
var buildinCertStore embed.FS
|
var buildinCertStore embed.FS
|
||||||
|
|
||||||
func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) {
|
func NewManager(certStore string, logger *logger.Logger) (*Manager, error) {
|
||||||
if !utils.FileExists(certStore) {
|
if !utils.FileExists(certStore) {
|
||||||
os.MkdirAll(certStore, 0775)
|
os.MkdirAll(certStore, 0775)
|
||||||
}
|
}
|
||||||
@@ -52,7 +61,7 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
|||||||
thisManager := Manager{
|
thisManager := Manager{
|
||||||
CertStore: certStore,
|
CertStore: certStore,
|
||||||
LoadedCerts: []*CertCache{},
|
LoadedCerts: []*CertCache{},
|
||||||
verbal: verbal,
|
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +73,25 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
|||||||
return &thisManager, nil
|
return &thisManager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default host specific TLS behavior
|
||||||
|
// This is used when no specific TLS behavior is defined for a server name
|
||||||
|
func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior {
|
||||||
|
return &HostSpecificTlsBehavior{
|
||||||
|
DisableSNI: false,
|
||||||
|
DisableLegacyCertificateMatching: false,
|
||||||
|
EnableAutoHTTPS: false,
|
||||||
|
PreferredCertificate: map[string]string{}, // No preferred certificate, use the first matching certificate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior, error) {
|
||||||
|
return GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetHostSpecificTlsBehavior(fn func(serverName string) (*HostSpecificTlsBehavior, error)) {
|
||||||
|
m.hostSpecificTlsBehavior = fn
|
||||||
|
}
|
||||||
|
|
||||||
// Update domain mapping from file
|
// Update domain mapping from file
|
||||||
func (m *Manager) UpdateLoadedCertList() error {
|
func (m *Manager) UpdateLoadedCertList() error {
|
||||||
//Get a list of certificates from file
|
//Get a list of certificates from file
|
||||||
@@ -161,24 +189,11 @@ func (m *Manager) ListCerts() ([]string, error) {
|
|||||||
|
|
||||||
// Get a certificate from disk where its certificate matches with the helloinfo
|
// Get a certificate from disk where its certificate matches with the helloinfo
|
||||||
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
//Check if the domain corrisponding cert exists
|
//Look for the certificate by hostname
|
||||||
pubKey := "./tmp/localhost.pem"
|
pubKey, priKey, err := m.GetCertificateByHostname(helloInfo.ServerName)
|
||||||
priKey := "./tmp/localhost.key"
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to get certificate for "+helloInfo.ServerName, err)
|
||||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
return nil, err
|
||||||
//Direct hit
|
|
||||||
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
|
|
||||||
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
|
||||||
} else if m.CertMatchExists(helloInfo.ServerName) {
|
|
||||||
//Use x509
|
|
||||||
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
|
|
||||||
} else {
|
|
||||||
//Fallback to legacy method of matching certificates
|
|
||||||
if m.DefaultCertExists() {
|
|
||||||
//Use default.pem and default.key
|
|
||||||
pubKey = filepath.Join(m.CertStore, "default.pem")
|
|
||||||
priKey = filepath.Join(m.CertStore, "default.key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load the cert and serve it
|
//Load the cert and serve it
|
||||||
@@ -190,6 +205,55 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
|
|||||||
return &cer, nil
|
return &cer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCertificateByHostname returns the certificate and private key for a given hostname
|
||||||
|
func (m *Manager) GetCertificateByHostname(hostname string) (string, string, error) {
|
||||||
|
//Check if the domain corrisponding cert exists
|
||||||
|
pubKey := "./tmp/localhost.pem"
|
||||||
|
priKey := "./tmp/localhost.key"
|
||||||
|
|
||||||
|
tlsBehavior, err := m.hostSpecificTlsBehavior(hostname)
|
||||||
|
if err != nil {
|
||||||
|
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
|
||||||
|
}
|
||||||
|
preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname]
|
||||||
|
if !ok {
|
||||||
|
preferredCertificate = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsBehavior.DisableSNI && preferredCertificate != "" &&
|
||||||
|
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".pem")) &&
|
||||||
|
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".key")) {
|
||||||
|
//User setup a Preferred certificate, use the preferred certificate directly
|
||||||
|
pubKey = filepath.Join(m.CertStore, preferredCertificate+".pem")
|
||||||
|
priKey = filepath.Join(m.CertStore, preferredCertificate+".key")
|
||||||
|
} else {
|
||||||
|
if !tlsBehavior.DisableLegacyCertificateMatching &&
|
||||||
|
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&
|
||||||
|
utils.FileExists(filepath.Join(m.CertStore, hostname+".key")) {
|
||||||
|
//Legacy filename matching, use the file names directly
|
||||||
|
//This is the legacy method of matching certificates, it will match the file names directly
|
||||||
|
//This is used for compatibility with Zoraxy v2 setups
|
||||||
|
pubKey = filepath.Join(m.CertStore, hostname+".pem")
|
||||||
|
priKey = filepath.Join(m.CertStore, hostname+".key")
|
||||||
|
} else if !tlsBehavior.DisableSNI &&
|
||||||
|
m.CertMatchExists(hostname) {
|
||||||
|
//SNI scan match, find the first matching certificate
|
||||||
|
pubKey, priKey = m.GetCertByX509CNHostname(hostname)
|
||||||
|
} else if tlsBehavior.EnableAutoHTTPS {
|
||||||
|
//Get certificate from CA, WIP
|
||||||
|
//TODO: Implement AutoHTTPS
|
||||||
|
} else {
|
||||||
|
//Fallback to legacy method of matching certificates
|
||||||
|
if m.DefaultCertExists() {
|
||||||
|
//Use default.pem and default.key
|
||||||
|
pubKey = filepath.Join(m.CertStore, "default.pem")
|
||||||
|
priKey = filepath.Join(m.CertStore, "default.key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pubKey, priKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check if both the default cert public key and private key exists
|
// Check if both the default cert public key and private key exists
|
||||||
func (m *Manager) DefaultCertExists() bool {
|
func (m *Manager) DefaultCertExists() bool {
|
||||||
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
||||||
@@ -220,7 +284,6 @@ func (m *Manager) RemoveCert(domain string) error {
|
|||||||
|
|
||||||
//Update the cert list
|
//Update the cert list
|
||||||
m.UpdateLoadedCertList()
|
m.UpdateLoadedCertList()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ type StaticWebServerStatus struct {
|
|||||||
WebRoot string
|
WebRoot string
|
||||||
Running bool
|
Running bool
|
||||||
EnableWebDirManager bool
|
EnableWebDirManager bool
|
||||||
|
DisableListenToAllInterface bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle getting current static web server status
|
// Handle getting current static web server status
|
||||||
@@ -33,6 +34,7 @@ func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
WebRoot: ws.option.WebRoot,
|
WebRoot: ws.option.WebRoot,
|
||||||
Running: ws.isRunning,
|
Running: ws.isRunning,
|
||||||
EnableWebDirManager: ws.option.EnableWebDirManager,
|
EnableWebDirManager: ws.option.EnableWebDirManager,
|
||||||
|
DisableListenToAllInterface: ws.option.DisableListenToAllInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
js, _ := json.Marshal(currentStatus)
|
js, _ := json.Marshal(currentStatus)
|
||||||
@@ -67,6 +69,12 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if newPort is a valid TCP port number (1-65535)
|
||||||
|
if newPort < 1 || newPort > 65535 {
|
||||||
|
utils.SendErrorResponse(w, "invalid port number given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = ws.ChangePort(strconv.Itoa(newPort))
|
err = ws.ChangePort(strconv.Itoa(newPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, err.Error())
|
||||||
@@ -91,3 +99,30 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
|
|||||||
ws.option.EnableDirectoryListing = enableList
|
ws.option.EnableDirectoryListing = enableList
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get or set disable listen to all interface settings
|
||||||
|
func (ws *WebServer) SetDisableListenToAllInterface(w http.ResponseWriter, r *http.Request) {
|
||||||
|
disableListen, err := utils.PostBool(r, "disable")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid setting given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ws.option.Sysdb.Write("webserv", "disableListenToAllInterface", disableListen)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to save setting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the option in the web server instance
|
||||||
|
ws.option.DisableListenToAllInterface = disableListen
|
||||||
|
|
||||||
|
// If the server is running and the setting is changed, we need to restart the server
|
||||||
|
if ws.IsRunning() {
|
||||||
|
err = ws.Restart()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "unable to restart web server: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
@@ -25,11 +25,19 @@ import (
|
|||||||
//go:embed templates/*
|
//go:embed templates/*
|
||||||
var templates embed.FS
|
var templates embed.FS
|
||||||
|
|
||||||
|
/*
|
||||||
|
WebServerOptions define the default option for the webserv
|
||||||
|
might get override by user settings loaded from db
|
||||||
|
|
||||||
|
Any changes in here might need to also update the StaticWebServerStatus struct
|
||||||
|
in handler.go. See handler.go for more information.
|
||||||
|
*/
|
||||||
type WebServerOptions struct {
|
type WebServerOptions struct {
|
||||||
Port string //Port for listening
|
Port string //Port for listening
|
||||||
EnableDirectoryListing bool //Enable listing of directory
|
EnableDirectoryListing bool //Enable listing of directory
|
||||||
WebRoot string //Folder for stroing the static web folders
|
WebRoot string //Folder for stroing the static web folders
|
||||||
EnableWebDirManager bool //Enable web file manager to handle files in web directory
|
EnableWebDirManager bool //Enable web file manager to handle files in web directory
|
||||||
|
DisableListenToAllInterface bool // Disable listening to all interfaces, only listen to localhost
|
||||||
Logger *logger.Logger //System logger
|
Logger *logger.Logger //System logger
|
||||||
Sysdb *database.Database //Database for storing configs
|
Sysdb *database.Database //Database for storing configs
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,11 @@ func (ws *WebServer) RestorePreviousState() {
|
|||||||
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
|
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
|
||||||
ws.option.EnableDirectoryListing = enableDirList
|
ws.option.EnableDirectoryListing = enableDirList
|
||||||
|
|
||||||
|
//Set disable listen to all interface
|
||||||
|
disableListenToAll := ws.option.DisableListenToAllInterface
|
||||||
|
ws.option.Sysdb.Read("webserv", "disableListenToAllInterface", &disableListenToAll)
|
||||||
|
ws.option.DisableListenToAllInterface = disableListenToAll
|
||||||
|
|
||||||
//Check the running state
|
//Check the running state
|
||||||
webservRunning := true
|
webservRunning := true
|
||||||
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
||||||
@@ -156,8 +169,12 @@ func (ws *WebServer) Start() error {
|
|||||||
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
|
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
|
||||||
ws.mux.Handle("/", ws.fsMiddleware(fs))
|
ws.mux.Handle("/", ws.fsMiddleware(fs))
|
||||||
|
|
||||||
|
listenAddr := ":" + ws.option.Port
|
||||||
|
if ws.option.DisableListenToAllInterface {
|
||||||
|
listenAddr = "127.0.0.1:" + ws.option.Port
|
||||||
|
}
|
||||||
ws.server = &http.Server{
|
ws.server = &http.Server{
|
||||||
Addr: ":" + ws.option.Port,
|
Addr: listenAddr,
|
||||||
Handler: ws.mux,
|
Handler: ws.mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +210,27 @@ func (ws *WebServer) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) Restart() error {
|
||||||
|
if ws.isRunning {
|
||||||
|
if err := ws.Stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ws.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server restarted. Listening on :"+ws.option.Port, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) IsRunning() bool {
|
||||||
|
ws.mu.Lock()
|
||||||
|
defer ws.mu.Unlock()
|
||||||
|
return ws.isRunning
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateDirectoryListing enables or disables directory listing.
|
// UpdateDirectoryListing enables or disables directory listing.
|
||||||
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
|
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
|
||||||
ws.option.EnableDirectoryListing = enable
|
ws.option.EnableDirectoryListing = enable
|
||||||
|
@@ -15,6 +15,7 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
|
"imuslab.com/zoraxy/mod/tlscert"
|
||||||
"imuslab.com/zoraxy/mod/uptime"
|
"imuslab.com/zoraxy/mod/uptime"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@@ -134,12 +135,12 @@ func ReverseProxtInit() {
|
|||||||
Load all conf from files
|
Load all conf from files
|
||||||
|
|
||||||
*/
|
*/
|
||||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
confs, _ := filepath.Glob(CONF_HTTP_PROXY + "/*.config")
|
||||||
for _, conf := range confs {
|
for _, conf := range confs {
|
||||||
err := LoadReverseProxyConfig(conf)
|
err := LoadReverseProxyConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +335,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
tags = filteredTags
|
tags = filteredTags
|
||||||
|
|
||||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||||
if eptype == "host" {
|
switch eptype {
|
||||||
|
case "host":
|
||||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "hostname not defined")
|
utils.SendErrorResponse(w, "hostname not defined")
|
||||||
@@ -415,7 +417,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
|
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
|
||||||
proxyEndpointCreated = &thisProxyEndpoint
|
proxyEndpointCreated = &thisProxyEndpoint
|
||||||
} else if eptype == "root" {
|
case "root":
|
||||||
//Get the default site options and target
|
//Get the default site options and target
|
||||||
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
|
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -469,7 +471,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyEndpointCreated = &rootRoutingEndpoint
|
proxyEndpointCreated = &rootRoutingEndpoint
|
||||||
} else {
|
default:
|
||||||
//Invalid eptype
|
//Invalid eptype
|
||||||
utils.SendErrorResponse(w, "invalid endpoint type")
|
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||||
return
|
return
|
||||||
@@ -556,6 +558,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
proxyRateLimit = 1000
|
proxyRateLimit = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable chunked Encoding
|
||||||
|
disableChunkedEncoding, _ := utils.PostBool(r, "dChunkedEnc")
|
||||||
|
|
||||||
//Load the previous basic auth credentials from current proxy rules
|
//Load the previous basic auth credentials from current proxy rules
|
||||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -596,6 +601,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
newProxyEndpoint.RateLimit = proxyRateLimit
|
newProxyEndpoint.RateLimit = proxyRateLimit
|
||||||
newProxyEndpoint.UseStickySession = useStickySession
|
newProxyEndpoint.UseStickySession = useStickySession
|
||||||
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
|
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
|
||||||
|
newProxyEndpoint.DisableChunkedTransferEncoding = disableChunkedEncoding
|
||||||
newProxyEndpoint.Tags = tags
|
newProxyEndpoint.Tags = tags
|
||||||
|
|
||||||
//Prepare to replace the current routing rule
|
//Prepare to replace the current routing rule
|
||||||
@@ -673,6 +679,70 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
utils.SendErrorResponse(w, "Method not supported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rootnameOrMatchingDomain, err := utils.PostPara(r, "ep")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid ep given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig, err := utils.PostPara(r, "tlsConfig")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid TLS config given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig = strings.TrimSpace(tlsConfig)
|
||||||
|
if tlsConfig == "" {
|
||||||
|
utils.SendErrorResponse(w, "TLS config cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newTlsConfig := &tlscert.HostSpecificTlsBehavior{}
|
||||||
|
err = json.Unmarshal([]byte(tlsConfig), newTlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid TLS config given: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load the target endpoint
|
||||||
|
ept, err := dynamicProxyRouter.LoadProxy(rootnameOrMatchingDomain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if newTlsConfig.PreferredCertificate == nil {
|
||||||
|
//No update needed, reuse the current TLS config
|
||||||
|
newTlsConfig.PreferredCertificate = ept.TlsOptions.PreferredCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
ept.TlsOptions = newTlsConfig
|
||||||
|
|
||||||
|
//Prepare to replace the current routing rule
|
||||||
|
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(ept)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||||
|
|
||||||
|
//Save it to file
|
||||||
|
err = SaveReverseProxyConfig(ept)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to save TLS config: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
utils.SendErrorResponse(w, "Method not supported")
|
utils.SendErrorResponse(w, "Method not supported")
|
||||||
@@ -1011,6 +1081,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
|||||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
js, err := json.Marshal(dynamicProxyRouter)
|
js, err := json.Marshal(dynamicProxyRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
SystemWideLogger.PrintAndLog("proxy-config", "Unable to marshal status data", err)
|
||||||
utils.SendErrorResponse(w, "Unable to marshal status data")
|
utils.SendErrorResponse(w, "Unable to marshal status data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
@@ -99,7 +100,7 @@ func startupSequence() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Create a TLS certificate manager
|
//Create a TLS certificate manager
|
||||||
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger)
|
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -366,6 +367,9 @@ func finalSequence() {
|
|||||||
|
|
||||||
//Inject routing rules
|
//Inject routing rules
|
||||||
registerBuildInRoutingRules()
|
registerBuildInRoutingRules()
|
||||||
|
|
||||||
|
//Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname
|
||||||
|
tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shutdown Sequence */
|
/* Shutdown Sequence */
|
||||||
|
@@ -203,7 +203,7 @@
|
|||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>Virtual Directory</th>
|
<th>Virtual Directory</th>
|
||||||
<th class="no-sort">Tags</th>
|
<th class="no-sort">Tags</th>
|
||||||
<th class="no-sort" style="width:50px; cursor: default !important;"></th>
|
<th class="no-sort" style="width:100px; cursor: default !important;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="httpProxyList">
|
<tbody id="httpProxyList">
|
||||||
@@ -223,10 +223,10 @@
|
|||||||
<div id="httprpEditModalSideMenu" class="four wide column">
|
<div id="httprpEditModalSideMenu" class="four wide column">
|
||||||
<div class="ui secondary fluid vertical menu">
|
<div class="ui secondary fluid vertical menu">
|
||||||
<a class="active item hrpedit_menu_item" cfgpage="downstream">
|
<a class="active item hrpedit_menu_item" cfgpage="downstream">
|
||||||
<i class="angle double white right icon"></i> <span class="editorSideMenuText">Downstream</span>
|
<i class="home icon"></i> <span class="editorSideMenuText">Host</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="item hrpedit_menu_item" cfgpage="upstream">
|
<a class="item hrpedit_menu_item" cfgpage="upstream">
|
||||||
<i class="angle double left icon"></i> <span class="editorSideMenuText">Upstream</span>
|
<i class="server icon"></i> <span class="editorSideMenuText">Destinations</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="item hrpedit_menu_item" cfgpage="vdirs">
|
<a class="item hrpedit_menu_item" cfgpage="vdirs">
|
||||||
<i class="angle folder icon"></i> <span class="editorSideMenuText">Virtual Directory</span>
|
<i class="angle folder icon"></i> <span class="editorSideMenuText">Virtual Directory</span>
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="httprpEditModalContentWindow" class="twelve wide column">
|
<div id="httprpEditModalContentWindow" class="twelve wide column">
|
||||||
<div style="height:100%;">
|
<div style="height:100%;">
|
||||||
<!-- Downstream -->
|
<!-- Host -->
|
||||||
<div class="rpconfig_content" rpcfg="downstream">
|
<div class="rpconfig_content" rpcfg="downstream">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -288,10 +288,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upstream -->
|
<!-- Destinations -->
|
||||||
<div class="rpconfig_content" rpcfg="upstream">
|
<div class="rpconfig_content" rpcfg="upstream">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<div class="upstream_list">
|
<b>Enabled Upstreams</b>
|
||||||
|
<div class="upstream_list" style="margin-top: 0.4em;">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic compact button editUpstreamButton" style="margin-left: 0.4em; margin-top: 1em;"><i class="grey server icon"></i> Edit Upstreams</button>
|
<button class="ui basic compact button editUpstreamButton" style="margin-left: 0.4em; margin-top: 1em;"><i class="grey server icon"></i> Edit Upstreams</button>
|
||||||
@@ -308,17 +309,18 @@
|
|||||||
<small>Enable stick session on load balancing</small></label>
|
<small>Enable stick session on load balancing</small></label>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui disabled checkbox" style="margin-top: 0.4em;">
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
<input type="checkbox" class="DisableChunkedTransferEncoding">
|
<input type="checkbox" class="DisableChunkedTransferEncoding">
|
||||||
<label>Disable Chunked Transfer Encoding<br>
|
<label>Disable Chunked Transfer Encoding<br>
|
||||||
<small>Enable this option if your upstream uses a legacy HTTP server implementation</small></label>
|
<small>Enable this option if your upstream uses a legacy HTTP server implementation (e.g. Proxmox / opencloud)</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Virtual Directories-->
|
<!-- Virtual Directories-->
|
||||||
<div class="rpconfig_content" rpcfg="vdirs">
|
<div class="rpconfig_content" rpcfg="vdirs">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<div class="vdir_list">
|
<b>List of Virtual Directories</b>
|
||||||
|
<div class="vdir_list" style="margin-top:0.4em;">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
@@ -336,10 +338,45 @@
|
|||||||
<!-- TLS / SSL -->
|
<!-- TLS / SSL -->
|
||||||
<div class="rpconfig_content" rpcfg="ssl">
|
<div class="rpconfig_content" rpcfg="ssl">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<p>Work In Progress <br>
|
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
|
||||||
Please use the outer-most menu TLS / SSL tab for now. </p>
|
<div class="ui blue message sni_grey_out_info" style="margin-bottom: 1em; display:none;">
|
||||||
|
<i class="info circle icon"></i>
|
||||||
|
Certificate dropdowns are greyed out because SNI is enabled
|
||||||
|
</div>
|
||||||
|
<table class="ui celled small compact table sortable Tls_resolve_list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th class="no-sort">Resolve to Certificate</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Rows will be dynamically populated -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="Tls_EnableSNI">
|
||||||
|
<label>Enable SNI<br>
|
||||||
|
<small>Resolve Server Name Indication (SNI) and automatically select a certificate</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="Tls_EnableLegacyCertificateMatching">
|
||||||
|
<label>Enable Legacy Certificate Matching<br>
|
||||||
|
<small>Use filename for hostname matching, faster but less accurate</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui disabled checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="Tls_EnableAutoHTTPS">
|
||||||
|
<label>Enable Auto HTTPS (WIP)<br>
|
||||||
|
<small>Automatically request a certificate for the domain</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div class="ui divider"></div>
|
||||||
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
|
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
|
||||||
|
<button class="ui basic small button getSelfSignCertBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="yellow lock icon"></i> Generate Self-Signed Certificate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Custom Headers -->
|
<!-- Custom Headers -->
|
||||||
@@ -549,6 +586,28 @@
|
|||||||
aliasDomains += `</small><br>`;
|
aliasDomains += `</small><br>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build the sorting value
|
||||||
|
let destSortValue = subd.ActiveOrigins.map(o => {
|
||||||
|
// Check if it's an IP address (with optional port)
|
||||||
|
let upstreamAddr = o.OriginIpOrDomain;
|
||||||
|
let subpath = "";
|
||||||
|
if (upstreamAddr.indexOf("/") !== -1) {
|
||||||
|
let parts = upstreamAddr.split("/");
|
||||||
|
subpath = parts.slice(1).join("/");
|
||||||
|
upstreamAddr = parts[0];
|
||||||
|
}
|
||||||
|
let ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/;
|
||||||
|
if (ipPortRegex.test(upstreamAddr)) {
|
||||||
|
let [ip, port] = upstreamAddr.split(":");
|
||||||
|
// Convert IP to hex
|
||||||
|
let hexIp = ip.split('.').map(x => ('00' + parseInt(x).toString(16)).slice(-2)).join('');
|
||||||
|
let hexPort = port ? (port.length < 5 ? port.padStart(5, '0') : port) : '';
|
||||||
|
return hexIp + (hexPort ? ':' + hexPort : '') + "/" + subpath;
|
||||||
|
}
|
||||||
|
// Otherwise, treat it as a domain name
|
||||||
|
return upstreamAddr;
|
||||||
|
}).join(",");
|
||||||
|
|
||||||
//Build tag list
|
//Build tag list
|
||||||
let tagList = renderTagList(subd);
|
let tagList = renderTagList(subd);
|
||||||
let tagListEmpty = (subd.Tags.length == 0);
|
let tagListEmpty = (subd.Tags.length == 0);
|
||||||
@@ -565,7 +624,7 @@
|
|||||||
${aliasDomains}
|
${aliasDomains}
|
||||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="" editable="true" datatype="domain">
|
<td data-label="" editable="true" datatype="domain" data-sort-value="${destSortValue}" style="word-break: break-all;">
|
||||||
<div class="upstreamList">
|
<div class="upstreamList">
|
||||||
${upstreams}
|
${upstreams}
|
||||||
</div>
|
</div>
|
||||||
@@ -586,7 +645,7 @@
|
|||||||
</td> -->
|
</td> -->
|
||||||
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
|
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
|
||||||
<button title="Edit Proxy Rule" class="ui circular small basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="ellipsis vertical icon"></i></button>
|
<button title="Edit Proxy Rule" class="ui circular small basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="ellipsis vertical icon"></i></button>
|
||||||
<!-- <button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button> -->
|
<button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
});
|
});
|
||||||
@@ -709,6 +768,112 @@
|
|||||||
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveTlsConfigs(uuid){
|
||||||
|
let enableSNI = $("#httprpEditModal .Tls_EnableSNI")[0].checked;
|
||||||
|
let enableLegacyCertificateMatching = $("#httprpEditModal .Tls_EnableLegacyCertificateMatching")[0].checked;
|
||||||
|
let enableAutoHTTPS = $("#httprpEditModal .Tls_EnableAutoHTTPS")[0].checked;
|
||||||
|
let newTlsOption = {
|
||||||
|
"DisableSNI": !enableSNI,
|
||||||
|
"DisableLegacyCertificateMatching": !enableLegacyCertificateMatching,
|
||||||
|
"EnableAutoHTTPS": enableAutoHTTPS,
|
||||||
|
}
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/proxy/setTlsConfig",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"ep": uuid,
|
||||||
|
"tlsConfig": JSON.stringify(newTlsOption)
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error !== undefined){
|
||||||
|
msgbox(data.error, false, 3000);
|
||||||
|
}else{
|
||||||
|
msgbox("TLS Config updated");
|
||||||
|
}
|
||||||
|
updateTlsResolveList(uuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTlsResolveList(uuid){
|
||||||
|
let editor = $("#httprpEditModalWrapper");
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").off("change");
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").remove();
|
||||||
|
|
||||||
|
//Update the TLS resolve list
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/cert/resolve?domain=" + uuid,
|
||||||
|
method: "GET",
|
||||||
|
success: function(data) {
|
||||||
|
// Populate the TLS resolve list
|
||||||
|
let resolveList = editor.find(".Tls_resolve_list tbody");
|
||||||
|
resolveList.empty(); // Clear existing entries
|
||||||
|
let primaryDomain = data.domain;
|
||||||
|
let aliasDomains = data.alias_domains || [];
|
||||||
|
let certMap = data.domain_key_pair;
|
||||||
|
|
||||||
|
// Add primary domain entry
|
||||||
|
resolveList.append(`
|
||||||
|
<tr>
|
||||||
|
<td>${primaryDomain}</td>
|
||||||
|
<td class="certificateDropdown" domain="${primaryDomain}">${certMap[primaryDomain] || "Fallback Certificate"}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
aliasDomains.forEach(alias => {
|
||||||
|
resolveList.append(`
|
||||||
|
<tr>
|
||||||
|
<td>${alias}</td>
|
||||||
|
<td class="certificateDropdown" domain="${alias}">${certMap[alias] || "Fallback Certificate"}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Generate the certificate dropdown
|
||||||
|
generateCertificateDropdown(function(dropdown) {
|
||||||
|
let SNIEnabled = editor.find(".Tls_EnableSNI")[0].checked;
|
||||||
|
editor.find(".certificateDropdown").html(dropdown);
|
||||||
|
editor.find(".certificateDropdown").each(function() {
|
||||||
|
let dropdownDomain = $(this).attr("domain");
|
||||||
|
let selectedCertname = certMap[dropdownDomain];
|
||||||
|
if (selectedCertname) {
|
||||||
|
$(this).find(".ui.dropdown").dropdown("set selected", selectedCertname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").dropdown({
|
||||||
|
onChange: function(value, text, $selectedItem) {
|
||||||
|
console.log("Selected certificate for domain:", $(this).parent().attr("domain"), "Value:", value);
|
||||||
|
let domain = $(this).parent().attr("domain");
|
||||||
|
let newCertificateName = value;
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/cert/setPreferredCertificate",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"domain": domain,
|
||||||
|
"certname": newCertificateName
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error !== undefined) {
|
||||||
|
msgbox(data.error, false, 3000);
|
||||||
|
} else {
|
||||||
|
msgbox("Preferred Certificate updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (SNIEnabled) {
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").addClass("disabled");
|
||||||
|
editor.find(".sni_grey_out_info").show();
|
||||||
|
}else{
|
||||||
|
editor.find(".sni_grey_out_info").hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function saveProxyInlineEdit(uuid){
|
function saveProxyInlineEdit(uuid){
|
||||||
let editor = $("#httprpEditModal");
|
let editor = $("#httprpEditModal");
|
||||||
|
|
||||||
@@ -719,6 +884,7 @@
|
|||||||
let requireRateLimit = $(editor).find(".RequireRateLimit")[0].checked;
|
let requireRateLimit = $(editor).find(".RequireRateLimit")[0].checked;
|
||||||
let rateLimit = $(editor).find(".RateLimit").val();
|
let rateLimit = $(editor).find(".RateLimit").val();
|
||||||
let bypassGlobalTLS = $(editor).find(".BypassGlobalTLS")[0].checked;
|
let bypassGlobalTLS = $(editor).find(".BypassGlobalTLS")[0].checked;
|
||||||
|
let disableChunkedTransferEncoding = $(editor).find(".DisableChunkedTransferEncoding")[0].checked;
|
||||||
let tags = getTagsArrayFromEndpoint(uuid);
|
let tags = getTagsArrayFromEndpoint(uuid);
|
||||||
if (tags.length > 0){
|
if (tags.length > 0){
|
||||||
tags = tags.join(",");
|
tags = tags.join(",");
|
||||||
@@ -726,7 +892,7 @@
|
|||||||
tags = "";
|
tags = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({
|
cfgPayload = {
|
||||||
"type": epttype,
|
"type": epttype,
|
||||||
"rootname": uuid,
|
"rootname": uuid,
|
||||||
"ss":useStickySession,
|
"ss":useStickySession,
|
||||||
@@ -734,24 +900,16 @@
|
|||||||
"bpgtls": bypassGlobalTLS,
|
"bpgtls": bypassGlobalTLS,
|
||||||
"authprovider" :authProviderType,
|
"authprovider" :authProviderType,
|
||||||
"rate" :requireRateLimit,
|
"rate" :requireRateLimit,
|
||||||
|
"dChunkedEnc": disableChunkedTransferEncoding,
|
||||||
"ratenum" :rateLimit,
|
"ratenum" :rateLimit,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
});
|
};
|
||||||
|
console.log("updating proxy config:", cfgPayload);
|
||||||
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/edit",
|
url: "/api/proxy/edit",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: cfgPayload,
|
||||||
"type": epttype,
|
|
||||||
"rootname": uuid,
|
|
||||||
"ss":useStickySession,
|
|
||||||
"dutm": DisableUptimeMonitor,
|
|
||||||
"bpgtls": bypassGlobalTLS,
|
|
||||||
"authprovider" :authProviderType,
|
|
||||||
"rate" :requireRateLimit,
|
|
||||||
"ratenum" :rateLimit,
|
|
||||||
"tags": tags,
|
|
||||||
},
|
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error !== undefined){
|
if (data.error !== undefined){
|
||||||
msgbox(data.error, false, 6000);
|
msgbox(data.error, false, 6000);
|
||||||
@@ -862,6 +1020,29 @@
|
|||||||
renewCertificate(renewDomainKey, false, btn);
|
renewCertificate(renewDomainKey, false, btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateSelfSignedCertificate(uuid, domains, btn=undefined){
|
||||||
|
let payload = JSON.stringify(domains);
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/cert/selfsign",
|
||||||
|
data: {
|
||||||
|
"cn": uuid,
|
||||||
|
"domains": payload
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error == undefined){
|
||||||
|
msgbox("Self-Signed Certificate Generated", true);
|
||||||
|
resyncProxyEditorConfig();
|
||||||
|
if (typeof(initManagedDomainCertificateList) != undefined){
|
||||||
|
//Re-init the managed domain certificate list
|
||||||
|
initManagedDomainCertificateList();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* Tags & Search */
|
/* Tags & Search */
|
||||||
function handleSearchInput(event){
|
function handleSearchInput(event){
|
||||||
if (event.key == "Escape"){
|
if (event.key == "Escape"){
|
||||||
@@ -990,6 +1171,28 @@
|
|||||||
return subd;
|
return subd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a certificate dropdown for the HTTP Proxy Rule Editor
|
||||||
|
// so user can pick which certificate they want to use for the current editing hostname
|
||||||
|
function generateCertificateDropdown(callback){
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/cert/list",
|
||||||
|
method: "GET",
|
||||||
|
success: function(data) {
|
||||||
|
let dropdown = $('<div class="ui fluid selection dropdown"></div>');
|
||||||
|
let menu = $('<div class="menu"></div>');
|
||||||
|
data.forEach(cert => {
|
||||||
|
menu.append(`<div class="item" data-value="${cert}">${cert}</div>`);
|
||||||
|
});
|
||||||
|
// Add a hidden input to store the selected certificate
|
||||||
|
dropdown.append('<input type="hidden" name="certificate">');
|
||||||
|
dropdown.append('<i class="dropdown icon"></i>');
|
||||||
|
dropdown.append('<div class="default text">Fallback Certificate</div>');
|
||||||
|
dropdown.append(menu);
|
||||||
|
callback(dropdown);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//Initialize the http proxy rule editor
|
//Initialize the http proxy rule editor
|
||||||
function initHttpProxyRuleEditorModal(rulepayload){
|
function initHttpProxyRuleEditorModal(rulepayload){
|
||||||
let subd = JSON.parse(JSON.stringify(rulepayload));
|
let subd = JSON.parse(JSON.stringify(rulepayload));
|
||||||
@@ -1092,39 +1295,6 @@
|
|||||||
});
|
});
|
||||||
editor.find(".downstream_alias_hostname").html(aliasHTML);
|
editor.find(".downstream_alias_hostname").html(aliasHTML);
|
||||||
|
|
||||||
//TODO: Move this to SSL TLS section
|
|
||||||
let enableQuickRequestButton = true;
|
|
||||||
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
|
||||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
|
||||||
let thisAliasName = subd.MatchingDomainAlias[i];
|
|
||||||
domains.push(thisAliasName);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
|
||||||
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
|
||||||
enableQuickRequestButton = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subd.MatchingDomainAlias != undefined){
|
|
||||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
|
||||||
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
|
||||||
enableQuickRequestButton = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
|
||||||
if (enableQuickRequestButton){
|
|
||||||
editor.find(".getCertificateBtn").removeClass("disabled");
|
|
||||||
}else{
|
|
||||||
editor.find(".getCertificateBtn").addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
|
||||||
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------------ Upstreams ------------ */
|
/* ------------ Upstreams ------------ */
|
||||||
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
||||||
editor.find(".editUpstreamButton").off("click").on("click", function(){
|
editor.find(".editUpstreamButton").off("click").on("click", function(){
|
||||||
@@ -1143,10 +1313,18 @@
|
|||||||
saveProxyInlineEdit(uuid);
|
saveProxyInlineEdit(uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
editor.find(".DisableChunkedTransferEncoding").off("change");
|
||||||
|
editor.find(".DisableChunkedTransferEncoding").prop("checked", subd.DisableChunkedTransferEncoding);
|
||||||
|
editor.find(".DisableChunkedTransferEncoding").on("change", function() {
|
||||||
|
saveProxyInlineEdit(uuid);
|
||||||
|
});
|
||||||
|
|
||||||
/* ------------ Vdirs ------------ */
|
/* ------------ Vdirs ------------ */
|
||||||
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
||||||
editor.find(".editVdirBtn").off("click").on("click", function(){
|
editor.find(".editVdirBtn").off("click").on("click", function(){
|
||||||
quickEditVdir(uuid);
|
quickEditVdir(uuid);
|
||||||
|
//Temporary restore scroll
|
||||||
|
$("body").css("overflow", "auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------ Alias ------------ */
|
/* ------------ Alias ------------ */
|
||||||
@@ -1244,6 +1422,60 @@
|
|||||||
editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent);
|
editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent);
|
||||||
|
|
||||||
/* ------------ TLS ------------ */
|
/* ------------ TLS ------------ */
|
||||||
|
updateTlsResolveList(uuid);
|
||||||
|
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
|
||||||
|
|
||||||
|
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
|
||||||
|
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
|
||||||
|
|
||||||
|
editor.find(".Tls_EnableSNI").off("change").on("change", function() {
|
||||||
|
saveTlsConfigs(uuid);
|
||||||
|
});
|
||||||
|
editor.find(".Tls_EnableLegacyCertificateMatching").off("change").on("change", function() {
|
||||||
|
saveTlsConfigs(uuid);
|
||||||
|
});
|
||||||
|
editor.find(".Tls_EnableAutoHTTPS").off("change").on("change", function() {
|
||||||
|
saveTlsConfigs(uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Quick access to get certificate for the current host */
|
||||||
|
let enableQuickRequestButton = true;
|
||||||
|
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||||
|
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||||
|
let thisAliasName = subd.MatchingDomainAlias[i];
|
||||||
|
domains.push(thisAliasName);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||||
|
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subd.MatchingDomainAlias != undefined){
|
||||||
|
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||||
|
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enableQuickRequestButton){
|
||||||
|
editor.find(".getCertificateBtn").removeClass("disabled");
|
||||||
|
}else{
|
||||||
|
editor.find(".getCertificateBtn").addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
||||||
|
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||||
|
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind event to self-signed certificate button
|
||||||
|
editor.find(".getSelfSignCertBtn").off("click").on("click", function() {
|
||||||
|
generateSelfSignedCertificate(uuid, domains, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------ Tags ------------ */
|
/* ------------ Tags ------------ */
|
||||||
(()=>{
|
(()=>{
|
||||||
@@ -1307,7 +1539,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Page Initialization Functions
|
Page Initialization Functions
|
||||||
*/
|
*/
|
||||||
@@ -1332,7 +1563,9 @@
|
|||||||
// there is a chance where the user has modified the Vdir
|
// there is a chance where the user has modified the Vdir
|
||||||
// we need to get the latest setting from server side and
|
// we need to get the latest setting from server side and
|
||||||
// render it again
|
// render it again
|
||||||
updateVdirInProxyEditor();
|
resyncProxyEditorConfig();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
$("body").css("overflow", "hidden");
|
||||||
} else {
|
} else {
|
||||||
listProxyEndpoints();
|
listProxyEndpoints();
|
||||||
//Reset the tag filter
|
//Reset the tag filter
|
||||||
|
@@ -1,797 +0,0 @@
|
|||||||
<div class="standardContainer">
|
|
||||||
<div class="ui basic segment">
|
|
||||||
<h2>HTTP Proxy</h2>
|
|
||||||
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
|
|
||||||
background-color: #00ca52 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subdEntry td:not(.ignoremw){
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.httpProxyListTools{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-select{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-select:hover{
|
|
||||||
text-decoration: underline;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="httpProxyListTools" style="margin-bottom: 1em;">
|
|
||||||
<div id="tagFilterDropdown" class="ui floating basic dropdown labeled icon button" style="min-width: 150px;">
|
|
||||||
<i class="filter icon"></i>
|
|
||||||
<span class="text">Filter by tags</span>
|
|
||||||
<div class="menu">
|
|
||||||
<div class="ui icon search input">
|
|
||||||
<i class="search icon"></i>
|
|
||||||
<input type="text" placeholder="Search tags...">
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="scrolling menu tagList">
|
|
||||||
<!--
|
|
||||||
Example:
|
|
||||||
<div class="item">
|
|
||||||
<div class="ui red empty circular label"></div>
|
|
||||||
Important
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<!-- Add more tag options dynamically -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui small input" style="width: 300px; height: 38px;">
|
|
||||||
<!-- Prevent the browser from filling the saved Zoraxy login account into the input searchInput below -->
|
|
||||||
<input type="password" autocomplete="off" hidden/>
|
|
||||||
<input type="text" id="searchInput" placeholder="Quick Search" onkeydown="handleSearchInput(event);" onchange="handleSearchInput(event);" onblur="handleSearchInput(event);">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
|
|
||||||
<table class="ui celled sortable unstackable compact table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Host</th>
|
|
||||||
<th>Destination</th>
|
|
||||||
<th>Virtual Directory</th>
|
|
||||||
<th>Tags</th>
|
|
||||||
<th style="max-width: 300px;">Advanced Settings</th>
|
|
||||||
<th class="no-sort" style="min-width:150px;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="httpProxyList">
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
|
|
||||||
<br><br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
/* List all proxy endpoints */
|
|
||||||
function listProxyEndpoints(){
|
|
||||||
$.get("/api/proxy/list?type=host", function(data){
|
|
||||||
$("#httpProxyList").html(``);
|
|
||||||
if (data.error !== undefined){
|
|
||||||
$("#httpProxyList").append(`<tr>
|
|
||||||
<td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
|
|
||||||
</tr>`);
|
|
||||||
}else if (data.length == 0){
|
|
||||||
$("#httpProxyList").append(`<tr>
|
|
||||||
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
|
||||||
</tr>`);
|
|
||||||
}else{
|
|
||||||
//Sort by RootOrMatchingDomain field
|
|
||||||
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
|
|
||||||
data.forEach(subd => {
|
|
||||||
let subdData = encodeURIComponent(JSON.stringify(subd));
|
|
||||||
|
|
||||||
//Build the upstream list
|
|
||||||
let upstreams = "";
|
|
||||||
if (subd.ActiveOrigins.length == 0){
|
|
||||||
//Invalid config
|
|
||||||
upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`;
|
|
||||||
}else{
|
|
||||||
subd.ActiveOrigins.forEach(upstream => {
|
|
||||||
console.log(upstream);
|
|
||||||
//Check if the upstreams require TLS connections
|
|
||||||
let tlsIcon = "";
|
|
||||||
if (upstream.RequireTLS){
|
|
||||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
|
||||||
if (upstream.SkipCertValidations){
|
|
||||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
|
|
||||||
|
|
||||||
upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let inboundTlsIcon = "";
|
|
||||||
if ($("#tls").checkbox("is checked")){
|
|
||||||
inboundTlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
|
||||||
if (subd.BypassGlobalTLS){
|
|
||||||
inboundTlsIcon = `<i class="grey lock icon" title="TLS Bypass Enabled"></i>`;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
inboundTlsIcon = `<i class="yellow lock open icon" title="Plain Text Mode"></i>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Build the virtual directory list
|
|
||||||
var vdList = `<div class="ui list">`;
|
|
||||||
subd.VirtualDirectories.forEach(vdir => {
|
|
||||||
vdList += `<div class="item">${vdir.MatchingPath} <i class="green angle double right icon"></i> ${vdir.Domain}</div>`;
|
|
||||||
});
|
|
||||||
vdList += `</div>`;
|
|
||||||
|
|
||||||
if (subd.VirtualDirectories.length == 0){
|
|
||||||
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Virtual Directory</small>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let enableChecked = "checked";
|
|
||||||
if (subd.Disabled){
|
|
||||||
enableChecked = "";
|
|
||||||
}
|
|
||||||
let httpProto = "http://";
|
|
||||||
if ($("#tls").checkbox("is checked")) {
|
|
||||||
httpProto = "https://";
|
|
||||||
} else {
|
|
||||||
httpProto = "http://";
|
|
||||||
}
|
|
||||||
let hostnameRedirectPort = currentListeningPort;
|
|
||||||
if (hostnameRedirectPort == 80 || hostnameRedirectPort == 443){
|
|
||||||
hostnameRedirectPort = "";
|
|
||||||
}else{
|
|
||||||
hostnameRedirectPort = ":" + hostnameRedirectPort;
|
|
||||||
}
|
|
||||||
let aliasDomains = ``;
|
|
||||||
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
|
|
||||||
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
|
|
||||||
subd.MatchingDomainAlias.forEach(alias => {
|
|
||||||
aliasDomains += `<a href="${httpProto}${alias}${hostnameRedirectPort}" target="_blank">${alias}</a>, `;
|
|
||||||
});
|
|
||||||
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
|
||||||
aliasDomains += `</small><br>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
|
||||||
<td data-label="" editable="true" datatype="inbound">
|
|
||||||
<a href="${httpProto}${subd.RootOrMatchingDomain}${hostnameRedirectPort}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
|
|
||||||
${aliasDomains}
|
|
||||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
|
||||||
</td>
|
|
||||||
<td data-label="" editable="true" datatype="domain">
|
|
||||||
<div class="upstreamList">
|
|
||||||
${upstreams}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
|
|
||||||
<td data-label="tags" payload="${encodeURIComponent(JSON.stringify(subd.Tags))}" datatype="tags">
|
|
||||||
<div class="tags-list">
|
|
||||||
${subd.Tags.length >0 ? subd.Tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join(""):"<small style='opacity: 0.3; pointer-events: none; user-select: none;'>No Tags</small>"}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Forward Auth`:``}
|
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
|
|
||||||
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
|
|
||||||
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
|
||||||
</td>
|
|
||||||
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
|
|
||||||
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
|
|
||||||
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
|
|
||||||
<label></label>
|
|
||||||
</div>
|
|
||||||
<button title="Edit Proxy Rule" class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
|
|
||||||
<button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>`);
|
|
||||||
});
|
|
||||||
populateTagFilterDropdown(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveAccessRuleNameOnHostRPlist();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Perform realtime alias update without refreshing the whole page
|
|
||||||
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
|
|
||||||
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
|
|
||||||
console.log(targetEle);
|
|
||||||
if (targetEle.length == 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let aliasDomains = ``;
|
|
||||||
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
|
|
||||||
aliasDomains = `Alias: `;
|
|
||||||
newAliasDomainList.forEach(alias => {
|
|
||||||
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
|
|
||||||
});
|
|
||||||
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
|
||||||
$(targetEle).html(aliasDomains);
|
|
||||||
$(targetEle).show();
|
|
||||||
}else{
|
|
||||||
$(targetEle).hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Resolve & Update all rule names on host PR list
|
|
||||||
function resolveAccessRuleNameOnHostRPlist(){
|
|
||||||
//Resolve the access filters
|
|
||||||
$.get("/api/access/list", function(data){
|
|
||||||
console.log(data);
|
|
||||||
if (data.error == undefined){
|
|
||||||
//Build a map base on the data
|
|
||||||
let accessRuleMap = {};
|
|
||||||
for (var i = 0; i < data.length; i++){
|
|
||||||
accessRuleMap[data[i].ID] = data[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$(".accessRuleNameUnderHost").each(function(){
|
|
||||||
let thisAccessRuleID = $(this).attr("ruleid");
|
|
||||||
if (thisAccessRuleID== ""){
|
|
||||||
thisAccessRuleID = "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thisAccessRuleID == "default"){
|
|
||||||
//No need to label default access rules
|
|
||||||
$(this).html("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rule = accessRuleMap[thisAccessRuleID];
|
|
||||||
if (rule == undefined){
|
|
||||||
//Missing config or config too old
|
|
||||||
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let icon = `<i class="ui grey filter icon"></i>`;
|
|
||||||
if (rule.ID == "default"){
|
|
||||||
icon = `<i class="ui yellow star icon"></i>`;
|
|
||||||
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
|
|
||||||
//This is a blacklist filter
|
|
||||||
icon = `<i class="ui red filter icon"></i>`;
|
|
||||||
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
|
|
||||||
//This is a whitelist filter
|
|
||||||
icon = `<i class="ui green filter icon"></i>`;
|
|
||||||
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
|
|
||||||
//Whitelist and blacklist filter
|
|
||||||
icon = `<i class="ui yellow filter icon"></i>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule != undefined){
|
|
||||||
$(this).html(`${icon} ${rule.Name}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//Update the access rule name on given epuuid, call by hostAccessEditor.html
|
|
||||||
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
|
|
||||||
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
|
|
||||||
resolveAccessRuleNameOnHostRPlist();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Inline editor for httprp.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function editEndpoint(uuid) {
|
|
||||||
uuid = uuid.hexDecode();
|
|
||||||
var row = $('tr[eptuuid="' + uuid + '"]');
|
|
||||||
var columns = row.find('td[data-label]');
|
|
||||||
var payload = $(row).attr("payload");
|
|
||||||
payload = JSON.parse(decodeURIComponent(payload));
|
|
||||||
console.log(payload);
|
|
||||||
columns.each(function(index) {
|
|
||||||
var column = $(this);
|
|
||||||
var oldValue = column.text().trim();
|
|
||||||
|
|
||||||
if ($(this).attr("editable") == "false"){
|
|
||||||
//This col do not allow edit. Skip
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an input element based on the column content
|
|
||||||
var input;
|
|
||||||
var datatype = $(this).attr("datatype");
|
|
||||||
if (datatype == "domain"){
|
|
||||||
let useStickySessionChecked = "";
|
|
||||||
if (payload.UseStickySession){
|
|
||||||
useStickySessionChecked = "checked";
|
|
||||||
}
|
|
||||||
|
|
||||||
let enableUptimeMonitor = "";
|
|
||||||
//Note the config file store the uptime monitor as disable, so we need to reverse the logic
|
|
||||||
if (!payload.DisableUptimeMonitor){
|
|
||||||
enableUptimeMonitor = "checked";
|
|
||||||
}
|
|
||||||
|
|
||||||
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="UseStickySession" ${useStickySessionChecked}>
|
|
||||||
<label>Use Sticky Session<br>
|
|
||||||
<small>Enable stick session on load balancing</small></label>
|
|
||||||
</div>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="EnableUptimeMonitor" ${enableUptimeMonitor}>
|
|
||||||
<label>Monitor Uptime<br>
|
|
||||||
<small>Enable active uptime monitor</small></label>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
column.append(input);
|
|
||||||
$(column).find(".upstreamList").addClass("editing");
|
|
||||||
}else if (datatype == "vdir"){
|
|
||||||
//Append a quick access button for vdir page
|
|
||||||
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
|
|
||||||
<i class="ui yellow folder icon"></i> Edit Virtual Directories
|
|
||||||
</button>`);
|
|
||||||
}else if (datatype == "tags"){
|
|
||||||
column.append(`
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<button class="ui basic compact fluid tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editTags('${uuid}');"><i class="ui purple tag icon"></i> Edit tags</button>
|
|
||||||
`);
|
|
||||||
}else if (datatype == "advanced"){
|
|
||||||
let authProvider = payload.AuthenticationProvider.AuthMethod;
|
|
||||||
|
|
||||||
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
|
|
||||||
let wsCheckstate = "";
|
|
||||||
if (skipWebSocketOriginCheck){
|
|
||||||
wsCheckstate = "checked";
|
|
||||||
}
|
|
||||||
|
|
||||||
let requireRateLimit = payload.RequireRateLimit;
|
|
||||||
let rateLimitCheckState = "";
|
|
||||||
if (requireRateLimit){
|
|
||||||
rateLimitCheckState = "checked";
|
|
||||||
}
|
|
||||||
let rateLimit = payload.RateLimit;
|
|
||||||
if (rateLimit == 0){
|
|
||||||
//This value is not set. Make it default to 100
|
|
||||||
rateLimit = 100;
|
|
||||||
}
|
|
||||||
let rateLimitDisableState = "";
|
|
||||||
if (!payload.RequireRateLimit){
|
|
||||||
rateLimitDisableState = "disabled";
|
|
||||||
}
|
|
||||||
|
|
||||||
column.empty().append(`
|
|
||||||
<div class="grouped fields authProviderPicker">
|
|
||||||
<label><b>Authentication Provider</b></label>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}>
|
|
||||||
<label>None (Anyone can access)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}>
|
|
||||||
<label>Basic Auth</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
|
|
||||||
<label>Forward Auth</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" value="3" name="authProviderType" ${authProvider==0x3?"checked":""}>
|
|
||||||
<label>OAuth2</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
|
|
||||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
|
|
||||||
|
|
||||||
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
|
|
||||||
<div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
|
|
||||||
<div class="title">
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
Security Options
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
|
|
||||||
<label>Require Rate Limit<br>
|
|
||||||
<small>Check this to enable rate limit on this inbound hostname</small></label>
|
|
||||||
</div><br>
|
|
||||||
<div class="ui mini right labeled fluid input ${rateLimitDisableState}" style="margin-top: 0.4em;">
|
|
||||||
<input type="number" class="RateLimit" value="${rateLimit}" min="1" >
|
|
||||||
<label class="ui basic label">
|
|
||||||
req / sec / IP
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
$('.authProviderPicker .ui.checkbox').checkbox();
|
|
||||||
} else if (datatype == "ratelimit"){
|
|
||||||
|
|
||||||
column.empty().append(`
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="RequireRateLimit" ${checkstate}>
|
|
||||||
<label>Require Rate Limit</label>
|
|
||||||
</div>
|
|
||||||
<div class="ui mini fluid input">
|
|
||||||
<input type="number" class="RateLimit" value="${rateLimit}" placeholder="100" min="1" max="1000" >
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
}else if (datatype == 'action'){
|
|
||||||
column.empty().append(`
|
|
||||||
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
|
|
||||||
<button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
|
|
||||||
|
|
||||||
`);
|
|
||||||
}else if (datatype == "inbound"){
|
|
||||||
let originalContent = $(column).html();
|
|
||||||
|
|
||||||
//Check if this host is covered within one of the certificates. If not, show the icon
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
//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));
|
|
||||||
|
|
||||||
column.empty().append(`${originalContent}
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
|
||||||
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
|
|
||||||
<label>Allow plain HTTP access<br>
|
|
||||||
<small>Allow inbound connections without TLS/SSL</small></label>
|
|
||||||
</div><br>
|
|
||||||
<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 ${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{
|
|
||||||
//Unknown field. Leave it untouched
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".endpointAdvanceConfig").accordion();
|
|
||||||
$("#httpProxyList").find(".editBtn").addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
//handleToggleRateLimitInput will get trigger if the "require rate limit" checkbox
|
|
||||||
// is changed and toggle the disable state of the rate limit input field
|
|
||||||
function handleToggleRateLimitInput(){
|
|
||||||
let isRateLimitEnabled = $("#httpProxyList input.RequireRateLimit")[0].checked;
|
|
||||||
if (isRateLimitEnabled){
|
|
||||||
$("#httpProxyList input.RateLimit").parent().removeClass("disabled");
|
|
||||||
}else{
|
|
||||||
$("#httpProxyList input.RateLimit").parent().addClass("disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitProxyInlineEdit(){
|
|
||||||
listProxyEndpoints();
|
|
||||||
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveProxyInlineEdit(uuid){
|
|
||||||
uuid = uuid.hexDecode();
|
|
||||||
var row = $('tr[eptuuid="' + uuid + '"]');
|
|
||||||
if (row.length == 0){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var epttype = "host";
|
|
||||||
let useStickySession = $(row).find(".UseStickySession")[0].checked;
|
|
||||||
let DisableUptimeMonitor = !$(row).find(".EnableUptimeMonitor")[0].checked;
|
|
||||||
let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val();
|
|
||||||
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
|
|
||||||
let rateLimit = $(row).find(".RateLimit").val();
|
|
||||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
|
||||||
let tags = getTagsArrayFromEndpoint(uuid);
|
|
||||||
if (tags.length > 0){
|
|
||||||
tags = tags.join(",");
|
|
||||||
}else{
|
|
||||||
tags = "";
|
|
||||||
}
|
|
||||||
$.cjax({
|
|
||||||
url: "/api/proxy/edit",
|
|
||||||
method: "POST",
|
|
||||||
data: {
|
|
||||||
"type": epttype,
|
|
||||||
"rootname": uuid,
|
|
||||||
"ss":useStickySession,
|
|
||||||
"dutm": DisableUptimeMonitor,
|
|
||||||
"bpgtls": bypassGlobalTLS,
|
|
||||||
"authprovider" :authProviderType,
|
|
||||||
"rate" :requireRateLimit,
|
|
||||||
"ratenum" :rateLimit,
|
|
||||||
"tags": tags,
|
|
||||||
},
|
|
||||||
success: function(data){
|
|
||||||
if (data.error !== undefined){
|
|
||||||
msgbox(data.error, false, 6000);
|
|
||||||
}else{
|
|
||||||
msgbox("Proxy endpoint updated");
|
|
||||||
listProxyEndpoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//Generic functions for delete rp endpoints
|
|
||||||
function deleteEndpoint(epoint){
|
|
||||||
epoint = decodeURIComponent(epoint).hexDecode();
|
|
||||||
if (confirm("Confirm remove proxy for :" + epoint + "?")){
|
|
||||||
$.cjax({
|
|
||||||
url: "/api/proxy/del",
|
|
||||||
method: "POST",
|
|
||||||
data: {ep: epoint},
|
|
||||||
success: function(data){
|
|
||||||
if (data.error == undefined){
|
|
||||||
listProxyEndpoints();
|
|
||||||
msgbox("Proxy Rule Deleted", true);
|
|
||||||
reloadUptimeList();
|
|
||||||
}else{
|
|
||||||
msgbox(data.error, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* button events */
|
|
||||||
function editBasicAuthCredentials(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editAccessRule(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editAliasHostnames(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickEditVdir(uuid){
|
|
||||||
openTabById("vdir");
|
|
||||||
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Open the custom header editor
|
|
||||||
function editCustomHeaders(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Open the load balance option
|
|
||||||
function editUpstreams(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/upstreams.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleProxyRuleToggle(object){
|
|
||||||
let endpointUUID = $(object).attr("eptuuid");
|
|
||||||
let isChecked = object.checked;
|
|
||||||
$.cjax({
|
|
||||||
url: "/api/proxy/toggle",
|
|
||||||
data: {
|
|
||||||
"ep": endpointUUID,
|
|
||||||
"enable": isChecked
|
|
||||||
},
|
|
||||||
success: function(data){
|
|
||||||
if (data.error != undefined){
|
|
||||||
msgbox(data.error, false);
|
|
||||||
}else{
|
|
||||||
if (isChecked){
|
|
||||||
msgbox("Proxy Rule Enabled");
|
|
||||||
}else{
|
|
||||||
msgbox("Proxy Rule Disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Certificate Shortcut
|
|
||||||
*/
|
|
||||||
|
|
||||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
|
||||||
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
|
|
||||||
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
|
|
||||||
tabSwitchEventBind["httprp"] = function(){
|
|
||||||
listProxyEndpoints();
|
|
||||||
|
|
||||||
//Reset the tag filter
|
|
||||||
$("#tagFilterDropdown").dropdown('set selected', "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tags & Search */
|
|
||||||
function handleSearchInput(event){
|
|
||||||
if (event.key == "Escape"){
|
|
||||||
$("#searchInput").val("");
|
|
||||||
}
|
|
||||||
filterProxyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to filter the proxy list
|
|
||||||
function filterProxyList() {
|
|
||||||
let searchInput = $("#searchInput").val().toLowerCase();
|
|
||||||
let selectedTag = $("#tagFilterDropdown").dropdown('get value');
|
|
||||||
$("#httpProxyList tr").each(function() {
|
|
||||||
let host = $(this).find("td[data-label='']").text().toLowerCase();
|
|
||||||
let tagElements = $(this).find("td[data-label='tags']");
|
|
||||||
let tags = tagElements.attr("payload");
|
|
||||||
tags = JSON.parse(decodeURIComponent(tags));
|
|
||||||
if ((host.includes(searchInput) || searchInput === "") && (tags.includes(selectedTag) || selectedTag === "")) {
|
|
||||||
$(this).show();
|
|
||||||
} else {
|
|
||||||
$(this).hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a color based on a tag name
|
|
||||||
function getTagColorByName(tagName) {
|
|
||||||
function hashCode(str) {
|
|
||||||
return str.split('').reduce((prevHash, currVal) =>
|
|
||||||
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
|
||||||
}
|
|
||||||
let hash = hashCode(tagName);
|
|
||||||
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
|
||||||
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
|
||||||
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagTextColor(tagName){
|
|
||||||
let color = getTagColorByName(tagName);
|
|
||||||
let r = parseInt(color.substr(1, 2), 16);
|
|
||||||
let g = parseInt(color.substr(3, 2), 16);
|
|
||||||
let b = parseInt(color.substr(5, 2), 16);
|
|
||||||
let brightness = Math.round(((r * 299) + (g * 587) + (b * 114)) / 1000);
|
|
||||||
return brightness > 125 ? "#000000" : "#ffffff";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the tag filter dropdown
|
|
||||||
function populateTagFilterDropdown(data) {
|
|
||||||
let tags = new Set();
|
|
||||||
data.forEach(subd => {
|
|
||||||
subd.Tags.forEach(tag => tags.add(tag));
|
|
||||||
});
|
|
||||||
tags = Array.from(tags).sort((a, b) => a.localeCompare(b));
|
|
||||||
let dropdownMenu = $("#tagFilterDropdown .tagList");
|
|
||||||
dropdownMenu.html(`<div class="item tag-select" data-value="">
|
|
||||||
<div class="ui grey empty circular label"></div>
|
|
||||||
Show all
|
|
||||||
</div>`);
|
|
||||||
tags.forEach(tag => {
|
|
||||||
let thisTagColor = getTagColorByName(tag);
|
|
||||||
dropdownMenu.append(`<div class="item tag-select" data-value="${tag}">
|
|
||||||
<div class="ui empty circular label" style="background-color: ${thisTagColor}; border-color: ${thisTagColor};" ></div>
|
|
||||||
${tag}
|
|
||||||
</div>`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit tags for a specific endpoint
|
|
||||||
function editTags(uuid){
|
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
|
||||||
ept: "host",
|
|
||||||
ep: uuid
|
|
||||||
}));
|
|
||||||
showSideWrapper("snippet/tagEditor.html?t=" + Date.now() + "#" + payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the tags preview from tag editing snippet
|
|
||||||
function renderTagsPreview(endpoint, tags){
|
|
||||||
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
|
||||||
//Update the tag DOM
|
|
||||||
let newTagDOM = tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join("");
|
|
||||||
$(targetProxyRuleEle).find(".tags-list").html(newTagDOM);
|
|
||||||
|
|
||||||
//Update the tag payload
|
|
||||||
$(targetProxyRuleEle).attr("payload", encodeURIComponent(JSON.stringify(tags)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagsArrayFromEndpoint(endpoint){
|
|
||||||
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
|
||||||
let tags = $(targetProxyRuleEle).attr("payload");
|
|
||||||
return JSON.parse(decodeURIComponent(tags));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the proxy list on page load
|
|
||||||
$(document).ready(function() {
|
|
||||||
listProxyEndpoints();
|
|
||||||
|
|
||||||
// Event listener for clicking on tags
|
|
||||||
$(document).on('click', '.tag-select', function() {
|
|
||||||
let tag = $(this).text().trim();
|
|
||||||
$('#tagFilterDropdown').dropdown('set selected', tag);
|
|
||||||
filterProxyList();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
@@ -3,18 +3,15 @@
|
|||||||
<h2>SSO</h2>
|
<h2>SSO</h2>
|
||||||
<p>Single Sign-On (SSO) and authentication providers settings </p>
|
<p>Single Sign-On (SSO) and authentication providers settings </p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui basic segment">
|
|
||||||
<div class="ui yellow message">
|
|
||||||
<div class="header">
|
|
||||||
Experimental Feature
|
|
||||||
</div>
|
|
||||||
<p>Please note that this feature is still in development and may not work as expected.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui basic segment">
|
<div class="ui top attached tabular menu ssoTabs">
|
||||||
<h3>Forward Auth</h3>
|
<a class="item active" data-tab="forward_auth_tab">Forward Auth</a>
|
||||||
|
<a class="item" data-tab="oauth2_tab">Oauth2</a>
|
||||||
|
<!-- <a class="item" data-tab="zoraxy_sso_tab">Zoraxy SSO</a> -->
|
||||||
|
</div>
|
||||||
|
<div class="ui bottom attached tab segment active" data-tab="forward_auth_tab">
|
||||||
|
<!-- Forward Auth -->
|
||||||
|
<h2>Forward Auth</h2>
|
||||||
<p>Configuration settings for the Forward Auth provider.</p>
|
<p>Configuration settings for the Forward Auth provider.</p>
|
||||||
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
|
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -25,6 +22,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://www.authelia.com" rel=”noopener noreferrer” target="_blank">Authelia</a></li>
|
<li><a href="https://www.authelia.com" rel=”noopener noreferrer” target="_blank">Authelia</a></li>
|
||||||
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
||||||
|
<li><a href="https://oauth2-proxy.github.io/oauth2-proxy/" rel=”noopener noreferrer” target="_blank">OAuth2 Proxy</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="ui form" action="#" id="forwardAuthSettings">
|
<form class="ui form" action="#" id="forwardAuthSettings">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -42,26 +40,43 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="forwardAuthResponseHeaders">Response Headers</label>
|
<label for="forwardAuthResponseHeaders">Response Headers</label>
|
||||||
<input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
|
<input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
|
||||||
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
|
<small>
|
||||||
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code></small>
|
Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
|
||||||
|
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="forwardAuthResponseClientHeaders">Response Client Headers</label>
|
<label for="forwardAuthResponseClientHeaders">Response Client Headers</label>
|
||||||
<input type="text" id="forwardAuthResponseClientHeaders" name="forwardAuthResponseClientHeaders" placeholder="Enter Forward Auth Response Client Headers">
|
<input type="text" id="forwardAuthResponseClientHeaders" name="forwardAuthResponseClientHeaders" placeholder="Enter Forward Auth Response Client Headers">
|
||||||
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the response sent to the client. If not set no headers are copied. <br>
|
<small>
|
||||||
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code></small>
|
Comma separated list of case-insensitive headers to copy from the authorization servers response to the <b><i>response sent to the client</i></b>. If not set no headers are copied. <br>
|
||||||
|
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="forwardAuthRequestHeaders">Request Headers</label>
|
<label for="forwardAuthRequestHeaders">Request Headers</label>
|
||||||
<input type="text" id="forwardAuthRequestHeaders" name="forwardAuthRequestHeaders" placeholder="Enter Forward Auth Request Headers">
|
<input type="text" id="forwardAuthRequestHeaders" name="forwardAuthRequestHeaders" placeholder="Enter Forward Auth Request Headers">
|
||||||
<small>Comma separated list of case-insensitive headers to copy from the original request to the request made to the authorization server. If not set all headers are copied. <br>
|
<small>
|
||||||
<strong>Example:</strong> <code>Cookie,Authorization</code></small>
|
Comma separated list of case-insensitive headers to copy from the original request to the <b><i>request made to the authorization server</i></b>. If not set all headers are copied. <br>
|
||||||
|
<strong>Recommendation:</strong> Generally it's recommended to leave this blank or use the below example for predictable results. <br>
|
||||||
|
<strong>Example:</strong> <code>Accept,X-Requested-With,Cookie,Authorization,Proxy-Authorization</code>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="forwardAuthRequestIncludedCookies">Request Included Cookies</label>
|
||||||
|
<input type="text" id="forwardAuthRequestIncludedCookies" name="forwardAuthRequestIncludedCookies" placeholder="Enter Forward Auth Request Included Cookies">
|
||||||
|
<small>
|
||||||
|
Comma separated list of case-sensitive cookie names to copy from the original request to the <b><i>request made to the authorization server</i></b>. If not set all cookies are included. This allows omitting all cookies not required by the authorization server.<br>
|
||||||
|
<strong>Example:</strong> <code>authelia_session,another_session</code>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
|
<label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
|
||||||
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
|
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
|
||||||
<small>Comma separated list of case-sensitive cookie names to exclude from the request to the backend. If not set no cookies are excluded. <br>
|
<small>
|
||||||
<strong>Example:</strong> <code>authelia_session,another_session</code></small>
|
Comma separated list of case-sensitive cookie names to exclude from the <b><i>request made to the backend application</i></b>. If not set no cookies are excluded. This allows omitting the cookie intended only for the authorization server.<br>
|
||||||
|
<strong>Example:</strong> <code>authelia_session,another_session</code>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,9 +84,9 @@
|
|||||||
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui bottom attached tab segment" data-tab="oauth2_tab">
|
||||||
<div class="ui basic segment">
|
<!-- Oauth 2 -->
|
||||||
<h3>OAuth 2.0</h3>
|
<h2>OAuth 2.0</h2>
|
||||||
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
|
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
|
||||||
|
|
||||||
<form class="ui form" action="#" id="oauth2Settings">
|
<form class="ui form" action="#" id="oauth2Settings">
|
||||||
@@ -117,10 +132,17 @@
|
|||||||
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui bottom attached tab segment" data-tab="zoraxy_sso_tab">
|
||||||
|
<!-- Zoraxy SSO -->
|
||||||
|
<h3>Zoraxy SSO</h3>
|
||||||
|
<p>Configuration settings for Zoraxy SSO provider.</p>
|
||||||
|
<p>Currently not implemented.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
$(".ssoTabs .item").tab();
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
/* Load forward-auth settings from backend */
|
/* Load forward-auth settings from backend */
|
||||||
$.cjax({
|
$.cjax({
|
||||||
@@ -129,10 +151,31 @@
|
|||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
$('#forwardAuthAddress').val(data.address);
|
$('#forwardAuthAddress').val(data.address);
|
||||||
|
if (data.responseHeaders != null) {
|
||||||
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthResponseHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.responseClientHeaders != null) {
|
||||||
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthResponseClientHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.requestHeaders != null) {
|
||||||
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.requestIncludedCookies != null) {
|
||||||
|
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestIncludedCookies').val("");
|
||||||
|
}
|
||||||
|
if (data.requestExcludedCookies != null) {
|
||||||
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestExcludedCookies').val("");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||||
@@ -170,6 +213,7 @@
|
|||||||
const responseHeaders = $('#forwardAuthResponseHeaders').val();
|
const responseHeaders = $('#forwardAuthResponseHeaders').val();
|
||||||
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
|
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
|
||||||
const requestHeaders = $('#forwardAuthRequestHeaders').val();
|
const requestHeaders = $('#forwardAuthRequestHeaders').val();
|
||||||
|
const requestIncludedCookies = $('#forwardAuthRequestIncludedCookies').val();
|
||||||
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
|
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
|
||||||
|
|
||||||
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
|
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
<th>Target Address</th>
|
<th>Target Address</th>
|
||||||
<th>Mode</th>
|
<th>Mode</th>
|
||||||
<th>Timeout (s)</th>
|
<th>Timeout (s)</th>
|
||||||
|
<th>Enable Logging</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -73,6 +74,22 @@
|
|||||||
<small>Forward UDP request on this listening socket</small></label>
|
<small>Forward UDP request on this listening socket</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<input type="checkbox" tabindex="0" name="useProxyProtocol" class="hidden">
|
||||||
|
<label>Enable Proxy Protocol V1<br>
|
||||||
|
<small>Enable TCP Proxy Protocol header V1</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<input type="checkbox" tabindex="0" name="enableLogging" class="hidden">
|
||||||
|
<label>Enable Logging<br>
|
||||||
|
<small>Enable logging of connection status and errors for this rule</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
|
||||||
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
|
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
|
||||||
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
|
||||||
@@ -120,7 +137,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function clearStreamProxyAddEditForm(){
|
function clearStreamProxyAddEditForm(){
|
||||||
$('#streamProxyForm input, #streamProxyForm select').val('');
|
$('#streamProxyForm').find('input:not([type=checkbox]), select').val('');
|
||||||
$('#streamProxyForm select').dropdown('clear');
|
$('#streamProxyForm select').dropdown('clear');
|
||||||
$("#streamProxyForm input[name=timeout]").val(10);
|
$("#streamProxyForm input[name=timeout]").val(10);
|
||||||
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
|
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
|
||||||
@@ -195,6 +212,10 @@
|
|||||||
modeText.push("UDP")
|
modeText.push("UDP")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.UseProxyProtocol){
|
||||||
|
modeText.push("ProxyProtocol V1")
|
||||||
|
}
|
||||||
|
|
||||||
modeText = modeText.join(" & ")
|
modeText = modeText.join(" & ")
|
||||||
|
|
||||||
var thisConfig = encodeURIComponent(JSON.stringify(config));
|
var thisConfig = encodeURIComponent(JSON.stringify(config));
|
||||||
@@ -207,6 +228,10 @@
|
|||||||
row.append($('<td>').text(config.ProxyTargetAddr));
|
row.append($('<td>').text(config.ProxyTargetAddr));
|
||||||
row.append($('<td>').text(modeText));
|
row.append($('<td>').text(modeText));
|
||||||
row.append($('<td>').text(config.Timeout));
|
row.append($('<td>').text(config.Timeout));
|
||||||
|
row.append($('<td>').html(config.EnableLogging ?
|
||||||
|
'<i class="green check icon" title="Logging Enabled"></i>' :
|
||||||
|
'<i class="red times icon" title="Logging Disabled"></i>'
|
||||||
|
));
|
||||||
row.append($('<td>').html(`
|
row.append($('<td>').html(`
|
||||||
${startButton}
|
${startButton}
|
||||||
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button>
|
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button>
|
||||||
@@ -252,6 +277,22 @@
|
|||||||
$(checkboxEle).checkbox("set unchecked");
|
$(checkboxEle).checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
}else if (key == "UseProxyProtocol"){
|
||||||
|
let checkboxEle = $("#streamProxyForm input[name=useProxyProtocol]").parent();
|
||||||
|
if (value === true){
|
||||||
|
$(checkboxEle).checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$(checkboxEle).checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}else if (key == "EnableLogging"){
|
||||||
|
let checkboxEle = $("#streamProxyForm input[name=enableLogging]").parent();
|
||||||
|
if (value === true){
|
||||||
|
$(checkboxEle).checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$(checkboxEle).checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
return;
|
||||||
}else if (key == "ListeningAddress"){
|
}else if (key == "ListeningAddress"){
|
||||||
field = $("#streamProxyForm input[name=listenAddr]");
|
field = $("#streamProxyForm input[name=listenAddr]");
|
||||||
}else if (key == "ProxyTargetAddr"){
|
}else if (key == "ProxyTargetAddr"){
|
||||||
@@ -301,6 +342,8 @@
|
|||||||
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
|
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
|
||||||
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
|
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
|
||||||
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
|
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
|
||||||
|
useProxyProtocol: $("#streamProxyForm input[name=useProxyProtocol]")[0].checked ,
|
||||||
|
enableLogging: $("#streamProxyForm input[name=enableLogging]")[0].checked ,
|
||||||
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
@@ -29,6 +29,13 @@
|
|||||||
<small>If this folder do not contains any index files, list the directory of this folder.</small>
|
<small>If this folder do not contains any index files, list the directory of this folder.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<input id="webserv_enableAllInterfaces" type="checkbox" class="hidden">
|
||||||
|
<label>Listening to All Interfaces</label>
|
||||||
|
<small>When disabled, the web server will only listen to localhost (127.0.0.1) and only reachable via reverse proxy rules.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Document Root Folder</label>
|
<label>Document Root Folder</label>
|
||||||
<input id="webserv_docRoot" type="text" readonly="true">
|
<input id="webserv_docRoot" type="text" readonly="true">
|
||||||
@@ -136,6 +143,13 @@
|
|||||||
$("#webserv_dirManager").remove();
|
$("#webserv_dirManager").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.DisableListenToAllInterface){
|
||||||
|
//Options on UI is flipped
|
||||||
|
$("#webserv_enableAllInterfaces").parent().checkbox("set checked");
|
||||||
|
}else{
|
||||||
|
$("#webserv_enableAllInterfaces").parent().checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
|
||||||
$("#webserv_listenPort").val(data.ListeningPort);
|
$("#webserv_listenPort").val(data.ListeningPort);
|
||||||
updateWebServLinkExample(data.ListeningPort);
|
updateWebServLinkExample(data.ListeningPort);
|
||||||
|
|
||||||
@@ -178,6 +192,23 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#webserv_enableAllInterfaces").off("change").on("change", function(){
|
||||||
|
let disable = !$(this)[0].checked;
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/webserv/disableListenAllInterface",
|
||||||
|
method: "POST",
|
||||||
|
data: {"disable": disable},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Listening interface setting updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
$("#webserv_listenPort").off("change").on("change", function(){
|
$("#webserv_listenPort").off("change").on("change", function(){
|
||||||
let newPort = $(this).val();
|
let newPort = $(this).val();
|
||||||
|
|
||||||
|
@@ -343,7 +343,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$(editorSideWrapper).each(function(){
|
$(editorSideWrapper).each(function(){
|
||||||
|
if ($(this)[0].contentWindow.setDarkTheme){
|
||||||
$(this)[0].contentWindow.setDarkTheme(false);
|
$(this)[0].contentWindow.setDarkTheme(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if ($("#pluginContextLoader").is(":visible")){
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
@@ -356,7 +358,9 @@
|
|||||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||||
}
|
}
|
||||||
$(editorSideWrapper).each(function(){
|
$(editorSideWrapper).each(function(){
|
||||||
|
if ($(this)[0].contentWindow.setDarkTheme){
|
||||||
$(this)[0].contentWindow.setDarkTheme(true);
|
$(this)[0].contentWindow.setDarkTheme(true);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if ($("#pluginContextLoader").is(":visible")){
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
||||||
|
@@ -22,7 +22,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<p>Tags currently applied to this host name / proxy rule</p>
|
<p>Tags currently applied to this host name / proxy rule</p>
|
||||||
<div style="max-height: 300px; overflow-y: scroll;">
|
<div>
|
||||||
<table class="ui compact basic unstackable celled table">
|
<table class="ui compact basic unstackable celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
Reference in New Issue
Block a user