mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-30 11:21:44 +02:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d2b8f224c | |||
877692695e | |||
b9c609e413 | |||
7426cc2bb1 | |||
bd71335f47 | |||
8bcc8e7095 | |||
b1824a66a3 | |||
cc6501db12 | |||
70d95bd4e4 | |||
b590e15ef2 | |||
b25f8aab3e | |||
c0578a33b6 | |||
55a525106a | |||
e3b68b9aad | |||
3f1c50c009 | |||
8f046a0b47 | |||
a98d86a303 | |||
73e6530862 | |||
0c753ae531 | |||
6353cc532a | |||
e049761f36 | |||
4dc7175588 |
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@ -31,8 +31,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup building file structure
|
- name: Setup building file structure
|
||||||
run: |
|
run: |
|
||||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||||
cp -lr $GITHUB_WORKSPACE/example/ $GITHUB_WORKSPACE/docker/
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
## Build Zoraxy
|
## Build Zoraxy
|
||||||
FROM docker.io/golang:bookworm AS build-zoraxy
|
FROM docker.io/golang:alpine AS build-zoraxy
|
||||||
|
|
||||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||||
mkdir -p /usr/local/bin/
|
mkdir -p /usr/local/bin/
|
||||||
@ -15,39 +15,54 @@ RUN go mod tidy &&\
|
|||||||
|
|
||||||
|
|
||||||
## Build ZeroTier
|
## Build ZeroTier
|
||||||
FROM docker.io/golang:bookworm AS build-zerotier
|
FROM docker.io/rust:1.79-alpine AS build-zerotier
|
||||||
|
|
||||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||||
mkdir -p /usr/local/bin/
|
mkdir -p /usr/local/bin/
|
||||||
|
|
||||||
WORKDIR /opt/zerotier/source/
|
WORKDIR /opt/zerotier/source/
|
||||||
|
|
||||||
RUN apt-get update -y &&\
|
RUN apk add --update --no-cache curl make gcc g++ linux-headers openssl-dev nano
|
||||||
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
|
||||||
|
|
||||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||||
cd ZeroTierOne-* &&\
|
cd ZeroTierOne-*/zeroidc &&\
|
||||||
make &&\
|
cargo update -p getrandom &&\
|
||||||
|
cd .. &&\
|
||||||
|
make -f make-linux.mk &&\
|
||||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||||
chmod 755 /usr/local/bin/zerotier-one
|
chmod 755 /usr/local/bin/zerotier-one
|
||||||
|
|
||||||
|
|
||||||
FROM docker.io/golang:bookworm
|
## Fetch plugin
|
||||||
|
FROM docker.io/golang:alpine AS fetch-plugin
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /opt/zoraxy/
|
||||||
|
|
||||||
|
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
|
||||||
|
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||||
|
|
||||||
|
|
||||||
|
## Main
|
||||||
|
FROM docker.io/golang:alpine
|
||||||
|
|
||||||
# If you build it yourself, you will need to add the example directory into the docker directory.
|
# If you build it yourself, you will need to add the example directory into the docker directory.
|
||||||
|
|
||||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
||||||
COPY --chmod=700 ./example/plugins/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
|
||||||
|
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 apt-get update -y &&\
|
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
mkdir -p /opt/zoraxy/plugin/ &&\
|
||||||
|
echo "tun" | tee -a /etc/modules
|
||||||
RUN mkdir -p /opt/zoraxy/plugin/
|
|
||||||
|
|
||||||
WORKDIR /opt/zoraxy/config/
|
WORKDIR /opt/zoraxy/config/
|
||||||
|
|
||||||
@ -72,6 +87,8 @@ ENV WEBROOT="./www"
|
|||||||
|
|
||||||
VOLUME [ "/opt/zoraxy/config/" ]
|
VOLUME [ "/opt/zoraxy/config/" ]
|
||||||
|
|
||||||
|
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
|
||||||
|
|
||||||
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
|
||||||
|
@ -19,6 +19,7 @@ Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Cha
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name zoraxy \
|
--name zoraxy \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
|
--add-host=host.docker.internal:host-gateway \
|
||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
-p 443:443 \
|
-p 443:443 \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
@ -47,6 +48,8 @@ services:
|
|||||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /etc/localtime:/etc/localtime
|
- /etc/localtime:/etc/localtime
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
FASTGEOIP: "true"
|
FASTGEOIP: "true"
|
||||||
```
|
```
|
||||||
@ -68,6 +71,11 @@ services:
|
|||||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||||
|
|
||||||
|
### Extra Hosts
|
||||||
|
| Host | Details |
|
||||||
|
|:-|:-|
|
||||||
|
| `host.docker.internal:host-gateway` | Resolves host.docker.internal to the host’s gateway IP on the Docker bridge network, allowing containers to access services running on the host machine. |
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
|
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
|
||||||
@ -95,6 +103,20 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
||||||
|
|
||||||
|
### ZeroTier
|
||||||
|
|
||||||
|
If you are running with ZeroTier, make sure to add the following flags to ensure ZeroTier functionality:
|
||||||
|
|
||||||
|
`--cap_add NET_ADMIN` and `--device /dev/net/tun:/dev/net/tun`
|
||||||
|
|
||||||
|
Or for Docker Compose:
|
||||||
|
```
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
```
|
||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
echo "Copying zoraxy_plugin to all mods..."
|
echo "Copying zoraxy_plugin to all mods..."
|
||||||
for dir in "$1"/*; do
|
for dir in "$1"/*; do
|
||||||
|
@ -12,5 +12,7 @@ services:
|
|||||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /etc/localtime:/etc/localtime
|
- /etc/localtime:/etc/localtime
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
FASTGEOIP: "true"
|
FASTGEOIP: "true"
|
||||||
|
@ -80,10 +80,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
|||||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for Authentication handlers like Authelia and OAUTH2
|
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||||
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
|
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
|
||||||
authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for redirection rules management functions
|
// Register the APIs for redirection rules management functions
|
||||||
@ -239,6 +238,10 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
|
|||||||
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
|
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
|
||||||
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
|
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
|
||||||
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
|
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
|
||||||
|
|
||||||
|
// Developer options
|
||||||
|
authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload)
|
||||||
|
authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||||
|
12
src/def.go
12
src/def.go
@ -13,12 +13,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authentik"
|
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authelia"
|
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dockerux"
|
"imuslab.com/zoraxy/mod/dockerux"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
@ -43,8 +41,9 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.2.1"
|
SYSTEM_VERSION = "3.2.2"
|
||||||
|
DEVELOPMENT_BUILD = false
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
TMP_FOLDER = "./tmp"
|
TMP_FOLDER = "./tmp"
|
||||||
@ -144,8 +143,7 @@ var (
|
|||||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||||
|
|
||||||
//Authentication Provider
|
//Authentication Provider
|
||||||
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
||||||
authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
|
|
||||||
|
|
||||||
//Helper modules
|
//Helper modules
|
||||||
EmailSender *email.Sender //Email sender that handle email sending
|
EmailSender *email.Sender //Email sender that handle email sending
|
||||||
|
7
src/mod/auth/sso/deprecated/authelia/README.txt
Normal file
7
src/mod/auth/sso/deprecated/authelia/README.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Module: authelia
|
||||||
|
|
||||||
|
Notice:
|
||||||
|
This module is **deprecated** and is no longer in use. It has been retained here for reference purposes only.
|
||||||
|
Consider using the updated implementation or alternative solutions as this module may be removed in future updates.
|
||||||
|
|
||||||
|
Original implementation: https://github.com/tobychui/zoraxy/pull/421
|
7
src/mod/auth/sso/deprecated/authentik/README.txt
Normal file
7
src/mod/auth/sso/deprecated/authentik/README.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Module: authentik
|
||||||
|
|
||||||
|
Notice:
|
||||||
|
This module is **deprecated** and is no longer in use. It has been retained here for reference purposes only.
|
||||||
|
Consider using the updated implementation or alternative solutions as this module may be removed in future updates.
|
||||||
|
|
||||||
|
Original implementation: https://github.com/tobychui/zoraxy/pull/568
|
46
src/mod/auth/sso/forward/const.go
Normal file
46
src/mod/auth/sso/forward/const.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package forward
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogTitle = "Forward Auth"
|
||||||
|
|
||||||
|
DatabaseTable = "auth_sso_forward"
|
||||||
|
|
||||||
|
DatabaseKeyAddress = "address"
|
||||||
|
DatabaseKeyResponseHeaders = "responseHeaders"
|
||||||
|
DatabaseKeyResponseClientHeaders = "responseClientHeaders"
|
||||||
|
DatabaseKeyRequestHeaders = "requestHeaders"
|
||||||
|
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
|
||||||
|
|
||||||
|
HeaderXForwardedProto = "X-Forwarded-Proto"
|
||||||
|
HeaderXForwardedHost = "X-Forwarded-Host"
|
||||||
|
HeaderXForwardedFor = "X-Forwarded-For"
|
||||||
|
HeaderXForwardedURI = "X-Forwarded-URI"
|
||||||
|
HeaderXForwardedMethod = "X-Forwarded-Method"
|
||||||
|
|
||||||
|
HeaderCookie = "Cookie"
|
||||||
|
|
||||||
|
HeaderUpgrade = "Upgrade"
|
||||||
|
HeaderConnection = "Connection"
|
||||||
|
HeaderTransferEncoding = "Transfer-Encoding"
|
||||||
|
HeaderTE = "TE"
|
||||||
|
HeaderTrailers = "Trailers"
|
||||||
|
HeaderKeepAlive = "Keep-Alive"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInternalServerError = errors.New("internal server error")
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
doNotCopyHeaders = []string{
|
||||||
|
HeaderUpgrade,
|
||||||
|
HeaderConnection,
|
||||||
|
HeaderTransferEncoding,
|
||||||
|
HeaderTE,
|
||||||
|
HeaderTrailers,
|
||||||
|
HeaderKeepAlive,
|
||||||
|
}
|
||||||
|
)
|
334
src/mod/auth/sso/forward/forward.go
Normal file
334
src/mod/auth/sso/forward/forward.go
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
package forward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthRouterOptions struct {
|
||||||
|
// Address of the forward auth endpoint.
|
||||||
|
Address string
|
||||||
|
|
||||||
|
// ResponseHeaders is a list of headers to be copied from the response if provided by the forward auth endpoint to
|
||||||
|
// the request.
|
||||||
|
ResponseHeaders []string
|
||||||
|
|
||||||
|
// ResponseClientHeaders is a list of headers to be copied from the response if provided by the forward auth
|
||||||
|
// endpoint to the response to the client.
|
||||||
|
ResponseClientHeaders []string
|
||||||
|
|
||||||
|
// RequestHeaders is a list of headers to be copied from the request to the authorization server. If empty all
|
||||||
|
// headers are copied.
|
||||||
|
RequestHeaders []string
|
||||||
|
|
||||||
|
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
|
||||||
|
RequestExcludedCookies []string
|
||||||
|
|
||||||
|
Logger *logger.Logger
|
||||||
|
Database *database.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthRouter struct {
|
||||||
|
client *http.Client
|
||||||
|
options *AuthRouterOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthRouter creates a new AuthRouter object
|
||||||
|
func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
|
||||||
|
options.Database.NewTable(DatabaseTable)
|
||||||
|
|
||||||
|
//Read settings from database if available.
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
|
||||||
|
|
||||||
|
responseHeaders, responseClientHeaders, requestHeaders, requestExcludedCookies := "", "", "", ""
|
||||||
|
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, &responseHeaders)
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyResponseClientHeaders, &responseClientHeaders)
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
|
||||||
|
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
|
||||||
|
|
||||||
|
options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||||
|
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
||||||
|
options.RequestHeaders = strings.Split(requestHeaders, ",")
|
||||||
|
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
||||||
|
|
||||||
|
return &AuthRouter{
|
||||||
|
client: &http.Client{
|
||||||
|
CheckRedirect: func(r *http.Request, via []*http.Request) (err error) {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAPIOptions is the internal handler for setting the options.
|
||||||
|
func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
ar.handleOptionsGET(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
ar.handleOptionsPOST(w, r)
|
||||||
|
default:
|
||||||
|
ar.handleOptionsMethodNotAllowed(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
js, _ := json.Marshal(map[string]interface{}{
|
||||||
|
DatabaseKeyAddress: ar.options.Address,
|
||||||
|
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
|
||||||
|
DatabaseKeyResponseClientHeaders: ar.options.ResponseClientHeaders,
|
||||||
|
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
|
||||||
|
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Update the settings
|
||||||
|
address, err := utils.PostPara(r, DatabaseKeyAddress)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "address not found")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are optional fields and can be empty strings.
|
||||||
|
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
|
||||||
|
responseClientHeaders, _ := utils.PostPara(r, DatabaseKeyResponseClientHeaders)
|
||||||
|
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
|
||||||
|
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
|
||||||
|
|
||||||
|
// Write changes to runtime
|
||||||
|
ar.options.Address = address
|
||||||
|
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||||
|
ar.options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
|
||||||
|
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
|
||||||
|
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
|
||||||
|
|
||||||
|
// Write changes to database
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address)
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseClientHeaders, responseClientHeaders)
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
|
||||||
|
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
|
||||||
|
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if ar.options.Address == "" {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil)
|
||||||
|
|
||||||
|
return ErrInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a request to Authz Server to verify the request
|
||||||
|
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err)
|
||||||
|
|
||||||
|
return ErrInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add opt-in support for copying the request body to the forward auth request.
|
||||||
|
headerCopyIncluded(r.Header, req.Header, ar.options.RequestHeaders, true)
|
||||||
|
|
||||||
|
// TODO: Add support for upstream headers.
|
||||||
|
rSetForwardedHeaders(r, req)
|
||||||
|
|
||||||
|
// Make the Authz Request.
|
||||||
|
respForwarded, err := ar.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err)
|
||||||
|
|
||||||
|
return ErrInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
defer respForwarded.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(respForwarded.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err)
|
||||||
|
|
||||||
|
return ErrInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
|
||||||
|
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
|
||||||
|
if len(ar.options.ResponseClientHeaders) != 0 {
|
||||||
|
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseClientHeaders, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ar.options.RequestExcludedCookies) != 0 {
|
||||||
|
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
|
||||||
|
headerCookieRedact(r, ar.options.RequestExcludedCookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ar.options.ResponseHeaders) != 0 {
|
||||||
|
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
|
||||||
|
// upstream server/next hop.
|
||||||
|
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the response.
|
||||||
|
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
|
||||||
|
|
||||||
|
w.WriteHeader(respForwarded.StatusCode)
|
||||||
|
if _, err = w.Write(body); err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err)
|
||||||
|
|
||||||
|
return ErrInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheme(r *http.Request) string {
|
||||||
|
if r.TLS != nil {
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCookieRedact(r *http.Request, excluded []string) {
|
||||||
|
original := r.Cookies()
|
||||||
|
|
||||||
|
if len(original) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []string
|
||||||
|
|
||||||
|
for _, cookie := range original {
|
||||||
|
if stringInSlice(cookie.Name, excluded) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies = append(cookies, cookie.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set(HeaderCookie, strings.Join(cookies, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
|
||||||
|
for key, values := range original {
|
||||||
|
// We should never copy the headers in the below list.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if stringInSliceFold(key, excludedHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
|
||||||
|
if allIfEmpty && len(includedHeaders) == 0 {
|
||||||
|
headerCopyAll(original, destination)
|
||||||
|
} else {
|
||||||
|
headerCopyIncludedExact(original, destination, includedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyAll(original, destination http.Header) {
|
||||||
|
for key, values := range original {
|
||||||
|
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
|
||||||
|
for _, key := range keys {
|
||||||
|
// We should never copy the headers in the below list, even if they're in the list provided by a user.
|
||||||
|
if stringInSliceFold(key, doNotCopyHeaders) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if values, ok := original[key]; ok {
|
||||||
|
destination[key] = append(destination[key], values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringInSlice(needle string, haystack []string) bool {
|
||||||
|
if len(haystack) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range haystack {
|
||||||
|
if needle == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringInSliceFold(needle string, haystack []string) bool {
|
||||||
|
if len(haystack) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range haystack {
|
||||||
|
if strings.EqualFold(needle, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func rSetForwardedHeaders(r, req *http.Request) {
|
||||||
|
if r.RemoteAddr != "" {
|
||||||
|
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||||
|
|
||||||
|
if ip := net.ParseIP(before); ip != nil {
|
||||||
|
req.Header.Set(HeaderXForwardedFor, ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set(HeaderXForwardedMethod, r.Method)
|
||||||
|
req.Header.Set(HeaderXForwardedProto, scheme(r))
|
||||||
|
req.Header.Set(HeaderXForwardedHost, r.Host)
|
||||||
|
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
|
||||||
|
}
|
@ -20,7 +20,7 @@ func (d *UXOptimizer) HandleDockerAvailable(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *UXOptimizer) HandleDockerContainersList(w http.ResponseWriter, r *http.Request) {
|
func (d *UXOptimizer) HandleDockerContainersList(w http.ResponseWriter, r *http.Request) {
|
||||||
apiClient, err := client.NewClientWithOpts(client.WithVersion("1.43"))
|
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.SystemWideLogger.PrintAndLog("Docker", "Unable to create new docker client", err)
|
d.SystemWideLogger.PrintAndLog("Docker", "Unable to create new docker client", err)
|
||||||
utils.SendErrorResponse(w, "Docker client initiation failed")
|
utils.SendErrorResponse(w, "Docker client initiation failed")
|
||||||
|
@ -32,20 +32,16 @@ and return a boolean indicate if the request is written to http.ResponseWriter
|
|||||||
*/
|
*/
|
||||||
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
|
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
|
||||||
requestHostname := r.Host
|
requestHostname := r.Host
|
||||||
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
|
|
||||||
|
switch sep.AuthenticationProvider.AuthMethod {
|
||||||
|
case AuthMethodBasic:
|
||||||
err := h.handleBasicAuthRouting(w, r, sep)
|
err := h.handleBasicAuthRouting(w, r, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
|
case AuthMethodForward:
|
||||||
err := h.handleAutheliaAuth(w, r)
|
err := h.handleForwardAuth(w, r)
|
||||||
if err != nil {
|
|
||||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik {
|
|
||||||
err := h.handleAuthentikAuth(w, r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||||
return true
|
return true
|
||||||
@ -106,13 +102,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Authelia */
|
/* Forward Auth */
|
||||||
|
|
||||||
// Handle authelia auth routing
|
// Handle forward auth routing
|
||||||
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
|
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
|
||||||
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
|
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r)
|
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,15 @@ import (
|
|||||||
// GetDefaultAuthenticationProvider return a default authentication provider
|
// GetDefaultAuthenticationProvider return a default authentication provider
|
||||||
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
|
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
|
||||||
return &AuthenticationProvider{
|
return &AuthenticationProvider{
|
||||||
AuthMethod: AuthMethodNone,
|
AuthMethod: AuthMethodNone,
|
||||||
BasicAuthCredentials: []*BasicAuthCredentials{},
|
BasicAuthCredentials: []*BasicAuthCredentials{},
|
||||||
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
||||||
BasicAuthGroupIDs: []string{},
|
BasicAuthGroupIDs: []string{},
|
||||||
AutheliaURL: "",
|
ForwardAuthURL: "",
|
||||||
UseHTTPS: false,
|
ForwardAuthResponseHeaders: []string{},
|
||||||
|
ForwardAuthResponseClientHeaders: []string{},
|
||||||
|
ForwardAuthRequestHeaders: []string{},
|
||||||
|
ForwardAuthRequestExcludedCookies: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,13 +9,12 @@ package dynamicproxy
|
|||||||
*/
|
*/
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authentik"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authelia"
|
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
@ -64,8 +63,7 @@ type RouterOption struct {
|
|||||||
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
|
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
|
||||||
|
|
||||||
/* Authentication Providers */
|
/* Authentication Providers */
|
||||||
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
ForwardAuthRouter *forward.AuthRouter
|
||||||
AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
|
|
||||||
|
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
Logger *logger.Logger //Logger for reverse proxy requets
|
Logger *logger.Logger //Logger for reverse proxy requets
|
||||||
@ -141,11 +139,10 @@ type HeaderRewriteRules struct {
|
|||||||
type AuthMethod int
|
type AuthMethod int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AuthMethodNone AuthMethod = iota //No authentication required
|
AuthMethodNone AuthMethod = iota //No authentication required
|
||||||
AuthMethodBasic //Basic Auth
|
AuthMethodBasic //Basic Auth
|
||||||
AuthMethodAuthelia //Authelia
|
AuthMethodForward //Forward
|
||||||
AuthMethodOauth2 //Oauth2
|
AuthMethodOauth2 //Oauth2
|
||||||
AuthMethodAuthentik
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthenticationProvider struct {
|
type AuthenticationProvider struct {
|
||||||
@ -155,9 +152,12 @@ type AuthenticationProvider struct {
|
|||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||||
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
|
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
|
||||||
|
|
||||||
/* Authelia Settings */
|
/* Forward Auth Settings */
|
||||||
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
|
ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth
|
||||||
UseHTTPS bool //Whether to use HTTPS for the Authelia server
|
ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request.
|
||||||
|
ForwardAuthResponseClientHeaders []string // List of headers to copy from the forward auth server response to the client response.
|
||||||
|
ForwardAuthRequestHeaders []string // List of headers to copy from the original request to the auth server. If empty all are copied.
|
||||||
|
ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server.
|
||||||
}
|
}
|
||||||
|
|
||||||
// A proxy endpoint record, a general interface for handling inbound routing
|
// A proxy endpoint record, a general interface for handling inbound routing
|
||||||
|
214
src/mod/plugins/development.go
Normal file
214
src/mod/plugins/development.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartHotReloadTicker starts the hot reload ticker
|
||||||
|
func (m *Manager) StartHotReloadTicker() error {
|
||||||
|
if m.pluginReloadTicker != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already started", nil)
|
||||||
|
return errors.New("hot reload ticker already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.pluginReloadTicker = time.NewTicker(time.Duration(m.Options.HotReloadInterval) * time.Second)
|
||||||
|
m.pluginReloadStop = make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.pluginReloadTicker.C:
|
||||||
|
err := m.UpdatePluginHashList(false)
|
||||||
|
if err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to update plugin hash list", err)
|
||||||
|
}
|
||||||
|
case <-m.pluginReloadStop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker started", nil)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopHotReloadTicker stops the hot reload ticker
|
||||||
|
func (m *Manager) StopHotReloadTicker() error {
|
||||||
|
if m.pluginReloadTicker != nil {
|
||||||
|
m.pluginReloadStop <- true
|
||||||
|
m.pluginReloadTicker.Stop()
|
||||||
|
m.pluginReloadTicker = nil
|
||||||
|
m.pluginReloadStop = nil
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker stopped", nil)
|
||||||
|
} else {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already stopped", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) InitPluginHashList() error {
|
||||||
|
return m.UpdatePluginHashList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the plugin hash list and if there are change, reload the plugin
|
||||||
|
func (m *Manager) UpdatePluginHashList(noReload bool) error {
|
||||||
|
for pluginId, plugin := range m.LoadedPlugins {
|
||||||
|
//Get the plugin Entry point
|
||||||
|
pluginEntryPoint, err := m.GetPluginEntryPoint(plugin.RootDir)
|
||||||
|
if err != nil {
|
||||||
|
//Unable to get the entry point of the plugin
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(pluginEntryPoint)
|
||||||
|
if err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to open plugin entry point: "+pluginEntryPoint, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
//Calculate the hash of the file
|
||||||
|
hasher := sha256.New()
|
||||||
|
if _, err := file.Seek(0, 0); err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to seek plugin entry point: "+pluginEntryPoint, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to copy plugin entry point: "+pluginEntryPoint, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
m.pluginCheckMutex.Lock()
|
||||||
|
if m.PluginHash[pluginId] != hash {
|
||||||
|
m.PluginHash[pluginId] = hash
|
||||||
|
m.pluginCheckMutex.Unlock()
|
||||||
|
if !noReload {
|
||||||
|
//Plugin file changed, reload the plugin
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin file changed, reloading plugin: "+pluginId, nil)
|
||||||
|
err := m.HotReloadPlugin(pluginId)
|
||||||
|
if err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to reload plugin: "+pluginId, err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin reloaded: "+pluginId, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin hash generated for: "+pluginId, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.pluginCheckMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the plugin from file system
|
||||||
|
func (m *Manager) HotReloadPlugin(pluginId string) error {
|
||||||
|
//Check if the plugin is currently running
|
||||||
|
thisPlugin, err := m.GetPluginByID(pluginId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisPlugin.IsRunning() {
|
||||||
|
err = m.StopPlugin(pluginId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove the plugin from the loaded plugins list
|
||||||
|
m.loadedPluginsMutex.Lock()
|
||||||
|
if _, ok := m.LoadedPlugins[pluginId]; ok {
|
||||||
|
delete(m.LoadedPlugins, pluginId)
|
||||||
|
} else {
|
||||||
|
m.loadedPluginsMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.loadedPluginsMutex.Unlock()
|
||||||
|
|
||||||
|
//Reload the plugin from disk, it should reload the plugin from latest version
|
||||||
|
m.ReloadPluginFromDisk()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Request handlers for developer options
|
||||||
|
*/
|
||||||
|
func (m *Manager) HandleEnableHotReload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
//Return the current status of hot reload
|
||||||
|
js, _ := json.Marshal(m.Options.EnableHotReload)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, err := utils.PostBool(r, "enabled")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "enabled not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Options.EnableHotReload = enabled
|
||||||
|
if enabled {
|
||||||
|
//Start the hot reload ticker
|
||||||
|
err := m.StartHotReloadTicker()
|
||||||
|
if err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
|
||||||
|
utils.SendErrorResponse(w, "Failed to start hot reload ticker")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload enabled", nil)
|
||||||
|
} else {
|
||||||
|
//Stop the hot reload ticker
|
||||||
|
err := m.StopHotReloadTicker()
|
||||||
|
if err != nil {
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to stop hot reload ticker", err)
|
||||||
|
utils.SendErrorResponse(w, "Failed to stop hot reload ticker")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload disabled", nil)
|
||||||
|
}
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) HandleSetHotReloadInterval(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
//Return the current status of hot reload
|
||||||
|
js, _ := json.Marshal(m.Options.HotReloadInterval)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interval, err := utils.PostInt(r, "interval")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "interval not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if interval < 1 {
|
||||||
|
utils.SendErrorResponse(w, "interval must be at least 1 second")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Options.HotReloadInterval = interval
|
||||||
|
|
||||||
|
//Restart the hot reload ticker
|
||||||
|
if m.pluginReloadTicker != nil {
|
||||||
|
m.StopHotReloadTicker()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
//Start the hot reload ticker again
|
||||||
|
m.StartHotReloadTicker()
|
||||||
|
}
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload interval set to "+strconv.Itoa(interval)+" sec", nil)
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
@ -11,11 +11,11 @@ import (
|
|||||||
// ListPluginGroups returns a map of plugin groups
|
// ListPluginGroups returns a map of plugin groups
|
||||||
func (m *Manager) ListPluginGroups() map[string][]string {
|
func (m *Manager) ListPluginGroups() map[string][]string {
|
||||||
pluginGroup := map[string][]string{}
|
pluginGroup := map[string][]string{}
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
for k, v := range m.Options.PluginGroups {
|
for k, v := range m.Options.PluginGroups {
|
||||||
pluginGroup[k] = append([]string{}, v...)
|
pluginGroup[k] = append([]string{}, v...)
|
||||||
}
|
}
|
||||||
m.Options.pluginGroupsMutex.RUnlock()
|
m.pluginGroupsMutex.RUnlock()
|
||||||
return pluginGroup
|
return pluginGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,26 +32,26 @@ func (m *Manager) AddPluginToGroup(tag, pluginID string) error {
|
|||||||
return errors.New("plugin is not a router type plugin")
|
return errors.New("plugin is not a router type plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Options.pluginGroupsMutex.Lock()
|
m.pluginGroupsMutex.Lock()
|
||||||
//Check if the tag exists
|
//Check if the tag exists
|
||||||
_, ok = m.Options.PluginGroups[tag]
|
_, ok = m.Options.PluginGroups[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
m.Options.PluginGroups[tag] = []string{pluginID}
|
m.Options.PluginGroups[tag] = []string{pluginID}
|
||||||
m.Options.pluginGroupsMutex.Unlock()
|
m.pluginGroupsMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add the plugin to the group
|
//Add the plugin to the group
|
||||||
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
|
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
|
||||||
|
|
||||||
m.Options.pluginGroupsMutex.Unlock()
|
m.pluginGroupsMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePluginFromGroup removes a plugin from a group
|
// RemovePluginFromGroup removes a plugin from a group
|
||||||
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
||||||
m.Options.pluginGroupsMutex.Lock()
|
m.pluginGroupsMutex.Lock()
|
||||||
defer m.Options.pluginGroupsMutex.Unlock()
|
defer m.pluginGroupsMutex.Unlock()
|
||||||
//Check if the tag exists
|
//Check if the tag exists
|
||||||
_, ok := m.Options.PluginGroups[tag]
|
_, ok := m.Options.PluginGroups[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -72,8 +72,8 @@ func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
|||||||
|
|
||||||
// RemovePluginGroup removes a plugin group
|
// RemovePluginGroup removes a plugin group
|
||||||
func (m *Manager) RemovePluginGroup(tag string) error {
|
func (m *Manager) RemovePluginGroup(tag string) error {
|
||||||
m.Options.pluginGroupsMutex.Lock()
|
m.pluginGroupsMutex.Lock()
|
||||||
defer m.Options.pluginGroupsMutex.Unlock()
|
defer m.pluginGroupsMutex.Unlock()
|
||||||
_, ok := m.Options.PluginGroups[tag]
|
_, ok := m.Options.PluginGroups[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("tag not found")
|
return errors.New("tag not found")
|
||||||
@ -84,12 +84,12 @@ func (m *Manager) RemovePluginGroup(tag string) error {
|
|||||||
|
|
||||||
// SavePluginGroupsFromFile loads plugin groups from a file
|
// SavePluginGroupsFromFile loads plugin groups from a file
|
||||||
func (m *Manager) SavePluginGroupsToFile() error {
|
func (m *Manager) SavePluginGroupsToFile() error {
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
pluginGroupsCopy := make(map[string][]string)
|
pluginGroupsCopy := make(map[string][]string)
|
||||||
for k, v := range m.Options.PluginGroups {
|
for k, v := range m.Options.PluginGroups {
|
||||||
pluginGroupsCopy[k] = append([]string{}, v...)
|
pluginGroupsCopy[k] = append([]string{}, v...)
|
||||||
}
|
}
|
||||||
m.Options.pluginGroupsMutex.RUnlock()
|
m.pluginGroupsMutex.RUnlock()
|
||||||
|
|
||||||
//Write to file
|
//Write to file
|
||||||
js, _ := json.Marshal(pluginGroupsCopy)
|
js, _ := json.Marshal(pluginGroupsCopy)
|
||||||
|
@ -47,15 +47,26 @@ func NewPluginManager(options *ManagerOptions) *Manager {
|
|||||||
//Create database table
|
//Create database table
|
||||||
options.Database.NewTable("plugins")
|
options.Database.NewTable("plugins")
|
||||||
|
|
||||||
return &Manager{
|
thisManager := &Manager{
|
||||||
LoadedPlugins: make(map[string]*Plugin),
|
LoadedPlugins: make(map[string]*Plugin),
|
||||||
tagPluginMap: sync.Map{},
|
tagPluginMap: sync.Map{},
|
||||||
tagPluginListMutex: sync.RWMutex{},
|
tagPluginListMutex: sync.RWMutex{},
|
||||||
tagPluginList: make(map[string][]*Plugin),
|
tagPluginList: make(map[string][]*Plugin),
|
||||||
Options: options,
|
Options: options,
|
||||||
|
PluginHash: make(map[string]string),
|
||||||
/* Internal */
|
/* Internal */
|
||||||
loadedPluginsMutex: sync.RWMutex{},
|
loadedPluginsMutex: sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if hot reload is enabled
|
||||||
|
if options.EnableHotReload {
|
||||||
|
err := thisManager.StartHotReloadTicker()
|
||||||
|
if err != nil {
|
||||||
|
options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thisManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload all plugins from disk
|
// Reload all plugins from disk
|
||||||
@ -104,11 +115,16 @@ func (m *Manager) ReloadPluginFromDisk() {
|
|||||||
m.loadedPluginsMutex.Lock()
|
m.loadedPluginsMutex.Lock()
|
||||||
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
|
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
|
||||||
m.loadedPluginsMutex.Unlock()
|
m.loadedPluginsMutex.Unlock()
|
||||||
m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil)
|
versionNumber := strconv.Itoa(thisPlugin.Spec.VersionMajor) + "." + strconv.Itoa(thisPlugin.Spec.VersionMinor) + "." + strconv.Itoa(thisPlugin.Spec.VersionPatch)
|
||||||
|
//Check if the plugin is enabled
|
||||||
|
m.Log("Found plugin: "+thisPlugin.Spec.Name+" (v"+versionNumber+")", nil)
|
||||||
|
|
||||||
// The default state of the plugin is disabled, so no need to start it
|
// The default state of the plugin is disabled, so no need to start it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Generate a hash list for plugins
|
||||||
|
m.InitPluginHashList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
||||||
@ -156,6 +172,8 @@ func (m *Manager) LoadPluginsFromDisk() error {
|
|||||||
//Generate the static forwarder radix tree
|
//Generate the static forwarder radix tree
|
||||||
m.UpdateTagsToPluginMaps()
|
m.UpdateTagsToPluginMaps()
|
||||||
|
|
||||||
|
//Generate a hash list for plugins
|
||||||
|
m.InitPluginHashList()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ import (
|
|||||||
// This will only load the plugin tags to option.PluginGroups map
|
// This will only load the plugin tags to option.PluginGroups map
|
||||||
// to push the changes to runtime, call UpdateTagsToPluginMaps()
|
// to push the changes to runtime, call UpdateTagsToPluginMaps()
|
||||||
func (m *Manager) LoadPluginGroupsFromConfig() error {
|
func (m *Manager) LoadPluginGroupsFromConfig() error {
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
defer m.pluginGroupsMutex.RUnlock()
|
||||||
|
|
||||||
//Read the config file
|
//Read the config file
|
||||||
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
|
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
|
||||||
@ -39,8 +39,8 @@ func (m *Manager) LoadPluginGroupsFromConfig() error {
|
|||||||
|
|
||||||
// AddPluginToTag adds a plugin to a tag
|
// AddPluginToTag adds a plugin to a tag
|
||||||
func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
defer m.pluginGroupsMutex.RUnlock()
|
||||||
|
|
||||||
//Check if the plugin exists
|
//Check if the plugin exists
|
||||||
_, err := m.GetPluginByID(pluginID)
|
_, err := m.GetPluginByID(pluginID)
|
||||||
@ -66,8 +66,8 @@ func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
|||||||
// RemovePluginFromTag removes a plugin from a tag
|
// RemovePluginFromTag removes a plugin from a tag
|
||||||
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
|
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
|
||||||
// Check if the plugin exists in Options.PluginGroups
|
// Check if the plugin exists in Options.PluginGroups
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
defer m.pluginGroupsMutex.RUnlock()
|
||||||
pluginList, ok := m.Options.PluginGroups[tag]
|
pluginList, ok := m.Options.PluginGroups[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@ -91,8 +91,8 @@ func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
|
|||||||
|
|
||||||
// savePluginTagMap saves the plugin tag map to the config file
|
// savePluginTagMap saves the plugin tag map to the config file
|
||||||
func (m *Manager) savePluginTagMap() error {
|
func (m *Manager) savePluginTagMap() error {
|
||||||
m.Options.pluginGroupsMutex.RLock()
|
m.pluginGroupsMutex.RLock()
|
||||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
defer m.pluginGroupsMutex.RUnlock()
|
||||||
|
|
||||||
js, _ := json.Marshal(m.Options.PluginGroups)
|
js, _ := json.Marshal(m.Options.PluginGroups)
|
||||||
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)
|
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
@ -45,8 +46,9 @@ type ManagerOptions struct {
|
|||||||
Database *database.Database `json:"-"`
|
Database *database.Database `json:"-"`
|
||||||
Logger *logger.Logger `json:"-"`
|
Logger *logger.Logger `json:"-"`
|
||||||
|
|
||||||
/* Internal */
|
/* Development */
|
||||||
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
|
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
|
||||||
|
HotReloadInterval int //The interval for checking the plugin file change, in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -56,6 +58,12 @@ type Manager struct {
|
|||||||
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
|
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
|
||||||
Options *ManagerOptions
|
Options *ManagerOptions
|
||||||
|
|
||||||
|
PluginHash map[string]string //The hash of the plugin file, used to check if the plugin file is changed
|
||||||
|
|
||||||
/* Internal */
|
/* Internal */
|
||||||
loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins
|
loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins
|
||||||
|
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
|
||||||
|
pluginCheckMutex sync.RWMutex //Mutex for the plugin hash
|
||||||
|
pluginReloadTicker *time.Ticker //Ticker for the plugin reload
|
||||||
|
pluginReloadStop chan bool //Channel to stop the plugin reload ticker
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package update
|
|||||||
import (
|
import (
|
||||||
v308 "imuslab.com/zoraxy/mod/update/v308"
|
v308 "imuslab.com/zoraxy/mod/update/v308"
|
||||||
v315 "imuslab.com/zoraxy/mod/update/v315"
|
v315 "imuslab.com/zoraxy/mod/update/v315"
|
||||||
|
v322 "imuslab.com/zoraxy/mod/update/v322"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Updater Core logic
|
// Updater Core logic
|
||||||
@ -19,6 +20,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
} else if fromVersion == 321 && toVersion == 322 {
|
||||||
|
//Updating from v3.2.1 to v3.2.2
|
||||||
|
err := v322.UpdateFrom321To322()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//ADD MORE VERSIONS HERE
|
//ADD MORE VERSIONS HERE
|
||||||
|
141
src/mod/update/v322/typedef321.go
Normal file
141
src/mod/update/v322/typedef321.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package v322
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyType int
|
||||||
|
|
||||||
|
// Pull from ratelimit.go
|
||||||
|
type RequestCountPerIpTable struct {
|
||||||
|
table sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull from special.go
|
||||||
|
type RoutingRule struct {
|
||||||
|
ID string //ID of the routing rule
|
||||||
|
Enabled bool //If the routing rule enabled
|
||||||
|
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
|
||||||
|
MatchRule func(r *http.Request) bool
|
||||||
|
RoutingHandler func(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
|
||||||
|
ProxyTypeHost //Host Proxy, match by host (domain) name
|
||||||
|
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Basic Auth Related Data structure*/
|
||||||
|
// Auth credential for basic auth on certain endpoints
|
||||||
|
type BasicAuthCredentials struct {
|
||||||
|
Username string
|
||||||
|
PasswordHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth credential for basic auth on certain endpoints
|
||||||
|
type BasicAuthUnhashedCredentials struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths to exclude in basic auth enabled proxy handler
|
||||||
|
type BasicAuthExceptionRule struct {
|
||||||
|
PathPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Routing Rule Data Structures */
|
||||||
|
|
||||||
|
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||||
|
// program structure than directly using ProxyEndpoint
|
||||||
|
type VirtualDirectoryEndpoint struct {
|
||||||
|
MatchingPath string //Matching prefix of the request path, also act as key
|
||||||
|
Domain string //Domain or IP to proxy to
|
||||||
|
RequireTLS bool //Target domain require TLS
|
||||||
|
SkipCertValidations bool //Set to true to accept self signed certs
|
||||||
|
Disabled bool //If the rule is enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules and settings for header rewriting
|
||||||
|
type HeaderRewriteRules struct {
|
||||||
|
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||||
|
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
|
||||||
|
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||||
|
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||||
|
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
|
||||||
|
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Authentication Providers
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthMethodNone AuthMethod = iota //No authentication required
|
||||||
|
AuthMethodBasic //Basic Auth
|
||||||
|
AuthMethodAuthelia //Authelia
|
||||||
|
AuthMethodOauth2 //Oauth2
|
||||||
|
AuthMethodAuthentik
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthenticationProvider struct {
|
||||||
|
AuthMethod AuthMethod //The authentication method to use
|
||||||
|
/* Basic Auth Settings */
|
||||||
|
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||||
|
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||||
|
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
|
||||||
|
|
||||||
|
/* Authelia Settings */
|
||||||
|
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
|
||||||
|
UseHTTPS bool //Whether to use HTTPS for the Authelia server
|
||||||
|
}
|
||||||
|
|
||||||
|
// A proxy endpoint record, a general interface for handling inbound routing
|
||||||
|
type ProxyEndpointv321 struct {
|
||||||
|
ProxyType ProxyType //The type of this proxy, see const def
|
||||||
|
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||||
|
MatchingDomainAlias []string //A list of domains that alias to this rule
|
||||||
|
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
|
||||||
|
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
|
||||||
|
UseStickySession bool //Use stick session for load balancing
|
||||||
|
UseActiveLoadBalance bool //Use active loadbalancing, default passive
|
||||||
|
Disabled bool //If the rule is disabled
|
||||||
|
|
||||||
|
//Inbound TLS/SSL Related
|
||||||
|
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||||
|
|
||||||
|
//Virtual Directories
|
||||||
|
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||||
|
|
||||||
|
//Custom Headers
|
||||||
|
HeaderRewriteRules *HeaderRewriteRules
|
||||||
|
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
|
||||||
|
|
||||||
|
//Authentication
|
||||||
|
AuthenticationProvider *AuthenticationProvider
|
||||||
|
|
||||||
|
// Rate Limiting
|
||||||
|
RequireRateLimit bool
|
||||||
|
RateLimit int64 // Rate limit in requests per second
|
||||||
|
|
||||||
|
//Uptime Monitor
|
||||||
|
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
|
||||||
|
|
||||||
|
//Access Control
|
||||||
|
AccessFilterUUID string //Access filter ID
|
||||||
|
|
||||||
|
//Fallback routing logic (Special Rule Sets Only)
|
||||||
|
DefaultSiteOption int //Fallback routing logic options
|
||||||
|
DefaultSiteValue string //Fallback routing target, optional
|
||||||
|
|
||||||
|
//Internal Logic Elements
|
||||||
|
Tags []string // Tags for the proxy endpoint
|
||||||
|
}
|
93
src/mod/update/v322/typedef322.go
Normal file
93
src/mod/update/v322/typedef322.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package v322
|
||||||
|
|
||||||
|
import "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Authentication Provider in v3.2.2
|
||||||
|
|
||||||
|
The only change is the removal of the deprecated Authelia and Authentik SSO
|
||||||
|
provider, and the addition of the new Forward Auth provider.
|
||||||
|
|
||||||
|
Need to map all provider with ID = 4 into 2 and remove the old provider configs
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AuthMethod int
|
||||||
|
|
||||||
|
/*
|
||||||
|
v3.2.1 Authentication Provider
|
||||||
|
const (
|
||||||
|
AuthMethodNone AuthMethod = iota //No authentication required
|
||||||
|
AuthMethodBasic //Basic Auth
|
||||||
|
AuthMethodAuthelia //Authelia => 2
|
||||||
|
AuthMethodOauth2 //Oauth2
|
||||||
|
AuthMethodAuthentik //Authentik => 4
|
||||||
|
)
|
||||||
|
|
||||||
|
v3.2.2 Authentication Provider
|
||||||
|
const (
|
||||||
|
AuthMethodNone AuthMethod = iota //No authentication required
|
||||||
|
AuthMethodBasic //Basic Auth
|
||||||
|
AuthMethodForward //Forward => 2
|
||||||
|
AuthMethodOauth2 //Oauth2
|
||||||
|
)
|
||||||
|
|
||||||
|
We need to merge both Authelia and Authentik into the Forward Auth provider, and remove
|
||||||
|
*/
|
||||||
|
//The updated structure of the authentication provider
|
||||||
|
type AuthenticationProviderV322 struct {
|
||||||
|
AuthMethod AuthMethod //The authentication method to use
|
||||||
|
/* Basic Auth Settings */
|
||||||
|
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||||
|
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||||
|
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
|
||||||
|
|
||||||
|
/* Forward Auth Settings */
|
||||||
|
ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth
|
||||||
|
ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request.
|
||||||
|
ForwardAuthResponseClientHeaders []string // List of headers to copy from the forward auth server response to the client response.
|
||||||
|
ForwardAuthRequestHeaders []string // List of headers to copy from the original request to the auth server. If empty all are copied.
|
||||||
|
ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A proxy endpoint record, a general interface for handling inbound routing
|
||||||
|
type ProxyEndpointv322 struct {
|
||||||
|
ProxyType ProxyType //The type of this proxy, see const def
|
||||||
|
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||||
|
MatchingDomainAlias []string //A list of domains that alias to this rule
|
||||||
|
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
|
||||||
|
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
|
||||||
|
UseStickySession bool //Use stick session for load balancing
|
||||||
|
UseActiveLoadBalance bool //Use active loadbalancing, default passive
|
||||||
|
Disabled bool //If the rule is disabled
|
||||||
|
|
||||||
|
//Inbound TLS/SSL Related
|
||||||
|
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||||
|
|
||||||
|
//Virtual Directories
|
||||||
|
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||||
|
|
||||||
|
//Custom Headers
|
||||||
|
HeaderRewriteRules *HeaderRewriteRules
|
||||||
|
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
|
||||||
|
|
||||||
|
//Authentication
|
||||||
|
AuthenticationProvider *AuthenticationProviderV322
|
||||||
|
|
||||||
|
// Rate Limiting
|
||||||
|
RequireRateLimit bool
|
||||||
|
RateLimit int64 // Rate limit in requests per second
|
||||||
|
|
||||||
|
//Uptime Monitor
|
||||||
|
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
|
||||||
|
|
||||||
|
//Access Control
|
||||||
|
AccessFilterUUID string //Access filter ID
|
||||||
|
|
||||||
|
//Fallback routing logic (Special Rule Sets Only)
|
||||||
|
DefaultSiteOption int //Fallback routing logic options
|
||||||
|
DefaultSiteValue string //Fallback routing target, optional
|
||||||
|
|
||||||
|
//Internal Logic Elements
|
||||||
|
Tags []string // Tags for the proxy endpoint
|
||||||
|
}
|
191
src/mod/update/v322/v322.go
Normal file
191
src/mod/update/v322/v322.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package v322
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
|
"imuslab.com/zoraxy/mod/update/updateutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateFrom321To322 updates proxy config files from v3.2.1 to v3.2.2
|
||||||
|
func UpdateFrom321To322() error {
|
||||||
|
// Load the configs
|
||||||
|
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup all the files
|
||||||
|
err = os.MkdirAll("./conf/proxy-321.old/", 0775)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldConfigFile := range oldConfigFiles {
|
||||||
|
// Extract the file name from the path
|
||||||
|
fileName := filepath.Base(oldConfigFile)
|
||||||
|
// Construct the backup file path
|
||||||
|
backupFile := filepath.Join("./conf/proxy-321.old/", fileName)
|
||||||
|
|
||||||
|
// Copy the file to the backup directory
|
||||||
|
err := updateutil.CopyFile(oldConfigFile, backupFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the config into the old struct
|
||||||
|
for _, oldConfigFile := range oldConfigFiles {
|
||||||
|
configContent, err := os.ReadFile(oldConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
thisOldConfigStruct := ProxyEndpointv321{}
|
||||||
|
err = json.Unmarshal(configContent, &thisOldConfigStruct)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the old struct to the new struct
|
||||||
|
thisNewConfigStruct := convertV321ToV322(thisOldConfigStruct)
|
||||||
|
|
||||||
|
// Write the new config to file
|
||||||
|
newConfigContent, err := json.MarshalIndent(thisNewConfigStruct, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to marshal new config "+filepath.Base(oldConfigFile), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(oldConfigFile, newConfigContent, 0664)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to write new config "+filepath.Base(oldConfigFile), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertV321ToV322(thisOldConfigStruct ProxyEndpointv321) ProxyEndpointv322 {
|
||||||
|
// Merge both Authelia and Authentik into the Forward Auth provider, and remove the old provider configs
|
||||||
|
if thisOldConfigStruct.AuthenticationProvider == nil {
|
||||||
|
//Configs before v3.1.7 with no authentication provider
|
||||||
|
// Set the default authentication provider
|
||||||
|
thisOldConfigStruct.AuthenticationProvider = &AuthenticationProvider{
|
||||||
|
AuthMethod: AuthMethodNone, // Default to no authentication
|
||||||
|
BasicAuthCredentials: []*BasicAuthCredentials{},
|
||||||
|
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
||||||
|
BasicAuthGroupIDs: []string{},
|
||||||
|
AutheliaURL: "",
|
||||||
|
UseHTTPS: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Override the old authentication provider with the new one
|
||||||
|
if thisOldConfigStruct.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
|
||||||
|
thisOldConfigStruct.AuthenticationProvider.AuthMethod = 2
|
||||||
|
} else if thisOldConfigStruct.AuthenticationProvider.AuthMethod == AuthMethodAuthentik {
|
||||||
|
thisOldConfigStruct.AuthenticationProvider.AuthMethod = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisOldConfigStruct.AuthenticationProvider.BasicAuthGroupIDs == nil {
|
||||||
|
//Create an empty basic auth group IDs array if it does not exist
|
||||||
|
thisOldConfigStruct.AuthenticationProvider.BasicAuthGroupIDs = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAuthenticationProvider := AuthenticationProviderV322{
|
||||||
|
AuthMethod: AuthMethodNone, // Default to no authentication
|
||||||
|
//Fill in the empty arrays
|
||||||
|
BasicAuthCredentials: []*BasicAuthCredentials{},
|
||||||
|
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
||||||
|
BasicAuthGroupIDs: []string{},
|
||||||
|
ForwardAuthURL: "",
|
||||||
|
ForwardAuthResponseHeaders: []string{},
|
||||||
|
ForwardAuthResponseClientHeaders: []string{},
|
||||||
|
ForwardAuthRequestHeaders: []string{},
|
||||||
|
ForwardAuthRequestExcludedCookies: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// In theory the old config should have a matching itoa value that
|
||||||
|
// can be converted to the new config
|
||||||
|
js, err := json.Marshal(thisOldConfigStruct.AuthenticationProvider)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Unable to marshal authentication provider "+thisOldConfigStruct.RootOrMatchingDomain, err.Error())
|
||||||
|
fmt.Println("Using default authentication provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(js, &newAuthenticationProvider)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Unable to unmarshal authentication provider "+thisOldConfigStruct.RootOrMatchingDomain, err.Error())
|
||||||
|
fmt.Println("Using default authentication provider")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Authentication provider for " + thisOldConfigStruct.RootOrMatchingDomain + " updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in any null values in the old config struct
|
||||||
|
// these are non-upgrader requires values that updates between v3.1.5 to v3.2.1
|
||||||
|
// will be in null state if not set by the user
|
||||||
|
if thisOldConfigStruct.VirtualDirectories == nil {
|
||||||
|
//Create an empty virtual directories array if it does not exist
|
||||||
|
thisOldConfigStruct.VirtualDirectories = []*VirtualDirectoryEndpoint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisOldConfigStruct.HeaderRewriteRules == nil {
|
||||||
|
//Create an empty header rewrite rules array if it does not exist
|
||||||
|
thisOldConfigStruct.HeaderRewriteRules = &HeaderRewriteRules{
|
||||||
|
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
|
||||||
|
RequestHostOverwrite: "",
|
||||||
|
HSTSMaxAge: 0,
|
||||||
|
EnablePermissionPolicyHeader: false,
|
||||||
|
PermissionPolicy: permissionpolicy.GetDefaultPermissionPolicy(),
|
||||||
|
DisableHopByHopHeaderRemoval: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisOldConfigStruct.Tags == nil {
|
||||||
|
//Create an empty tags array if it does not exist
|
||||||
|
thisOldConfigStruct.Tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisOldConfigStruct.MatchingDomainAlias == nil {
|
||||||
|
//Create an empty matching domain alias array if it does not exist
|
||||||
|
thisOldConfigStruct.MatchingDomainAlias = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the config struct
|
||||||
|
thisNewConfigStruct := ProxyEndpointv322{
|
||||||
|
ProxyType: thisOldConfigStruct.ProxyType,
|
||||||
|
RootOrMatchingDomain: thisOldConfigStruct.RootOrMatchingDomain,
|
||||||
|
MatchingDomainAlias: thisOldConfigStruct.MatchingDomainAlias,
|
||||||
|
ActiveOrigins: thisOldConfigStruct.ActiveOrigins,
|
||||||
|
InactiveOrigins: thisOldConfigStruct.InactiveOrigins,
|
||||||
|
UseStickySession: thisOldConfigStruct.UseStickySession,
|
||||||
|
UseActiveLoadBalance: thisOldConfigStruct.UseActiveLoadBalance,
|
||||||
|
Disabled: thisOldConfigStruct.Disabled,
|
||||||
|
BypassGlobalTLS: thisOldConfigStruct.BypassGlobalTLS,
|
||||||
|
VirtualDirectories: thisOldConfigStruct.VirtualDirectories,
|
||||||
|
HeaderRewriteRules: thisOldConfigStruct.HeaderRewriteRules,
|
||||||
|
EnableWebsocketCustomHeaders: thisOldConfigStruct.EnableWebsocketCustomHeaders,
|
||||||
|
RequireRateLimit: thisOldConfigStruct.RequireRateLimit,
|
||||||
|
RateLimit: thisOldConfigStruct.RateLimit,
|
||||||
|
DisableUptimeMonitor: thisOldConfigStruct.DisableUptimeMonitor,
|
||||||
|
AccessFilterUUID: thisOldConfigStruct.AccessFilterUUID,
|
||||||
|
DefaultSiteOption: thisOldConfigStruct.DefaultSiteOption,
|
||||||
|
DefaultSiteValue: thisOldConfigStruct.DefaultSiteValue,
|
||||||
|
Tags: thisOldConfigStruct.Tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new authentication provider
|
||||||
|
thisNewConfigStruct.AuthenticationProvider = &newAuthenticationProvider
|
||||||
|
|
||||||
|
return thisNewConfigStruct
|
||||||
|
}
|
@ -115,8 +115,7 @@ func ReverseProxtInit() {
|
|||||||
StatisticCollector: statisticCollector,
|
StatisticCollector: statisticCollector,
|
||||||
WebDirectory: *path_webserver,
|
WebDirectory: *path_webserver,
|
||||||
AccessController: accessController,
|
AccessController: accessController,
|
||||||
AutheliaRouter: autheliaRouter,
|
ForwardAuthRouter: forwardAuthRouter,
|
||||||
AuthentikRouter: authentikRouter,
|
|
||||||
LoadBalancer: loadBalancer,
|
LoadBalancer: loadBalancer,
|
||||||
PluginManager: pluginManager,
|
PluginManager: pluginManager,
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
@ -585,11 +584,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
if authProviderType == 1 {
|
if authProviderType == 1 {
|
||||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
|
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
|
||||||
} else if authProviderType == 2 {
|
} else if authProviderType == 2 {
|
||||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
|
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward
|
||||||
} else if authProviderType == 3 {
|
} else if authProviderType == 3 {
|
||||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
|
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
|
||||||
} else if authProviderType == 4 {
|
|
||||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik
|
|
||||||
} else {
|
} else {
|
||||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
|
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
|
||||||
}
|
}
|
||||||
|
43
src/start.go
43
src/start.go
@ -9,13 +9,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authentik"
|
|
||||||
|
|
||||||
"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"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/authelia"
|
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/database/dbinc"
|
"imuslab.com/zoraxy/mod/database/dbinc"
|
||||||
"imuslab.com/zoraxy/mod/dockerux"
|
"imuslab.com/zoraxy/mod/dockerux"
|
||||||
@ -143,18 +141,10 @@ func startupSequence() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create authentication providers
|
//Create authentication providers
|
||||||
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
|
forwardAuthRouter = forward.NewAuthRouter(&forward.AuthRouterOptions{
|
||||||
UseHTTPS: false, // Automatic populate in router initiation
|
Address: "",
|
||||||
AutheliaURL: "", // Automatic populate in router initiation
|
Logger: SystemWideLogger,
|
||||||
Logger: SystemWideLogger,
|
Database: sysdb,
|
||||||
Database: sysdb,
|
|
||||||
})
|
|
||||||
|
|
||||||
authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{
|
|
||||||
UseHTTPS: false, // Automatic populate in router initiation
|
|
||||||
AuthentikURL: "", // Automatic populate in router initiation
|
|
||||||
Logger: SystemWideLogger,
|
|
||||||
Database: sysdb,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//Create a statistic collector
|
//Create a statistic collector
|
||||||
@ -317,21 +307,26 @@ func startupSequence() {
|
|||||||
pluginFolder := *path_plugin
|
pluginFolder := *path_plugin
|
||||||
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
|
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
|
||||||
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
|
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
|
||||||
PluginDir: pluginFolder,
|
PluginDir: pluginFolder,
|
||||||
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
|
|
||||||
ZoraxyVersion: SYSTEM_VERSION,
|
|
||||||
ZoraxyUUID: nodeUUID,
|
|
||||||
DevelopmentBuild: *development_build,
|
|
||||||
},
|
|
||||||
PluginStoreURLs: []string{
|
|
||||||
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
|
|
||||||
},
|
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
Logger: SystemWideLogger,
|
Logger: SystemWideLogger,
|
||||||
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
|
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
|
||||||
CSRFTokenGen: func(r *http.Request) string {
|
CSRFTokenGen: func(r *http.Request) string {
|
||||||
return csrf.Token(r)
|
return csrf.Token(r)
|
||||||
},
|
},
|
||||||
|
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
|
||||||
|
ZoraxyVersion: SYSTEM_VERSION,
|
||||||
|
ZoraxyUUID: nodeUUID,
|
||||||
|
DevelopmentBuild: *development_build,
|
||||||
|
},
|
||||||
|
/* Plugin Store URLs */
|
||||||
|
PluginStoreURLs: []string{
|
||||||
|
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
|
||||||
|
//TO BE ADDED
|
||||||
|
},
|
||||||
|
/* Developer Options */
|
||||||
|
EnableHotReload: *development_build, //Default to true if development build
|
||||||
|
HotReloadInterval: 5, //seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
//Sync latest plugin list from the plugin store
|
//Sync latest plugin list from the plugin store
|
||||||
|
@ -185,9 +185,8 @@
|
|||||||
</td>
|
</td>
|
||||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
|
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Forward Auth`:``}
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
|
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x4?`<i class="ui blue key icon"></i> Authentik`:``}
|
|
||||||
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
|
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
|
||||||
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
||||||
@ -393,13 +392,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
|
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
|
||||||
<label>Authelia</label>
|
<label>Forward Auth</label>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" value="4" name="authProviderType" ${authProvider==0x4?"checked":""}>
|
|
||||||
<label>Authentik</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -185,6 +185,33 @@
|
|||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="ui basic segment advanceoptions">
|
||||||
|
<div class="ui accordion advanceSettings">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Developer Settings
|
||||||
|
</div>
|
||||||
|
<div class="content ui form">
|
||||||
|
<div class="ui inverted message" style="margin-top: 0.6em;">
|
||||||
|
<div class="header">Developer Only</div>
|
||||||
|
<p>These functions are intended for developers only. Enabling them may add latency to plugin loading & routing. Proceed with caution.<br>
|
||||||
|
<b>Tips: You can start zoraxy with -dev=true to enable auto-reload when start</b></p>
|
||||||
|
</div>
|
||||||
|
<div id="enablePluginAutoReload" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
||||||
|
<input id="enable_plugin_auto_reload" type="checkbox">
|
||||||
|
<label>Enable Plugin Auto Reload<br>
|
||||||
|
<small>Automatic reload plugin when the plugin binary changed</small></label>
|
||||||
|
</div>
|
||||||
|
<br><br>
|
||||||
|
<div class="field" style="max-width: 50%;margin-bottom: 0px;">
|
||||||
|
<label>Check Interval</label>
|
||||||
|
<input type="number" id="autoreload-interval" placeholder="Check Interval" min="1" max="60" step="1" value="1">
|
||||||
|
</div>
|
||||||
|
<small>Specify the interval (in seconds) for checking plugin changes. <br>Minimum is 1 second, maximum is 60 seconds.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
|
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
|
||||||
</div>
|
</div>
|
||||||
@ -592,6 +619,95 @@ function uninstallPlugin(pluginId, pluginName, btn=undefined) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Developer Settings */
|
||||||
|
|
||||||
|
function initDeveloperSettings() {
|
||||||
|
// Fetch the auto reload status
|
||||||
|
$.get('/api/plugins/developer/enableAutoReload', function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the checkbox for Plugin Auto Reload
|
||||||
|
if (data == true) {
|
||||||
|
$("#enablePluginAutoReload").checkbox('set checked');
|
||||||
|
} else {
|
||||||
|
$("#enablePluginAutoReload").checkbox('set unchecked');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the auto reload interval
|
||||||
|
$.get('/api/plugins/developer/setAutoReloadInterval', function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the input value for Auto Reload Interval
|
||||||
|
if (data) {
|
||||||
|
$("#autoreload-interval").val(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEventsToDeveloperSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEventsToDeveloperSettings(){
|
||||||
|
$("#enablePluginAutoReload").checkbox({
|
||||||
|
onChecked: function() {
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/developer/enableAutoReload',
|
||||||
|
type: 'POST',
|
||||||
|
data: { "enabled": true },
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox("Plugin Auto Reload enabled", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUnchecked: function() {
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/developer/enableAutoReload',
|
||||||
|
type: 'POST',
|
||||||
|
data: { "enabled": false },
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox("Plugin Auto Reload disabled", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#autoreload-interval").on("change", function() {
|
||||||
|
const interval = $(this).val();
|
||||||
|
if (interval < 1 || interval > 60) {
|
||||||
|
msgbox("Interval must be between 1 and 60 seconds", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/developer/setAutoReloadInterval',
|
||||||
|
type: 'POST',
|
||||||
|
data: { "interval": interval },
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox("Auto Reload Interval updated to " + interval + " seconds", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initDeveloperSettings();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,44 +14,59 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h3>Authelia</h3>
|
<h3>Forward Auth</h3>
|
||||||
<p>Configuration settings for Authelia authentication provider.</p>
|
<p>Configuration settings for the Forward Auth provider.</p>
|
||||||
|
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Allows the request to flow through to the backend when the authorization server responds with a 200-299 status code.</li>
|
||||||
|
<li>Responds with the response from the authorization server.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Example authorization servers that support this:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://www.authelia.com" rel=”noopener noreferrer” target="_blank">Authelia</a></li>
|
||||||
|
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
||||||
|
</ul>
|
||||||
<form class="ui form">
|
<form class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="autheliaServerUrl">Authelia Server URL</label>
|
<label for="forwardAuthAddress">Address</label>
|
||||||
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
|
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
|
||||||
<small>Example: auth.example.com</small>
|
<small>The full remote address or URL of the authorization servers forward auth endpoint. <strong>Example:</strong> https://auth.example.com/authz/forward-auth</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="ui basic segment advanceoptions" style="margin-top:0.6em;">
|
||||||
<div class="ui checkbox">
|
<div class="ui advancedSSOForwardAuthOptions accordion">
|
||||||
<input type="checkbox" id="useHttps" name="useHttps">
|
<div class="title">
|
||||||
<label for="useHttps">Use HTTPS</label>
|
<i class="dropdown icon"></i>
|
||||||
<small>Check this if your authelia server uses HTTPS</small>
|
Advanced Options
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="field">
|
||||||
|
<label for="forwardAuthResponseHeaders">Response Headers</label>
|
||||||
|
<input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
|
||||||
|
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
|
||||||
|
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code></small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="forwardAuthResponseClientHeaders">Response Client Headers</label>
|
||||||
|
<input type="text" id="forwardAuthResponseClientHeaders" name="forwardAuthResponseClientHeaders" placeholder="Enter Forward Auth Response Client Headers">
|
||||||
|
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the response sent to the client. If not set no headers are copied. <br>
|
||||||
|
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code></small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="forwardAuthRequestHeaders">Request Headers</label>
|
||||||
|
<input type="text" id="forwardAuthRequestHeaders" name="forwardAuthRequestHeaders" placeholder="Enter Forward Auth Request Headers">
|
||||||
|
<small>Comma separated list of case-insensitive headers to copy from the original request to the request made to the authorization server. If not set all headers are copied. <br>
|
||||||
|
<strong>Example:</strong> <code>Cookie,Authorization</code></small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
|
||||||
|
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
|
||||||
|
<small>Comma separated list of case-sensitive cookie names to exclude from the request to the backend. If not set no cookies are excluded. <br>
|
||||||
|
<strong>Example:</strong> <code>authelia_session,another_session</code></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
|
<button class="ui basic button" onclick="event.preventDefault(); updateForwardAuthSettings();"><i class="green check icon"></i> Apply Change</button>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui basic segment">
|
|
||||||
<h3>Authentik</h3>
|
|
||||||
<p>Configuration settings for Authentik authentication provider.</p>
|
|
||||||
|
|
||||||
<form class="ui form">
|
|
||||||
<div class="field">
|
|
||||||
<label for="authentikServerUrl">Authentik Server URL</label>
|
|
||||||
<input type="text" id="authentikServerUrl" name="authentikServerUrl" placeholder="Enter Authentik Server URL">
|
|
||||||
<small>Example: auth.example.com</small>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui checkbox">
|
|
||||||
<input type="checkbox" id="authentikUseHttps" name="useHttps">
|
|
||||||
<label for="authentikUseHttps">Use HTTPS</label>
|
|
||||||
<small>Check this if your Authentik server uses HTTPS</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="ui basic button" onclick="event.preventDefault(); updateAuthentikSettings();"><i class="green check icon"></i> Apply Change</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
@ -60,24 +75,15 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: '/api/sso/Authelia',
|
url: '/api/sso/forward-auth',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
$('#autheliaServerUrl').val(data.autheliaURL);
|
$('#forwardAuthAddress').val(data.address);
|
||||||
$('#useHttps').prop('checked', data.useHTTPS);
|
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||||
},
|
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
||||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
||||||
}
|
|
||||||
});
|
|
||||||
$.cjax({
|
|
||||||
url: '/api/sso/Authentik',
|
|
||||||
method: 'GET',
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(data) {
|
|
||||||
$('#authentikServerUrl').val(data.authentikURL);
|
|
||||||
$('#authentikUseHttps').prop('checked', data.useHTTPS);
|
|
||||||
},
|
},
|
||||||
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);
|
||||||
@ -85,51 +91,35 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateAutheliaSettings(){
|
function updateForwardAuthSettings() {
|
||||||
var autheliaServerUrl = $('#autheliaServerUrl').val();
|
const address = $('#forwardAuthAddress').val();
|
||||||
var useHttps = $('#useHttps').prop('checked');
|
const responseHeaders = $('#forwardAuthResponseHeaders').val();
|
||||||
|
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
|
||||||
|
const requestHeaders = $('#forwardAuthRequestHeaders').val();
|
||||||
|
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
|
||||||
|
|
||||||
|
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
|
||||||
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: '/api/sso/Authelia',
|
url: '/api/sso/forward-auth',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
autheliaURL: autheliaServerUrl,
|
address: address,
|
||||||
useHTTPS: useHttps
|
responseHeaders: responseHeaders,
|
||||||
|
responseClientHeaders: responseClientHeaders,
|
||||||
|
requestHeaders: requestHeaders,
|
||||||
|
requestExcludedCookies: requestExcludedCookies
|
||||||
},
|
},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (data.error != undefined) {
|
if (data.error !== undefined) {
|
||||||
$.msgbox(data.error, false);
|
msgbox(data.error, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
msgbox('Authelia settings updated', true);
|
msgbox('Forward Auth settings updated', true);
|
||||||
console.log('Authelia settings updated:', data);
|
console.log('Forward Auth settings updated:', data);
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
console.error('Error updating Authelia settings:', textStatus, errorThrown);
|
console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateAuthentikSettings(){
|
|
||||||
var authentikServerUrl = $('#authentikServerUrl').val();
|
|
||||||
var useHttps = $('#authentikUseHttps').prop('checked');
|
|
||||||
|
|
||||||
$.cjax({
|
|
||||||
url: '/api/sso/Authentik',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
authentikURL: authentikServerUrl,
|
|
||||||
useHTTPS: useHttps
|
|
||||||
},
|
|
||||||
success: function(data) {
|
|
||||||
if (data.error != undefined) {
|
|
||||||
$.msgbox(data.error, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
msgbox('Authentik settings updated', true);
|
|
||||||
console.log('Authentik settings updated:', data);
|
|
||||||
},
|
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
|
||||||
console.error('Error updating Authentik settings:', textStatus, errorThrown);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- Notes: This should be open in its original path-->
|
<!-- Notes: This should be open in its original path -->
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css" />
|
<link rel="stylesheet" href="../script/semantic/semantic.min.css" />
|
||||||
<script src="../script/jquery-3.6.0.min.js"></script>
|
<script src="../script/jquery-3.6.0.min.js"></script>
|
||||||
<script src="../script/semantic/semantic.min.js"></script>
|
<script src="../script/semantic/semantic.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<link rel="stylesheet" href="../darktheme.css">
|
<link rel="stylesheet" href="../darktheme.css" />
|
||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br />
|
<br />
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
@ -17,48 +17,428 @@
|
|||||||
<input
|
<input
|
||||||
id="searchbar"
|
id="searchbar"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search Containers ..."
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="showOnlyRunning" class="hidden" />
|
||||||
|
<label for="showOnlyRunning">Show Only Running Containers</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input type="checkbox" id="showUnexposed" class="hidden" />
|
<input type="checkbox" id="showUnexposed" class="hidden" />
|
||||||
<label for="showUnexposed"
|
<label for="showUnexposed"
|
||||||
>Show Containers with unexposed ports
|
>Show Containers with Unexposed Ports</label
|
||||||
<br />
|
>
|
||||||
<small
|
|
||||||
>Please make sure Zoraxy and the target container share a
|
|
||||||
network</small
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Networked Containers Lists -->
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
List of Docker Containers
|
Containers on Zoraxy's Networks
|
||||||
<div class="sub header">
|
<div class="sub header">
|
||||||
Below is a list of all detected Docker containers currently running
|
These containers share a network with Zoraxy.<br />
|
||||||
on the system.
|
Your networks must support Docker DNS-based name resolution.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="containersList" class="ui middle aligned divided list active">
|
<div id="networkedList" class="ui middle aligned divided list">
|
||||||
<div class="ui loader active"></div>
|
<div class="ui active loader"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui horizontal divider"></div>
|
<div class="ui horizontal divider"></div>
|
||||||
<div id="containersAddedListHeader" class="ui header" hidden>
|
<!-- Host Mode Containers List -->
|
||||||
Already added containers:
|
<div id="hostmodeListHeader" class="ui header" hidden>
|
||||||
|
<div class="content">
|
||||||
|
Containers using Host Network
|
||||||
|
<div class="sub header">
|
||||||
|
These containers use the host network configuration.<br />
|
||||||
|
Ports must be manually configured.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div id="hostmodeList" class="ui middle aligned divided list"></div>
|
||||||
id="containersAddedList"
|
<div class="ui horizontal divider"></div>
|
||||||
class="ui middle aligned divided list"
|
<!-- Other Containers List -->
|
||||||
></div>
|
<div id="othersListHeader" class="ui header" hidden>
|
||||||
|
<div class="content">
|
||||||
|
Containers on different Networks
|
||||||
|
<div class="sub header">
|
||||||
|
These containers are not connected to Zoraxy's networks.<br />
|
||||||
|
Manual configuration is required.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="othersList" class="ui middle aligned divided list"></div>
|
||||||
|
<div class="ui horizontal divider"></div>
|
||||||
|
<!-- Existing List -->
|
||||||
|
<div id="existingListHeader" class="ui header" hidden>
|
||||||
|
Containers with existing Proxy Rules
|
||||||
|
<div class="sub header">
|
||||||
|
These containers are already configured in the proxy rules.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="existingList" class="ui middle aligned divided list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// debounce function to prevent excessive calls to a function
|
// DOM elements
|
||||||
|
const $networkedList = $("#networkedList");
|
||||||
|
|
||||||
|
const $hostmodeListHeader = $("#hostmodeListHeader");
|
||||||
|
const $hostmodeList = $("#hostmodeList");
|
||||||
|
|
||||||
|
const $othersListHeader = $("#othersListHeader");
|
||||||
|
const $othersList = $("#othersList");
|
||||||
|
|
||||||
|
const $existingListHeader = $("#existingListHeader");
|
||||||
|
const $existingList = $("#existingList");
|
||||||
|
|
||||||
|
const $searchbar = $("#searchbar");
|
||||||
|
const $showOnlyRunning = $("#showOnlyRunning");
|
||||||
|
const $showUnexposed = $("#showUnexposed");
|
||||||
|
|
||||||
|
// maps for containers
|
||||||
|
let networkedEntries = {};
|
||||||
|
let hostmodeEntries = {};
|
||||||
|
let othersEntries = {};
|
||||||
|
let existingEntries = {};
|
||||||
|
|
||||||
|
// initial load
|
||||||
|
$(document).ready(() => {
|
||||||
|
loadCheckboxState("showUnexposed", $showUnexposed);
|
||||||
|
loadCheckboxState("showOnlyRunning", $showOnlyRunning);
|
||||||
|
initializeEventListeners();
|
||||||
|
getDockerContainers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// event listeners
|
||||||
|
function initializeEventListeners() {
|
||||||
|
$showUnexposed.on("change", () => {
|
||||||
|
saveCheckboxState("showUnexposed", $showUnexposed);
|
||||||
|
getDockerContainers();
|
||||||
|
});
|
||||||
|
|
||||||
|
$showOnlyRunning.on("change", () => {
|
||||||
|
saveCheckboxState("showOnlyRunning", $showOnlyRunning);
|
||||||
|
getDockerContainers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// debounce searchbar input to prevent excessive filtering
|
||||||
|
$searchbar.on(
|
||||||
|
"input",
|
||||||
|
debounce(() => filterLists($searchbar.val().toLowerCase()), 300)
|
||||||
|
);
|
||||||
|
|
||||||
|
$networkedList.on("click", ".add-button", (event) => {
|
||||||
|
const key = $(event.currentTarget).data("key");
|
||||||
|
if (networkedEntries[key]) {
|
||||||
|
parent.addContainerItem(networkedEntries[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$hostmodeList.on("click", ".add-button", (event) => {
|
||||||
|
const key = $(event.currentTarget).data("key");
|
||||||
|
if (hostmodeEntries[key]) {
|
||||||
|
parent.addContainerItem(hostmodeEntries[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// filter lists by toggling item visibility
|
||||||
|
function filterLists(searchTerm) {
|
||||||
|
$(".list .item").each((_, item) => {
|
||||||
|
const content = $(item).text().toLowerCase();
|
||||||
|
$(item).toggle(content.includes(searchTerm));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset UI and state
|
||||||
|
function reset() {
|
||||||
|
networkedEntries = {};
|
||||||
|
hostmodeEntries = {};
|
||||||
|
othersEntries = {};
|
||||||
|
existingEntries = {};
|
||||||
|
|
||||||
|
$networkedList.empty();
|
||||||
|
$hostmodeList.empty();
|
||||||
|
$othersList.empty();
|
||||||
|
$existingList.empty();
|
||||||
|
|
||||||
|
$hostmodeListHeader.attr("hidden", true);
|
||||||
|
$othersListHeader.attr("hidden", true);
|
||||||
|
$existingListHeader.attr("hidden", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// process docker data
|
||||||
|
async function getDockerContainers() {
|
||||||
|
reset();
|
||||||
|
$networkedList.html('<div class="ui active loader"></div>');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [hostData, dockerData] = await Promise.all([
|
||||||
|
$.get("/api/proxy/list?type=host"),
|
||||||
|
$.get("/api/docker/containers"),
|
||||||
|
]);
|
||||||
|
if (!hostData.error && !dockerData.error) {
|
||||||
|
processDockerData(hostData, dockerData);
|
||||||
|
} else {
|
||||||
|
showError(hostData.error || dockerData.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
parent.msgbox("Error loading data: " + error.message, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDockerData(hostData, dockerData) {
|
||||||
|
const { containers } = dockerData;
|
||||||
|
const existingTargets = new Set(
|
||||||
|
hostData.flatMap(({ ActiveOrigins }) =>
|
||||||
|
ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// identify the Zoraxy container to determine shared networks
|
||||||
|
const zoraxyContainer = containers.find(
|
||||||
|
(container) =>
|
||||||
|
container.Labels &&
|
||||||
|
container.Labels["com.imuslab.zoraxy.container-identifier"] ===
|
||||||
|
"Zoraxy"
|
||||||
|
);
|
||||||
|
|
||||||
|
const zoraxyNetworkIDs = zoraxyContainer
|
||||||
|
? Object.values(zoraxyContainer.NetworkSettings.Networks).map(
|
||||||
|
(network) => network.NetworkID
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// iterate over all containers
|
||||||
|
containers.forEach((container) => {
|
||||||
|
// skip containers in network mode "none"
|
||||||
|
if (container.HostConfig.NetworkMode === "none") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip containers not running, if the option is enabled
|
||||||
|
if (
|
||||||
|
container.State !== "running" &&
|
||||||
|
$showOnlyRunning.prop("checked")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitize container name
|
||||||
|
const containerName = container.Names[0].replace(/^\//, "");
|
||||||
|
|
||||||
|
// containers in network mode "host" should resolve to "host.docker.internal"
|
||||||
|
if (
|
||||||
|
container.HostConfig.NetworkMode === "host" &&
|
||||||
|
!hostmodeEntries[container.Id]
|
||||||
|
) {
|
||||||
|
hostmodeEntries[container.Id] = {
|
||||||
|
name: containerName,
|
||||||
|
ip: "host.docker.internal",
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// networks that are shared with Zoraxy
|
||||||
|
const sharedNetworks = Object.values(
|
||||||
|
container.NetworkSettings.Networks
|
||||||
|
).filter((network) => zoraxyNetworkIDs.includes(network.NetworkID));
|
||||||
|
|
||||||
|
if (!sharedNetworks.length) {
|
||||||
|
const ips = Object.values(container.NetworkSettings.Networks).map(
|
||||||
|
(network) => network.IPAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
const ports = container.Ports.map((portObject) => {
|
||||||
|
return portObject.PublicPort || portObject.PrivatePort;
|
||||||
|
});
|
||||||
|
|
||||||
|
othersEntries[container.Id] = {
|
||||||
|
name: containerName,
|
||||||
|
ips,
|
||||||
|
ports,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the container to the networked list, using it's name as address
|
||||||
|
container.Ports.forEach((portObject) => {
|
||||||
|
const port = portObject.PublicPort || portObject.PrivatePort;
|
||||||
|
const key = `${containerName}:${port}`;
|
||||||
|
|
||||||
|
// always include existing proxy-rule targets
|
||||||
|
if (existingTargets.has(key)) {
|
||||||
|
if (!existingEntries[key]) {
|
||||||
|
existingEntries[key] = {
|
||||||
|
name: containerName,
|
||||||
|
ip: containerName,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise, include only if exposed or checkbox is checked
|
||||||
|
else if (portObject.PublicPort || $showUnexposed.is(":checked")) {
|
||||||
|
if (!networkedEntries[key]) {
|
||||||
|
networkedEntries[key] = {
|
||||||
|
name: containerName,
|
||||||
|
ip: containerName,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// finally update the UI
|
||||||
|
updateNetworkedList();
|
||||||
|
updateHostmodeList();
|
||||||
|
updateOthersList();
|
||||||
|
updateExistingList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update networked list
|
||||||
|
function updateNetworkedList() {
|
||||||
|
$networkedList.empty();
|
||||||
|
let html = "";
|
||||||
|
Object.entries(networkedEntries)
|
||||||
|
.sort()
|
||||||
|
.forEach(([key, entry]) => {
|
||||||
|
html += `
|
||||||
|
<div class="item">
|
||||||
|
<div class="content" style="display: flex; justify-content: space-between;">
|
||||||
|
<div>
|
||||||
|
<div class="header">${entry.name}</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>${entry.ip}:${entry.port}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui button add-button" data-key="${key}">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$networkedList.append(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update hostmode list
|
||||||
|
function updateHostmodeList() {
|
||||||
|
$hostmodeList.empty();
|
||||||
|
let html = "";
|
||||||
|
Object.entries(hostmodeEntries)
|
||||||
|
.sort()
|
||||||
|
.forEach(([key, entry]) => {
|
||||||
|
html += `
|
||||||
|
<div class="item">
|
||||||
|
<div class="content" style="display: flex; justify-content: space-between;">
|
||||||
|
<div>
|
||||||
|
<div class="header">${entry.name}</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>${entry.ip}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui right floated button add-button" data-key="${key}">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$hostmodeList.append(html);
|
||||||
|
if (Object.keys(hostmodeEntries).length) {
|
||||||
|
$hostmodeListHeader.removeAttr("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update others list
|
||||||
|
function updateOthersList() {
|
||||||
|
$othersList.empty();
|
||||||
|
let html = "";
|
||||||
|
Object.entries(othersEntries)
|
||||||
|
.sort()
|
||||||
|
.forEach(([key, entry]) => {
|
||||||
|
html += `
|
||||||
|
<div class="item">
|
||||||
|
<div class="header">${entry.name}</div>
|
||||||
|
${
|
||||||
|
entry.ips.length === 0 ||
|
||||||
|
entry.ips.every((ip) => ip === "") ||
|
||||||
|
entry.ports.length === 0 ||
|
||||||
|
entry.ports.every((port) => port === "")
|
||||||
|
? `<div class="description">
|
||||||
|
<p>No IPs or Ports</p>
|
||||||
|
</div>`
|
||||||
|
: `<div class="description">
|
||||||
|
<p>
|
||||||
|
IPs: ${entry.ips.join(", ")}<br />
|
||||||
|
Ports: ${entry.ports.join(", ")}
|
||||||
|
</p>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$othersList.append(html);
|
||||||
|
if (Object.keys(othersEntries).length) {
|
||||||
|
$othersListHeader.removeAttr("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update existing rules list
|
||||||
|
function updateExistingList() {
|
||||||
|
$existingList.empty();
|
||||||
|
let html = "";
|
||||||
|
Object.entries(existingEntries)
|
||||||
|
.sort()
|
||||||
|
.forEach(([key, entry]) => {
|
||||||
|
html += `
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">${entry.name}</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>${entry.ip}:${entry.port}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$existingList.append(html);
|
||||||
|
if (Object.keys(existingEntries).length) {
|
||||||
|
$existingListHeader.removeAttr("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// show error message
|
||||||
|
function showError(error) {
|
||||||
|
$networkedList.html(
|
||||||
|
`<div class="ui basic segment"><i class="ui red times icon"></i> ${error}</div>`
|
||||||
|
);
|
||||||
|
parent.msgbox(`Error loading data: ${error}`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// utils
|
||||||
|
//
|
||||||
|
|
||||||
|
// local storage handling
|
||||||
|
function loadCheckboxState(id, $elem) {
|
||||||
|
const state = localStorage.getItem(id);
|
||||||
|
if (state !== null) {
|
||||||
|
$elem.prop("checked", state === "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCheckboxState(id, $elem) {
|
||||||
|
localStorage.setItem(id, $elem.prop("checked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// debounce function
|
||||||
function debounce(func, delay) {
|
function debounce(func, delay) {
|
||||||
let timeout;
|
let timeout;
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
@ -66,177 +446,6 @@
|
|||||||
timeout = setTimeout(() => func(...args), delay);
|
timeout = setTimeout(() => func(...args), delay);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait until DOM is fully loaded before executing script
|
|
||||||
$(document).ready(() => {
|
|
||||||
const $containersList = $("#containersList");
|
|
||||||
const $containersAddedList = $("#containersAddedList");
|
|
||||||
const $containersAddedListHeader = $("#containersAddedListHeader");
|
|
||||||
const $searchbar = $("#searchbar");
|
|
||||||
const $showUnexposed = $("#showUnexposed");
|
|
||||||
|
|
||||||
let lines = {};
|
|
||||||
let linesAdded = {};
|
|
||||||
|
|
||||||
// load showUnexposed checkbox state from local storage
|
|
||||||
function loadShowUnexposedState() {
|
|
||||||
const storedState = localStorage.getItem("showUnexposed");
|
|
||||||
if (storedState !== null) {
|
|
||||||
$showUnexposed.prop("checked", storedState === "true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save showUnexposed checkbox state to local storage
|
|
||||||
function saveShowUnexposedState() {
|
|
||||||
localStorage.setItem("showUnexposed", $showUnexposed.prop("checked"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch docker containers
|
|
||||||
function getDockerContainers() {
|
|
||||||
$containersList.html('<div class="ui loader active"></div>');
|
|
||||||
$containersAddedList.empty();
|
|
||||||
$containersAddedListHeader.attr("hidden", true);
|
|
||||||
|
|
||||||
lines = {};
|
|
||||||
linesAdded = {};
|
|
||||||
|
|
||||||
const hostRequest = $.get("/api/proxy/list?type=host");
|
|
||||||
const dockerRequest = $.get("/api/docker/containers");
|
|
||||||
|
|
||||||
Promise.all([hostRequest, dockerRequest])
|
|
||||||
.then(([hostData, dockerData]) => {
|
|
||||||
if (!hostData.error && !dockerData.error) {
|
|
||||||
processDockerData(hostData, dockerData);
|
|
||||||
} else {
|
|
||||||
showError(hostData.error || dockerData.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
parent.msgbox("Error loading data: " + error.message, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// process docker data and update ui
|
|
||||||
function processDockerData(hostData, dockerData) {
|
|
||||||
const { containers } = dockerData;
|
|
||||||
const existingTargets = new Set(
|
|
||||||
hostData.flatMap(({ ActiveOrigins }) =>
|
|
||||||
ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
containers.forEach((container) => {
|
|
||||||
const name = container.Names[0].replace(/^\//, "");
|
|
||||||
container.Ports.forEach((portObject) => {
|
|
||||||
let port = portObject.PublicPort || portObject.PrivatePort;
|
|
||||||
if (!portObject.PublicPort && !$showUnexposed.is(":checked"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// if port is not exposed, use container's name and let docker handle the routing
|
|
||||||
// BUT this will only work if the container is on the same network as Zoraxy
|
|
||||||
const targetAddress = portObject.IP || name;
|
|
||||||
const key = `${name}-${port}`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
existingTargets.has(`${targetAddress}:${port}`) &&
|
|
||||||
!linesAdded[key]
|
|
||||||
) {
|
|
||||||
linesAdded[key] = { name, ip: targetAddress, port };
|
|
||||||
} else if (!lines[key]) {
|
|
||||||
lines[key] = { name, ip: targetAddress, port };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// update ui
|
|
||||||
updateContainersList();
|
|
||||||
updateAddedContainersList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// update containers list
|
|
||||||
function updateContainersList() {
|
|
||||||
$containersList.empty();
|
|
||||||
Object.entries(lines).forEach(([key, line]) => {
|
|
||||||
$containersList.append(`
|
|
||||||
<div class="item">
|
|
||||||
<div class="right floated content">
|
|
||||||
<div class="ui button add-button" data-key="${key}">Add</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="header">${line.name}</div>
|
|
||||||
<div class="description">${line.ip}:${line.port}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
$containersList.find(".loader").removeClass("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the added containers list
|
|
||||||
function updateAddedContainersList() {
|
|
||||||
Object.entries(linesAdded).forEach(([key, line]) => {
|
|
||||||
$containersAddedList.append(`
|
|
||||||
<div class="item">
|
|
||||||
<div class="content">
|
|
||||||
<div class="header">${line.name}</div>
|
|
||||||
<div class="description">${line.ip}:${line.port}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
if (Object.keys(linesAdded).length) {
|
|
||||||
$containersAddedListHeader.removeAttr("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// show error message
|
|
||||||
function showError(error) {
|
|
||||||
$containersList.html(
|
|
||||||
`<div class="ui basic segment"><i class="ui red times icon"></i> ${error}</div>`
|
|
||||||
);
|
|
||||||
parent.msgbox(`Error loading data: ${error}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// event listeners
|
|
||||||
//
|
|
||||||
|
|
||||||
$showUnexposed.on("change", () => {
|
|
||||||
saveShowUnexposedState(); // save the new state to local storage
|
|
||||||
getDockerContainers();
|
|
||||||
});
|
|
||||||
|
|
||||||
$searchbar.on(
|
|
||||||
"input",
|
|
||||||
debounce(() => {
|
|
||||||
// debounce searchbar input with 300ms delay, then filter list
|
|
||||||
// this prevents excessive calls to the filter function
|
|
||||||
const search = $searchbar.val().toLowerCase();
|
|
||||||
$("#containersList .item").each((index, item) => {
|
|
||||||
const content = $(item).text().toLowerCase();
|
|
||||||
$(item).toggle(content.includes(search));
|
|
||||||
});
|
|
||||||
}, 300)
|
|
||||||
);
|
|
||||||
|
|
||||||
$containersList.on("click", ".add-button", (event) => {
|
|
||||||
const key = $(event.currentTarget).data("key");
|
|
||||||
if (lines[key]) {
|
|
||||||
parent.addContainerItem(lines[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// initial calls
|
|
||||||
//
|
|
||||||
|
|
||||||
// load state of showUnexposed checkbox
|
|
||||||
loadShowUnexposedState();
|
|
||||||
|
|
||||||
// initial load of docker containers
|
|
||||||
getDockerContainers();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -58,28 +58,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
|
<button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
|
||||||
<!-- <div class="ui divider"></div>
|
<!-- <div class="ui divider"></div>
|
||||||
<div class="ui basic segment advanceoptions">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
Advance Settings
|
Advance Settings
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Plugin Store URLs</p>
|
<p>Plugin Store URLs</p>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<textarea id="pluginStoreURLs" rows="5"></textarea>
|
<textarea id="pluginStoreURLs" rows="5"></textarea>
|
||||||
<label>Enter plugin store URLs, separating each URL with a new line</label>
|
<label>Enter plugin store URLs, separating each URL with a new line</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="savePluginStoreURLs()">
|
<button class="ui basic button" onclick="savePluginStoreURLs()">
|
||||||
<i class="ui green save icon"></i>Save
|
<i class="ui green save icon"></i>Save
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
-->
|
||||||
-->
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="field" >
|
<div class="field" >
|
||||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||||
|
Reference in New Issue
Block a user