mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-15 09:29:23 +02:00
Compare commits
20 Commits
45506c8772
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0f621d0edd | ||
![]() |
9230f9374d | ||
![]() |
c982541a40 | ||
![]() |
6493a82e5f | ||
![]() |
39e05032c9 | ||
![]() |
077192e08e | ||
![]() |
223ae9e112 | ||
![]() |
aff1975c5a | ||
![]() |
5c6950ca56 | ||
![]() |
70b1ccfa6e | ||
![]() |
100c1e9c04 | ||
![]() |
a33600d3e2 | ||
![]() |
c4c10d2130 | ||
![]() |
4d3d1b25cb | ||
![]() |
118b5e5114 | ||
![]() |
ad53b894c0 | ||
![]() |
a0a394885c | ||
![]() |
51334a3a75 | ||
![]() |
6f5fadc085 | ||
![]() |
e225407b03 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -29,8 +29,6 @@ src/Zoraxy_*_*
|
|||||||
src/certs/*
|
src/certs/*
|
||||||
src/rules/*
|
src/rules/*
|
||||||
src/README.md
|
src/README.md
|
||||||
docker/ContainerTester.sh
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
src/mod/acme/test/stackoverflow.pem
|
src/mod/acme/test/stackoverflow.pem
|
||||||
/tools/dns_challenge_update/code-gen/acmedns
|
/tools/dns_challenge_update/code-gen/acmedns
|
||||||
/tools/dns_challenge_update/code-gen/lego
|
/tools/dns_challenge_update/code-gen/lego
|
||||||
@@ -41,11 +39,15 @@ src/sys.uuid
|
|||||||
src/zoraxy
|
src/zoraxy
|
||||||
src/log/
|
src/log/
|
||||||
|
|
||||||
|
|
||||||
# dev-tags
|
# dev-tags
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
/Entrypoint.sh
|
/Entrypoint.sh
|
||||||
|
|
||||||
|
# docker testing stuff
|
||||||
|
docker/test/
|
||||||
|
docker/container-builder.sh
|
||||||
|
docker/docker-compose.yaml
|
||||||
|
|
||||||
# plugins
|
# plugins
|
||||||
example/plugins/ztnc/ztnc.db
|
example/plugins/ztnc/ztnc.db
|
||||||
example/plugins/ztnc/authtoken.secret
|
example/plugins/ztnc/authtoken.secret
|
||||||
@@ -58,3 +60,5 @@ sys.*
|
|||||||
www/html/index.html
|
www/html/index.html
|
||||||
*.exe
|
*.exe
|
||||||
/src/dist
|
/src/dist
|
||||||
|
|
||||||
|
/src/plugins
|
||||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,3 +1,20 @@
|
|||||||
|
# v3.2.5 20 Jul 2025
|
||||||
|
|
||||||
|
|
||||||
|
+ Added new API endpoint /api/proxy/setTlsConfig (for HTTP Proxy Editor TLS tab)
|
||||||
|
+ Refactored TLS certificate management APIs with new handlers
|
||||||
|
+ Removed redundant functions from src/cert.go and delegated to tlsCertManager
|
||||||
|
+ Code optimization in tlscert module
|
||||||
|
+ Introduced a new constant CONF_FOLDER and updated configuration storage paths (phasing out hard coded paths)
|
||||||
|
+ Updated functions to set default TLS options when missing, default to SNI
|
||||||
|
+ Added Proxy Protocol v1 support in stream proxy [jemmy1794](https://github.com/jemmy1794)
|
||||||
|
+ Fixed Proxy UI bug [jemmy1794](https://github.com/jemmy1794)
|
||||||
|
+ Fixed assign static server to localhost or all interfaces [#688](https://github.com/tobychui/zoraxy/issues/688)
|
||||||
|
+ fixed empty SSO parameters by [7brend7](https://github.com/7brend7)
|
||||||
|
+ sort list of loaded certificates by expire date by [7brend7](https://github.com/7brend7)
|
||||||
|
+ Docker hardening by [PassiveLemon](https://github.com/PassiveLemon)
|
||||||
|
+ Fixed sort by destination [#713](https://github.com/tobychui/zoraxy/issues/713)
|
||||||
|
|
||||||
# v3.2.4 28 Jun 2025
|
# v3.2.4 28 Jun 2025
|
||||||
|
|
||||||
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
|
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
|
||||||
|
@@ -34,34 +34,18 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
|||||||
chmod 755 /usr/local/bin/zerotier-one
|
chmod 755 /usr/local/bin/zerotier-one
|
||||||
|
|
||||||
|
|
||||||
## Fetch plugin
|
|
||||||
FROM docker.io/golang:alpine AS fetch-plugin
|
|
||||||
|
|
||||||
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
RUN apk add --update --no-cache git
|
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/
|
|
||||||
|
|
||||||
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
|
|
||||||
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
|
|
||||||
## Main
|
## Main
|
||||||
FROM docker.io/golang:alpine
|
FROM docker.io/alpine:latest
|
||||||
|
|
||||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
RUN apk add --update --no-cache python3 sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||||
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
COPY --chmod=700 ./entrypoint.py /opt/zoraxy/
|
||||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
|
||||||
|
|
||||||
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
|
||||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||||
|
|
||||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
RUN mkdir -p /opt/zoraxy/plugin/ &&\
|
||||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
|
||||||
echo "tun" | tee -a /etc/modules
|
echo "tun" | tee -a /etc/modules
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/config/
|
WORKDIR /opt/zoraxy/config/
|
||||||
@@ -89,7 +73,7 @@ VOLUME [ "/opt/zoraxy/config/" ]
|
|||||||
|
|
||||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
ENTRYPOINT [ "python3", "-u", "/opt/zoraxy/entrypoint.py" ]
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1
|
||||||
|
|
||||||
|
@@ -119,18 +119,14 @@ Or for Docker Compose:
|
|||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
Zoraxy includes a (experimental) store to download and use official plugins right from inside Zoraxy, no preparation required.
|
||||||
|
For those looking to use custom plugins, build your plugins and place them inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location).
|
||||||
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Plugins are currently experimental.
|
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
To build the Docker image:
|
To build the Docker image:
|
||||||
- Check out the repository/branch.
|
- Check out the repository/branch.
|
||||||
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
|
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
|
||||||
- Run the build command with `docker build -t zoraxy_build .`
|
- Run the build command with `docker build -t zoraxy_build .`
|
||||||
- You can now use the image `zoraxy_build`
|
- You can now use the image `zoraxy_build`
|
||||||
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
echo "Copying zoraxy_plugin to all mods..."
|
|
||||||
for dir in "$1"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Running go mod tidy and go build for all directories..."
|
|
||||||
for dir in "$1"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
cd "$dir" || exit 1
|
|
||||||
go mod tidy
|
|
||||||
go build
|
|
||||||
cd "$1" || exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
131
docker/entrypoint.py
Normal file
131
docker/entrypoint.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.symlink(config_dir, zt_path, target_is_directory=True)
|
||||||
|
except FileExistsError:
|
||||||
|
print(f"Symlink {zt_path} already exists, skipping creation.")
|
||||||
|
|
||||||
|
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"
|
|
||||||
|
|
17
src/api.go
17
src/api.go
@@ -72,15 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
|||||||
|
|
||||||
// Register the APIs for TLS / SSL certificate management functions
|
// Register the APIs for TLS / SSL certificate management functions
|
||||||
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||||
|
//Global certificate settings
|
||||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
|
||||||
authRouter.HandleFunc("/api/cert/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/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
|
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||||
|
336
src/cert.go
336
src/cert.go
@@ -1,188 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if the default certificates is correctly setup
|
|
||||||
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
type CheckResult struct {
|
|
||||||
DefaultPubExists bool
|
|
||||||
DefaultPriExists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, pri := tlsCertManager.DefaultCertExistsSep()
|
|
||||||
js, _ := json.Marshal(CheckResult{
|
|
||||||
pub,
|
|
||||||
pri,
|
|
||||||
})
|
|
||||||
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a list of domains where the certificates covers
|
|
||||||
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
filenames, err := tlsCertManager.ListCertDomains()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showDate, _ := utils.GetPara(r, "date")
|
|
||||||
if showDate == "true" {
|
|
||||||
type CertInfo struct {
|
|
||||||
Domain string
|
|
||||||
LastModifiedDate string
|
|
||||||
ExpireDate string
|
|
||||||
RemainingDays int
|
|
||||||
UseDNS bool
|
|
||||||
}
|
|
||||||
|
|
||||||
results := []*CertInfo{}
|
|
||||||
|
|
||||||
for _, filename := range filenames {
|
|
||||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
|
||||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
|
||||||
fileInfo, err := os.Stat(certFilepath)
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
certExpireTime := "Unknown"
|
|
||||||
certBtyes, err := os.ReadFile(certFilepath)
|
|
||||||
expiredIn := 0
|
|
||||||
if err != nil {
|
|
||||||
//Unable to load this file
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
//Cert loaded. Check its expire time
|
|
||||||
block, _ := pem.Decode(certBtyes)
|
|
||||||
if block != nil {
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err == nil {
|
|
||||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
duration := cert.NotAfter.Sub(time.Now())
|
|
||||||
|
|
||||||
// Convert the duration to days
|
|
||||||
expiredIn = int(duration.Hours() / 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
|
||||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
|
||||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
|
||||||
if err == nil {
|
|
||||||
useDNSValidation = certInfo.UseDNS
|
|
||||||
}
|
|
||||||
|
|
||||||
thisCertInfo := CertInfo{
|
|
||||||
Domain: filename,
|
|
||||||
LastModifiedDate: modifiedTime,
|
|
||||||
ExpireDate: certExpireTime,
|
|
||||||
RemainingDays: expiredIn,
|
|
||||||
UseDNS: useDNSValidation,
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, &thisCertInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
} else {
|
|
||||||
response, err := json.Marshal(filenames)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all certificates and map all their domains to the cert filename
|
|
||||||
func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
|
||||||
filenames, err := os.ReadDir("./conf/certs/")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
certnameToDomainMap := map[string]string{}
|
|
||||||
for _, filename := range filenames {
|
|
||||||
if filename.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
|
||||||
|
|
||||||
certBtyes, err := os.ReadFile(certFilepath)
|
|
||||||
if err != nil {
|
|
||||||
// Unable to load this file
|
|
||||||
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// Cert loaded. Check its expiry time
|
|
||||||
block, _ := pem.Decode(certBtyes)
|
|
||||||
if block != nil {
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err == nil {
|
|
||||||
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
|
||||||
for _, dnsName := range cert.DNSNames {
|
|
||||||
certnameToDomainMap[dnsName] = certname
|
|
||||||
}
|
|
||||||
certnameToDomainMap[cert.Subject.CommonName] = certname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requireCompact, _ := utils.GetPara(r, "compact")
|
|
||||||
if requireCompact == "true" {
|
|
||||||
result := make(map[string][]string)
|
|
||||||
|
|
||||||
for key, value := range certnameToDomainMap {
|
|
||||||
if _, ok := result[value]; !ok {
|
|
||||||
result[value] = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
result[value] = append(result[value], key)
|
|
||||||
}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(result)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(certnameToDomainMap)
|
|
||||||
utils.SendJSONResponse(w, string(js))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle front-end toggling TLS mode
|
// Handle front-end toggling TLS mode
|
||||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
currentTlsSetting := true //Default to true
|
currentTlsSetting := true //Default to true
|
||||||
@@ -193,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodGet {
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
//Get the current status
|
//Get the current status
|
||||||
js, _ := json.Marshal(currentTlsSetting)
|
js, _ := json.Marshal(currentTlsSetting)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else if r.Method == http.MethodPost {
|
case http.MethodPost:
|
||||||
newState, err := utils.PostBool(r, "set")
|
newState, err := utils.PostBool(r, "set")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "new state not set or invalid")
|
utils.SendErrorResponse(w, "new state not set or invalid")
|
||||||
@@ -213,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||||
}
|
}
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
} else {
|
default:
|
||||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,135 +58,21 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
|||||||
js, _ := json.Marshal(reqLatestTLS)
|
js, _ := json.Marshal(reqLatestTLS)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else {
|
} else {
|
||||||
if newState == "true" {
|
switch newState {
|
||||||
|
case "true":
|
||||||
sysdb.Write("settings", "forceLatestTLS", true)
|
sysdb.Write("settings", "forceLatestTLS", true)
|
||||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||||
} else if newState == "false" {
|
case "false":
|
||||||
sysdb.Write("settings", "forceLatestTLS", false)
|
sysdb.Write("settings", "forceLatestTLS", false)
|
||||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||||
} else {
|
default:
|
||||||
utils.SendErrorResponse(w, "invalid state given")
|
utils.SendErrorResponse(w, "invalid state given")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle download of the selected certificate
|
|
||||||
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// get the certificate name
|
|
||||||
certname, err := utils.GetPara(r, "certname")
|
|
||||||
if err != nil {
|
|
||||||
utils.SendErrorResponse(w, "invalid certname given")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certname = filepath.Base(certname) //prevent path escape
|
|
||||||
|
|
||||||
// check if the cert exists
|
|
||||||
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
|
|
||||||
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
|
|
||||||
|
|
||||||
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
|
||||||
//Zip them and serve them via http download
|
|
||||||
seeking, _ := utils.GetBool(r, "seek")
|
|
||||||
if seeking {
|
|
||||||
//This request only check if the key exists. Do not provide download
|
|
||||||
utils.SendOK(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Serve both file in zip
|
|
||||||
zipTmpFolder := "./tmp/download"
|
|
||||||
os.MkdirAll(zipTmpFolder, 0775)
|
|
||||||
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
|
||||||
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
|
||||||
|
|
||||||
// Serve the zip file
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
|
||||||
http.ServeFile(w, r, zipFileName)
|
|
||||||
} else {
|
|
||||||
//Not both key exists
|
|
||||||
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle upload of the certificate
|
|
||||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// check if request method is POST
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the key type
|
|
||||||
keytype, err := utils.GetPara(r, "ktype")
|
|
||||||
overWriteFilename := ""
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the domain
|
|
||||||
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)
|
|
||||||
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("./conf/certs", 0775)
|
|
||||||
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// copy file contents to destination file
|
|
||||||
_, err = io.Copy(f, file)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Update cert list
|
|
||||||
tlsCertManager.UpdateLoadedCertList()
|
|
||||||
|
|
||||||
// send response
|
|
||||||
fmt.Fprintln(w, "File upload successful!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
||||||
// get the domain
|
// get the domain
|
||||||
domain, err := utils.GetPara(r, "domain")
|
domain, err := utils.GetPara(r, "domain")
|
||||||
@@ -441,15 +154,40 @@ func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle cert remove
|
func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
//Get the domain
|
||||||
domain, err := utils.PostPara(r, "domain")
|
domain, err := utils.PostPara(r, "domain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "invalid domain given")
|
utils.SendErrorResponse(w, "invalid domain given")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = tlsCertManager.RemoveCert(domain)
|
|
||||||
|
//Get the certificate name
|
||||||
|
certName, err := utils.PostPara(r, "certname")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, err.Error())
|
utils.SendErrorResponse(w, "invalid certificate name given")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Load the target endpoint
|
||||||
|
ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set the preferred certificate for the domain
|
||||||
|
err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SaveReverseProxyConfig(ept)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
@@ -108,9 +108,9 @@ func filterProxyConfigFilename(filename string) string {
|
|||||||
|
|
||||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||||
//Get filename for saving
|
//Get filename for saving
|
||||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
filename := filepath.Join(CONF_HTTP_PROXY, endpoint.RootOrMatchingDomain+".config")
|
||||||
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
|
||||||
filename = "./conf/proxy/root.config"
|
filename = filepath.Join(CONF_HTTP_PROXY, "root.config")
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = filterProxyConfigFilename(filename)
|
filename = filterProxyConfigFilename(filename)
|
||||||
@@ -125,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RemoveReverseProxyConfig(endpoint string) error {
|
func RemoveReverseProxyConfig(endpoint string) error {
|
||||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config")
|
||||||
if endpoint == "/" {
|
if endpoint == "/" {
|
||||||
filename = "./conf/proxy/root.config"
|
filename = filepath.Join(CONF_HTTP_PROXY, "/root.config")
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = filterProxyConfigFilename(filename)
|
filename = filterProxyConfigFilename(filename)
|
||||||
@@ -179,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Specify the folder path to be zipped
|
// Specify the folder path to be zipped
|
||||||
if !utils.FileExists("./conf") {
|
if !utils.FileExists(CONF_FOLDER) {
|
||||||
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
folderPath := "./conf"
|
folderPath := CONF_FOLDER
|
||||||
|
|
||||||
// Set the Content-Type header to indicate it's a zip file
|
// Set the Content-Type header to indicate it's a zip file
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
@@ -284,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Create the target directory to unzip the files
|
// Create the target directory to unzip the files
|
||||||
targetDir := "./conf"
|
targetDir := CONF_FOLDER
|
||||||
if utils.FileExists(targetDir) {
|
if utils.FileExists(targetDir) {
|
||||||
//Backup the old config to old
|
//Backup the old config to old
|
||||||
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
|
||||||
//os.Rename(*path_conf, backupPath)
|
//os.Rename(*path_conf, backupPath)
|
||||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
os.Rename(CONF_FOLDER, CONF_FOLDER+".old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(targetDir, os.ModePerm)
|
err = os.MkdirAll(targetDir, os.ModePerm)
|
||||||
|
23
src/def.go
23
src/def.go
@@ -44,7 +44,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.2.4"
|
SYSTEM_VERSION = "3.2.5"
|
||||||
DEVELOPMENT_BUILD = false
|
DEVELOPMENT_BUILD = false
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
@@ -63,14 +63,19 @@ const (
|
|||||||
LOG_EXTENSION = ".log"
|
LOG_EXTENSION = ".log"
|
||||||
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
||||||
|
|
||||||
/* Configuration Folder Storage Path Constants */
|
/*
|
||||||
CONF_HTTP_PROXY = "./conf/proxy"
|
Configuration Folder Storage Path Constants
|
||||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
Note: No tailing slash in the path
|
||||||
CONF_CERT_STORE = "./conf/certs"
|
*/
|
||||||
CONF_REDIRECTION = "./conf/redirect"
|
CONF_FOLDER = "./conf"
|
||||||
CONF_ACCESS_RULE = "./conf/access"
|
CONF_HTTP_PROXY = CONF_FOLDER + "/proxy"
|
||||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
CONF_STREAM_PROXY = CONF_FOLDER + "/streamproxy"
|
||||||
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
|
CONF_CERT_STORE = CONF_FOLDER + "/certs"
|
||||||
|
CONF_REDIRECTION = CONF_FOLDER + "/redirect"
|
||||||
|
CONF_ACCESS_RULE = CONF_FOLDER + "/access"
|
||||||
|
CONF_PATH_RULE = CONF_FOLDER + "/rules/pathrules"
|
||||||
|
CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json"
|
||||||
|
CONF_GEODB_PATH = CONF_FOLDER + "/geodb"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* System Startup Flags */
|
/* System Startup Flags */
|
||||||
|
@@ -69,7 +69,7 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
if *geoDbUpdate {
|
if *geoDbUpdate {
|
||||||
geodb.DownloadGeoDBUpdate("./conf/geodb")
|
geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -58,11 +58,20 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
|||||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
|
||||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
||||||
|
|
||||||
options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
// Helper function to clean empty strings from split results
|
||||||
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
cleanSplit := func(s string) []string {
|
||||||
options.RequestHeaders = strings.Split(requestHeaders, ",")
|
if s == "" {
|
||||||
options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
|
return nil
|
||||||
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
}
|
||||||
|
|
||||||
|
return strings.Split(s, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
options.ResponseHeaders = cleanSplit(responseHeaders)
|
||||||
|
options.ResponseClientHeaders = cleanSplit(responseClientHeaders)
|
||||||
|
options.RequestHeaders = cleanSplit(requestHeaders)
|
||||||
|
options.RequestIncludedCookies = cleanSplit(requestIncludedCookies)
|
||||||
|
options.RequestExcludedCookies = cleanSplit(requestExcludedCookies)
|
||||||
|
|
||||||
return &AuthRouter{
|
return &AuthRouter{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
|
@@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
sep := h.Parent.GetProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && !sep.Disabled {
|
if sep != nil && !sep.Disabled {
|
||||||
//Matching proxy rule found
|
//Matching proxy rule found
|
||||||
//Access Check (blacklist / whitelist)
|
//Access Check (blacklist / whitelist)
|
||||||
|
59
src/mod/dynamicproxy/certificate.go
Normal file
59
src/mod/dynamicproxy/certificate.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package dynamicproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/tlscert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (router *Router) ResolveHostSpecificTlsBehaviorForHostname(hostname string) (*tlscert.HostSpecificTlsBehavior, error) {
|
||||||
|
if hostname == "" {
|
||||||
|
return nil, errors.New("hostname cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
ept := router.GetProxyEndpointFromHostname(hostname)
|
||||||
|
if ept == nil {
|
||||||
|
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the endpoint has a specific TLS behavior
|
||||||
|
if ept.TlsOptions != nil {
|
||||||
|
imported := &tlscert.HostSpecificTlsBehavior{}
|
||||||
|
router.tlsBehaviorMutex.RLock()
|
||||||
|
// Deep copy the TlsOptions using JSON marshal/unmarshal
|
||||||
|
data, err := json.Marshal(ept.TlsOptions)
|
||||||
|
if err != nil {
|
||||||
|
router.tlsBehaviorMutex.RUnlock()
|
||||||
|
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||||
|
}
|
||||||
|
router.tlsBehaviorMutex.RUnlock()
|
||||||
|
if err := json.Unmarshal(data, imported); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
|
||||||
|
}
|
||||||
|
return imported, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router *Router) SetPreferredCertificateForDomain(ept *ProxyEndpoint, domain string, certName string) error {
|
||||||
|
if ept == nil || certName == "" {
|
||||||
|
return errors.New("endpoint and certificate name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the preferred certificate for the endpoint
|
||||||
|
if ept.TlsOptions == nil {
|
||||||
|
ept.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
|
||||||
|
}
|
||||||
|
|
||||||
|
router.tlsBehaviorMutex.Lock()
|
||||||
|
if ept.TlsOptions.PreferredCertificate == nil {
|
||||||
|
ept.TlsOptions.PreferredCertificate = make(map[string]string)
|
||||||
|
}
|
||||||
|
ept.TlsOptions.PreferredCertificate[domain] = certName
|
||||||
|
router.tlsBehaviorMutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error {
|
|||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
sep := router.GetProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && sep.BypassGlobalTLS {
|
if sep != nil && sep.BypassGlobalTLS {
|
||||||
//Allow routing via non-TLS handler
|
//Allow routing via non-TLS handler
|
||||||
originalHostHeader := r.Host
|
originalHostHeader := r.Host
|
||||||
@@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
|||||||
hostname = r.Host
|
hostname = r.Host
|
||||||
}
|
}
|
||||||
hostname = strings.Split(hostname, ":")[0]
|
hostname = strings.Split(hostname, ":")[0]
|
||||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
|
||||||
return subdEndpoint != nil
|
return subdEndpoint != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
|
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
|
||||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||||
hostname = strings.ToLower(hostname)
|
hostname = strings.ToLower(hostname)
|
||||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||||
@@ -63,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Wildcard not match. Check for alias
|
//Wildcard not match. Check for alias
|
||||||
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
|
if len(ep.MatchingDomainAlias) > 0 {
|
||||||
for _, aliasDomain := range ep.MatchingDomainAlias {
|
for _, aliasDomain := range ep.MatchingDomainAlias {
|
||||||
match, err := filepath.Match(aliasDomain, hostname)
|
match, err := filepath.Match(aliasDomain, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -78,13 +78,17 @@ type Router struct {
|
|||||||
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
|
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
|
||||||
Running bool //If the router is running
|
Running bool //If the router is running
|
||||||
Root *ProxyEndpoint //Root proxy endpoint, default site
|
Root *ProxyEndpoint //Root proxy endpoint, default site
|
||||||
|
|
||||||
|
/* Internals */
|
||||||
mux http.Handler //HTTP handler
|
mux http.Handler //HTTP handler
|
||||||
server *http.Server //HTTP server
|
server *http.Server //HTTP server
|
||||||
tlsListener net.Listener //TLS listener, handle SNI routing
|
|
||||||
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
|
||||||
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
|
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
|
||||||
|
|
||||||
|
tlsListener net.Listener //TLS listener, handle SNI routing
|
||||||
|
tlsBehaviorMutex sync.RWMutex //Mutex for tlsBehavior map
|
||||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||||
|
|
||||||
rateLimterStop chan bool //Stop channel for rate limiter
|
rateLimterStop chan bool //Stop channel for rate limiter
|
||||||
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ import (
|
|||||||
type SniffResult int
|
type SniffResult int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
|
|||||||
payload.rawRequest = r
|
payload.rawRequest = r
|
||||||
|
|
||||||
sniffResult := handler(&payload)
|
sniffResult := handler(&payload)
|
||||||
if sniffResult == SniffResultAccpet {
|
if sniffResult == SniffResultAccept {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK"))
|
w.Write([]byte("OK"))
|
||||||
} else {
|
} else {
|
||||||
|
93
src/mod/tlscert/certgen.go
Normal file
93
src/mod/tlscert/certgen.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package tlscert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateSelfSignedCertificate generates a self-signed ECDSA certificate and saves it to the specified files.
|
||||||
|
func (m *Manager) GenerateSelfSignedCertificate(cn string, sans []string, certFile string, keyFile string) error {
|
||||||
|
// Generate private key (ECDSA P-256)
|
||||||
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to generate private key", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate template
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cn, // Common Name for the certificate
|
||||||
|
Organization: []string{"aroz.org"}, // Organization name
|
||||||
|
OrganizationalUnit: []string{"Zoraxy"}, // Organizational Unit
|
||||||
|
Country: []string{"US"}, // Country code
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour), // valid for 1 year
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: sans, // Subject Alternative Names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create self-signed certificate
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to create certificate", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old certificate file if it exists
|
||||||
|
certPath := filepath.Join(m.CertStore, certFile)
|
||||||
|
if _, err := os.Stat(certPath); err == nil {
|
||||||
|
os.Remove(certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old key file if it exists
|
||||||
|
keyPath := filepath.Join(m.CertStore, keyFile)
|
||||||
|
if _, err := os.Stat(keyPath); err == nil {
|
||||||
|
os.Remove(keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write certificate to file
|
||||||
|
certOut, err := os.Create(filepath.Join(m.CertStore, certFile))
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to open cert file for writing: "+certFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer certOut.Close()
|
||||||
|
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to write certificate to file: "+certFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode private key to PEM
|
||||||
|
privBytes, err := x509.MarshalECPrivateKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Unable to marshal ECDSA private key", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyOut, err := os.Create(filepath.Join(m.CertStore, keyFile))
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to open key file for writing: "+keyFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer keyOut.Close()
|
||||||
|
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Failed to write private key to file: "+keyFile, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Logger.PrintAndLog("tls-router", "Certificate and key generated: "+certFile+", "+keyFile, nil)
|
||||||
|
return nil
|
||||||
|
}
|
352
src/mod/tlscert/handler.go
Normal file
352
src/mod/tlscert/handler.go
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
package tlscert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle cert remove
|
||||||
|
func (m *Manager) HandleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
domain, err := utils.PostPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid domain given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = m.RemoveCert(domain)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle download of the selected certificate
|
||||||
|
func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// get the certificate name
|
||||||
|
certname, err := utils.GetPara(r, "certname")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid certname given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
certname = filepath.Base(certname) //prevent path escape
|
||||||
|
|
||||||
|
// check if the cert exists
|
||||||
|
pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key")
|
||||||
|
priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem")
|
||||||
|
|
||||||
|
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
||||||
|
//Zip them and serve them via http download
|
||||||
|
seeking, _ := utils.GetBool(r, "seek")
|
||||||
|
if seeking {
|
||||||
|
//This request only check if the key exists. Do not provide download
|
||||||
|
utils.SendOK(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serve both file in zip
|
||||||
|
zipTmpFolder := "./tmp/download"
|
||||||
|
os.MkdirAll(zipTmpFolder, 0775)
|
||||||
|
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
||||||
|
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
||||||
|
|
||||||
|
// Serve the zip file
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
http.ServeFile(w, r, zipFileName)
|
||||||
|
} else {
|
||||||
|
//Not both key exists
|
||||||
|
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle upload of the certificate
|
||||||
|
func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// check if request method is POST
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the key type
|
||||||
|
keytype, err := utils.GetPara(r, "ktype")
|
||||||
|
overWriteFilename := ""
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the domain
|
||||||
|
domain, err := utils.GetPara(r, "domain")
|
||||||
|
if err != nil {
|
||||||
|
//Assume localhost
|
||||||
|
domain = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keytype {
|
||||||
|
case "pub":
|
||||||
|
overWriteFilename = domain + ".pem"
|
||||||
|
case "pri":
|
||||||
|
overWriteFilename = domain + ".key"
|
||||||
|
default:
|
||||||
|
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse multipart form data
|
||||||
|
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get file from form data
|
||||||
|
file, _, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// create file in upload directory
|
||||||
|
os.MkdirAll(m.CertStore, 0775)
|
||||||
|
f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// copy file contents to destination file
|
||||||
|
_, err = io.Copy(f, file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update cert list
|
||||||
|
m.UpdateLoadedCertList()
|
||||||
|
|
||||||
|
// send response
|
||||||
|
fmt.Fprintln(w, "File upload successful!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all certificates and map all their domains to the cert filename
|
||||||
|
func (m *Manager) HandleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filenames, err := os.ReadDir(m.CertStore)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certnameToDomainMap := map[string]string{}
|
||||||
|
for _, filename := range filenames {
|
||||||
|
if filename.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certFilepath := filepath.Join(m.CertStore, filename.Name())
|
||||||
|
|
||||||
|
certBtyes, err := os.ReadFile(certFilepath)
|
||||||
|
if err != nil {
|
||||||
|
// Unable to load this file
|
||||||
|
m.Logger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Cert loaded. Check its expiry time
|
||||||
|
block, _ := pem.Decode(certBtyes)
|
||||||
|
if block != nil {
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
||||||
|
for _, dnsName := range cert.DNSNames {
|
||||||
|
certnameToDomainMap[dnsName] = certname
|
||||||
|
}
|
||||||
|
certnameToDomainMap[cert.Subject.CommonName] = certname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requireCompact, _ := utils.GetPara(r, "compact")
|
||||||
|
if requireCompact == "true" {
|
||||||
|
result := make(map[string][]string)
|
||||||
|
|
||||||
|
for key, value := range certnameToDomainMap {
|
||||||
|
if _, ok := result[value]; !ok {
|
||||||
|
result[value] = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
result[value] = append(result[value], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(result)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(certnameToDomainMap)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a list of domains where the certificates covers
|
||||||
|
func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filenames, err := m.ListCertDomains()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showDate, _ := utils.GetBool(r, "date")
|
||||||
|
if showDate {
|
||||||
|
type CertInfo struct {
|
||||||
|
Domain string
|
||||||
|
LastModifiedDate string
|
||||||
|
ExpireDate string
|
||||||
|
RemainingDays int
|
||||||
|
UseDNS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []*CertInfo{}
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
certFilepath := filepath.Join(m.CertStore, filename+".pem")
|
||||||
|
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||||
|
fileInfo, err := os.Stat(certFilepath)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
certExpireTime := "Unknown"
|
||||||
|
certBtyes, err := os.ReadFile(certFilepath)
|
||||||
|
expiredIn := 0
|
||||||
|
if err != nil {
|
||||||
|
//Unable to load this file
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
//Cert loaded. Check its expire time
|
||||||
|
block, _ := pem.Decode(certBtyes)
|
||||||
|
if block != nil {
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
duration := cert.NotAfter.Sub(time.Now())
|
||||||
|
|
||||||
|
// Convert the duration to days
|
||||||
|
expiredIn = int(duration.Hours() / 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
certInfoFilename := filepath.Join(m.CertStore, filename+".json")
|
||||||
|
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||||
|
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||||
|
if err == nil {
|
||||||
|
useDNSValidation = certInfo.UseDNS
|
||||||
|
}
|
||||||
|
|
||||||
|
thisCertInfo := CertInfo{
|
||||||
|
Domain: filename,
|
||||||
|
LastModifiedDate: modifiedTime,
|
||||||
|
ExpireDate: certExpireTime,
|
||||||
|
RemainingDays: expiredIn,
|
||||||
|
UseDNS: useDNSValidation,
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, &thisCertInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert ExpireDate to date object and sort asc
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
|
||||||
|
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
|
||||||
|
return date1.Before(date2)
|
||||||
|
})
|
||||||
|
|
||||||
|
js, _ := json.Marshal(results)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(js)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := json.Marshal(filenames)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the default certificates is correctly setup
|
||||||
|
func (m *Manager) HandleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type CheckResult struct {
|
||||||
|
DefaultPubExists bool
|
||||||
|
DefaultPriExists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, pri := m.DefaultCertExistsSep()
|
||||||
|
js, _ := json.Marshal(CheckResult{
|
||||||
|
pub,
|
||||||
|
pri,
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the common name from the request
|
||||||
|
cn, err := utils.GetPara(r, "cn")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Common name not provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domains, err := utils.PostPara(r, "domains")
|
||||||
|
if err != nil {
|
||||||
|
//No alias domains provided, use the common name as the only domain
|
||||||
|
domains = "[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
SANs := []string{}
|
||||||
|
if err := json.Unmarshal([]byte(domains), &SANs); err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Invalid domains format: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//SANs = append([]string{cn}, SANs...)
|
||||||
|
priKeyFilename := domainToFilename(cn, ".key")
|
||||||
|
pubKeyFilename := domainToFilename(cn, ".pem")
|
||||||
|
|
||||||
|
// Generate self-signed certificate
|
||||||
|
err = m.GenerateSelfSignedCertificate(cn, SANs, pubKeyFilename, priKeyFilename)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to generate self-signed certificate: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the certificate store
|
||||||
|
err = m.UpdateLoadedCertList()
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to update certificate store: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
@@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
|||||||
|
|
||||||
return matchingDomain
|
return matchingDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert a domain name to a filename format
|
||||||
|
func domainToFilename(domain string, ext string) string {
|
||||||
|
// Replace wildcard '*' with '_'
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
|
if strings.HasPrefix(domain, "*") {
|
||||||
|
domain = "_" + strings.TrimPrefix(domain, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add .pem extension
|
||||||
|
ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot
|
||||||
|
return domain + "." + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenameToDomain(filename string) string {
|
||||||
|
// Remove the extension
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
if ext != "" {
|
||||||
|
filename = strings.TrimSuffix(filename, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(filename, "_") {
|
||||||
|
filename = "*" + filename[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
@@ -24,7 +24,7 @@ type HostSpecificTlsBehavior struct {
|
|||||||
DisableSNI bool //If SNI is enabled for this server name
|
DisableSNI bool //If SNI is enabled for this server name
|
||||||
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled 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
|
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
|
||||||
PreferredCertificate string //Preferred certificate for this server name, if empty, use the first matching certificate
|
PreferredCertificate map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -34,13 +34,12 @@ type Manager struct {
|
|||||||
|
|
||||||
/* External handlers */
|
/* External handlers */
|
||||||
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
|
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
|
||||||
verbal bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed localhost.pem localhost.key
|
//go:embed localhost.pem localhost.key
|
||||||
var buildinCertStore embed.FS
|
var buildinCertStore embed.FS
|
||||||
|
|
||||||
func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) {
|
func NewManager(certStore string, logger *logger.Logger) (*Manager, error) {
|
||||||
if !utils.FileExists(certStore) {
|
if !utils.FileExists(certStore) {
|
||||||
os.MkdirAll(certStore, 0775)
|
os.MkdirAll(certStore, 0775)
|
||||||
}
|
}
|
||||||
@@ -63,7 +62,6 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
|||||||
CertStore: certStore,
|
CertStore: certStore,
|
||||||
LoadedCerts: []*CertCache{},
|
LoadedCerts: []*CertCache{},
|
||||||
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
|
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
|
||||||
verbal: verbal,
|
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior {
|
|||||||
DisableSNI: false,
|
DisableSNI: false,
|
||||||
DisableLegacyCertificateMatching: false,
|
DisableLegacyCertificateMatching: false,
|
||||||
EnableAutoHTTPS: false,
|
EnableAutoHTTPS: false,
|
||||||
PreferredCertificate: "",
|
PreferredCertificate: map[string]string{}, // No preferred certificate, use the first matching certificate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +88,10 @@ func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior
|
|||||||
return GetDefaultHostSpecificTlsBehavior(), nil
|
return GetDefaultHostSpecificTlsBehavior(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetHostSpecificTlsBehavior(fn func(serverName string) (*HostSpecificTlsBehavior, error)) {
|
||||||
|
m.hostSpecificTlsBehavior = fn
|
||||||
|
}
|
||||||
|
|
||||||
// Update domain mapping from file
|
// Update domain mapping from file
|
||||||
func (m *Manager) UpdateLoadedCertList() error {
|
func (m *Manager) UpdateLoadedCertList() error {
|
||||||
//Get a list of certificates from file
|
//Get a list of certificates from file
|
||||||
@@ -213,13 +215,17 @@ func (m *Manager) GetCertificateByHostname(hostname string) (string, string, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
|
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
|
||||||
}
|
}
|
||||||
|
preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname]
|
||||||
|
if !ok {
|
||||||
|
preferredCertificate = ""
|
||||||
|
}
|
||||||
|
|
||||||
if tlsBehavior.DisableSNI && tlsBehavior.PreferredCertificate != "" &&
|
if tlsBehavior.DisableSNI && preferredCertificate != "" &&
|
||||||
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")) &&
|
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".pem")) &&
|
||||||
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")) {
|
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".key")) {
|
||||||
//User setup a Preferred certificate, use the preferred certificate directly
|
//User setup a Preferred certificate, use the preferred certificate directly
|
||||||
pubKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")
|
pubKey = filepath.Join(m.CertStore, preferredCertificate+".pem")
|
||||||
priKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")
|
priKey = filepath.Join(m.CertStore, preferredCertificate+".key")
|
||||||
} else {
|
} else {
|
||||||
if !tlsBehavior.DisableLegacyCertificateMatching &&
|
if !tlsBehavior.DisableLegacyCertificateMatching &&
|
||||||
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&
|
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&
|
||||||
|
@@ -135,12 +135,12 @@ func ReverseProxtInit() {
|
|||||||
Load all conf from files
|
Load all conf from files
|
||||||
|
|
||||||
*/
|
*/
|
||||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
confs, _ := filepath.Glob(CONF_HTTP_PROXY + "/*.config")
|
||||||
for _, conf := range confs {
|
for _, conf := range confs {
|
||||||
err := LoadReverseProxyConfig(conf)
|
err := LoadReverseProxyConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +389,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
//TLS
|
//TLS
|
||||||
BypassGlobalTLS: useBypassGlobalTLS,
|
BypassGlobalTLS: useBypassGlobalTLS,
|
||||||
AccessFilterUUID: accessRuleID,
|
AccessFilterUUID: accessRuleID,
|
||||||
|
TlsOptions: tlscert.GetDefaultHostSpecificTlsBehavior(),
|
||||||
|
|
||||||
//VDir
|
//VDir
|
||||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
//Custom headers
|
//Custom headers
|
||||||
@@ -717,6 +719,11 @@ func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newTlsConfig.PreferredCertificate == nil {
|
||||||
|
//No update needed, reuse the current TLS config
|
||||||
|
newTlsConfig.PreferredCertificate = ept.TlsOptions.PreferredCertificate
|
||||||
|
}
|
||||||
|
|
||||||
ept.TlsOptions = newTlsConfig
|
ept.TlsOptions = newTlsConfig
|
||||||
|
|
||||||
//Prepare to replace the current routing rule
|
//Prepare to replace the current routing rule
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
@@ -99,7 +100,7 @@ func startupSequence() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Create a TLS certificate manager
|
//Create a TLS certificate manager
|
||||||
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger)
|
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -366,6 +367,9 @@ func finalSequence() {
|
|||||||
|
|
||||||
//Inject routing rules
|
//Inject routing rules
|
||||||
registerBuildInRoutingRules()
|
registerBuildInRoutingRules()
|
||||||
|
|
||||||
|
//Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname
|
||||||
|
tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shutdown Sequence */
|
/* Shutdown Sequence */
|
||||||
|
@@ -339,11 +339,15 @@
|
|||||||
<div class="rpconfig_content" rpcfg="ssl">
|
<div class="rpconfig_content" rpcfg="ssl">
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
|
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
|
||||||
<table class="ui celled small compact table Tls_resolve_list">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>Resolve to Certificate</th>
|
<th class="no-sort">Resolve to Certificate</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -359,18 +363,20 @@
|
|||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
<input type="checkbox" class="Tls_EnableLegacyCertificateMatching">
|
<input type="checkbox" class="Tls_EnableLegacyCertificateMatching">
|
||||||
<label>Enable Legacy Certificate Matching<br>
|
<label>Enable Legacy Certificate Matching<br>
|
||||||
<small>Use legacy filename / hostname matching for loading certificates</small>
|
<small>Use filename for hostname matching, faster but less accurate</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
<div class="ui disabled checkbox" style="margin-top: 0.4em;">
|
||||||
<input type="checkbox" class="Tls_EnableAutoHTTPS">
|
<input type="checkbox" class="Tls_EnableAutoHTTPS">
|
||||||
<label>Enable Auto HTTPS<br>
|
<label>Enable Auto HTTPS (WIP)<br>
|
||||||
<small>Automatically request a certificate for the domain</small>
|
<small>Automatically request a certificate for the domain</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div class="ui divider"></div>
|
||||||
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
|
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
|
||||||
|
<button class="ui basic small button getSelfSignCertBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="yellow lock icon"></i> Generate Self-Signed Certificate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Custom Headers -->
|
<!-- Custom Headers -->
|
||||||
@@ -580,6 +586,28 @@
|
|||||||
aliasDomains += `</small><br>`;
|
aliasDomains += `</small><br>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build the sorting value
|
||||||
|
let destSortValue = subd.ActiveOrigins.map(o => {
|
||||||
|
// Check if it's an IP address (with optional port)
|
||||||
|
let upstreamAddr = o.OriginIpOrDomain;
|
||||||
|
let subpath = "";
|
||||||
|
if (upstreamAddr.indexOf("/") !== -1) {
|
||||||
|
let parts = upstreamAddr.split("/");
|
||||||
|
subpath = parts.slice(1).join("/");
|
||||||
|
upstreamAddr = parts[0];
|
||||||
|
}
|
||||||
|
let ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/;
|
||||||
|
if (ipPortRegex.test(upstreamAddr)) {
|
||||||
|
let [ip, port] = upstreamAddr.split(":");
|
||||||
|
// Convert IP to hex
|
||||||
|
let hexIp = ip.split('.').map(x => ('00' + parseInt(x).toString(16)).slice(-2)).join('');
|
||||||
|
let hexPort = port ? (port.length < 5 ? port.padStart(5, '0') : port) : '';
|
||||||
|
return hexIp + (hexPort ? ':' + hexPort : '') + "/" + subpath;
|
||||||
|
}
|
||||||
|
// Otherwise, treat it as a domain name
|
||||||
|
return upstreamAddr;
|
||||||
|
}).join(",");
|
||||||
|
|
||||||
//Build tag list
|
//Build tag list
|
||||||
let tagList = renderTagList(subd);
|
let tagList = renderTagList(subd);
|
||||||
let tagListEmpty = (subd.Tags.length == 0);
|
let tagListEmpty = (subd.Tags.length == 0);
|
||||||
@@ -596,7 +624,7 @@
|
|||||||
${aliasDomains}
|
${aliasDomains}
|
||||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="" editable="true" datatype="domain">
|
<td data-label="" editable="true" datatype="domain" data-sort-value="${destSortValue}" style="word-break: break-all;">
|
||||||
<div class="upstreamList">
|
<div class="upstreamList">
|
||||||
${upstreams}
|
${upstreams}
|
||||||
</div>
|
</div>
|
||||||
@@ -747,7 +775,7 @@
|
|||||||
let newTlsOption = {
|
let newTlsOption = {
|
||||||
"DisableSNI": !enableSNI,
|
"DisableSNI": !enableSNI,
|
||||||
"DisableLegacyCertificateMatching": !enableLegacyCertificateMatching,
|
"DisableLegacyCertificateMatching": !enableLegacyCertificateMatching,
|
||||||
"EnableAutoHTTPS": enableAutoHTTPS
|
"EnableAutoHTTPS": enableAutoHTTPS,
|
||||||
}
|
}
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/setTlsConfig",
|
url: "/api/proxy/setTlsConfig",
|
||||||
@@ -769,6 +797,9 @@
|
|||||||
|
|
||||||
function updateTlsResolveList(uuid){
|
function updateTlsResolveList(uuid){
|
||||||
let editor = $("#httprpEditModalWrapper");
|
let editor = $("#httprpEditModalWrapper");
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").off("change");
|
||||||
|
editor.find(".certificateDropdown .ui.dropdown").remove();
|
||||||
|
|
||||||
//Update the TLS resolve list
|
//Update the TLS resolve list
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/cert/resolve?domain=" + uuid,
|
url: "/api/cert/resolve?domain=" + uuid,
|
||||||
@@ -785,17 +816,60 @@
|
|||||||
resolveList.append(`
|
resolveList.append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${primaryDomain}</td>
|
<td>${primaryDomain}</td>
|
||||||
<td>${certMap[primaryDomain] || "Fallback Certificate"}</td>
|
<td class="certificateDropdown" domain="${primaryDomain}">${certMap[primaryDomain] || "Fallback Certificate"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
aliasDomains.forEach(alias => {
|
aliasDomains.forEach(alias => {
|
||||||
resolveList.append(`
|
resolveList.append(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${alias}</td>
|
<td>${alias}</td>
|
||||||
<td>${certMap[alias] || "Fallback Certificate"}</td>
|
<td class="certificateDropdown" domain="${alias}">${certMap[alias] || "Fallback Certificate"}</td>
|
||||||
</tr>
|
</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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -946,6 +1020,29 @@
|
|||||||
renewCertificate(renewDomainKey, false, btn);
|
renewCertificate(renewDomainKey, false, btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateSelfSignedCertificate(uuid, domains, btn=undefined){
|
||||||
|
let payload = JSON.stringify(domains);
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/cert/selfsign",
|
||||||
|
data: {
|
||||||
|
"cn": uuid,
|
||||||
|
"domains": payload
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error == undefined){
|
||||||
|
msgbox("Self-Signed Certificate Generated", true);
|
||||||
|
resyncProxyEditorConfig();
|
||||||
|
if (typeof(initManagedDomainCertificateList) != undefined){
|
||||||
|
//Re-init the managed domain certificate list
|
||||||
|
initManagedDomainCertificateList();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* Tags & Search */
|
/* Tags & Search */
|
||||||
function handleSearchInput(event){
|
function handleSearchInput(event){
|
||||||
if (event.key == "Escape"){
|
if (event.key == "Escape"){
|
||||||
@@ -1074,6 +1171,28 @@
|
|||||||
return subd;
|
return subd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a certificate dropdown for the HTTP Proxy Rule Editor
|
||||||
|
// so user can pick which certificate they want to use for the current editing hostname
|
||||||
|
function generateCertificateDropdown(callback){
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/cert/list",
|
||||||
|
method: "GET",
|
||||||
|
success: function(data) {
|
||||||
|
let dropdown = $('<div class="ui fluid selection dropdown"></div>');
|
||||||
|
let menu = $('<div class="menu"></div>');
|
||||||
|
data.forEach(cert => {
|
||||||
|
menu.append(`<div class="item" data-value="${cert}">${cert}</div>`);
|
||||||
|
});
|
||||||
|
// Add a hidden input to store the selected certificate
|
||||||
|
dropdown.append('<input type="hidden" name="certificate">');
|
||||||
|
dropdown.append('<i class="dropdown icon"></i>');
|
||||||
|
dropdown.append('<div class="default text">Fallback Certificate</div>');
|
||||||
|
dropdown.append(menu);
|
||||||
|
callback(dropdown);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//Initialize the http proxy rule editor
|
//Initialize the http proxy rule editor
|
||||||
function initHttpProxyRuleEditorModal(rulepayload){
|
function initHttpProxyRuleEditorModal(rulepayload){
|
||||||
let subd = JSON.parse(JSON.stringify(rulepayload));
|
let subd = JSON.parse(JSON.stringify(rulepayload));
|
||||||
@@ -1176,39 +1295,6 @@
|
|||||||
});
|
});
|
||||||
editor.find(".downstream_alias_hostname").html(aliasHTML);
|
editor.find(".downstream_alias_hostname").html(aliasHTML);
|
||||||
|
|
||||||
//TODO: Move this to SSL TLS section
|
|
||||||
let enableQuickRequestButton = true;
|
|
||||||
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
|
||||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
|
||||||
let thisAliasName = subd.MatchingDomainAlias[i];
|
|
||||||
domains.push(thisAliasName);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
|
||||||
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
|
||||||
enableQuickRequestButton = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subd.MatchingDomainAlias != undefined){
|
|
||||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
|
||||||
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
|
||||||
enableQuickRequestButton = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
|
||||||
if (enableQuickRequestButton){
|
|
||||||
editor.find(".getCertificateBtn").removeClass("disabled");
|
|
||||||
}else{
|
|
||||||
editor.find(".getCertificateBtn").addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
|
||||||
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------------ Upstreams ------------ */
|
/* ------------ Upstreams ------------ */
|
||||||
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
||||||
editor.find(".editUpstreamButton").off("click").on("click", function(){
|
editor.find(".editUpstreamButton").off("click").on("click", function(){
|
||||||
@@ -1237,6 +1323,8 @@
|
|||||||
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
||||||
editor.find(".editVdirBtn").off("click").on("click", function(){
|
editor.find(".editVdirBtn").off("click").on("click", function(){
|
||||||
quickEditVdir(uuid);
|
quickEditVdir(uuid);
|
||||||
|
//Temporary restore scroll
|
||||||
|
$("body").css("overflow", "auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------ Alias ------------ */
|
/* ------------ Alias ------------ */
|
||||||
@@ -1335,9 +1423,16 @@
|
|||||||
|
|
||||||
/* ------------ TLS ------------ */
|
/* ------------ TLS ------------ */
|
||||||
updateTlsResolveList(uuid);
|
updateTlsResolveList(uuid);
|
||||||
|
if (subd.TlsOptions){
|
||||||
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
|
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
|
||||||
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
|
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
|
||||||
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
|
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
|
||||||
|
}else{
|
||||||
|
//Use default options
|
||||||
|
editor.find(".Tls_EnableSNI").prop("checked", true);
|
||||||
|
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", true);
|
||||||
|
editor.find(".Tls_EnableAutoHTTPS").prop("checked", false);
|
||||||
|
}
|
||||||
|
|
||||||
editor.find(".Tls_EnableSNI").off("change").on("change", function() {
|
editor.find(".Tls_EnableSNI").off("change").on("change", function() {
|
||||||
saveTlsConfigs(uuid);
|
saveTlsConfigs(uuid);
|
||||||
@@ -1349,6 +1444,45 @@
|
|||||||
saveTlsConfigs(uuid);
|
saveTlsConfigs(uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Quick access to get certificate for the current host */
|
||||||
|
let enableQuickRequestButton = true;
|
||||||
|
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||||
|
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||||
|
let thisAliasName = subd.MatchingDomainAlias[i];
|
||||||
|
domains.push(thisAliasName);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||||
|
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subd.MatchingDomainAlias != undefined){
|
||||||
|
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||||
|
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enableQuickRequestButton){
|
||||||
|
editor.find(".getCertificateBtn").removeClass("disabled");
|
||||||
|
}else{
|
||||||
|
editor.find(".getCertificateBtn").addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
||||||
|
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||||
|
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind event to self-signed certificate button
|
||||||
|
editor.find(".getSelfSignCertBtn").off("click").on("click", function() {
|
||||||
|
generateSelfSignedCertificate(uuid, domains, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------ Tags ------------ */
|
/* ------------ Tags ------------ */
|
||||||
(()=>{
|
(()=>{
|
||||||
let payload = encodeURIComponent(JSON.stringify({
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
@@ -1411,7 +1545,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Page Initialization Functions
|
Page Initialization Functions
|
||||||
*/
|
*/
|
||||||
@@ -1436,7 +1569,9 @@
|
|||||||
// there is a chance where the user has modified the Vdir
|
// there is a chance where the user has modified the Vdir
|
||||||
// we need to get the latest setting from server side and
|
// we need to get the latest setting from server side and
|
||||||
// render it again
|
// render it again
|
||||||
updateVdirInProxyEditor();
|
resyncProxyEditorConfig();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
$("body").css("overflow", "hidden");
|
||||||
} else {
|
} else {
|
||||||
listProxyEndpoints();
|
listProxyEndpoints();
|
||||||
//Reset the tag filter
|
//Reset the tag filter
|
||||||
|
@@ -151,11 +151,31 @@
|
|||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
$('#forwardAuthAddress').val(data.address);
|
$('#forwardAuthAddress').val(data.address);
|
||||||
|
if (data.responseHeaders != null) {
|
||||||
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthResponseHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.responseClientHeaders != null) {
|
||||||
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthResponseClientHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.requestHeaders != null) {
|
||||||
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestHeaders').val("");
|
||||||
|
}
|
||||||
|
if (data.requestIncludedCookies != null) {
|
||||||
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
|
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestIncludedCookies').val("");
|
||||||
|
}
|
||||||
|
if (data.requestExcludedCookies != null) {
|
||||||
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
||||||
|
} else {
|
||||||
|
$('#forwardAuthRequestExcludedCookies').val("");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||||
|
@@ -137,7 +137,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function clearStreamProxyAddEditForm(){
|
function clearStreamProxyAddEditForm(){
|
||||||
$('#streamProxyForm input, #streamProxyForm select').val('');
|
$('#streamProxyForm').find('input:not([type=checkbox]), select').val('');
|
||||||
$('#streamProxyForm select').dropdown('clear');
|
$('#streamProxyForm select').dropdown('clear');
|
||||||
$("#streamProxyForm input[name=timeout]").val(10);
|
$("#streamProxyForm input[name=timeout]").val(10);
|
||||||
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
|
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
|
||||||
|
Reference in New Issue
Block a user