49 Commits

Author SHA1 Message Date
Toby Chui
223ae9e112 Merge pull request #751 from tobychui/v3.2.5
* Added new API endpoint /api/proxy/setTlsConfig (for HTTP Proxy Editor TLS tab)
* Refactored TLS certificate management APIs with new handlers
* Removed redundant functions from src/cert.go and delegated to tlsCertManager
* Code optimization in tlscert module
* Introduced a new constant CONF_FOLDER and updated configuration storage paths (phasing out hard coded paths) 
* Updated functions to set default TLS options when missing, default to SNI 

By @jemmy1794 

* Added Proxy Protocol v1 support in stream proxy
* Fixed Proxy UI bug
2025-07-20 14:27:56 +08:00
Toby Chui
aff1975c5a Updated version code and defs
- Updated version code
- Replaced hardcoded path of some config folder string with const value
2025-07-20 14:03:39 +08:00
Toby Chui
5c6950ca56 Merge pull request #744 from jemmy1794/Stream_Proxy_v3.2.5
Fix Stream Proxy TCP/UDP selection not saved initially #742
2025-07-19 12:57:25 +08:00
Toby Chui
70b1ccfa6e Merge pull request #739 from AnthonyMichaelTDM/typo
fix: typo in dynamic_router.go
2025-07-16 14:20:02 +08:00
Anthony Rubick
100c1e9c04 fix: typo in dynamic_router.go
SniffResultAccpet should be SniffResultAccept
2025-07-15 22:05:06 -07:00
Jemmy
a33600d3e2 Fix Stream Proxy TCP/UDP selection not saved initially #742
- Reset the value of the form correctly in `streamprox.html`. Ref #742.
2025-07-16 08:11:25 +08:00
Toby Chui
c4c10d2130 Fixed #713
- Fixed sorting destination not working bug
2025-07-12 19:52:59 +08:00
Toby Chui
4d3d1b25cb Restructure TLS options
- Moved certification related functions into tlscert module
- Added specific host TLS behavior logic
- Added support for disabling SNI and manually overwrite preferred certificate to serve
- Fixed SSO requestHeaders null bug
2025-07-12 19:30:55 +08:00
Toby Chui
118b5e5114 Merge pull request #723 from 7brend7/fix-empty-sso-advanced-params
fix empty sso advanced parameters
2025-07-08 19:00:25 +08:00
Toby Chui
ad53b894c0 Update src/mod/auth/sso/forward/forward.go
Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
2025-07-08 12:38:08 +08:00
Toby Chui
a0a394885c Merge pull request #728 from PassiveLemon/hardening
Docker changes
2025-07-08 09:37:27 +08:00
PassiveLemon
51334a3a75 Docker: Switch to a python entrypoint 2025-07-07 13:34:50 -04:00
PassiveLemon
6f5fadc085 Docker: Do not automatically build Zoraxy plugins 2025-07-07 13:33:57 -04:00
Toby Chui
45506c8772 Added cert resolve viewer
- Added certificate resolve viewer on HTTP proxy rule editor
- Exposed SNI options (wip)
- Code optimize
2025-07-07 14:18:10 +08:00
Toby Chui
c091b9d1ca Added content security policy structure
- Added content security policy header generators structure (current not in used)
2025-07-07 13:25:07 +08:00
Toby Chui
691cb603ce Merge pull request #724 from 7brend7/sort-list-of-certificates
sort list of loaded certificates by ExpireDate
2025-07-07 10:51:35 +08:00
Borys Anikiyenko
e53724d6e5 sort list of loaded certificates by ExpireDate 2025-07-06 22:40:10 +03:00
Borys Anikiyenko
e225407b03 fix empty sso advanced parameters 2025-07-06 22:25:17 +03:00
Toby Chui
273cae2a98 Merge pull request #719 from jemmy1794/main
Add EnableLogging to Stream Proxy for log control
2025-07-03 20:25:39 +08:00
jemmy1794
6b3b89f7bf Add EnableLogging to Stream Proxy for log control
- Add `EnableLogging` to control TCP/UDP Connection logs to reduce log latency.
- Add `Enable Logging` Option in Stream Proxy rule.
- Update Stream Proxy UI.
2025-07-03 09:01:46 +08:00
Toby Chui
2d611a559a Optimized structure for stream proxy
- Separated instance and config for stream proxy
2025-07-02 21:03:57 +08:00
Toby Chui
6c5eba01c2 Update README.md
Added more contributors in community maintained section name list
2025-07-02 20:42:14 +08:00
Toby Chui
f641797d10 Merge pull request #718 from jemmy1794/Stream-Proxy
Add Proxy Protocol V1 option in TCP Stream Proxy and update Stream Proxy UI
2025-07-02 20:40:08 +08:00
Jemmy
f92ff068f3 Added Proxy Protocol V1 to Stream Proxy UI
- Added a checkbox for Proxy Protocol V1.
- Modified related Config setting function.
2025-07-02 18:04:26 +08:00
Jemmy
b59ac47c8c Added Proxy Protocol V1 function.
- Added useProxyProtocol in ProxyRelayConfig
- Added writeProxyProtocolHeaderV1 function
2025-07-02 17:58:26 +08:00
Toby Chui
8030f3d62a Fixed #688
- Added auto restart after config change in static web server
2025-06-30 20:34:42 +08:00
Toby Chui
f8f623e3e4 Update .gitignore
Ignored dist folder
2025-06-28 17:00:31 +08:00
Toby Chui
061839756c Merge pull request #711 from Morethanevil/main
Update CHANGELOG.md
2025-06-28 14:34:58 +08:00
Marcel
1dcaa0c257 Update CHANGELOG.md 2025-06-28 08:31:20 +02:00
Toby Chui
ffd3909964 Merge pull request #710 from tobychui/v3.2.4
V3.2.4 update
2025-06-28 10:06:23 +08:00
Toby Chui
3ddccdffce Merge branch 'v3.2.4' of https://github.com/tobychui/zoraxy into v3.2.4 2025-06-27 22:02:29 +08:00
Toby Chui
929d4cc82a Optimized SSO UI
- Added tab menu to SSO settings
2025-06-27 22:02:28 +08:00
Toby Chui
4f1cd8a571 Merge pull request #705 from jemmy1794/v3.2.4
Fix: #659
2025-06-24 14:24:26 +08:00
Jemmy
f6b3656bb1 Fix: #659
Listen UDP port on (0.0.0.0)* address.
2025-06-24 13:10:58 +08:00
Toby Chui
74a816216e Merge pull request #702 from PassiveLemon/main
Release type Docker workflows
2025-06-19 07:11:14 +08:00
PassiveLemon
4a093cf096 Merge branch 'tobychui:main' into main 2025-06-18 16:54:24 -04:00
PassiveLemon
68f9fccf3a refactor: release type workflows 2025-06-18 16:53:51 -04:00
Toby Chui
f276040ad0 Added experimental fix for #695
Added prefix trim and location filter for oauth authrozied redirection
2025-06-16 21:21:50 +08:00
Toby Chui
2f40593daf Updated version code 2025-06-16 21:12:49 +08:00
Toby Chui
0b6dbd49bb Fixed #694
- Uncommented the delete proxy rule button
- Added redirection path escape in dpcore
2025-06-16 20:16:36 +08:00
Toby Chui
eb07917c14 Merge pull request #693 from tobychui/v3.2.3
- Added new HTTP proxy UI
- Added inbound host name edit function
- Merged SSO implementations
- Added disable chunked transfer encoding checkbox
2025-06-15 21:54:29 +08:00
Toby Chui
217bc48001 Merge branch 'main' into v3.2.3 2025-06-15 21:49:48 +08:00
Toby Chui
38cfab4a09 Merge pull request #692 from james-d-elliott/feat-forward-auth-improvements
feat(sso): forward auth improvements
2025-06-15 14:50:29 +08:00
Toby Chui
217e5e90ff Fixed #672
Fixing a minor log print logic
2025-06-15 13:56:10 +08:00
Toby Chui
4a37a989a0 Added Disable Chunk Transfer Encoding option
- Added disable chunk transfer encoding on UI #685
- Added optional to disable static web server listen to all interface #688
2025-06-15 13:46:35 +08:00
James Elliott
eb540b774d refactor: factorize 500 errors
This just factorizes the handling of 500 Internal Server Errors.
2025-06-15 12:14:14 +10:00
James Elliott
26d03f9ad4 feat(sso): forward auth improvements
This adds a couple of key improvements to the Forward Auth SSO implementation. Primarily it adds an included cookies setting which allows filtering cookies to the authorization server. Secondly it fixes a bug where the headerCopyIncluded function was case-sensitive. Documentation in the code and on the web UI is clearer to resolve some common questions and issues. Lastly it moves a lot of funcs to the util.go file and adds fairly comprehensive tests.
2025-06-15 11:57:38 +10:00
Toby Chui
650d61ba24 Added plugin doc link to homepage
- Fixed plugin doc css error
- Added plugin doc link to homepage footer
2025-06-12 19:26:15 +08:00
Toby Chui
366a44a992 Restored old httprp page
- Restored old httprp page
- Moved dev page to httprp_new.html
2025-06-10 22:19:33 +08:00
53 changed files with 2526 additions and 1745 deletions

