mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-21 05:34:29 +02:00
Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4e32f31f0a | ||
![]() |
381184cd92 | ||
![]() |
223ae9e112 | ||
![]() |
aff1975c5a | ||
![]() |
ad2519d894 | ||
![]() |
40f915f7fb | ||
![]() |
e3e31d9f22 | ||
![]() |
be5f631b9f | ||
![]() |
f9e51bfd27 | ||
![]() |
39b5da36d9 | ||
![]() |
5c6950ca56 | ||
![]() |
d187c32a8a | ||
![]() |
ed8f9b7337 | ||
![]() |
46cfc02493 | ||
![]() |
2d43890fcf | ||
![]() |
5a38c1d407 | ||
![]() |
dd93f9a2c4 | ||
![]() |
70b1ccfa6e | ||
![]() |
100c1e9c04 | ||
![]() |
a33600d3e2 | ||
![]() |
c4c10d2130 | ||
![]() |
4d3d1b25cb | ||
![]() |
118b5e5114 | ||
![]() |
ad53b894c0 | ||
![]() |
a0a394885c | ||
![]() |
51334a3a75 | ||
![]() |
6f5fadc085 | ||
![]() |
45506c8772 | ||
![]() |
c091b9d1ca | ||
![]() |
691cb603ce | ||
![]() |
e53724d6e5 | ||
![]() |
e225407b03 | ||
![]() |
273cae2a98 | ||
![]() |
6b3b89f7bf | ||
![]() |
2d611a559a | ||
![]() |
6c5eba01c2 | ||
![]() |
f641797d10 | ||
![]() |
f92ff068f3 | ||
![]() |
b59ac47c8c | ||
![]() |
8030f3d62a | ||
![]() |
f8f623e3e4 | ||
![]() |
061839756c | ||
![]() |
1dcaa0c257 | ||
![]() |
ffd3909964 | ||
![]() |
3ddccdffce | ||
![]() |
929d4cc82a | ||
![]() |
4f1cd8a571 | ||
![]() |
f6b3656bb1 | ||
![]() |
74a816216e | ||
![]() |
4a093cf096 | ||
![]() |
68f9fccf3a | ||
![]() |
f276040ad0 | ||
![]() |
2f40593daf | ||
![]() |
0b6dbd49bb |
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -2,7 +2,7 @@ name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [ released, prereleased ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
@@ -33,7 +33,8 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
with:
|
||||
context: ./docker
|
||||
@@ -45,3 +46,15 @@ jobs:
|
||||
cache-from: type=gha
|
||||
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
|
||||
|
||||
|
13
.gitignore
vendored
13
.gitignore
vendored
@@ -29,8 +29,6 @@ src/Zoraxy_*_*
|
||||
src/certs/*
|
||||
src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/docker-compose.yaml
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
/tools/dns_challenge_update/code-gen/acmedns
|
||||
/tools/dns_challenge_update/code-gen/lego
|
||||
@@ -41,11 +39,15 @@ src/sys.uuid
|
||||
src/zoraxy
|
||||
src/log/
|
||||
|
||||
|
||||
# dev-tags
|
||||
/Dockerfile
|
||||
/Entrypoint.sh
|
||||
|
||||
# docker testing stuff
|
||||
docker/test/
|
||||
docker/container-builder.sh
|
||||
docker/docker-compose.yaml
|
||||
|
||||
# plugins
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
@@ -56,4 +58,7 @@ log
|
||||
tmp
|
||||
sys.*
|
||||
www/html/index.html
|
||||
*.exe
|
||||
*.exe
|
||||
/src/dist
|
||||
|
||||
/src/plugins
|
||||
|
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,3 +1,36 @@
|
||||
# v3.2.4 28 Jun 2025
|
||||
|
||||
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
|
||||
|
||||
|
||||
+ Added Authentik support by [JokerQyou](https://github.com/tobychui/zoraxy/commits?author=JokerQyou)
|
||||
+ Added pluginsystem and moved GAN and Zerotier to plugins
|
||||
+ Add loopback detection [#573](https://github.com/tobychui/zoraxy/issues/573)
|
||||
+ Fixed Dark theme not working with Advanced Option accordion [#591](https://github.com/tobychui/zoraxy/issues/591)
|
||||
+ Update logger to include UserAgent by [Raithmir](https://github.com/Raithmir)
|
||||
+ Fixed memory usage in UI [#600](https://github.com/tobychui/zoraxy/issues/600)
|
||||
+ Added docker-compose.yml by [SamuelPalubaCZ](https://github.com/tobychui/zoraxy/commits?author=SamuelPalubaCZ)
|
||||
+ Added more statistics for proxy hosts [#201](https://github.com/tobychui/zoraxy/issues/201) and [#608](https://github.com/tobychui/zoraxy/issues/608)
|
||||
+ Fixed origin field in logs [#618](https://github.com/tobychui/zoraxy/issues/618)
|
||||
+ Added FreeBSD support by Andreas Burri
|
||||
+ Fixed HTTP proxy redirect [#626](https://github.com/tobychui/zoraxy/issues/626)
|
||||
+ Fixed proxy handling #629](https://github.com/tobychui/zoraxy/issues/629)
|
||||
+ Move Scope ID handling into CIDR check by [Nirostar](https://github.com/tobychui/zoraxy/commits?author=Nirostar)
|
||||
+ Prevent the browser from filling the saved Zoraxy login account by [WHFo](https://github.com/tobychui/zoraxy/commits?author=WHFo)
|
||||
+ Added port number and http proto to http proxy list link
|
||||
+ Fixed headers for authelia by [james-d-elliott](https://github.com/tobychui/zoraxy/commits?author=james-d-elliott)
|
||||
+ Refactored docker container list and UI improvements by [eyerrock](https://github.com/tobychui/zoraxy/commits?author=eyerrock)
|
||||
+ Refactored Dockerfile by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||
+ Added new HTTP proxy UI
|
||||
+ Added inbound host name edit function
|
||||
+ Added static web server option to disable listen to all interface
|
||||
+ Merged SSO implementations (Oauth2) [#649](https://github.com/tobychui/zoraxy/pull/649)
|
||||
+ Merged forward-auth optimization [#692(https://github.com/tobychui/zoraxy/pull/692)
|
||||
+ Optimized SSO UI
|
||||
+ Refactored docker image workflows by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
|
||||
+ Added disable chunked transfer encoding checkbox (for upstreams that uses legacy HTTP implementations)
|
||||
+ Bug fixes [#694](https://github.com/tobychui/zoraxy/issues/694), [#659](https://github.com/tobychui/zoraxy/issues/659) by [jemmy1794](https://github.com/tobychui/zoraxy/commits?author=jemmy1794), [#695](https://github.com/tobychui/zoraxy/issues/695)
|
||||
|
||||
# v3.1.9 1 Mar 2025
|
||||
|
||||
+ Fixed netstat underflow bug
|
||||
|
@@ -200,6 +200,10 @@ Some section of Zoraxy are contributed by our amazing community and if you have
|
||||
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)
|
||||
|
||||
- Change Log [@Morethanevil](https://github.com/Morethanevil)
|
||||
|
||||
### Looking for Maintainer
|
||||
|
||||
- ACME DNS Challenge Module
|
||||
|
2
docker/.gitignore
vendored
Normal file
2
docker/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
example/
|
||||
src/
|
@@ -34,34 +34,18 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
||||
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
|
||||
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 ./build_plugins.sh /usr/local/bin/build_plugins
|
||||
|
||||
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
COPY --chmod=700 ./entrypoint.py /opt/zoraxy/
|
||||
|
||||
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
|
||||
|
||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
RUN mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
echo "tun" | tee -a /etc/modules
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
@@ -89,7 +73,7 @@ VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
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
|
||||
|
||||
|
@@ -119,18 +119,14 @@ Or for Docker Compose:
|
||||
|
||||
### Plugins
|
||||
|
||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
### Building
|
||||
|
||||
To build the Docker image:
|
||||
- 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 .`
|
||||
- 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.
|
||||
|
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Running go mod tidy and go build for all directories..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cd "$dir" || exit 1
|
||||
go mod tidy
|
||||
go build
|
||||
cd "$1" || exit 1
|
||||
fi
|
||||
done
|
||||
|
128
docker/entrypoint.py
Normal file
128
docker/entrypoint.py
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
zoraxy_proc = None
|
||||
zerotier_proc = None
|
||||
|
||||
def getenv(key, default=None):
|
||||
return os.environ.get(key, default)
|
||||
|
||||
def run(command):
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {command} - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def popen(command):
|
||||
proc = subprocess.Popen(command)
|
||||
time.sleep(1)
|
||||
if proc.poll() is not None:
|
||||
print(f"{command} exited early with code {proc.returncode}")
|
||||
raise RuntimeError(f"Failed to start {command}")
|
||||
return proc
|
||||
|
||||
def cleanup(_signum, _frame):
|
||||
print("Shutdown signal received. Cleaning up...")
|
||||
|
||||
global zoraxy_proc, zerotier_proc
|
||||
|
||||
if zoraxy_proc and zoraxy_proc.poll() is None:
|
||||
print("Terminating Zoraxy...")
|
||||
zoraxy_proc.terminate()
|
||||
|
||||
if zerotier_proc and zerotier_proc.poll() is None:
|
||||
print("Terminating ZeroTier-One...")
|
||||
zerotier_proc.terminate()
|
||||
|
||||
if zoraxy_proc:
|
||||
try:
|
||||
zoraxy_proc.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
zoraxy_proc.kill()
|
||||
zoraxy_proc.wait()
|
||||
|
||||
if zerotier_proc:
|
||||
try:
|
||||
zerotier_proc.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
zerotier_proc.kill()
|
||||
zerotier_proc.wait()
|
||||
|
||||
try:
|
||||
os.unlink("/var/lib/zerotier-one")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Failed to unlink ZeroTier socket: {e}")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
def start_zerotier():
|
||||
print("Starting ZeroTier...")
|
||||
|
||||
global zerotier_proc
|
||||
|
||||
config_dir = "/opt/zoraxy/config/zerotier/"
|
||||
zt_path = "/var/lib/zerotier-one"
|
||||
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
os.symlink(config_dir, zt_path, target_is_directory=True)
|
||||
|
||||
zerotier_proc = popen(["zerotier-one"])
|
||||
|
||||
def start_zoraxy():
|
||||
print("Starting Zoraxy...")
|
||||
|
||||
global zoraxy_proc
|
||||
|
||||
zoraxy_args = [
|
||||
"zoraxy",
|
||||
f"-autorenew={getenv('AUTORENEW', '86400')}",
|
||||
f"-cfgupgrade={getenv('CFGUPGRADE', 'true')}",
|
||||
f"-db={getenv('DB', 'auto')}",
|
||||
f"-docker={getenv('DOCKER', 'true')}",
|
||||
f"-earlyrenew={getenv('EARLYRENEW', '30')}",
|
||||
f"-fastgeoip={getenv('FASTGEOIP', 'false')}",
|
||||
f"-mdns={getenv('MDNS', 'true')}",
|
||||
f"-mdnsname={getenv('MDNSNAME', "''")}",
|
||||
f"-noauth={getenv('NOAUTH', 'false')}",
|
||||
f"-plugin={getenv('PLUGIN', '/opt/zoraxy/plugin/')}",
|
||||
f"-port=:{getenv('PORT', '8000')}",
|
||||
f"-sshlb={getenv('SSHLB', 'false')}",
|
||||
f"-update_geoip={getenv('UPDATE_GEOIP', 'false')}",
|
||||
f"-version={getenv('VERSION', 'false')}",
|
||||
f"-webfm={getenv('WEBFM', 'true')}",
|
||||
f"-webroot={getenv('WEBROOT', './www')}",
|
||||
]
|
||||
|
||||
zoraxy_proc = popen(zoraxy_args)
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, cleanup)
|
||||
signal.signal(signal.SIGINT, cleanup)
|
||||
|
||||
print("Updating CA certificates...")
|
||||
run(["update-ca-certificates"])
|
||||
|
||||
print("Updating GeoIP data...")
|
||||
run(["zoraxy", "-update_geoip=true"])
|
||||
|
||||
os.chdir("/opt/zoraxy/config/")
|
||||
|
||||
if getenv("ZEROTIER", "false") == "true":
|
||||
start_zerotier()
|
||||
|
||||
start_zoraxy()
|
||||
|
||||
signal.pause()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cleanup() {
|
||||
echo "Stop signal received. Shutting down..."
|
||||
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
||||
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
||||
unlink /var/lib/zerotier-one/zerotier/
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup SIGTERM SIGINT TERM INT
|
||||
|
||||
update-ca-certificates && echo "CA certificates updated."
|
||||
zoraxy -update_geoip=true && echo "GeoIP data updated ."
|
||||
|
||||
echo "Building plugins..."
|
||||
cd /opt/zoraxy/plugin/ || exit 1
|
||||
build_plugins "$PWD"
|
||||
echo "Plugins built."
|
||||
cd /opt/zoraxy/config/ || exit 1
|
||||
|
||||
if [ "$ZEROTIER" = "true" ]; then
|
||||
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
|
||||
mkdir -p /opt/zoraxy/config/zerotier/
|
||||
fi
|
||||
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
|
||||
zerotier-one -d &
|
||||
zerotierpid=$!
|
||||
echo "ZeroTier daemon started."
|
||||
fi
|
||||
|
||||
echo "Starting Zoraxy..."
|
||||
zoraxy \
|
||||
-autorenew="$AUTORENEW" \
|
||||
-cfgupgrade="$CFGUPGRADE" \
|
||||
-db="$DB" \
|
||||
-docker="$DOCKER" \
|
||||
-earlyrenew="$EARLYRENEW" \
|
||||
-fastgeoip="$FASTGEOIP" \
|
||||
-mdns="$MDNS" \
|
||||
-mdnsname="$MDNSNAME" \
|
||||
-noauth="$NOAUTH" \
|
||||
-plugin="$PLUGIN" \
|
||||
-port=:"$PORT" \
|
||||
-sshlb="$SSHLB" \
|
||||
-update_geoip="$UPDATE_GEOIP" \
|
||||
-version="$VERSION" \
|
||||
-webfm="$WEBFM" \
|
||||
-webroot="$WEBROOT" \
|
||||
&
|
||||
|
||||
zoraxypid=$!
|
||||
wait "$zoraxypid"
|
||||
wait "$zerotierpid"
|
||||
|
3
example/plugins/api-call-example/go.mod
Normal file
3
example/plugins/api-call-example/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aroz.org/zoraxy/api-call-example
|
||||
|
||||
go 1.24.5
|
54
example/plugins/api-call-example/main.go
Normal file
54
example/plugins/api-call-example/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.api_call_example"
|
||||
UI_PATH = "/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Serve the plugin intro spect
|
||||
// This will print the plugin intro spect and exit if the -introspect flag is provided
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "API Call Example Plugin",
|
||||
Author: "Anthony Rubick",
|
||||
AuthorContact: "",
|
||||
Description: "An example plugin for making API calls",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Endpoint: "/plugin/api/access/list",
|
||||
Reason: "Used to display all configured Access Rules",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error serving introspect: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
http.HandleFunc(UI_PATH+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
RenderUI(runtimeCfg, w, r)
|
||||
})
|
||||
|
||||
serverAddr := fmt.Sprintf("127.0.0.1:%d", runtimeCfg.Port)
|
||||
fmt.Printf("Starting API Call Example Plugin on %s\n", serverAddr)
|
||||
http.ListenAndServe(serverAddr, nil)
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
# Zoraxy Plugin
|
||||
|
||||
## Overview
|
||||
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Copy the Module:**
|
||||
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
|
||||
|
||||
2. **Include the Structure:**
|
||||
- Ensure that you maintain the directory structure and file organization as provided in this module.
|
||||
|
||||
3. **Modify as Needed:**
|
||||
- Customize the copied module to implement the desired functionality for your plugin.
|
||||
|
||||
## Directory Structure
|
||||
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
|
||||
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages
|
@@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
|
||||
// The targetFsPrefix should be relative to the root of the embed.FS
|
||||
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
type PluginType int
|
||||
|
||||
const (
|
||||
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
|
||||
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
|
||||
)
|
||||
|
||||
type StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
|
||||
const (
|
||||
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
|
||||
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
|
||||
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
|
||||
)
|
||||
|
||||
type SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
type IntroSpect struct {
|
||||
/* Plugin metadata */
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
/*
|
||||
|
||||
Endpoint Settings
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
ServeIntroSpect Function
|
||||
|
||||
This function will check if the plugin is initialized with -introspect flag,
|
||||
if so, it will print the intro spect and exit
|
||||
|
||||
Place this function at the beginning of your plugin main function
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ConfigureSpec Payload
|
||||
|
||||
Zoraxy will start your plugin with -configure flag,
|
||||
the plugin shell read this payload as JSON and configure itself
|
||||
by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
RecvExecuteConfigureSpec Function
|
||||
|
||||
This function will read the configure spec from Zoraxy
|
||||
and return the ConfigureSpec object
|
||||
|
||||
Place this function after ServeIntroSpect function in your plugin main function
|
||||
*/
|
||||
func RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("No port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
272
example/plugins/api-call-example/ui.go
Normal file
272
example/plugins/api-call-example/ui.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to the permitted endpoint
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Make sure to set the Authorization header
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey) // Use the API key from the runtime config
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
// Check if the response status is OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return string(respDump), fmt.Errorf("received non-OK response status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to the permitted endpoint with an invalid key
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use an invalid API key
|
||||
req.Header.Set("Authorization", "Bearer invalid-key")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func unaccessibleEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to an endpoint that is not permitted
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/api/acme/listExpiredDomains", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use the API key from the runtime config
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func unpermittedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
|
||||
// Make an API call to an endpoint that is plugin-accessible but is not permitted
|
||||
client := &http.Client{}
|
||||
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/proxy/list", cfg.ZoraxyPort)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
// Use the API key from the runtime config
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error making API call: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error dumping response: %v", err)
|
||||
}
|
||||
|
||||
return string(respDump), nil
|
||||
}
|
||||
|
||||
func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
|
||||
// make several types of API calls to demonstrate the plugin functionality
|
||||
accessList, err := allowedEndpoint(config)
|
||||
var RenderedAccessListHTML string
|
||||
if err != nil {
|
||||
if accessList != "" {
|
||||
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p><pre>%s</pre>", err, html.EscapeString(accessList))
|
||||
} else {
|
||||
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p>", err)
|
||||
}
|
||||
} else {
|
||||
// Render the access list as HTML
|
||||
RenderedAccessListHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(accessList))
|
||||
}
|
||||
|
||||
// Make an API call with an invalid key
|
||||
invalidKeyResponse, err := allowedEndpointInvalidKey(config)
|
||||
var RenderedInvalidKeyResponseHTML string
|
||||
if err != nil {
|
||||
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<p>Error with invalid key: %v</p>", err)
|
||||
} else {
|
||||
// Render the invalid key response as HTML
|
||||
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(invalidKeyResponse))
|
||||
}
|
||||
|
||||
// Make an API call to an endpoint that is not plugin-accessible
|
||||
unaccessibleResponse, err := unaccessibleEndpoint(config)
|
||||
var RenderedUnaccessibleResponseHTML string
|
||||
if err != nil {
|
||||
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<p>Error with unaccessible endpoint: %v</p>", err)
|
||||
} else {
|
||||
// Render the unaccessible response as HTML
|
||||
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unaccessibleResponse))
|
||||
}
|
||||
|
||||
// Make an API call to an endpoint that is plugin-accessible but is not permitted
|
||||
unpermittedResponse, err := unpermittedEndpoint(config)
|
||||
var RenderedUnpermittedResponseHTML string
|
||||
if err != nil {
|
||||
RenderedUnpermittedResponseHTML = fmt.Sprintf("<p>Error with unpermitted endpoint: %v</p>", err)
|
||||
} else {
|
||||
// Render the unpermitted response as HTML
|
||||
RenderedUnpermittedResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unpermittedResponse))
|
||||
}
|
||||
|
||||
// Render the UI for the plugin
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>API Call Example Plugin UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<style>
|
||||
.response-block {
|
||||
background-color: var(--theme_bg_primary);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.response-block:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.response-block h3 {
|
||||
margin-top: 0;
|
||||
color: var(--text_color);
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.response-block.success {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
.response-block.error {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
.response-block.warning {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
.response-content {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.response-content pre {
|
||||
background-color: var(--theme_highlight);
|
||||
border: 1px solid var(--theme_divider);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
height: 200px;
|
||||
max-height: 80vh;
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
|
||||
<h1>Welcome to the API Call Example Plugin UI</h1>
|
||||
<p>Plugin is running on port: ` + strconv.Itoa(config.Port) + `</p>
|
||||
|
||||
<h2>API Call Examples</h2>
|
||||
|
||||
<div class="response-block success">
|
||||
<h3>✅ Allowed Endpoint (Valid API Key)</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/access/list</code> with a valid API key:</p>
|
||||
<div class="response-content">
|
||||
` + RenderedAccessListHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block warning">
|
||||
<h3>⚠️ Invalid API Key</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/access/list</code> with an invalid API key:</p>
|
||||
<div class="response-content">
|
||||
` + RenderedInvalidKeyResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block warning">
|
||||
<h3>⚠️ Unpermitted Endpoint</h3>
|
||||
<p>Making a GET request to <code>/plugin/api/proxy/list</code> (not a permitted endpoint):</p>
|
||||
<div class="response-content">
|
||||
` + RenderedUnpermittedResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-block error">
|
||||
<h3>❌ Disallowed Endpoint</h3>
|
||||
<p>Making a GET request to <code>/api/acme/listExpiredDomains</code> (not a plugin-accessible endpoint):</p>
|
||||
<div class="response-content">
|
||||
` + RenderedUnaccessibleResponseHTML + `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
w.Write([]byte(html))
|
||||
}
|
19
src/api.go
19
src/api.go
@@ -34,6 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig)
|
||||
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
@@ -71,14 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
|
||||
// Register the APIs for TLS / SSL certificate management functions
|
||||
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||
//Global certificate settings
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
|
||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
|
||||
authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
|
||||
|
||||
//Certificate store functions
|
||||
authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
|
||||
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
|
||||
|
381
src/cert.go
381
src/cert.go
@@ -1,180 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"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
|
||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
currentTlsSetting := true //Default to true
|
||||
@@ -185,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
//Get the current status
|
||||
js, _ := json.Marshal(currentTlsSetting)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if r.Method == http.MethodPost {
|
||||
case http.MethodPost:
|
||||
newState, err := utils.PostBool(r, "set")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "new state not set or invalid")
|
||||
@@ -205,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
}
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
default:
|
||||
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)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
switch newState {
|
||||
case "true":
|
||||
sysdb.Write("settings", "forceLatestTLS", true)
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||
} else if newState == "false" {
|
||||
case "false":
|
||||
sysdb.Write("settings", "forceLatestTLS", false)
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||
} else {
|
||||
default:
|
||||
utils.SendErrorResponse(w, "invalid state given")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle download of the selected certificate
|
||||
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
|
||||
}
|
||||
|
||||
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
//Assume localhost
|
||||
domain = "default"
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart form data
|
||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
// get the proxy rule, the pass in domain value must be root or matching domain
|
||||
proxyRule, err := dynamicProxyRouter.GetProxyEndpointById(domain, false)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
//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
|
||||
}
|
||||
}
|
||||
|
||||
// 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("./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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
// 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
|
||||
}
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
//Make sure pubkey and private key are not empty
|
||||
if pubkey == "" || prikey == "" {
|
||||
domainKeyPairs[thisDomain] = ""
|
||||
} else {
|
||||
//Store the key pair
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//A domain must be UseDNSValidation if it is a wildcard domain or its alias is a wildcard domain
|
||||
useDNSValidation := strings.HasPrefix(proxyRule.RootOrMatchingDomain, "*")
|
||||
for _, alias := range aliasDomains {
|
||||
if strings.HasPrefix(alias, "*") || strings.HasPrefix(domain, "*") {
|
||||
useDNSValidation = true
|
||||
}
|
||||
}
|
||||
|
||||
type CertInfo struct {
|
||||
Domain string `json:"domain"`
|
||||
AliasDomains []string `json:"alias_domains"`
|
||||
DomainKeyPair map[string]string `json:"domain_key_pair"`
|
||||
UseDNSValidation bool `json:"use_dns_validation"`
|
||||
}
|
||||
|
||||
result := &CertInfo{
|
||||
Domain: proxyRule.RootOrMatchingDomain,
|
||||
AliasDomains: aliasDomains,
|
||||
DomainKeyPair: domainKeyPairs,
|
||||
UseDNSValidation: useDNSValidation,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle cert remove
|
||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||
func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
//Get the domain
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
err = tlsCertManager.RemoveCert(domain)
|
||||
|
||||
//Get the certificate name
|
||||
certName, err := utils.PostPara(r, "certname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
utils.SendErrorResponse(w, "invalid certificate name given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target endpoint
|
||||
ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
|
||||
return
|
||||
}
|
||||
|
||||
//Set the preferred certificate for the domain
|
||||
err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = SaveReverseProxyConfig(ept)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@@ -59,12 +60,18 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
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
|
||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||
}
|
||||
|
||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||
switch thisConfigEndpoint.ProxyType {
|
||||
case dynamicproxy.ProxyTypeRoot:
|
||||
//This is a root config file
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
@@ -73,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||
|
||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
|
||||
case dynamicproxy.ProxyTypeHost:
|
||||
//This is a host config file
|
||||
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
@@ -81,7 +88,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
}
|
||||
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
||||
} else {
|
||||
default:
|
||||
return errors.New("not supported proxy type")
|
||||
}
|
||||
|
||||
@@ -101,9 +108,9 @@ func filterProxyConfigFilename(filename string) string {
|
||||
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//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 {
|
||||
filename = "./conf/proxy/root.config"
|
||||
filename = filepath.Join(CONF_HTTP_PROXY, "root.config")
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
@@ -118,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(endpoint string) error {
|
||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
||||
filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config")
|
||||
if endpoint == "/" {
|
||||
filename = "./conf/proxy/root.config"
|
||||
filename = filepath.Join(CONF_HTTP_PROXY, "/root.config")
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
@@ -172,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Specify the folder path to be zipped
|
||||
if !utils.FileExists("./conf") {
|
||||
if !utils.FileExists(CONF_FOLDER) {
|
||||
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
||||
return
|
||||
}
|
||||
folderPath := "./conf"
|
||||
folderPath := CONF_FOLDER
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
@@ -277,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// Create the target directory to unzip the files
|
||||
targetDir := "./conf"
|
||||
targetDir := CONF_FOLDER
|
||||
if utils.FileExists(targetDir) {
|
||||
//Backup the old config to old
|
||||
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
||||
//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)
|
||||
|
26
src/def.go
26
src/def.go
@@ -44,7 +44,7 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.2.3"
|
||||
SYSTEM_VERSION = "3.2.6"
|
||||
DEVELOPMENT_BUILD = false
|
||||
|
||||
/* System Constants */
|
||||
@@ -63,14 +63,19 @@ const (
|
||||
LOG_EXTENSION = ".log"
|
||||
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
||||
|
||||
/* Configuration Folder Storage Path Constants */
|
||||
CONF_HTTP_PROXY = "./conf/proxy"
|
||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||
CONF_CERT_STORE = "./conf/certs"
|
||||
CONF_REDIRECTION = "./conf/redirect"
|
||||
CONF_ACCESS_RULE = "./conf/access"
|
||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
|
||||
/*
|
||||
Configuration Folder Storage Path Constants
|
||||
Note: No tailing slash in the path
|
||||
*/
|
||||
CONF_FOLDER = "./conf"
|
||||
CONF_HTTP_PROXY = CONF_FOLDER + "/proxy"
|
||||
CONF_STREAM_PROXY = CONF_FOLDER + "/streamproxy"
|
||||
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 */
|
||||
@@ -144,6 +149,9 @@ var (
|
||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//Plugin auth related
|
||||
pluginApiKeyManager *auth.APIKeyManager //API key manager for plugin authentication
|
||||
|
||||
//Authentication Provider
|
||||
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
||||
oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
|
||||
|
@@ -69,7 +69,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
if *geoDbUpdate {
|
||||
geodb.DownloadGeoDBUpdate("./conf/geodb")
|
||||
geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ func main() {
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs(webminPanelMux)
|
||||
initRestAPI(webminPanelMux)
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
|
112
src/mod/auth/plugin_apikey_manager.go
Normal file
112
src/mod/auth/plugin_apikey_manager.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// PluginAPIKey represents an API key for a plugin
|
||||
type PluginAPIKey struct {
|
||||
PluginID string
|
||||
APIKey string
|
||||
PermittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint // List of permitted API endpoints
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// APIKeyManager manages API keys for plugins
|
||||
type APIKeyManager struct {
|
||||
keys map[string]*PluginAPIKey // key: API key, value: plugin info
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAPIKeyManager creates a new API key manager
|
||||
func NewAPIKeyManager() *APIKeyManager {
|
||||
return &APIKeyManager{
|
||||
keys: make(map[string]*PluginAPIKey),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAPIKey generates a new API key for a plugin
|
||||
func (m *APIKeyManager) GenerateAPIKey(pluginID string, permittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint) (*PluginAPIKey, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Generate a cryptographically secure random key
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
// Hash the random bytes to create the API key
|
||||
hash := sha256.Sum256(bytes)
|
||||
apiKey := hex.EncodeToString(hash[:])
|
||||
|
||||
// Create the plugin API key
|
||||
pluginAPIKey := &PluginAPIKey{
|
||||
PluginID: pluginID,
|
||||
APIKey: apiKey,
|
||||
PermittedEndpoints: permittedEndpoints,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Store the API key
|
||||
m.keys[apiKey] = pluginAPIKey
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKey validates an API key and returns the associated plugin information
|
||||
func (m *APIKeyManager) ValidateAPIKey(apiKey string) (*PluginAPIKey, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
pluginAPIKey, exists := m.keys[apiKey]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("invalid API key")
|
||||
}
|
||||
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKeyForEndpoint validates an API key for a specific endpoint
|
||||
func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string, apiKey string) (*PluginAPIKey, error) {
|
||||
pluginAPIKey, err := m.ValidateAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the endpoint is permitted
|
||||
for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints {
|
||||
if permittedEndpoint.Endpoint == endpoint && permittedEndpoint.Method == method {
|
||||
return pluginAPIKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("endpoint not permitted for this API key")
|
||||
}
|
||||
|
||||
// RevokeAPIKeysForPlugin revokes all API keys for a specific plugin
|
||||
func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
keysToRemove := []string{}
|
||||
for apiKey, pluginAPIKey := range m.keys {
|
||||
if pluginAPIKey.PluginID == pluginID {
|
||||
keysToRemove = append(keysToRemove, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
for _, apiKey := range keysToRemove {
|
||||
delete(m.keys, apiKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
94
src/mod/auth/plugin_middleware.go
Normal file
94
src/mod/auth/plugin_middleware.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Handles the API-Key based authentication for plugins
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_API_PREFIX = "/plugin"
|
||||
)
|
||||
|
||||
type PluginMiddlewareOptions struct {
|
||||
DeniedHandler http.HandlerFunc //Thing(s) to do when request is rejected
|
||||
ApiKeyManager *APIKeyManager
|
||||
TargetMux *http.ServeMux
|
||||
}
|
||||
|
||||
// PluginAuthMiddleware provides authentication middleware for plugin API requests
|
||||
type PluginAuthMiddleware struct {
|
||||
option PluginMiddlewareOptions
|
||||
endpoints map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
// NewPluginAuthMiddleware creates a new plugin authentication middleware
|
||||
func NewPluginAuthMiddleware(option PluginMiddlewareOptions) *PluginAuthMiddleware {
|
||||
return &PluginAuthMiddleware{
|
||||
option: option,
|
||||
endpoints: make(map[string]http.HandlerFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PluginAuthMiddleware) HandleAuthCheck(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
// Check for API key in the Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
// No authorization header
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a plugin API key (Bearer token)
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
// Not a Bearer token
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the API key
|
||||
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Validate the API key for this endpoint
|
||||
_, err := m.option.ApiKeyManager.ValidateAPIKeyForEndpoint(r.URL.Path, r.Method, apiKey)
|
||||
if err != nil {
|
||||
// Invalid API key or endpoint not permitted
|
||||
m.option.DeniedHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the original handler
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
// wraps an HTTP handler with plugin authentication middleware
|
||||
func (m *PluginAuthMiddleware) HandleFunc(endpoint string, handler http.HandlerFunc) error {
|
||||
// ensure the endpoint is prefixed with PLUGIN_API_PREFIX
|
||||
if !strings.HasPrefix(endpoint, PLUGIN_API_PREFIX) {
|
||||
endpoint = PLUGIN_API_PREFIX + endpoint
|
||||
}
|
||||
|
||||
// Check if the endpoint already registered
|
||||
if _, exist := m.endpoints[endpoint]; exist {
|
||||
fmt.Println("WARNING! Duplicated registering of plugin api endpoint: " + endpoint)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
m.endpoints[endpoint] = handler
|
||||
|
||||
wrappedHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
m.HandleAuthCheck(w, r, handler)
|
||||
}
|
||||
|
||||
// Ok. Register handler
|
||||
if m.option.TargetMux == nil {
|
||||
http.HandleFunc(endpoint, wrappedHandler)
|
||||
} else {
|
||||
m.option.TargetMux.HandleFunc(endpoint, wrappedHandler)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -58,11 +58,20 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
||||
|
||||
options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
||||
options.RequestHeaders = strings.Split(requestHeaders, ",")
|
||||
options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
|
||||
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
||||
// Helper function to clean empty strings from split results
|
||||
cleanSplit := func(s string) []string {
|
||||
if s == "" {
|
||||
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{
|
||||
client: &http.Client{
|
||||
|
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OAuth2RouterOptions struct {
|
||||
@@ -250,7 +251,19 @@ func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request)
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
w.Header().Add("Set-Cookie", cookie.String())
|
||||
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
return errors.New("authorized")
|
||||
}
|
||||
unauthorized := false
|
||||
|
@@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
sep := h.Parent.GetProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
//Matching proxy rule found
|
||||
//Access Check (blacklist / whitelist)
|
||||
|
59
src/mod/dynamicproxy/certificate.go
Normal file
59
src/mod/dynamicproxy/certificate.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
)
|
||||
|
||||
func (router *Router) ResolveHostSpecificTlsBehaviorForHostname(hostname string) (*tlscert.HostSpecificTlsBehavior, error) {
|
||||
if hostname == "" {
|
||||
return nil, errors.New("hostname cannot be empty")
|
||||
}
|
||||
|
||||
ept := router.GetProxyEndpointFromHostname(hostname)
|
||||
if ept == nil {
|
||||
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||
}
|
||||
|
||||
// Check if the endpoint has a specific TLS behavior
|
||||
if ept.TlsOptions != nil {
|
||||
imported := &tlscert.HostSpecificTlsBehavior{}
|
||||
router.tlsBehaviorMutex.RLock()
|
||||
// Deep copy the TlsOptions using JSON marshal/unmarshal
|
||||
data, err := json.Marshal(ept.TlsOptions)
|
||||
if err != nil {
|
||||
router.tlsBehaviorMutex.RUnlock()
|
||||
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||
}
|
||||
router.tlsBehaviorMutex.RUnlock()
|
||||
if err := json.Unmarshal(data, imported); err != nil {
|
||||
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||
}
|
||||
return imported, nil
|
||||
}
|
||||
|
||||
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||
}
|
||||
|
||||
func (router *Router) SetPreferredCertificateForDomain(ept *ProxyEndpoint, domain string, certName string) error {
|
||||
if ept == nil || certName == "" {
|
||||
return errors.New("endpoint and certificate name cannot be empty")
|
||||
}
|
||||
|
||||
// Set the preferred certificate for the endpoint
|
||||
if ept.TlsOptions == nil {
|
||||
ept.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
|
||||
}
|
||||
|
||||
router.tlsBehaviorMutex.Lock()
|
||||
if ept.TlsOptions.PreferredCertificate == nil {
|
||||
ept.TlsOptions.PreferredCertificate = make(map[string]string)
|
||||
}
|
||||
ept.TlsOptions.PreferredCertificate[domain] = certName
|
||||
router.tlsBehaviorMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
@@ -330,7 +330,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
locationRewrite := res.Header.Get("Location")
|
||||
originLocation := res.Header.Get("Location")
|
||||
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://") {
|
||||
//Full path
|
||||
//Replace the forwarded target with expected Host
|
||||
|
@@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||
sep := router.GetProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
@@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||
subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
|
||||
return subdEndpoint != nil
|
||||
}
|
||||
|
||||
|
123
src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
Normal file
123
src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package permissionpolicy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Content Security Policy
|
||||
|
||||
This is a content security policy header modifier that changes
|
||||
the request content security policy fields
|
||||
|
||||
author: tobychui
|
||||
|
||||
//TODO: intergrate this with the dynamic proxy module
|
||||
*/
|
||||
|
||||
type ContentSecurityPolicy struct {
|
||||
DefaultSrc []string `json:"default_src"`
|
||||
ScriptSrc []string `json:"script_src"`
|
||||
StyleSrc []string `json:"style_src"`
|
||||
ImgSrc []string `json:"img_src"`
|
||||
ConnectSrc []string `json:"connect_src"`
|
||||
FontSrc []string `json:"font_src"`
|
||||
ObjectSrc []string `json:"object_src"`
|
||||
MediaSrc []string `json:"media_src"`
|
||||
FrameSrc []string `json:"frame_src"`
|
||||
WorkerSrc []string `json:"worker_src"`
|
||||
ChildSrc []string `json:"child_src"`
|
||||
ManifestSrc []string `json:"manifest_src"`
|
||||
PrefetchSrc []string `json:"prefetch_src"`
|
||||
FormAction []string `json:"form_action"`
|
||||
FrameAncestors []string `json:"frame_ancestors"`
|
||||
BaseURI []string `json:"base_uri"`
|
||||
Sandbox []string `json:"sandbox"`
|
||||
ReportURI []string `json:"report_uri"`
|
||||
ReportTo []string `json:"report_to"`
|
||||
UpgradeInsecureRequests bool `json:"upgrade_insecure_requests"`
|
||||
BlockAllMixedContent bool `json:"block_all_mixed_content"`
|
||||
}
|
||||
|
||||
// GetDefaultContentSecurityPolicy returns a ContentSecurityPolicy struct with default permissive settings
|
||||
func GetDefaultContentSecurityPolicy() *ContentSecurityPolicy {
|
||||
return &ContentSecurityPolicy{
|
||||
DefaultSrc: []string{"*"},
|
||||
ScriptSrc: []string{"*"},
|
||||
StyleSrc: []string{"*"},
|
||||
ImgSrc: []string{"*"},
|
||||
ConnectSrc: []string{"*"},
|
||||
FontSrc: []string{"*"},
|
||||
ObjectSrc: []string{"*"},
|
||||
MediaSrc: []string{"*"},
|
||||
FrameSrc: []string{"*"},
|
||||
WorkerSrc: []string{"*"},
|
||||
ChildSrc: []string{"*"},
|
||||
ManifestSrc: []string{"*"},
|
||||
PrefetchSrc: []string{"*"},
|
||||
FormAction: []string{"*"},
|
||||
FrameAncestors: []string{"*"},
|
||||
BaseURI: []string{"*"},
|
||||
Sandbox: []string{},
|
||||
ReportURI: []string{},
|
||||
ReportTo: []string{},
|
||||
UpgradeInsecureRequests: false,
|
||||
BlockAllMixedContent: false,
|
||||
}
|
||||
}
|
||||
|
||||
// ToHeader converts a ContentSecurityPolicy struct into a CSP header key-value pair
|
||||
func (csp *ContentSecurityPolicy) ToHeader() []string {
|
||||
directives := []string{}
|
||||
|
||||
addDirective := func(name string, sources []string) {
|
||||
if len(sources) > 0 {
|
||||
directives = append(directives, name+" "+strings.Join(sources, " "))
|
||||
}
|
||||
}
|
||||
|
||||
addDirective("default-src", csp.DefaultSrc)
|
||||
addDirective("script-src", csp.ScriptSrc)
|
||||
addDirective("style-src", csp.StyleSrc)
|
||||
addDirective("img-src", csp.ImgSrc)
|
||||
addDirective("connect-src", csp.ConnectSrc)
|
||||
addDirective("font-src", csp.FontSrc)
|
||||
addDirective("object-src", csp.ObjectSrc)
|
||||
addDirective("media-src", csp.MediaSrc)
|
||||
addDirective("frame-src", csp.FrameSrc)
|
||||
addDirective("worker-src", csp.WorkerSrc)
|
||||
addDirective("child-src", csp.ChildSrc)
|
||||
addDirective("manifest-src", csp.ManifestSrc)
|
||||
addDirective("prefetch-src", csp.PrefetchSrc)
|
||||
addDirective("form-action", csp.FormAction)
|
||||
addDirective("frame-ancestors", csp.FrameAncestors)
|
||||
addDirective("base-uri", csp.BaseURI)
|
||||
if len(csp.Sandbox) > 0 {
|
||||
directives = append(directives, "sandbox "+strings.Join(csp.Sandbox, " "))
|
||||
}
|
||||
if len(csp.ReportURI) > 0 {
|
||||
addDirective("report-uri", csp.ReportURI)
|
||||
}
|
||||
if len(csp.ReportTo) > 0 {
|
||||
addDirective("report-to", csp.ReportTo)
|
||||
}
|
||||
if csp.UpgradeInsecureRequests {
|
||||
directives = append(directives, "upgrade-insecure-requests")
|
||||
}
|
||||
if csp.BlockAllMixedContent {
|
||||
directives = append(directives, "block-all-mixed-content")
|
||||
}
|
||||
|
||||
headerValue := strings.Join(directives, "; ")
|
||||
return []string{"Content-Security-Policy", headerValue}
|
||||
}
|
||||
|
||||
// InjectContentSecurityPolicyHeader injects the CSP header into the response
|
||||
func InjectContentSecurityPolicyHeader(w http.ResponseWriter, csp *ContentSecurityPolicy) {
|
||||
if csp == nil || w.Header().Get("Content-Security-Policy") != "" {
|
||||
return
|
||||
}
|
||||
headerKV := csp.ToHeader()
|
||||
w.Header().Set(headerKV[0], headerKV[1])
|
||||
}
|
@@ -34,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
||||
}
|
||||
|
||||
// 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
|
||||
hostname = strings.ToLower(hostname)
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
@@ -63,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
|
||||
}
|
||||
|
||||
//Wildcard not match. Check for alias
|
||||
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
|
||||
if len(ep.MatchingDomainAlias) > 0 {
|
||||
for _, aliasDomain := range ep.MatchingDomainAlias {
|
||||
match, err := filepath.Match(aliasDomain, hostname)
|
||||
if err != nil {
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -105,3 +106,49 @@ func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain str
|
||||
|
||||
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")
|
||||
}
|
||||
|
@@ -75,16 +75,20 @@ type RouterOption struct {
|
||||
/* Router Object */
|
||||
type Router struct {
|
||||
Option *RouterOption
|
||||
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
|
||||
Root *ProxyEndpoint //Root proxy endpoint, default site
|
||||
mux http.Handler //HTTP handler
|
||||
server *http.Server //HTTP server
|
||||
tlsListener net.Listener //TLS listener, handle SNI routing
|
||||
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
||||
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
|
||||
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
|
||||
Root *ProxyEndpoint //Root proxy endpoint, default site
|
||||
|
||||
/* Internals */
|
||||
mux http.Handler //HTTP handler
|
||||
server *http.Server //HTTP server
|
||||
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
||||
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
|
||||
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
||||
}
|
||||
@@ -175,7 +179,8 @@ type ProxyEndpoint struct {
|
||||
Disabled bool //If the rule is disabled
|
||||
|
||||
//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
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
@@ -41,6 +41,18 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
Port: getRandomPortNumber(),
|
||||
RuntimeConst: *m.Options.SystemConst,
|
||||
}
|
||||
|
||||
// Generate API key if the plugin has permitted endpoints
|
||||
if len(thisPlugin.Spec.PermittedAPIEndpoints) > 0 {
|
||||
apiKey, err := m.Options.APIKeyManager.GenerateAPIKey(thisPlugin.Spec.ID, thisPlugin.Spec.PermittedAPIEndpoints)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pluginConfiguration.APIKey = apiKey.APIKey
|
||||
pluginConfiguration.ZoraxyPort = m.Options.ZoraxyPort
|
||||
m.Log("Generated API key for plugin "+thisPlugin.Spec.Name, nil)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(pluginConfiguration)
|
||||
|
||||
//Start the plugin with given configuration
|
||||
@@ -270,6 +282,13 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
||||
thisPlugin.Enabled = false
|
||||
thisPlugin.StopAllStaticPathRouters()
|
||||
thisPlugin.StopDynamicForwardRouter()
|
||||
|
||||
//Clean up API key
|
||||
err = m.Options.APIKeyManager.RevokeAPIKeysForPlugin(thisPlugin.Spec.ID)
|
||||
if err != nil {
|
||||
m.Log("Failed to revoke API keys for plugin "+thisPlugin.Spec.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@@ -42,10 +43,14 @@ type ManagerOptions struct {
|
||||
|
||||
/* Runtime */
|
||||
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
|
||||
ZoraxyPort int //The port of the Zoraxy instance, used for API calls
|
||||
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
|
||||
Database *database.Database `json:"-"`
|
||||
Logger *logger.Logger `json:"-"`
|
||||
|
||||
/* API Key Management */
|
||||
APIKeyManager *auth.APIKeyManager `json:"-"` //The API key manager for the plugins
|
||||
|
||||
/* Development */
|
||||
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
|
||||
HotReloadInterval int //The interval for checking the plugin file change, in seconds
|
||||
|
@@ -17,7 +17,7 @@ import (
|
||||
type SniffResult int
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
if sniffResult == SniffResultAccept {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
|
@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
type PermittedAPIEndpoint struct {
|
||||
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
|
||||
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
|
||||
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
@@ -97,7 +103,10 @@ type IntroSpect struct {
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
|
||||
|
||||
/* API Access Control */
|
||||
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,8 +135,10 @@ by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
|
||||
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
|
@@ -47,15 +47,19 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
|
||||
enableLogging, _ := utils.PostBool(r, "enableLogging")
|
||||
|
||||
//Create the target config
|
||||
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
|
||||
Name: name,
|
||||
ListeningAddr: strings.TrimSpace(listenAddr),
|
||||
ProxyAddr: strings.TrimSpace(proxyAddr),
|
||||
Timeout: timeout,
|
||||
UseTCP: useTCP,
|
||||
UseUDP: useUDP,
|
||||
Name: name,
|
||||
ListeningAddr: strings.TrimSpace(listenAddr),
|
||||
ProxyAddr: strings.TrimSpace(proxyAddr),
|
||||
Timeout: timeout,
|
||||
UseTCP: useTCP,
|
||||
UseUDP: useUDP,
|
||||
UseProxyProtocol: useProxyProtocol,
|
||||
EnableLogging: enableLogging,
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(newConfigUUID)
|
||||
@@ -75,6 +79,8 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
|
||||
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
|
||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
|
||||
enableLogging, _ := utils.PostBool(r, "enableLogging")
|
||||
|
||||
newTimeoutStr, _ := utils.PostPara(r, "timeout")
|
||||
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
|
||||
err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
|
||||
err = m.EditConfig(newConfig)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
|
110
src/mod/streamproxy/instances.go
Normal file
110
src/mod/streamproxy/instances.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package streamproxy
|
||||
|
||||
/*
|
||||
Instances.go
|
||||
|
||||
This file contains the methods to start, stop, and manage the proxy relay instances.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *ProxyRelayInstance) LogMsg(message string, originalError error) {
|
||||
if !c.EnableLogging {
|
||||
return
|
||||
}
|
||||
|
||||
if originalError != nil {
|
||||
log.Println(message, "error:", originalError)
|
||||
} else {
|
||||
log.Println(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Start a proxy if stopped
|
||||
func (c *ProxyRelayInstance) Start() error {
|
||||
if c.IsRunning() {
|
||||
c.Running = true
|
||||
return errors.New("proxy already running")
|
||||
}
|
||||
|
||||
// Create a stopChan to control the loop
|
||||
tcpStopChan := make(chan bool)
|
||||
udpStopChan := make(chan bool)
|
||||
|
||||
//Start the proxy service
|
||||
if c.UseUDP {
|
||||
c.udpStopChan = udpStopChan
|
||||
go func() {
|
||||
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
|
||||
if err != nil {
|
||||
if !c.UseTCP {
|
||||
c.Running = false
|
||||
c.udpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
||||
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if c.UseTCP {
|
||||
c.tcpStopChan = tcpStopChan
|
||||
go func() {
|
||||
//Default to transport mode
|
||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.tcpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//Successfully spawned off the proxy routine
|
||||
c.Running = true
|
||||
c.parent.SaveConfigToDatabase()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return if a proxy config is running
|
||||
func (c *ProxyRelayInstance) IsRunning() bool {
|
||||
return c.tcpStopChan != nil || c.udpStopChan != nil
|
||||
}
|
||||
|
||||
// Restart a proxy config
|
||||
func (c *ProxyRelayInstance) Restart() {
|
||||
if c.IsRunning() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(3000 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayInstance) Stop() {
|
||||
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
||||
|
||||
if c.udpStopChan != nil {
|
||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = nil
|
||||
}
|
||||
|
||||
if c.tcpStopChan != nil {
|
||||
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
||||
c.tcpStopChan <- true
|
||||
c.tcpStopChan = nil
|
||||
}
|
||||
|
||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
@@ -8,7 +8,6 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@@ -24,24 +23,44 @@ import (
|
||||
*/
|
||||
|
||||
type ProxyRelayOptions struct {
|
||||
Name string
|
||||
ListeningAddr string
|
||||
ProxyAddr string
|
||||
Timeout int
|
||||
UseTCP bool
|
||||
UseUDP bool
|
||||
Name string
|
||||
ListeningAddr string
|
||||
ProxyAddr string
|
||||
Timeout int
|
||||
UseTCP bool
|
||||
UseUDP bool
|
||||
UseProxyProtocol bool
|
||||
EnableLogging bool
|
||||
}
|
||||
|
||||
type ProxyRelayConfig struct {
|
||||
UUID string //A UUIDv4 representing this config
|
||||
Name string //Name of the config
|
||||
Running bool //Status, read only
|
||||
AutoStart bool //If the service suppose to started automatically
|
||||
ListeningAddress string //Listening Address, usually 127.0.0.1:port
|
||||
ProxyTargetAddr string //Proxy target address
|
||||
UseTCP bool //Enable TCP proxy
|
||||
UseUDP bool //Enable UDP proxy
|
||||
Timeout int //Timeout for connection in sec
|
||||
// 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
|
||||
Name string //Name of the config
|
||||
Running bool //Status, read only
|
||||
AutoStart bool //If the service suppose to started automatically
|
||||
ListeningAddress string //Listening Address, usually 127.0.0.1:port
|
||||
ProxyTargetAddr string //Proxy target address
|
||||
UseTCP bool //Enable TCP proxy
|
||||
UseUDP bool //Enable UDP proxy
|
||||
UseProxyProtocol bool //Enable Proxy Protocol
|
||||
EnableLogging bool //Enable logging for ProxyInstance
|
||||
Timeout int //Timeout for connection in sec
|
||||
|
||||
/* Internal */
|
||||
tcpStopChan chan bool //Stop channel for TCP listener
|
||||
udpStopChan chan bool //Stop channel for UDP listener
|
||||
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
|
||||
@@ -60,13 +79,14 @@ type Options struct {
|
||||
type Manager struct {
|
||||
//Config and stores
|
||||
Options *Options
|
||||
Configs []*ProxyRelayConfig
|
||||
Configs []*ProxyRelayInstance
|
||||
|
||||
//Realtime Statistics
|
||||
Connections int //currently connected connect counts
|
||||
|
||||
}
|
||||
|
||||
// NewStreamProxy creates a new stream proxy manager with the given options
|
||||
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
if !utils.FileExists(options.ConfigStore) {
|
||||
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||
@@ -76,7 +96,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
}
|
||||
|
||||
//Load relay configs from db
|
||||
previousRules := []*ProxyRelayConfig{}
|
||||
previousRules := []*ProxyRelayInstance{}
|
||||
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,7 +109,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
|
||||
continue
|
||||
}
|
||||
thisRelayConfig := &ProxyRelayConfig{}
|
||||
thisRelayConfig := &ProxyRelayInstance{}
|
||||
err = json.Unmarshal(configBytes, thisRelayConfig)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// NewConfig creates a new proxy relay config with the given options
|
||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
//Generate two zero value for atomic int64
|
||||
aAcc := atomic.Int64{}
|
||||
@@ -150,13 +171,15 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
bAcc.Store(0)
|
||||
//Generate a new config from options
|
||||
configUUID := uuid.New().String()
|
||||
thisConfig := ProxyRelayConfig{
|
||||
thisConfig := ProxyRelayInstance{
|
||||
UUID: configUUID,
|
||||
Name: config.Name,
|
||||
ListeningAddress: config.ListeningAddr,
|
||||
ProxyTargetAddr: config.ProxyAddr,
|
||||
UseTCP: config.UseTCP,
|
||||
UseUDP: config.UseUDP,
|
||||
UseProxyProtocol: config.UseProxyProtocol,
|
||||
EnableLogging: config.EnableLogging,
|
||||
Timeout: config.Timeout,
|
||||
tcpStopChan: nil,
|
||||
udpStopChan: nil,
|
||||
@@ -170,7 +193,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
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
|
||||
for _, config := range m.Configs {
|
||||
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
|
||||
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
|
||||
foundConfig, err := m.GetConfigByUUID(configUUID)
|
||||
foundConfig, err := m.GetConfigByUUID(newConfig.InstanceUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate and update the fields
|
||||
if newName != "" {
|
||||
foundConfig.Name = newName
|
||||
if newConfig.NewName != "" {
|
||||
foundConfig.Name = newConfig.NewName
|
||||
}
|
||||
if newListeningAddr != "" {
|
||||
foundConfig.ListeningAddress = newListeningAddr
|
||||
if newConfig.NewListeningAddr != "" {
|
||||
foundConfig.ListeningAddress = newConfig.NewListeningAddr
|
||||
}
|
||||
if newProxyAddr != "" {
|
||||
foundConfig.ProxyTargetAddr = newProxyAddr
|
||||
if newConfig.NewProxyAddr != "" {
|
||||
foundConfig.ProxyTargetAddr = newConfig.NewProxyAddr
|
||||
}
|
||||
|
||||
foundConfig.UseTCP = useTCP
|
||||
foundConfig.UseUDP = useUDP
|
||||
foundConfig.UseTCP = newConfig.UseTCP
|
||||
foundConfig.UseUDP = newConfig.UseUDP
|
||||
foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol
|
||||
foundConfig.EnableLogging = newConfig.EnableLogging
|
||||
|
||||
if newTimeout != -1 {
|
||||
if newTimeout < 0 {
|
||||
if newConfig.NewTimeout != -1 {
|
||||
if newConfig.NewTimeout < 0 {
|
||||
return errors.New("invalid timeout value given")
|
||||
}
|
||||
foundConfig.Timeout = newTimeout
|
||||
foundConfig.Timeout = newConfig.NewTimeout
|
||||
}
|
||||
|
||||
m.SaveConfigToDatabase()
|
||||
@@ -215,12 +240,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
||||
if foundConfig.IsRunning() {
|
||||
foundConfig.Restart()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the config from file by UUID
|
||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
//Remove the config from file
|
||||
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -250,91 +274,3 @@ func (m *Manager) SaveConfigToDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Config Functions
|
||||
*/
|
||||
|
||||
// Start a proxy if stopped
|
||||
func (c *ProxyRelayConfig) Start() error {
|
||||
if c.IsRunning() {
|
||||
c.Running = true
|
||||
return errors.New("proxy already running")
|
||||
}
|
||||
|
||||
// Create a stopChan to control the loop
|
||||
tcpStopChan := make(chan bool)
|
||||
udpStopChan := make(chan bool)
|
||||
|
||||
//Start the proxy service
|
||||
if c.UseUDP {
|
||||
c.udpStopChan = udpStopChan
|
||||
go func() {
|
||||
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
|
||||
if err != nil {
|
||||
if !c.UseTCP {
|
||||
c.Running = false
|
||||
c.udpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
||||
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if c.UseTCP {
|
||||
c.tcpStopChan = tcpStopChan
|
||||
go func() {
|
||||
//Default to transport mode
|
||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.tcpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//Successfully spawned off the proxy routine
|
||||
c.Running = true
|
||||
c.parent.SaveConfigToDatabase()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return if a proxy config is running
|
||||
func (c *ProxyRelayConfig) IsRunning() bool {
|
||||
return c.tcpStopChan != nil || c.udpStopChan != nil
|
||||
}
|
||||
|
||||
// Restart a proxy config
|
||||
func (c *ProxyRelayConfig) Restart() {
|
||||
if c.IsRunning() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(3000 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayConfig) Stop() {
|
||||
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
||||
|
||||
if c.udpStopChan != nil {
|
||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = nil
|
||||
}
|
||||
|
||||
if c.tcpStopChan != nil {
|
||||
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
||||
c.tcpStopChan <- true
|
||||
c.tcpStopChan = nil
|
||||
}
|
||||
|
||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
|
||||
stopChan := make(chan bool)
|
||||
|
||||
// Create a ProxyRelayConfig with dummy values
|
||||
config := &streamproxy.ProxyRelayConfig{
|
||||
config := &streamproxy.ProxyRelayInstance{
|
||||
Timeout: 1,
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package streamproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
@@ -30,48 +31,67 @@ func isValidPort(port string) bool {
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accumulator.Add(n) //Add to accumulator
|
||||
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()
|
||||
//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()
|
||||
}
|
||||
|
||||
func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
|
||||
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||
func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error {
|
||||
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
|
||||
// wait tow goroutines
|
||||
wg.Add(2)
|
||||
go connCopy(conn1, conn2, &wg, aTob)
|
||||
go connCopy(conn2, conn1, &wg, bToa)
|
||||
//blocking when the wg is locked
|
||||
go c.connCopy(conn1, conn2, &wg, aTob)
|
||||
go c.connCopy(conn2, conn1, &wg, bToa)
|
||||
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()
|
||||
if err != nil {
|
||||
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 !c.parent.Options.AccessControlHandler(conn) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
|
||||
return conn, err
|
||||
c.LogMsg("[√] accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]", nil)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func startListener(address string) (net.Listener, error) {
|
||||
@@ -92,7 +112,7 @@ func startListener(address string) (net.Listener, error) {
|
||||
portA -> server
|
||||
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
|
||||
if isValidPort(allowPort) {
|
||||
//number only, e.g. 8080
|
||||
@@ -112,7 +132,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
|
||||
//Start stop handler
|
||||
go func() {
|
||||
<-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()
|
||||
}()
|
||||
|
||||
@@ -129,18 +149,32 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
|
||||
}
|
||||
|
||||
go func(targetAddress string) {
|
||||
log.Println("[+]", "start connect host:["+targetAddress+"]")
|
||||
c.LogMsg("[+] start connect host:["+targetAddress+"]", nil)
|
||||
target, err := net.Dial("tcp", targetAddress)
|
||||
if err != nil {
|
||||
// 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()
|
||||
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)
|
||||
return
|
||||
}
|
||||
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
|
||||
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||
c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -53,7 +53,7 @@ func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn,
|
||||
}
|
||||
|
||||
// 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
|
||||
for {
|
||||
// Read from server
|
||||
@@ -74,7 +74,7 @@ func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lise
|
||||
}
|
||||
|
||||
// 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 {
|
||||
conn := clientServerConn.(*udpClientServerConn)
|
||||
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
|
||||
//We need to add the loopback address into it
|
||||
if isValidPort(address1) {
|
||||
@@ -90,8 +90,8 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
|
||||
address1 = ":" + address1
|
||||
}
|
||||
if strings.HasPrefix(address1, ":") {
|
||||
//Prepend 127.0.0.1 to the address
|
||||
address1 = "127.0.0.1" + address1
|
||||
//Prepend 0.0.0.0 to the address
|
||||
address1 = "0.0.0.0" + address1
|
||||
}
|
||||
|
||||
lisener, targetAddr, err := initUDPConnections(address1, address2)
|
||||
@@ -138,12 +138,12 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
|
||||
continue
|
||||
}
|
||||
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
|
||||
go c.RunUDPConnectionRelay(conn, lisener)
|
||||
|
||||
} else {
|
||||
log.Println("[UDP] Found connection for client " + saddr)
|
||||
c.LogMsg("[UDP] Found connection for client "+saddr, nil)
|
||||
conn = rawConn.(*udpClientServerConn)
|
||||
}
|
||||
|
||||
|
93
src/mod/tlscert/certgen.go
Normal file
93
src/mod/tlscert/certgen.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package tlscert
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateSelfSignedCertificate generates a self-signed ECDSA certificate and saves it to the specified files.
|
||||
func (m *Manager) GenerateSelfSignedCertificate(cn string, sans []string, certFile string, keyFile string) error {
|
||||
// Generate private key (ECDSA P-256)
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to generate private key", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn, // Common Name for the certificate
|
||||
Organization: []string{"aroz.org"}, // Organization name
|
||||
OrganizationalUnit: []string{"Zoraxy"}, // Organizational Unit
|
||||
Country: []string{"US"}, // Country code
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // valid for 1 year
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: sans, // Subject Alternative Names
|
||||
}
|
||||
|
||||
// Create self-signed certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to create certificate", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove old certificate file if it exists
|
||||
certPath := filepath.Join(m.CertStore, certFile)
|
||||
if _, err := os.Stat(certPath); err == nil {
|
||||
os.Remove(certPath)
|
||||
}
|
||||
|
||||
// Remove old key file if it exists
|
||||
keyPath := filepath.Join(m.CertStore, keyFile)
|
||||
if _, err := os.Stat(keyPath); err == nil {
|
||||
os.Remove(keyPath)
|
||||
}
|
||||
|
||||
// Write certificate to file
|
||||
certOut, err := os.Create(filepath.Join(m.CertStore, certFile))
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to open cert file for writing: "+certFile, err)
|
||||
return err
|
||||
}
|
||||
defer certOut.Close()
|
||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to write certificate to file: "+certFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Encode private key to PEM
|
||||
privBytes, err := x509.MarshalECPrivateKey(privKey)
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Unable to marshal ECDSA private key", err)
|
||||
return err
|
||||
}
|
||||
keyOut, err := os.Create(filepath.Join(m.CertStore, keyFile))
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to open key file for writing: "+keyFile, err)
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to write private key to file: "+keyFile, err)
|
||||
return err
|
||||
}
|
||||
m.Logger.PrintAndLog("tls-router", "Certificate and key generated: "+certFile+", "+keyFile, nil)
|
||||
return nil
|
||||
}
|
352
src/mod/tlscert/handler.go
Normal file
352
src/mod/tlscert/handler.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package tlscert
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Handle cert remove
|
||||
func (m *Manager) HandleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
err = m.RemoveCert(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Handle download of the selected certificate
|
||||
func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||
// get the certificate name
|
||||
certname, err := utils.GetPara(r, "certname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid certname given")
|
||||
return
|
||||
}
|
||||
certname = filepath.Base(certname) //prevent path escape
|
||||
|
||||
// check if the cert exists
|
||||
pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key")
|
||||
priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem")
|
||||
|
||||
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
||||
//Zip them and serve them via http download
|
||||
seeking, _ := utils.GetBool(r, "seek")
|
||||
if seeking {
|
||||
//This request only check if the key exists. Do not provide download
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
//Serve both file in zip
|
||||
zipTmpFolder := "./tmp/download"
|
||||
os.MkdirAll(zipTmpFolder, 0775)
|
||||
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
||||
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
||||
|
||||
// Serve the zip file
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
http.ServeFile(w, r, zipFileName)
|
||||
} else {
|
||||
//Not both key exists
|
||||
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upload of the certificate
|
||||
func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// check if request method is POST
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// get the key type
|
||||
keytype, err := utils.GetPara(r, "ktype")
|
||||
overWriteFilename := ""
|
||||
if err != nil {
|
||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
//Assume localhost
|
||||
domain = "default"
|
||||
}
|
||||
|
||||
switch keytype {
|
||||
case "pub":
|
||||
overWriteFilename = domain + ".pem"
|
||||
case "pri":
|
||||
overWriteFilename = domain + ".key"
|
||||
default:
|
||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart form data
|
||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get file from form data
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// create file in upload directory
|
||||
os.MkdirAll(m.CertStore, 0775)
|
||||
f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// copy file contents to destination file
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
m.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
||||
// List all certificates and map all their domains to the cert filename
|
||||
func (m *Manager) HandleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := os.ReadDir(m.CertStore)
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
certnameToDomainMap := map[string]string{}
|
||||
for _, filename := range filenames {
|
||||
if filename.IsDir() {
|
||||
continue
|
||||
}
|
||||
certFilepath := filepath.Join(m.CertStore, filename.Name())
|
||||
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
m.Logger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
||||
for _, dnsName := range cert.DNSNames {
|
||||
certnameToDomainMap[dnsName] = certname
|
||||
}
|
||||
certnameToDomainMap[cert.Subject.CommonName] = certname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requireCompact, _ := utils.GetPara(r, "compact")
|
||||
if requireCompact == "true" {
|
||||
result := make(map[string][]string)
|
||||
|
||||
for key, value := range certnameToDomainMap {
|
||||
if _, ok := result[value]; !ok {
|
||||
result[value] = make([]string, 0)
|
||||
}
|
||||
|
||||
result[value] = append(result[value], key)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(certnameToDomainMap)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Return a list of domains where the certificates covers
|
||||
func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := m.ListCertDomains()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
showDate, _ := utils.GetBool(r, "date")
|
||||
if showDate {
|
||||
type CertInfo struct {
|
||||
Domain string
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
UseDNS bool
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join(m.CertStore, filename+".pem")
|
||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||
return
|
||||
}
|
||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||
|
||||
certExpireTime := "Unknown"
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
expiredIn := 0
|
||||
if err != nil {
|
||||
//Unable to load this file
|
||||
continue
|
||||
} else {
|
||||
//Cert loaded. Check its expire time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||
|
||||
duration := cert.NotAfter.Sub(time.Now())
|
||||
|
||||
// Convert the duration to days
|
||||
expiredIn = int(duration.Hours() / 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
certInfoFilename := filepath.Join(m.CertStore, filename+".json")
|
||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||
if err == nil {
|
||||
useDNSValidation = certInfo.UseDNS
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
UseDNS: useDNSValidation,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
}
|
||||
|
||||
// convert ExpireDate to date object and sort asc
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
|
||||
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
|
||||
return date1.Before(date2)
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := json.Marshal(filenames)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
// Check if the default certificates is correctly setup
|
||||
func (m *Manager) HandleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||
type CheckResult struct {
|
||||
DefaultPubExists bool
|
||||
DefaultPriExists bool
|
||||
}
|
||||
|
||||
pub, pri := m.DefaultCertExistsSep()
|
||||
js, _ := json.Marshal(CheckResult{
|
||||
pub,
|
||||
pri,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the common name from the request
|
||||
cn, err := utils.GetPara(r, "cn")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Common name not provided")
|
||||
return
|
||||
}
|
||||
|
||||
domains, err := utils.PostPara(r, "domains")
|
||||
if err != nil {
|
||||
//No alias domains provided, use the common name as the only domain
|
||||
domains = "[]"
|
||||
}
|
||||
|
||||
SANs := []string{}
|
||||
if err := json.Unmarshal([]byte(domains), &SANs); err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid domains format: "+err.Error())
|
||||
return
|
||||
}
|
||||
//SANs = append([]string{cn}, SANs...)
|
||||
priKeyFilename := domainToFilename(cn, ".key")
|
||||
pubKeyFilename := domainToFilename(cn, ".pem")
|
||||
|
||||
// Generate self-signed certificate
|
||||
err = m.GenerateSelfSignedCertificate(cn, SANs, pubKeyFilename, priKeyFilename)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to generate self-signed certificate: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the certificate store
|
||||
err = m.UpdateLoadedCertList()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to update certificate store: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
@@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
|
||||
return matchingDomain
|
||||
}
|
||||
|
||||
// Convert a domain name to a filename format
|
||||
func domainToFilename(domain string, ext string) string {
|
||||
// Replace wildcard '*' with '_'
|
||||
domain = strings.TrimSpace(domain)
|
||||
if strings.HasPrefix(domain, "*") {
|
||||
domain = "_" + strings.TrimPrefix(domain, "*")
|
||||
}
|
||||
|
||||
// Add .pem extension
|
||||
ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot
|
||||
return domain + "." + ext
|
||||
}
|
||||
|
||||
func filenameToDomain(filename string) string {
|
||||
// Remove the extension
|
||||
ext := filepath.Ext(filename)
|
||||
if ext != "" {
|
||||
filename = strings.TrimSuffix(filename, ext)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filename, "_") {
|
||||
filename = "*" + filename[1:]
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
@@ -20,17 +20,26 @@ type CertCache struct {
|
||||
PriKey string
|
||||
}
|
||||
|
||||
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 {
|
||||
CertStore string //Path where all the certs are stored
|
||||
LoadedCerts []*CertCache //A list of loaded certs
|
||||
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
|
||||
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) {
|
||||
os.MkdirAll(certStore, 0775)
|
||||
}
|
||||
@@ -50,10 +59,10 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
||||
}
|
||||
|
||||
thisManager := Manager{
|
||||
CertStore: certStore,
|
||||
LoadedCerts: []*CertCache{},
|
||||
verbal: verbal,
|
||||
Logger: logger,
|
||||
CertStore: certStore,
|
||||
LoadedCerts: []*CertCache{},
|
||||
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
err := thisManager.UpdateLoadedCertList()
|
||||
@@ -64,6 +73,25 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
||||
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
|
||||
func (m *Manager) UpdateLoadedCertList() error {
|
||||
//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
|
||||
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
//Check if the domain corrisponding cert exists
|
||||
pubKey := "./tmp/localhost.pem"
|
||||
priKey := "./tmp/localhost.key"
|
||||
|
||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
||||
//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")
|
||||
}
|
||||
//Look for the certificate by hostname
|
||||
pubKey, priKey, err := m.GetCertificateByHostname(helloInfo.ServerName)
|
||||
if err != nil {
|
||||
m.Logger.PrintAndLog("tls-router", "Failed to get certificate for "+helloInfo.ServerName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Load the cert and serve it
|
||||
@@ -190,6 +205,55 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
|
||||
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
|
||||
func (m *Manager) DefaultCertExists() bool {
|
||||
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
|
||||
m.UpdateLoadedCertList()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -69,6 +69,12 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
||||
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))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
@@ -106,6 +112,17 @@ func (ws *WebServer) SetDisableListenToAllInterface(w http.ResponseWriter, r *ht
|
||||
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)
|
||||
}
|
||||
|
@@ -210,6 +210,27 @@ func (ws *WebServer) Stop() error {
|
||||
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.
|
||||
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
|
||||
ws.option.EnableDirectoryListing = enable
|
||||
|
135
src/plugin_api.go
Normal file
135
src/plugin_api.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
)
|
||||
|
||||
// Register the APIs for HTTP proxy management functions
|
||||
func RegisterHTTPProxyRestAPI(authMiddleware *auth.PluginAuthMiddleware) {
|
||||
/* Reverse Proxy Settings & Status */
|
||||
authMiddleware.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authMiddleware.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authMiddleware.HandleFunc("/api/proxy/listTags", ReverseProxyListTags)
|
||||
authMiddleware.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
/* Reverse proxy upstream (load balance) */
|
||||
authMiddleware.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
|
||||
/* Reverse proxy virtual directory */
|
||||
authMiddleware.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||
/* Reverse proxy user-defined header */
|
||||
authMiddleware.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
/* Reverse proxy auth related */
|
||||
authMiddleware.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
}
|
||||
|
||||
// Register the APIs for redirection rules management functions
|
||||
func RegisterRedirectionRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
}
|
||||
|
||||
// Register the APIs for access rules management functions
|
||||
func RegisterAccessRuleRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
/* Access Rules Settings & Status */
|
||||
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
||||
// authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
||||
// authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||
// authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||
// authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||
/* Blacklist */
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
/* Whitelist */
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
||||
authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback)
|
||||
/* Quick Ban List */
|
||||
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
|
||||
}
|
||||
|
||||
// Register the APIs for path blocking rules management functions, WIP
|
||||
func RegisterPathRuleRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
|
||||
}
|
||||
|
||||
// Register the APIs statistic anlysis and uptime monitoring functions
|
||||
func RegisterStatisticalRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
/* Traffic Summary */
|
||||
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
||||
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
|
||||
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
|
||||
/* Zoraxy Analytic */
|
||||
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
||||
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
||||
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
||||
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
/* UpTime Monitor */
|
||||
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
||||
}
|
||||
|
||||
// Register the APIs for Stream (TCP / UDP) Proxy management functions
|
||||
func RegisterStreamProxyRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
|
||||
}
|
||||
|
||||
// Register the APIs for mDNS service management functions
|
||||
func RegisterMDNSRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
||||
}
|
||||
|
||||
// Register the APIs for Static Web Server management functions
|
||||
func RegisterStaticWebServerRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
/* Static Web Server Controls */
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
|
||||
/* File Manager */
|
||||
if *allowWebFileManager {
|
||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterPluginRestAPI(authRouter *auth.PluginAuthMiddleware) {
|
||||
authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins)
|
||||
authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo)
|
||||
|
||||
authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups)
|
||||
|
||||
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
|
||||
}
|
||||
|
||||
/* Register all the APIs */
|
||||
func initRestAPI(targetMux *http.ServeMux) {
|
||||
authMiddleware := auth.NewPluginAuthMiddleware(
|
||||
auth.PluginMiddlewareOptions{
|
||||
TargetMux: targetMux,
|
||||
ApiKeyManager: pluginApiKeyManager,
|
||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
//Register the APIs
|
||||
RegisterHTTPProxyRestAPI(authMiddleware)
|
||||
RegisterRedirectionRestAPI(authMiddleware)
|
||||
RegisterAccessRuleRestAPI(authMiddleware)
|
||||
RegisterPathRuleRestAPI(authMiddleware)
|
||||
RegisterStatisticalRestAPI(authMiddleware)
|
||||
RegisterStreamProxyRestAPI(authMiddleware)
|
||||
RegisterMDNSRestAPI(authMiddleware)
|
||||
RegisterStaticWebServerRestAPI(authMiddleware)
|
||||
RegisterPluginRestAPI(authMiddleware)
|
||||
}
|
@@ -15,6 +15,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@@ -134,12 +135,12 @@ func ReverseProxtInit() {
|
||||
Load all conf from files
|
||||
|
||||
*/
|
||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||
confs, _ := filepath.Glob(CONF_HTTP_PROXY + "/*.config")
|
||||
for _, conf := range confs {
|
||||
err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||
if eptype == "host" {
|
||||
switch eptype {
|
||||
case "host":
|
||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "hostname not defined")
|
||||
@@ -415,7 +417,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
|
||||
proxyEndpointCreated = &thisProxyEndpoint
|
||||
} else if eptype == "root" {
|
||||
case "root":
|
||||
//Get the default site options and target
|
||||
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
|
||||
if err != nil {
|
||||
@@ -469,7 +471,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
proxyEndpointCreated = &rootRoutingEndpoint
|
||||
} else {
|
||||
default:
|
||||
//Invalid eptype
|
||||
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||
return
|
||||
@@ -677,6 +679,70 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
if r.Method != http.MethodPost {
|
||||
utils.SendErrorResponse(w, "Method not supported")
|
||||
@@ -1015,6 +1081,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, err := json.Marshal(dynamicProxyRouter)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to marshal status data", err)
|
||||
utils.SendErrorResponse(w, "Unable to marshal status data")
|
||||
return
|
||||
}
|
||||
|
19
src/start.go
19
src/start.go
@@ -1,15 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
@@ -98,8 +100,11 @@ func startupSequence() {
|
||||
http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
// Create an API key manager for plugin authentication
|
||||
pluginApiKeyManager = auth.NewAPIKeyManager()
|
||||
|
||||
//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 {
|
||||
panic(err)
|
||||
}
|
||||
@@ -312,11 +317,18 @@ func startupSequence() {
|
||||
*/
|
||||
pluginFolder := *path_plugin
|
||||
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
|
||||
ZoraxyAddrPort, err := netip.ParseAddrPort(*webUIPort)
|
||||
ZoraxyPort := 8000
|
||||
if err == nil && ZoraxyAddrPort.IsValid() && ZoraxyAddrPort.Port() > 0 {
|
||||
ZoraxyPort = int(ZoraxyAddrPort.Port())
|
||||
}
|
||||
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
|
||||
PluginDir: pluginFolder,
|
||||
Database: sysdb,
|
||||
Logger: SystemWideLogger,
|
||||
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
|
||||
APIKeyManager: pluginApiKeyManager,
|
||||
ZoraxyPort: ZoraxyPort,
|
||||
CSRFTokenGen: func(r *http.Request) string {
|
||||
return csrf.Token(r)
|
||||
},
|
||||
@@ -366,6 +378,9 @@ func finalSequence() {
|
||||
|
||||
//Inject routing rules
|
||||
registerBuildInRoutingRules()
|
||||
|
||||
//Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname
|
||||
tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname)
|
||||
}
|
||||
|
||||
/* Shutdown Sequence */
|
||||
|
@@ -203,7 +203,7 @@
|
||||
<th>Destination</th>
|
||||
<th>Virtual Directory</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>
|
||||
</thead>
|
||||
<tbody id="httpProxyList">
|
||||
@@ -338,10 +338,45 @@
|
||||
<!-- TLS / SSL -->
|
||||
<div class="rpconfig_content" rpcfg="ssl">
|
||||
<div class="ui segment">
|
||||
<p>Work In Progress <br>
|
||||
Please use the outer-most menu TLS / SSL tab for now. </p>
|
||||
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</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>
|
||||
<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 getSelfSignCertBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="yellow lock icon"></i> Generate Self-Signed Certificate</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Custom Headers -->
|
||||
@@ -551,6 +586,28 @@
|
||||
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
|
||||
let tagList = renderTagList(subd);
|
||||
let tagListEmpty = (subd.Tags.length == 0);
|
||||
@@ -567,7 +624,7 @@
|
||||
${aliasDomains}
|
||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||
</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">
|
||||
${upstreams}
|
||||
</div>
|
||||
@@ -588,7 +645,7 @@
|
||||
</td> -->
|
||||
<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="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>
|
||||
</tr>`);
|
||||
});
|
||||
@@ -711,6 +768,112 @@
|
||||
$("#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){
|
||||
let editor = $("#httprpEditModal");
|
||||
|
||||
@@ -857,6 +1020,29 @@
|
||||
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 */
|
||||
function handleSearchInput(event){
|
||||
if (event.key == "Escape"){
|
||||
@@ -985,6 +1171,28 @@
|
||||
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
|
||||
function initHttpProxyRuleEditorModal(rulepayload){
|
||||
let subd = JSON.parse(JSON.stringify(rulepayload));
|
||||
@@ -1086,39 +1294,6 @@
|
||||
|
||||
});
|
||||
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 ------------ */
|
||||
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
||||
@@ -1148,6 +1323,8 @@
|
||||
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
||||
editor.find(".editVdirBtn").off("click").on("click", function(){
|
||||
quickEditVdir(uuid);
|
||||
//Temporary restore scroll
|
||||
$("body").css("overflow", "auto");
|
||||
});
|
||||
|
||||
/* ------------ Alias ------------ */
|
||||
@@ -1245,6 +1422,60 @@
|
||||
editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent);
|
||||
|
||||
/* ------------ 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 ------------ */
|
||||
(()=>{
|
||||
@@ -1308,7 +1539,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Page Initialization Functions
|
||||
*/
|
||||
@@ -1333,7 +1563,9 @@
|
||||
// there is a chance where the user has modified the Vdir
|
||||
// we need to get the latest setting from server side and
|
||||
// render it again
|
||||
updateVdirInProxyEditor();
|
||||
resyncProxyEditorConfig();
|
||||
window.scrollTo(0, 0);
|
||||
$("body").css("overflow", "hidden");
|
||||
} else {
|
||||
listProxyEndpoints();
|
||||
//Reset the tag filter
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,15 @@
|
||||
<h2>SSO</h2>
|
||||
<p>Single Sign-On (SSO) and authentication providers settings </p>
|
||||
</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 basic segment">
|
||||
<h3>Forward Auth</h3>
|
||||
<div class="ui top attached tabular menu ssoTabs">
|
||||
<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>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
|
||||
<ul>
|
||||
@@ -86,10 +83,10 @@
|
||||
</div>
|
||||
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<h3>OAuth 2.0</h3>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="oauth2_tab">
|
||||
<!-- Oauth 2 -->
|
||||
<h2>OAuth 2.0</h2>
|
||||
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
|
||||
|
||||
<form class="ui form" action="#" id="oauth2Settings">
|
||||
@@ -134,11 +131,18 @@
|
||||
</div>
|
||||
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
$(".ssoTabs .item").tab();
|
||||
|
||||
$(document).ready(function() {
|
||||
/* Load forward-auth settings from backend */
|
||||
$.cjax({
|
||||
@@ -147,11 +151,31 @@
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$('#forwardAuthAddress').val(data.address);
|
||||
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
||||
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
|
||||
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
||||
if (data.responseHeaders != null) {
|
||||
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||
} else {
|
||||
$('#forwardAuthResponseHeaders').val("");
|
||||
}
|
||||
if (data.responseClientHeaders != null) {
|
||||
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||
} else {
|
||||
$('#forwardAuthResponseClientHeaders').val("");
|
||||
}
|
||||
if (data.requestHeaders != null) {
|
||||
$('#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(","));
|
||||
} else {
|
||||
$('#forwardAuthRequestExcludedCookies').val("");
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||
|
@@ -16,6 +16,7 @@
|
||||
<th>Target Address</th>
|
||||
<th>Mode</th>
|
||||
<th>Timeout (s)</th>
|
||||
<th>Enable Logging</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -73,6 +74,22 @@
|
||||
<small>Forward UDP request on this listening socket</small></label>
|
||||
</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="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>
|
||||
@@ -120,7 +137,7 @@
|
||||
});
|
||||
|
||||
function clearStreamProxyAddEditForm(){
|
||||
$('#streamProxyForm input, #streamProxyForm select').val('');
|
||||
$('#streamProxyForm').find('input:not([type=checkbox]), select').val('');
|
||||
$('#streamProxyForm select').dropdown('clear');
|
||||
$("#streamProxyForm input[name=timeout]").val(10);
|
||||
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
|
||||
@@ -195,6 +212,10 @@
|
||||
modeText.push("UDP")
|
||||
}
|
||||
|
||||
if (config.UseProxyProtocol){
|
||||
modeText.push("ProxyProtocol V1")
|
||||
}
|
||||
|
||||
modeText = modeText.join(" & ")
|
||||
|
||||
var thisConfig = encodeURIComponent(JSON.stringify(config));
|
||||
@@ -207,6 +228,10 @@
|
||||
row.append($('<td>').text(config.ProxyTargetAddr));
|
||||
row.append($('<td>').text(modeText));
|
||||
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(`
|
||||
${startButton}
|
||||
<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");
|
||||
}
|
||||
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"){
|
||||
field = $("#streamProxyForm input[name=listenAddr]");
|
||||
}else if (key == "ProxyTargetAddr"){
|
||||
@@ -301,6 +342,8 @@
|
||||
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
|
||||
useTCP: $("#streamProxyForm input[name=useTCP]")[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()),
|
||||
},
|
||||
success: function(response) {
|
||||
|
@@ -343,7 +343,9 @@
|
||||
}
|
||||
|
||||
$(editorSideWrapper).each(function(){
|
||||
$(this)[0].contentWindow.setDarkTheme(false);
|
||||
if ($(this)[0].contentWindow.setDarkTheme){
|
||||
$(this)[0].contentWindow.setDarkTheme(false);
|
||||
}
|
||||
})
|
||||
|
||||
if ($("#pluginContextLoader").is(":visible")){
|
||||
@@ -356,7 +358,9 @@
|
||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||
}
|
||||
$(editorSideWrapper).each(function(){
|
||||
$(this)[0].contentWindow.setDarkTheme(true);
|
||||
if ($(this)[0].contentWindow.setDarkTheme){
|
||||
$(this)[0].contentWindow.setDarkTheme(true);
|
||||
}
|
||||
})
|
||||
if ($("#pluginContextLoader").is(":visible")){
|
||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
||||
|
@@ -139,6 +139,26 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Plugin IntroSpect Permitted API Endpoints</h4>
|
||||
<p>The following API endpoints are registered by this plugin and will be accessible by the plugin's API key:</p>
|
||||
<table class="ui basic celled unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Method</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- This tbody will be filled by JavaScript -->
|
||||
<tbody id="plugin_permitted_api_endpoints">
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Note that the API endpoints are only accessible by the plugin's API key.
|
||||
If the plugin does not have an API key, it will not be able to access these endpoints.
|
||||
API keys are generated automatically by Zoraxy when a plugin with permitted API endpoints is enabled.
|
||||
</p>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,6 +239,22 @@
|
||||
$("#dynamic_capture_sniffing_path").text(dynamicCaptureSniffingPath);
|
||||
$("#dynamic_capture_ingress").text(dynamicCaptureIngress);
|
||||
$("#registered_ui_proxy_path").text(registeredUIProxyPath);
|
||||
|
||||
//Update permitted API endpoints
|
||||
let apiEndpoints = data.Spec.permitted_api_endpoints;
|
||||
if (apiEndpoints == null || apiEndpoints.length == 0) {
|
||||
$("#plugin_permitted_api_endpoints").html('<tr><td colspan="3">No API endpoints registered</td></tr>');
|
||||
} else {
|
||||
let endpointRows = '';
|
||||
apiEndpoints.forEach(function(endpoint) {
|
||||
endpointRows += `<tr>
|
||||
<td>${endpoint.endpoint}</td>
|
||||
<td>${endpoint.method}</td>
|
||||
<td>${endpoint.reason}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$("#plugin_permitted_api_endpoints").html(endpointRows);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user