View File

@@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", &currentTlsSetting) sysdb.Read("settings", "usetls", &currentTlsSetting)
} }
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"`
DomainKeyPair map[string]string `json:"domain_key_pair"`
UseDNSValidation bool `json:"use_dns_validation"`
}
// send response result := &CertInfo{
fmt.Fprintln(w, "File upload successful!") Domain: proxyRule.RootOrMatchingDomain,
AliasDomains: aliasDomains,
DomainKeyPair: domainKeyPairs,
UseDNSValidation: useDNSValidation,
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
} }
// Handle cert remove func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
func handleCertRemove(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)
} }

View File

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

View File

@@ -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 */

View File

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

View File

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

View File

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

View File

@@ -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)
}
return ErrInternalServerError
func headerCookieRedact(r *http.Request, excluded []string) {
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)
} }

View 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)
}

View 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)
})
}
}

View File

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

View File

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

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

View File

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

View File

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

View 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])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package streamproxy
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"net" "net"
@@ -30,48 +31,67 @@ 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
} }
//Check if connection in blacklist or whitelist // Check if connection in blacklist or whitelist
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
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)
} }
} }

View File

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

View 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
View 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */

View File

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

View File

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

View File

@@ -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}.`);

View File

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

View File

@@ -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();

View File

@@ -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);

View File

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