mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-29 02:41:45 +02:00
Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d2b8f224c | |||
877692695e | |||
b9c609e413 | |||
7426cc2bb1 | |||
bd71335f47 | |||
8bcc8e7095 | |||
b1824a66a3 | |||
cc6501db12 | |||
70d95bd4e4 | |||
b590e15ef2 | |||
b25f8aab3e | |||
c0578a33b6 | |||
55a525106a | |||
e3b68b9aad | |||
3f1c50c009 | |||
8f046a0b47 | |||
0e5550487e | |||
9781735983 | |||
a98d86a303 | |||
73e6530862 | |||
0c753ae531 | |||
6353cc532a | |||
e049761f36 | |||
4dc7175588 | |||
ffc67ede12 | |||
6750c7fe3d | |||
36c2c9a00e | |||
4f026e8c07 | |||
72b100aab0 | |||
291f12e5ea | |||
0c8dfd8aa0 | |||
76e2861fea | |||
b23b967165 | |||
d682d52eb7 | |||
23eeeee701 | |||
e961e52dea | |||
b863a9720f | |||
ca7cd0476c | |||
a3cccee162 | |||
b9b992a817 | |||
19d5695f1a | |||
bcfc777d15 | |||
caa64ada76 | |||
ac91a3fef1 | |||
05f1743ecd | |||
d4c1225f75 | |||
f245a61d32 | |||
5c2b8e4c31 | |||
f6eef46d3f | |||
3adc669db9 | |||
85201885f0 | |||
44b65d1bfa | |||
6cb9e8e427 | |||
d4b1cc8c57 | |||
6a8057c3a7 | |||
ebf6ad6600 |
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
|
16
README.md
16
README.md
@ -121,6 +121,8 @@ Usage of zoraxy:
|
||||
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-plugin string
|
||||
Plugin folder path (default "./plugins")
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-sshlb
|
||||
@ -135,10 +137,6 @@ Usage of zoraxy:
|
||||
Enable web file manager for static web server root folder (default true)
|
||||
-webroot string
|
||||
Static web server root folder. Only allow change in start paramters (default "./www")
|
||||
-ztauth string
|
||||
ZeroTier authtoken for the local node
|
||||
-ztport int
|
||||
ZeroTier controller API port (default 9993)
|
||||
```
|
||||
|
||||
### External Permission Management Mode
|
||||
@ -197,6 +195,16 @@ Loopback web SSH connections, by default, are disabled. This means that if you a
|
||||
./zoraxy -sshlb=true
|
||||
```
|
||||
|
||||
## Community Maintained Sections
|
||||
|
||||
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
|
||||
|
||||
- Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
Thank you so much for your contributions!
|
||||
|
||||
## Sponsor This Project
|
||||
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
@ -1,5 +1,5 @@
|
||||
## Build Zoraxy
|
||||
FROM docker.io/golang:bookworm AS build-zoraxy
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
@ -15,37 +15,54 @@ RUN go mod tidy &&\
|
||||
|
||||
|
||||
## 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/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
||||
RUN apk add --update --no-cache curl make gcc g++ linux-headers openssl-dev nano
|
||||
|
||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||
cd ZeroTierOne-* &&\
|
||||
make &&\
|
||||
cd ZeroTierOne-*/zeroidc &&\
|
||||
cargo update -p getrandom &&\
|
||||
cd .. &&\
|
||||
make -f make-linux.mk &&\
|
||||
mv ./zerotier-one /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.
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
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-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/plugin/
|
||||
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
|
||||
mkdir -p /opt/zoraxy/plugin/ &&\
|
||||
echo "tun" | tee -a /etc/modules
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
@ -70,6 +87,8 @@ ENV WEBROOT="./www"
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
|
||||
|
||||
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
|
||||
|
@ -19,6 +19,7 @@ Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Cha
|
||||
docker run -d \
|
||||
--name zoraxy \
|
||||
--restart unless-stopped \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
@ -47,6 +48,8 @@ services:
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
FASTGEOIP: "true"
|
||||
```
|
||||
@ -68,6 +71,11 @@ services:
|
||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||
| `/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
|
||||
|
||||
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]
|
||||
> 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
|
||||
|
||||
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..."
|
||||
for dir in "$1"/*; do
|
||||
|
18
docker/docker-compose.yml
Normal file
18
docker/docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
||||
services:
|
||||
zoraxy:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
FASTGEOIP: "true"
|
@ -30,7 +30,6 @@
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="dom-i18n.min.js"></script>
|
||||
<link href="main.css" rel="stylesheet">
|
||||
<script src="main.js" defer></script>
|
||||
|
||||
<!-- Css stuffs-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
@ -57,7 +56,7 @@
|
||||
<i class="ui arrow up icon"></i>
|
||||
</div>
|
||||
<button id="rwdmenubtn" class="ui black big icon button"><i class="ui bars icon"></i></button>
|
||||
<div id="mainmenu" class="ui segment" style="background: transparent !important;">
|
||||
<div id="mainmenu" class="ui segment">
|
||||
<div class="ui container">
|
||||
<div class="ui small stackable secondary menu">
|
||||
<div class="item">
|
||||
@ -244,7 +243,7 @@
|
||||
// Bildschirmfotos
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ui three column grid">
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/1.png">
|
||||
</div>
|
||||
@ -594,5 +593,7 @@
|
||||
openModal($(this).attr('src'));
|
||||
});
|
||||
</script>
|
||||
<!-- Locales -->
|
||||
<script src="main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
@ -66,6 +66,7 @@ body.zh-cn *:not(i){
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#slideshowBanner .ui.basic.white.button{
|
||||
@ -85,6 +86,9 @@ body.zh-cn *:not(i){
|
||||
#rwdmenubtn{
|
||||
display:none;
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #6cacff;
|
||||
color: #6cacff;
|
||||
}
|
||||
|
||||
#mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled){
|
||||
@ -165,7 +169,7 @@ body.zh-cn *:not(i){
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
margin-left: 10%;
|
||||
transform: translateX(0%) translateY(-50%);
|
||||
color: white;
|
||||
}
|
||||
@ -332,6 +336,7 @@ body.zh-cn *:not(i){
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #fdfdfd !important;
|
||||
}
|
||||
|
||||
#rwdmenubtn{
|
||||
@ -357,6 +362,11 @@ body.zh-cn *:not(i){
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#slideshowBanner .title{
|
||||
padding: 1em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips{
|
||||
margin-top: 2em;
|
||||
display: block;
|
||||
@ -367,6 +377,24 @@ body.zh-cn *:not(i){
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#techspec .videoScrollBar{
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
|
||||
padding-top: 2em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
.introvideo {
|
||||
display: block !important;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.introvideo iframe{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#download .stackable.tabular.menu .active.item{
|
||||
background-color: rgb(243, 243, 243);
|
||||
border-width: 0;
|
||||
|
17
docs/main.js
17
docs/main.js
@ -25,13 +25,16 @@ var i18n = domI18n({
|
||||
defaultLanguage: 'en'
|
||||
});
|
||||
|
||||
let userLang = navigator.language || navigator.userLanguage;
|
||||
console.log("User language: " + userLang);
|
||||
userLang = userLang.split("-")[0];
|
||||
if (!languages.includes(userLang)) {
|
||||
userLang = 'en';
|
||||
}
|
||||
i18n.changeLanguage(userLang);
|
||||
$(document).ready(function(){
|
||||
let userLang = navigator.language || navigator.userLanguage;
|
||||
console.log("User language: " + userLang);
|
||||
userLang = userLang.split("-")[0];
|
||||
if (!languages.includes(userLang)) {
|
||||
userLang = 'en';
|
||||
}
|
||||
i18n.changeLanguage(userLang);
|
||||
$("body").attr("class", userLang);
|
||||
});
|
||||
|
||||
|
||||
/* Main Menu */
|
||||
|
@ -1,5 +1,5 @@
|
||||
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
|
||||
temp = $(subst /, ,$@)
|
||||
os = $(word 1, $(temp))
|
||||
arch = $(word 2, $(temp))
|
||||
|
15
src/api.go
15
src/api.go
@ -80,9 +80,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||
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) {
|
||||
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
|
||||
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
|
||||
}
|
||||
|
||||
// Register the APIs for redirection rules management functions
|
||||
@ -233,6 +233,15 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup)
|
||||
authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup)
|
||||
authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup)
|
||||
|
||||
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
|
||||
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
|
||||
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
|
||||
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
|
||||
@ -318,7 +327,7 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
|
||||
// Register the standard web services URLs
|
||||
var staticWebRes http.Handler
|
||||
if DEVELOPMENT_BUILD {
|
||||
if *development_build {
|
||||
staticWebRes = http.FileServer(http.Dir("web/"))
|
||||
} else {
|
||||
subFS, err := fs.Sub(webres, "web")
|
||||
|
13
src/def.go
13
src/def.go
@ -16,7 +16,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"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/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
@ -42,8 +42,8 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.2.0"
|
||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||
SYSTEM_VERSION = "3.2.2"
|
||||
DEVELOPMENT_BUILD = false
|
||||
|
||||
/* System Constants */
|
||||
TMP_FOLDER = "./tmp"
|
||||
@ -99,8 +99,9 @@ var (
|
||||
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
|
||||
path_plugin = flag.String("plugin", "./plugins", "Plugin folder path")
|
||||
|
||||
/* Maintaince Function Flags */
|
||||
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
|
||||
/* Maintaince & Development Function Flags */
|
||||
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
|
||||
development_build = flag.Bool("dev", false, "Use external web folder for UI development")
|
||||
)
|
||||
|
||||
/* Global Variables and Handlers */
|
||||
@ -142,7 +143,7 @@ var (
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//Authentication Provider
|
||||
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
|
18
src/go.mod
18
src/go.mod
@ -5,11 +5,11 @@ go 1.22.0
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/go-session/session v3.1.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
@ -19,7 +19,6 @@ require (
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
|
||||
@ -27,30 +26,22 @@ require (
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
||||
github.com/monperrus/crawler-user-agents v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
||||
github.com/tidwall/buntdb v1.1.2 // indirect
|
||||
github.com/tidwall/gjson v1.12.1 // indirect
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -111,11 +102,9 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-resty/resty/v2 v2.16.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
@ -187,7 +176,6 @@ require (
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
|
79
src/go.sum
79
src/go.sum
@ -76,15 +76,11 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
@ -186,7 +182,6 @@ github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
@ -202,8 +197,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
|
||||
@ -222,16 +215,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
|
||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
@ -242,16 +231,11 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
@ -307,7 +291,6 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@ -320,7 +303,6 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq
|
||||
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
|
||||
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
|
||||
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
@ -377,8 +359,6 @@ github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
@ -395,11 +375,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
|
||||
@ -408,8 +386,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -493,11 +469,11 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/monperrus/crawler-user-agents v1.1.0 h1:Xy8ZrhizT+y2FONWFFdKOP+3BhH97BDLuG7W/MswoGI=
|
||||
github.com/monperrus/crawler-user-agents v1.1.0/go.mod h1:GfRyKbsbxSrRxTPYnVi4U/0stQd6BcFCxDy6i6IxQ0M=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
@ -531,7 +507,6 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
@ -609,10 +584,7 @@ github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D
|
||||
github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
@ -628,7 +600,6 @@ github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHei
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA=
|
||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
@ -661,7 +632,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@ -675,25 +645,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krc
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M=
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
||||
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
|
||||
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
|
||||
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@ -705,11 +656,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
|
||||
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||
github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8=
|
||||
@ -717,27 +663,12 @@ github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+H
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw=
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@ -783,7 +714,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
@ -861,7 +791,6 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -903,7 +832,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -919,7 +847,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1009,7 +936,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -1071,7 +997,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
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
|
@ -3,9 +3,10 @@ package authelia
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@ -93,25 +94,20 @@ func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Requ
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
|
||||
//Remove tailing slash if any
|
||||
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
|
||||
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
|
||||
autheliaURL := &url.URL{
|
||||
Scheme: protocol,
|
||||
Host: ar.options.AutheliaURL,
|
||||
}
|
||||
|
||||
//Make a request to Authelia to verify the request
|
||||
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
|
||||
req, err := http.NewRequest("POST", autheliaURL.JoinPath("api", "verify").String(), nil)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
|
||||
originalURL := rOriginalHeaders(r, req)
|
||||
|
||||
// Copy cookies from the incoming request
|
||||
for _, cookie := range r.Cookies() {
|
||||
@ -127,10 +123,42 @@ func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
redirectURL := autheliaURL.JoinPath()
|
||||
|
||||
query := redirectURL.Query()
|
||||
|
||||
query.Set("rd", originalURL.String())
|
||||
query.Set("rm", r.Method)
|
||||
|
||||
http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rOriginalHeaders(r, req *http.Request) *url.URL {
|
||||
if r.RemoteAddr != "" {
|
||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||
|
||||
if ip := net.ParseIP(before); ip != nil {
|
||||
req.Header.Set("X-Forwarded-For", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
originalURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: r.Host,
|
||||
Path: r.URL.Path,
|
||||
RawPath: r.URL.RawPath,
|
||||
}
|
||||
|
||||
if r.TLS != nil {
|
||||
originalURL.Scheme = "https"
|
||||
}
|
||||
|
||||
req.Header.Add("X-Forwarded-Method", r.Method)
|
||||
req.Header.Add("X-Original-URL", originalURL.String())
|
||||
|
||||
return originalURL
|
||||
}
|
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
|
169
src/mod/auth/sso/deprecated/authentik/authentik.go
Normal file
169
src/mod/auth/sso/deprecated/authentik/authentik.go
Normal file
@ -0,0 +1,169 @@
|
||||
package authentik
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AuthentikRouterOptions struct {
|
||||
UseHTTPS bool //If the Authentik server is using HTTPS
|
||||
AuthentikURL string //The URL of the Authentik server
|
||||
Logger *logger.Logger
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type AuthentikRouter struct {
|
||||
options *AuthentikRouterOptions
|
||||
}
|
||||
|
||||
// NewAuthentikRouter creates a new AuthentikRouter object
|
||||
func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter {
|
||||
options.Database.NewTable("authentik")
|
||||
|
||||
//Read settings from database, if exists
|
||||
options.Database.Read("authentik", "authentikURL", &options.AuthentikURL)
|
||||
options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS)
|
||||
|
||||
return &AuthentikRouter{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS
|
||||
func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"useHTTPS": ar.options.UseHTTPS,
|
||||
"authentikURL": ar.options.AuthentikURL,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the settings
|
||||
AuthentikURL, err := utils.PostPara(r, "authentikURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "authentikURL not found")
|
||||
return
|
||||
}
|
||||
|
||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
||||
if err != nil {
|
||||
useHTTPS = false
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.AuthentikURL = AuthentikURL
|
||||
ar.options.UseHTTPS = useHTTPS
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("authentik", "authentikURL", AuthentikURL)
|
||||
ar.options.Database.Write("authentik", "useHTTPS", useHTTPS)
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// HandleAuthentikAuth is the internal handler for Authentik authentication
|
||||
// Set useHTTPS to true if your Authentik server is using HTTPS
|
||||
// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com
|
||||
func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
const outpostPrefix = "outpost.goauthentik.io"
|
||||
client := &http.Client{}
|
||||
|
||||
if ar.options.AuthentikURL == "" {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
return errors.New("authentik URL not set")
|
||||
}
|
||||
protocol := "http"
|
||||
if ar.options.UseHTTPS {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
authentikBaseURL := protocol + "://" + ar.options.AuthentikURL
|
||||
//Remove tailing slash if any
|
||||
authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/")
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
reqUrl := scheme + "://" + r.Host + r.RequestURI
|
||||
// Pass request to outpost if path matches outpost prefix
|
||||
if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) {
|
||||
req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
req.Header.Set("X-Original-URL", reqUrl)
|
||||
req.Header.Set("Host", r.Host)
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
if resp, err := client.Do(req); err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errors.New("internal server error")
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
for k := range resp.Header {
|
||||
w.Header().Set(k, resp.Header.Get(k))
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err = io.Copy(w, resp.Body); err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Make a request to Authentik to verify the request
|
||||
req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
req.Header.Set("X-Original-URL", reqUrl)
|
||||
|
||||
// Copy cookies from the incoming request
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Making the verification request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String())
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
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) {
|
||||
apiClient, err := client.NewClientWithOpts(client.WithVersion("1.43"))
|
||||
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
d.SystemWideLogger.PrintAndLog("Docker", "Unable to create new docker client", err)
|
||||
utils.SendErrorResponse(w, "Docker client initiation failed")
|
||||
|
@ -48,7 +48,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
//Check if this is a redirection url
|
||||
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
|
||||
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
|
||||
h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", "")
|
||||
h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", r.Host, "")
|
||||
return
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sep.RequireRateLimit {
|
||||
err := h.handleRateLimitRouting(w, r, sep)
|
||||
if err != nil {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307, r.Host, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307, r.Host, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -186,6 +186,9 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
//Do not log default site requests to avoid flooding the logs
|
||||
//h.Parent.logRequest(r, false, 307, "root", domainOnly, "")
|
||||
|
||||
//No vdir match. Route via root router
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
case DefaultSite_Redirect:
|
||||
@ -208,19 +211,19 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
if hostname == domainOnly {
|
||||
h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly)
|
||||
h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly, "")
|
||||
http.Error(w, "Loopback redirects due to invalid settings", 500)
|
||||
return
|
||||
}
|
||||
|
||||
h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly, "")
|
||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||
case DefaultSite_NotFoundPage:
|
||||
//Serve the not found page, use template if exists
|
||||
h.serve404PageWithTemplate(w, r)
|
||||
case DefaultSite_NoResponse:
|
||||
//No response. Just close the connection
|
||||
h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly)
|
||||
h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly, "")
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@ -234,11 +237,11 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
conn.Close()
|
||||
case DefaultSite_TeaPot:
|
||||
//I'm a teapot
|
||||
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
|
||||
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly, "")
|
||||
http.Error(w, "I'm a teapot", http.StatusTeapot)
|
||||
default:
|
||||
//Unknown routing option. Send empty response
|
||||
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
|
||||
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly, "")
|
||||
http.Error(w, "544 - No Route Defined", 544)
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
|
||||
|
||||
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
|
||||
if isBlocked {
|
||||
h.Parent.logRequest(r, false, 403, blockedReason, "")
|
||||
h.Parent.logRequest(r, false, 403, blockedReason, r.Host, "")
|
||||
}
|
||||
return isBlocked
|
||||
}
|
||||
|
@ -31,16 +31,19 @@ and return a boolean indicate if the request is written to http.ResponseWriter
|
||||
- false: the request is not handled (usually means auth ok), continue to the next handler
|
||||
*/
|
||||
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
|
||||
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
|
||||
requestHostname := r.Host
|
||||
|
||||
switch sep.AuthenticationProvider.AuthMethod {
|
||||
case AuthMethodBasic:
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||
return true
|
||||
}
|
||||
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
|
||||
err := h.handleAutheliaAuth(w, r)
|
||||
case AuthMethodForward:
|
||||
err := h.handleForwardAuth(w, r)
|
||||
if err != nil {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -51,11 +54,8 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt
|
||||
|
||||
/* Basic Auth */
|
||||
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
err := handleBasicAuth(w, r, pe)
|
||||
if err != nil {
|
||||
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
|
||||
}
|
||||
return err
|
||||
//Wrapper for oop style
|
||||
return handleBasicAuth(w, r, pe)
|
||||
}
|
||||
|
||||
// Handle basic auth logic
|
||||
@ -75,6 +75,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("401 - Unauthorized"))
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
@ -94,15 +95,16 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
if !matchingFound {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("401 - Unauthorized"))
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* Authelia */
|
||||
/* Forward Auth */
|
||||
|
||||
// Handle authelia auth routing
|
||||
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
|
||||
// Handle forward auth routing
|
||||
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
|
||||
}
|
||||
|
@ -17,12 +17,15 @@ import (
|
||||
// GetDefaultAuthenticationProvider return a default authentication provider
|
||||
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
|
||||
return &AuthenticationProvider{
|
||||
AuthMethod: AuthMethodNone,
|
||||
BasicAuthCredentials: []*BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
||||
BasicAuthGroupIDs: []string{},
|
||||
AutheliaURL: "",
|
||||
UseHTTPS: false,
|
||||
AuthMethod: AuthMethodNone,
|
||||
BasicAuthCredentials: []*BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
|
||||
BasicAuthGroupIDs: []string{},
|
||||
ForwardAuthURL: "",
|
||||
ForwardAuthResponseHeaders: []string{},
|
||||
ForwardAuthResponseClientHeaders: []string{},
|
||||
ForwardAuthRequestHeaders: []string{},
|
||||
ForwardAuthRequestExcludedCookies: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
||||
thisTransporter := http.DefaultTransport
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
|
||||
optimalConcurrentConnection := 256
|
||||
if dpcOptions.MaxConcurrentConnection > 0 {
|
||||
optimalConcurrentConnection = dpcOptions.MaxConcurrentConnection
|
||||
@ -137,18 +136,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
||||
}
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
apath, bpath := a.EscapedPath(), b.EscapedPath()
|
||||
aslash, bslash := strings.HasSuffix(apath, "/"), strings.HasPrefix(bpath, "/")
|
||||
@ -352,7 +339,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
|
||||
//Back to the root of this proxy object
|
||||
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
|
||||
locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation
|
||||
} else {
|
||||
//Relative path. Do not modifiy location header
|
||||
|
@ -36,6 +36,24 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
//Do not modify location header
|
||||
return urlString, nil
|
||||
}
|
||||
|
||||
//Issue #626: Check if the location header is another subdomain with port
|
||||
//E.g. Proxy config: blog.example.com -> 127.0.0.1:80
|
||||
//Check if it is actually redirecting to (*.)blog.example.com:8080 instead of current domain
|
||||
//like Location: http://x.blog.example.com:1234/
|
||||
_, newLocationPort, err := net.SplitHostPort(u.Host)
|
||||
if (newLocationPort == "80" || newLocationPort == "443") && err == nil {
|
||||
//Port 80 or 443, some web server use this to switch between http and https
|
||||
//E.g. http://example.com:80 -> https://example.com:443
|
||||
//E.g. http://example.com:443 -> https://example.com:80
|
||||
//That usually means the user have invalidly configured the web server to use port 80 or 443
|
||||
//for http or https. We should not modify the location header in this case.
|
||||
|
||||
} else if strings.Contains(u.Host, ":") && err == nil {
|
||||
//Other port numbers. Do not modify location header
|
||||
return urlString, nil
|
||||
}
|
||||
|
||||
u.Host = rrr.OriginalHost
|
||||
|
||||
if strings.Contains(rrr.ProxyDomain, "/") {
|
||||
|
@ -155,7 +155,7 @@ func (router *Router) StartProxyService() error {
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
|
||||
router.logRequest(r, false, 404, "vdir-http", r.Host)
|
||||
router.logRequest(r, false, 404, "vdir-http", r.Host, "")
|
||||
}
|
||||
|
||||
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
|
||||
|
108
src/mod/dynamicproxy/exploits/exploits.go
Normal file
108
src/mod/dynamicproxy/exploits/exploits.go
Normal file
@ -0,0 +1,108 @@
|
||||
package exploits
|
||||
|
||||
/*
|
||||
exploits.go
|
||||
|
||||
This file is used to define routing rules that blocks common exploits.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
agents "github.com/monperrus/crawler-user-agents"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
}
|
||||
|
||||
func NewExploitDetector() *Detector {
|
||||
return &Detector{}
|
||||
}
|
||||
|
||||
// RequestContainCommonExploits checks if the request contains common exploits
|
||||
// such as SQL injection, file injection, and other common attack patterns.
|
||||
func (d *Detector) RequestContainCommonExploits(r *http.Request) bool {
|
||||
query := r.URL.RawQuery
|
||||
userAgent := r.UserAgent()
|
||||
|
||||
// Block SQL injections
|
||||
sqlInjectionPatterns := []string{
|
||||
`union.*select.*\(`,
|
||||
`union.*all.*select.*`,
|
||||
`concat.*\(`,
|
||||
}
|
||||
for _, pattern := range sqlInjectionPatterns {
|
||||
if match, _ := regexp.MatchString(pattern, query); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Block file injections
|
||||
fileInjectionPatterns := []string{
|
||||
`[a-zA-Z0-9_]=http://`,
|
||||
`[a-zA-Z0-9_]=(\.\.//?)+`,
|
||||
`[a-zA-Z0-9_]=/([a-z0-9_.]//?)+`,
|
||||
}
|
||||
for _, pattern := range fileInjectionPatterns {
|
||||
if match, _ := regexp.MatchString(pattern, query); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Block common exploits
|
||||
commonExploitPatterns := []string{
|
||||
`(<|%3C).*script.*(>|%3E)`,
|
||||
`GLOBALS(=|\[|\%[0-9A-Z]{0,2})`,
|
||||
`_REQUEST(=|\[|\%[0-9A-Z]{0,2})`,
|
||||
`proc/self/environ`,
|
||||
`mosConfig_[a-zA-Z_]{1,21}(=|\%3D)`,
|
||||
`base64_(en|de)code\(.*\)`,
|
||||
}
|
||||
for _, pattern := range commonExploitPatterns {
|
||||
if match, _ := regexp.MatchString(pattern, query); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Block spam
|
||||
spamPatterns := []string{
|
||||
`\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b`,
|
||||
`\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b`,
|
||||
`\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b`,
|
||||
`\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b`,
|
||||
}
|
||||
for _, pattern := range spamPatterns {
|
||||
if match, _ := regexp.MatchString(pattern, query); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Block user agents
|
||||
userAgentPatterns := []string{
|
||||
`Indy Library`,
|
||||
`libwww-perl`,
|
||||
`GetRight`,
|
||||
`GetWeb!`,
|
||||
`Go!Zilla`,
|
||||
`Download Demon`,
|
||||
`Go-Ahead-Got-It`,
|
||||
`TurnitinBot`,
|
||||
`GrabNet`,
|
||||
}
|
||||
for _, pattern := range userAgentPatterns {
|
||||
if match, _ := regexp.MatchString(pattern, userAgent); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RequestIsMadeByBots checks if the request is made by bots or crawlers
|
||||
func (d *Detector) RequestIsMadeByBots(r *http.Request) bool {
|
||||
userAgent := r.UserAgent()
|
||||
return agents.IsCrawler(userAgent)
|
||||
}
|
@ -116,13 +116,13 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
reqHostname := r.Host
|
||||
/* Load balancing */
|
||||
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
|
||||
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname(), r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
if selectedUpstream.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
|
||||
h.Parent.logRequest(r, true, 101, "host-websocket", reqHostname, selectedUpstream.OriginIpOrDomain)
|
||||
|
||||
if target.HeaderRewriteRules == nil {
|
||||
target.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
|
||||
@ -161,12 +161,11 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
return
|
||||
}
|
||||
|
||||
originalHostHeader := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
r.URL, _ = url.Parse(reqHostname)
|
||||
}
|
||||
|
||||
//Populate the user-defined headers with the values from the request
|
||||
@ -188,7 +187,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
//Handle the request reverse proxy
|
||||
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||
OriginalHost: originalHostHeader,
|
||||
OriginalHost: reqHostname,
|
||||
UseTLS: selectedUpstream.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
@ -201,28 +200,28 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
|
||||
//validate the error
|
||||
var dnsError *net.DNSError
|
||||
upstreamHostname := selectedUpstream.OriginIpOrDomain
|
||||
if err != nil {
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 404, "host-http", reqHostname, upstreamHostname)
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
//Request canceled by client, usually due to manual refresh before page load
|
||||
http.Error(w, "Request canceled", http.StatusRequestTimeout)
|
||||
h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", reqHostname, upstreamHostname)
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 521, "host-http", reqHostname, upstreamHostname)
|
||||
}
|
||||
}
|
||||
|
||||
h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, true, statusCode, "host-http", reqHostname, upstreamHostname)
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
@ -242,7 +241,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
|
||||
}
|
||||
|
||||
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
||||
h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain)
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: target.SkipCertValidations,
|
||||
SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
|
||||
@ -254,12 +253,12 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
return
|
||||
}
|
||||
|
||||
originalHostHeader := r.Host
|
||||
reqHostname := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
r.URL, _ = url.Parse(reqHostname)
|
||||
}
|
||||
|
||||
//Populate the user-defined headers with the values from the request
|
||||
@ -282,7 +281,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
//Handle the virtual directory reverse proxy request
|
||||
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
OriginalHost: reqHostname,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.MatchingPath,
|
||||
UpstreamHeaders: upstreamHeaders,
|
||||
@ -296,19 +295,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.Parent.logRequest(r, false, 404, "vdir-http", target.Domain)
|
||||
h.Parent.logRequest(r, false, 404, "vdir-http", reqHostname, target.Domain)
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain)
|
||||
h.Parent.logRequest(r, false, 521, "vdir-http", reqHostname, target.Domain)
|
||||
}
|
||||
}
|
||||
h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain)
|
||||
h.Parent.logRequest(r, true, statusCode, "vdir-http", reqHostname, target.Domain)
|
||||
|
||||
}
|
||||
|
||||
// This logger collect data for the statistical analysis. For log to file logger, check the Logger and LogHTTPRequest handler
|
||||
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
|
||||
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, originalHostname string, upstreamHostname string) {
|
||||
if router.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
requestInfo := statistic.RequestInfo{
|
||||
@ -320,10 +319,11 @@ func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, for
|
||||
Referer: r.Referer(),
|
||||
UserAgent: r.UserAgent(),
|
||||
RequestURL: r.Host + r.RequestURI,
|
||||
Target: target,
|
||||
Target: originalHostname,
|
||||
Upstream: upstreamHostname,
|
||||
}
|
||||
router.Option.StatisticCollector.RecordRequest(requestInfo)
|
||||
}()
|
||||
}
|
||||
router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode)
|
||||
router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode, originalHostname, upstreamHostname)
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (t *RequestCountPerIpTable) Clear() {
|
||||
func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
err := h.Parent.handleRateLimit(w, r, pe)
|
||||
if err != nil {
|
||||
h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname(), "")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package rewrite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
@ -14,6 +15,11 @@ func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
|
||||
// Request-specific variables
|
||||
vars["$host"] = r.Host
|
||||
vars["$remote_addr"] = r.RemoteAddr
|
||||
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteIP = r.RemoteAddr // Fallback to the full RemoteAddr if parsing fails
|
||||
}
|
||||
vars["$remote_ip"] = remoteIP
|
||||
vars["$request_uri"] = r.RequestURI
|
||||
vars["$request_method"] = r.Method
|
||||
vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"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/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
@ -63,7 +63,7 @@ type RouterOption struct {
|
||||
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
|
||||
|
||||
/* Authentication Providers */
|
||||
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||
ForwardAuthRouter *forward.AuthRouter
|
||||
|
||||
/* Utilities */
|
||||
Logger *logger.Logger //Logger for reverse proxy requets
|
||||
@ -139,10 +139,10 @@ type HeaderRewriteRules struct {
|
||||
type AuthMethod int
|
||||
|
||||
const (
|
||||
AuthMethodNone AuthMethod = iota //No authentication required
|
||||
AuthMethodBasic //Basic Auth
|
||||
AuthMethodAuthelia //Authelia
|
||||
AuthMethodOauth2 //Oauth2
|
||||
AuthMethodNone AuthMethod = iota //No authentication required
|
||||
AuthMethodBasic //Basic Auth
|
||||
AuthMethodForward //Forward
|
||||
AuthMethodOauth2 //Oauth2
|
||||
)
|
||||
|
||||
type AuthenticationProvider struct {
|
||||
@ -152,9 +152,12 @@ type AuthenticationProvider struct {
|
||||
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
|
||||
/* 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
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
// Log HTTP request. Note that this must run in go routine to prevent any blocking
|
||||
// in reverse proxy router
|
||||
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int) {
|
||||
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int, downstreamHostname string, upstreamHostname string) {
|
||||
go func() {
|
||||
l.ValidateAndUpdateLogFilepath()
|
||||
if l.logger == nil || l.file == nil {
|
||||
@ -26,7 +26,9 @@ func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int
|
||||
clientIP := netutils.GetRequesterIP(r)
|
||||
requestURI := r.RequestURI
|
||||
statusCodeString := strconv.Itoa(statusCode)
|
||||
//fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
|
||||
//Pretty print for debugging
|
||||
//fmt.Printf("------------\nRequest URL: %s (class: %s) \nUpstream Hostname: %s\nDownstream Hostname: %s\nStatus Code: %s\n", r.URL, reqclass, upstreamHostname, downstreamHostname, statusCodeString)
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + downstreamHostname + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
}()
|
||||
}
|
||||
|
@ -87,6 +87,11 @@ func MatchIpWildcard(ipAddress, wildcard string) bool {
|
||||
|
||||
// Match ip address with CIDR
|
||||
func MatchIpCIDR(ip string, cidr string) bool {
|
||||
// Trim away scope ID if present in IP (e.g. fe80::1%eth0)
|
||||
if i := strings.Index(ip, "%"); i != -1 {
|
||||
ip = ip[:i]
|
||||
}
|
||||
|
||||
// parse the CIDR string
|
||||
_, cidrnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
|
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
|
||||
func (m *Manager) ListPluginGroups() map[string][]string {
|
||||
pluginGroup := map[string][]string{}
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
for k, v := range m.Options.PluginGroups {
|
||||
pluginGroup[k] = append([]string{}, v...)
|
||||
}
|
||||
m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RUnlock()
|
||||
return pluginGroup
|
||||
}
|
||||
|
||||
@ -32,26 +32,26 @@ func (m *Manager) AddPluginToGroup(tag, pluginID string) error {
|
||||
return errors.New("plugin is not a router type plugin")
|
||||
}
|
||||
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
m.pluginGroupsMutex.Lock()
|
||||
//Check if the tag exists
|
||||
_, ok = m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
m.Options.PluginGroups[tag] = []string{pluginID}
|
||||
m.Options.pluginGroupsMutex.Unlock()
|
||||
m.pluginGroupsMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
//Add the plugin to the group
|
||||
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
|
||||
|
||||
m.Options.pluginGroupsMutex.Unlock()
|
||||
m.pluginGroupsMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePluginFromGroup removes a plugin from a group
|
||||
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
defer m.Options.pluginGroupsMutex.Unlock()
|
||||
m.pluginGroupsMutex.Lock()
|
||||
defer m.pluginGroupsMutex.Unlock()
|
||||
//Check if the tag exists
|
||||
_, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
@ -72,8 +72,8 @@ func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
||||
|
||||
// RemovePluginGroup removes a plugin group
|
||||
func (m *Manager) RemovePluginGroup(tag string) error {
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
defer m.Options.pluginGroupsMutex.Unlock()
|
||||
m.pluginGroupsMutex.Lock()
|
||||
defer m.pluginGroupsMutex.Unlock()
|
||||
_, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
return errors.New("tag not found")
|
||||
@ -84,12 +84,12 @@ func (m *Manager) RemovePluginGroup(tag string) error {
|
||||
|
||||
// SavePluginGroupsFromFile loads plugin groups from a file
|
||||
func (m *Manager) SavePluginGroupsToFile() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
pluginGroupsCopy := make(map[string][]string)
|
||||
for k, v := range m.Options.PluginGroups {
|
||||
pluginGroupsCopy[k] = append([]string{}, v...)
|
||||
}
|
||||
m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Write to file
|
||||
js, _ := json.Marshal(pluginGroupsCopy)
|
||||
|
@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/* Plugin Store */
|
||||
|
@ -274,13 +274,10 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
||||
}
|
||||
|
||||
// Check if the plugin is still running
|
||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
|
||||
func (m *Manager) PluginIsRunning(pluginID string) bool {
|
||||
plugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if plugin.process == nil {
|
||||
return false
|
||||
}
|
||||
return plugin.process.ProcessState == nil
|
||||
return plugin.IsRunning()
|
||||
}
|
||||
|
@ -47,15 +47,84 @@ func NewPluginManager(options *ManagerOptions) *Manager {
|
||||
//Create database table
|
||||
options.Database.NewTable("plugins")
|
||||
|
||||
return &Manager{
|
||||
thisManager := &Manager{
|
||||
LoadedPlugins: make(map[string]*Plugin),
|
||||
tagPluginMap: sync.Map{},
|
||||
tagPluginListMutex: sync.RWMutex{},
|
||||
tagPluginList: make(map[string][]*Plugin),
|
||||
Options: options,
|
||||
PluginHash: make(map[string]string),
|
||||
/* Internal */
|
||||
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
|
||||
func (m *Manager) ReloadPluginFromDisk() {
|
||||
//Check each of the current plugins if the directory exists
|
||||
//If not, remove the plugin from the loaded plugins list
|
||||
m.loadedPluginsMutex.Lock()
|
||||
for pluginID, plugin := range m.LoadedPlugins {
|
||||
if !utils.FileExists(plugin.RootDir) {
|
||||
m.Log("Plugin directory not found, removing plugin from runtime: "+pluginID, nil)
|
||||
delete(m.LoadedPlugins, pluginID)
|
||||
//Remove the plugin enable state from the database
|
||||
m.Options.Database.Delete("plugins", pluginID)
|
||||
}
|
||||
}
|
||||
|
||||
m.loadedPluginsMutex.Unlock()
|
||||
|
||||
//Scan the plugin directory for new plugins
|
||||
foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir)
|
||||
if err != nil {
|
||||
m.Log("Failed to read plugin directory", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, folder := range foldersInPluginDir {
|
||||
if folder.IsDir() {
|
||||
pluginPath := filepath.Join(m.Options.PluginDir, folder.Name())
|
||||
thisPlugin, err := m.LoadPluginSpec(pluginPath)
|
||||
if err != nil {
|
||||
m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err)
|
||||
continue
|
||||
}
|
||||
|
||||
//Check if the plugin id is already loaded into the runtime
|
||||
m.loadedPluginsMutex.RLock()
|
||||
_, ok := m.LoadedPlugins[thisPlugin.Spec.ID]
|
||||
m.loadedPluginsMutex.RUnlock()
|
||||
if ok {
|
||||
//Plugin already loaded, skip it
|
||||
continue
|
||||
}
|
||||
|
||||
thisPlugin.RootDir = filepath.ToSlash(pluginPath)
|
||||
thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy)
|
||||
m.loadedPluginsMutex.Lock()
|
||||
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
|
||||
m.loadedPluginsMutex.Unlock()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
//Generate a hash list for plugins
|
||||
m.InitPluginHashList()
|
||||
}
|
||||
|
||||
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
||||
@ -82,7 +151,7 @@ func (m *Manager) LoadPluginsFromDisk() error {
|
||||
m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil)
|
||||
|
||||
// If the plugin was enabled, start it now
|
||||
fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
|
||||
//fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
|
||||
if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) {
|
||||
err = m.StartPlugin(thisPlugin.Spec.ID)
|
||||
if err != nil {
|
||||
@ -103,6 +172,8 @@ func (m *Manager) LoadPluginsFromDisk() error {
|
||||
//Generate the static forwarder radix tree
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
//Generate a hash list for plugins
|
||||
m.InitPluginHashList()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -258,3 +329,8 @@ func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longe
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// IsRunning checks if the plugin is currently running
|
||||
func (p *Plugin) IsRunning() bool {
|
||||
return p.process != nil && p.process.Process != nil
|
||||
}
|
||||
|
356
src/mod/plugins/store.go
Normal file
356
src/mod/plugins/store.go
Normal file
@ -0,0 +1,356 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugin Store
|
||||
*/
|
||||
|
||||
// See https://github.com/aroz-online/zoraxy-official-plugins/blob/main/directories/index.json for the standard format
|
||||
|
||||
type Checksums struct {
|
||||
LinuxAmd64 string `json:"linux_amd64"`
|
||||
Linux386 string `json:"linux_386"`
|
||||
LinuxArm string `json:"linux_arm"`
|
||||
LinuxArm64 string `json:"linux_arm64"`
|
||||
LinuxMipsle string `json:"linux_mipsle"`
|
||||
LinuxRiscv64 string `json:"linux_riscv64"`
|
||||
WindowsAmd64 string `json:"windows_amd64"`
|
||||
}
|
||||
|
||||
type DownloadablePlugin struct {
|
||||
IconPath string
|
||||
PluginIntroSpect zoraxy_plugin.IntroSpect //Plugin introspect information
|
||||
ChecksumsSHA256 Checksums //Checksums for the plugin binary
|
||||
DownloadURLs map[string]string //Download URLs for different platforms
|
||||
}
|
||||
|
||||
/* Plugin Store Index List Sync */
|
||||
//Update the plugin list from the plugin store URLs
|
||||
func (m *Manager) UpdateDownloadablePluginList() error {
|
||||
//Get downloadable plugins from each of the plugin store URLS
|
||||
m.Options.DownloadablePluginCache = []*DownloadablePlugin{}
|
||||
for _, url := range m.Options.PluginStoreURLs {
|
||||
pluginList, err := m.getPluginListFromURL(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get plugin list from %s: %w", url, err)
|
||||
}
|
||||
m.Options.DownloadablePluginCache = append(m.Options.DownloadablePluginCache, pluginList...)
|
||||
}
|
||||
|
||||
m.Options.LastSuccPluginSyncTime = time.Now().Unix()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the plugin list from the URL
|
||||
func (m *Manager) getPluginListFromURL(url string) ([]*DownloadablePlugin, error) {
|
||||
//Get the plugin list from the URL
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get plugin list from %s: %s", url, resp.Status)
|
||||
}
|
||||
|
||||
var pluginList []*DownloadablePlugin
|
||||
content, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugin list from %s: %w", url, err)
|
||||
}
|
||||
content = []byte(strings.TrimSpace(string(content)))
|
||||
|
||||
err = json.Unmarshal(content, &pluginList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal plugin list from %s: %w", url, err)
|
||||
}
|
||||
|
||||
return pluginList, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin {
|
||||
//List all downloadable plugins
|
||||
if len(m.Options.DownloadablePluginCache) == 0 {
|
||||
return []*DownloadablePlugin{}
|
||||
}
|
||||
return m.Options.DownloadablePluginCache
|
||||
}
|
||||
|
||||
// InstallPlugin installs the given plugin by moving it to the PluginDir.
|
||||
func (m *Manager) InstallPlugin(plugin *DownloadablePlugin) error {
|
||||
pluginDir := filepath.Join(m.Options.PluginDir, plugin.PluginIntroSpect.Name)
|
||||
pluginFile := plugin.PluginIntroSpect.Name
|
||||
if runtime.GOOS == "windows" {
|
||||
pluginFile += ".exe"
|
||||
}
|
||||
|
||||
//Check if the plugin id already exists in runtime plugin map
|
||||
if _, ok := m.LoadedPlugins[plugin.PluginIntroSpect.ID]; ok {
|
||||
return fmt.Errorf("plugin already installed: %s", plugin.PluginIntroSpect.ID)
|
||||
}
|
||||
|
||||
// Create the plugin directory if it doesn't exist
|
||||
err := os.MkdirAll(pluginDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create plugin directory: %w", err)
|
||||
}
|
||||
|
||||
// Download the plugin binary
|
||||
downloadURL, ok := plugin.DownloadURLs[runtime.GOOS+"_"+runtime.GOARCH]
|
||||
if !ok {
|
||||
return fmt.Errorf("no download URL available for the current platform")
|
||||
}
|
||||
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download plugin: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download plugin: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Write the plugin binary to the plugin directory
|
||||
pluginPath := filepath.Join(pluginDir, pluginFile)
|
||||
out, err := os.Create(pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create plugin file: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
return fmt.Errorf("failed to write plugin file: %w", err)
|
||||
}
|
||||
|
||||
// Make the plugin executable
|
||||
err = os.Chmod(pluginPath, 0755)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
return fmt.Errorf("failed to set executable permissions: %w", err)
|
||||
}
|
||||
|
||||
// Verify the checksum of the downloaded plugin binary
|
||||
checksums, err := plugin.ChecksumsSHA256.GetCurrentPlatformChecksum()
|
||||
if err == nil {
|
||||
if !verifyChecksumForFile(pluginPath, checksums) {
|
||||
out.Close()
|
||||
return fmt.Errorf("checksum verification failed for plugin binary")
|
||||
}
|
||||
}
|
||||
|
||||
//Ok, also download the icon if exists
|
||||
if plugin.IconPath != "" {
|
||||
iconURL := strings.TrimSpace(plugin.IconPath)
|
||||
if iconURL != "" {
|
||||
resp, err := http.Get(iconURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download plugin icon: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
//Save the icon to the plugin directory
|
||||
iconPath := filepath.Join(pluginDir, "icon.png")
|
||||
out, err := os.Create(iconPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create plugin icon file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
io.Copy(out, resp.Body)
|
||||
}
|
||||
}
|
||||
//Close the plugin exeutable
|
||||
out.Close()
|
||||
|
||||
//Reload the plugin list
|
||||
m.ReloadPluginFromDisk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UninstallPlugin uninstalls the plugin by removing its directory.
|
||||
func (m *Manager) UninstallPlugin(pluginID string) error {
|
||||
|
||||
//Stop the plugin process if it's running
|
||||
plugin, ok := m.LoadedPlugins[pluginID]
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", pluginID)
|
||||
}
|
||||
|
||||
if plugin.IsRunning() {
|
||||
err := m.StopPlugin(plugin.Spec.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop plugin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
//Make sure the plugin process is stopped
|
||||
m.Options.Logger.PrintAndLog("plugin-manager", "Removing plugin in 3 seconds...", nil)
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Remove the plugin directory
|
||||
err := os.RemoveAll(plugin.RootDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove plugin directory: %w", err)
|
||||
}
|
||||
|
||||
//Reload the plugin list
|
||||
m.ReloadPluginFromDisk()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentPlatformChecksum returns the checksum for the current platform
|
||||
func (c *Checksums) GetCurrentPlatformChecksum() (string, error) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
return c.LinuxAmd64, nil
|
||||
case "386":
|
||||
return c.Linux386, nil
|
||||
case "arm":
|
||||
return c.LinuxArm, nil
|
||||
case "arm64":
|
||||
return c.LinuxArm64, nil
|
||||
case "mipsle":
|
||||
return c.LinuxMipsle, nil
|
||||
case "riscv64":
|
||||
return c.LinuxRiscv64, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
|
||||
}
|
||||
case "windows":
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
return c.WindowsAmd64, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyChecksum verifies the checksum of the downloaded plugin binary.
|
||||
func verifyChecksumForFile(filePath string, checksum string) bool {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return false
|
||||
}
|
||||
calculatedChecksum := fmt.Sprintf("%x", hash.Sum(nil))
|
||||
|
||||
return calculatedChecksum == checksum
|
||||
}
|
||||
|
||||
/*
|
||||
Handlers for Plugin Store
|
||||
*/
|
||||
|
||||
func (m *Manager) HandleListDownloadablePlugins(w http.ResponseWriter, r *http.Request) {
|
||||
//List all downloadable plugins
|
||||
plugins := m.ListDownloadablePlugins()
|
||||
js, _ := json.Marshal(plugins)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// HandleResyncPluginList is the handler for resyncing the plugin list from the plugin store URLs
|
||||
func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
//Make sure this function require csrf token
|
||||
utils.SendErrorResponse(w, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
//Resync the plugin list from the plugin store URLs
|
||||
err := m.UpdateDownloadablePluginList()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to resync plugin list: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleInstallPlugin is the handler for installing a plugin
|
||||
func (m *Manager) HandleInstallPlugin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
utils.SendErrorResponse(w, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
pluginID, err := utils.PostPara(r, "pluginID")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "pluginID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the plugin info from cache
|
||||
var plugin *DownloadablePlugin
|
||||
for _, p := range m.Options.DownloadablePluginCache {
|
||||
if p.PluginIntroSpect.ID == pluginID {
|
||||
plugin = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if plugin == nil {
|
||||
utils.SendErrorResponse(w, "Plugin not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Install the plugin (implementation depends on your system)
|
||||
err = m.InstallPlugin(plugin)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to install plugin: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleUninstallPlugin is the handler for uninstalling a plugin
|
||||
func (m *Manager) HandleUninstallPlugin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
utils.SendErrorResponse(w, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
pluginID, err := utils.PostPara(r, "pluginID")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "pluginID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Uninstall the plugin (implementation depends on your system)
|
||||
err = m.UninstallPlugin(pluginID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to uninstall plugin: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
52
src/mod/plugins/store_test.go
Normal file
52
src/mod/plugins/store_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateDownloadablePluginList(t *testing.T) {
|
||||
mockManager := &Manager{
|
||||
Options: &ManagerOptions{
|
||||
DownloadablePluginCache: []*DownloadablePlugin{},
|
||||
PluginStoreURLs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
//Inject a mock URL for testing
|
||||
mockManager.Options.PluginStoreURLs = []string{"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json"}
|
||||
|
||||
err := mockManager.UpdateDownloadablePluginList()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(mockManager.Options.DownloadablePluginCache) == 0 {
|
||||
t.Fatalf("expected plugin cache to be updated, but it was empty")
|
||||
}
|
||||
|
||||
if mockManager.Options.LastSuccPluginSyncTime == 0 {
|
||||
t.Fatalf("expected LastSuccPluginSyncTime to be updated, but it was not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPluginListFromURL(t *testing.T) {
|
||||
mockManager := &Manager{
|
||||
Options: &ManagerOptions{
|
||||
DownloadablePluginCache: []*DownloadablePlugin{},
|
||||
PluginStoreURLs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
pluginList, err := mockManager.getPluginListFromURL("https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(pluginList) == 0 {
|
||||
t.Fatalf("expected plugin list to be populated, but it was empty")
|
||||
}
|
||||
|
||||
for _, plugin := range pluginList {
|
||||
t.Logf("Plugin: %+v", plugin)
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@ import (
|
||||
// This will only load the plugin tags to option.PluginGroups map
|
||||
// to push the changes to runtime, call UpdateTagsToPluginMaps()
|
||||
func (m *Manager) LoadPluginGroupsFromConfig() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
defer m.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Read the config file
|
||||
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
|
||||
@ -39,8 +39,8 @@ func (m *Manager) LoadPluginGroupsFromConfig() error {
|
||||
|
||||
// AddPluginToTag adds a plugin to a tag
|
||||
func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
defer m.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Check if the plugin exists
|
||||
_, err := m.GetPluginByID(pluginID)
|
||||
@ -66,8 +66,8 @@ func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
||||
// RemovePluginFromTag removes a plugin from a tag
|
||||
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
|
||||
// Check if the plugin exists in Options.PluginGroups
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
defer m.pluginGroupsMutex.RUnlock()
|
||||
pluginList, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
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
|
||||
func (m *Manager) savePluginTagMap() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
m.pluginGroupsMutex.RLock()
|
||||
defer m.pluginGroupsMutex.RUnlock()
|
||||
|
||||
js, _ := json.Marshal(m.Options.PluginGroups)
|
||||
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
@ -29,18 +30,25 @@ type Plugin struct {
|
||||
}
|
||||
|
||||
type ManagerOptions struct {
|
||||
/* Plugins */
|
||||
PluginDir string //The directory where the plugins are stored
|
||||
PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs
|
||||
PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file
|
||||
|
||||
/* Plugin Downloader */
|
||||
PluginStoreURLs []string //The plugin store URLs, used to download the plugins
|
||||
DownloadablePluginCache []*DownloadablePlugin //The cache for the downloadable plugins, key is the plugin ID and value is the DownloadablePlugin struct
|
||||
LastSuccPluginSyncTime int64 //The last sync time for the plugin store URLs, used to check if the plugin store URLs need to be synced again
|
||||
|
||||
/* Runtime */
|
||||
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
|
||||
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
|
||||
Database *database.Database `json:"-"`
|
||||
Logger *logger.Logger `json:"-"`
|
||||
|
||||
/* Internal */
|
||||
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
|
||||
/* Development */
|
||||
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
|
||||
HotReloadInterval int //The interval for checking the plugin file change, in seconds
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@ -50,6 +58,12 @@ type Manager struct {
|
||||
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
|
||||
Options *ManagerOptions
|
||||
|
||||
PluginHash map[string]string //The hash of the plugin file, used to check if the plugin file is changed
|
||||
|
||||
/* Internal */
|
||||
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
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64)
|
||||
// +build windows,amd64 linux,mipsle linux,riscv64
|
||||
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64)
|
||||
// +build windows,amd64 linux,mipsle linux,riscv64 freebsd,amd64
|
||||
|
||||
package sshprox
|
||||
|
||||
import "embed"
|
||||
|
||||
/*
|
||||
Bianry embedding
|
||||
Binary embedding
|
||||
|
||||
Make sure when compile, gotty binary exists in static.gotty
|
||||
*/
|
||||
|
@ -36,6 +36,8 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti
|
||||
Referer: make(map[string]int),
|
||||
UserAgent: make(map[string]int),
|
||||
RequestURL: make(map[string]int),
|
||||
Downstreams: make(map[string]int),
|
||||
Upstreams: make(map[string]int),
|
||||
}
|
||||
|
||||
for _, export := range exports {
|
||||
@ -66,6 +68,14 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti
|
||||
for key, value := range export.RequestURL {
|
||||
mergedExport.RequestURL[key] += value
|
||||
}
|
||||
|
||||
for key, value := range export.Downstreams {
|
||||
mergedExport.Downstreams[key] += value
|
||||
}
|
||||
|
||||
for key, value := range export.Upstreams {
|
||||
mergedExport.Upstreams[key] += value
|
||||
}
|
||||
}
|
||||
|
||||
return mergedExport
|
||||
|
@ -24,12 +24,14 @@ type DailySummary struct {
|
||||
ErrorRequest int64 //Invalid request of the day, including error or not found
|
||||
ValidRequest int64 //Valid request of the day
|
||||
//Type counters
|
||||
ForwardTypes *sync.Map //Map that hold the forward types
|
||||
RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter
|
||||
RequestClientIp *sync.Map //Map that hold all unique request IPs
|
||||
Referer *sync.Map //Map that store where the user was refered from
|
||||
UserAgent *sync.Map //Map that store the useragent of the request
|
||||
RequestURL *sync.Map //Request URL of the request object
|
||||
ForwardTypes *sync.Map //Map that hold the forward types
|
||||
RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter
|
||||
RequestClientIp *sync.Map //Map that hold all unique request IPs
|
||||
Referer *sync.Map //Map that store where the user was refered from
|
||||
UserAgent *sync.Map //Map that store the useragent of the request
|
||||
RequestURL *sync.Map //Request URL of the request object
|
||||
DownstreamHostnames *sync.Map //Request count of downstream hostname
|
||||
UpstreamHostnames *sync.Map //Forwarded request count of upstream hostname
|
||||
}
|
||||
|
||||
type RequestInfo struct {
|
||||
@ -42,6 +44,7 @@ type RequestInfo struct {
|
||||
UserAgent string //UserAgent of the downstream request
|
||||
RequestURL string //Request URL
|
||||
Target string //Target domain or hostname
|
||||
Upstream string ////Upstream domain or hostname, if the request is forwarded to upstream
|
||||
}
|
||||
|
||||
type CollectorOption struct {
|
||||
@ -233,6 +236,24 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
|
||||
} else {
|
||||
c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
|
||||
}
|
||||
|
||||
//Record the downstream hostname
|
||||
//This is the hostname that the user visited, not the target domain
|
||||
ds, ok := c.DailySummary.DownstreamHostnames.Load(ri.Target)
|
||||
if !ok {
|
||||
c.DailySummary.DownstreamHostnames.Store(ri.Target, 1)
|
||||
} else {
|
||||
c.DailySummary.DownstreamHostnames.Store(ri.Target, ds.(int)+1)
|
||||
}
|
||||
|
||||
//Record the upstream hostname
|
||||
//This is the selected load balancer upstream hostname or ip
|
||||
us, ok := c.DailySummary.UpstreamHostnames.Load(ri.Upstream)
|
||||
if !ok {
|
||||
c.DailySummary.UpstreamHostnames.Store(ri.Upstream, 1)
|
||||
} else {
|
||||
c.DailySummary.UpstreamHostnames.Store(ri.Upstream, us.(int)+1)
|
||||
}
|
||||
}()
|
||||
|
||||
//ADD MORE HERE IF NEEDED
|
||||
@ -271,15 +292,17 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
|
||||
|
||||
func NewDailySummary() *DailySummary {
|
||||
return &DailySummary{
|
||||
TotalRequest: 0,
|
||||
ErrorRequest: 0,
|
||||
ValidRequest: 0,
|
||||
ForwardTypes: &sync.Map{},
|
||||
RequestOrigin: &sync.Map{},
|
||||
RequestClientIp: &sync.Map{},
|
||||
Referer: &sync.Map{},
|
||||
UserAgent: &sync.Map{},
|
||||
RequestURL: &sync.Map{},
|
||||
TotalRequest: 0,
|
||||
ErrorRequest: 0,
|
||||
ValidRequest: 0,
|
||||
ForwardTypes: &sync.Map{},
|
||||
RequestOrigin: &sync.Map{},
|
||||
RequestClientIp: &sync.Map{},
|
||||
Referer: &sync.Map{},
|
||||
UserAgent: &sync.Map{},
|
||||
RequestURL: &sync.Map{},
|
||||
DownstreamHostnames: &sync.Map{},
|
||||
UpstreamHostnames: &sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,21 @@ type DailySummaryExport struct {
|
||||
Referer map[string]int
|
||||
UserAgent map[string]int
|
||||
RequestURL map[string]int
|
||||
Downstreams map[string]int
|
||||
Upstreams map[string]int
|
||||
}
|
||||
|
||||
func SyncMapToMapStringInt(syncMap *sync.Map) map[string]int {
|
||||
result := make(map[string]int)
|
||||
syncMap.Range(func(key, value interface{}) bool {
|
||||
strKey, okKey := key.(string)
|
||||
intValue, okValue := value.(int)
|
||||
if okKey && okValue {
|
||||
result[strKey] = intValue
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func DailySummaryToExport(summary DailySummary) DailySummaryExport {
|
||||
@ -26,77 +41,53 @@ func DailySummaryToExport(summary DailySummary) DailySummaryExport {
|
||||
Referer: make(map[string]int),
|
||||
UserAgent: make(map[string]int),
|
||||
RequestURL: make(map[string]int),
|
||||
Downstreams: make(map[string]int),
|
||||
Upstreams: make(map[string]int),
|
||||
}
|
||||
|
||||
summary.ForwardTypes.Range(func(key, value interface{}) bool {
|
||||
export.ForwardTypes[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
summary.RequestOrigin.Range(func(key, value interface{}) bool {
|
||||
export.RequestOrigin[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
summary.RequestClientIp.Range(func(key, value interface{}) bool {
|
||||
export.RequestClientIp[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
summary.Referer.Range(func(key, value interface{}) bool {
|
||||
export.Referer[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
summary.UserAgent.Range(func(key, value interface{}) bool {
|
||||
export.UserAgent[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
summary.RequestURL.Range(func(key, value interface{}) bool {
|
||||
export.RequestURL[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
export.ForwardTypes = SyncMapToMapStringInt(summary.ForwardTypes)
|
||||
export.RequestOrigin = SyncMapToMapStringInt(summary.RequestOrigin)
|
||||
export.RequestClientIp = SyncMapToMapStringInt(summary.RequestClientIp)
|
||||
export.Referer = SyncMapToMapStringInt(summary.Referer)
|
||||
export.UserAgent = SyncMapToMapStringInt(summary.UserAgent)
|
||||
export.RequestURL = SyncMapToMapStringInt(summary.RequestURL)
|
||||
export.Downstreams = SyncMapToMapStringInt(summary.DownstreamHostnames)
|
||||
export.Upstreams = SyncMapToMapStringInt(summary.UpstreamHostnames)
|
||||
|
||||
return export
|
||||
}
|
||||
|
||||
func MapStringIntToSyncMap(m map[string]int) *sync.Map {
|
||||
syncMap := &sync.Map{}
|
||||
for k, v := range m {
|
||||
syncMap.Store(k, v)
|
||||
}
|
||||
return syncMap
|
||||
}
|
||||
|
||||
func DailySummaryExportToSummary(export DailySummaryExport) DailySummary {
|
||||
summary := DailySummary{
|
||||
TotalRequest: export.TotalRequest,
|
||||
ErrorRequest: export.ErrorRequest,
|
||||
ValidRequest: export.ValidRequest,
|
||||
ForwardTypes: &sync.Map{},
|
||||
RequestOrigin: &sync.Map{},
|
||||
RequestClientIp: &sync.Map{},
|
||||
Referer: &sync.Map{},
|
||||
UserAgent: &sync.Map{},
|
||||
RequestURL: &sync.Map{},
|
||||
TotalRequest: export.TotalRequest,
|
||||
ErrorRequest: export.ErrorRequest,
|
||||
ValidRequest: export.ValidRequest,
|
||||
ForwardTypes: &sync.Map{},
|
||||
RequestOrigin: &sync.Map{},
|
||||
RequestClientIp: &sync.Map{},
|
||||
Referer: &sync.Map{},
|
||||
UserAgent: &sync.Map{},
|
||||
RequestURL: &sync.Map{},
|
||||
DownstreamHostnames: &sync.Map{},
|
||||
UpstreamHostnames: &sync.Map{},
|
||||
}
|
||||
|
||||
for k, v := range export.ForwardTypes {
|
||||
summary.ForwardTypes.Store(k, v)
|
||||
}
|
||||
|
||||
for k, v := range export.RequestOrigin {
|
||||
summary.RequestOrigin.Store(k, v)
|
||||
}
|
||||
|
||||
for k, v := range export.RequestClientIp {
|
||||
summary.RequestClientIp.Store(k, v)
|
||||
}
|
||||
|
||||
for k, v := range export.Referer {
|
||||
summary.Referer.Store(k, v)
|
||||
}
|
||||
|
||||
for k, v := range export.UserAgent {
|
||||
summary.UserAgent.Store(k, v)
|
||||
}
|
||||
|
||||
for k, v := range export.RequestURL {
|
||||
summary.RequestURL.Store(k, v)
|
||||
}
|
||||
summary.ForwardTypes = MapStringIntToSyncMap(export.ForwardTypes)
|
||||
summary.RequestOrigin = MapStringIntToSyncMap(export.RequestOrigin)
|
||||
summary.RequestClientIp = MapStringIntToSyncMap(export.RequestClientIp)
|
||||
summary.Referer = MapStringIntToSyncMap(export.Referer)
|
||||
summary.UserAgent = MapStringIntToSyncMap(export.UserAgent)
|
||||
summary.RequestURL = MapStringIntToSyncMap(export.RequestURL)
|
||||
summary.DownstreamHostnames = MapStringIntToSyncMap(export.Downstreams)
|
||||
summary.UpstreamHostnames = MapStringIntToSyncMap(export.Upstreams)
|
||||
|
||||
return summary
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package update
|
||||
import (
|
||||
v308 "imuslab.com/zoraxy/mod/update/v308"
|
||||
v315 "imuslab.com/zoraxy/mod/update/v315"
|
||||
v322 "imuslab.com/zoraxy/mod/update/v322"
|
||||
)
|
||||
|
||||
// Updater Core logic
|
||||
@ -19,6 +20,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
|
||||
if err != nil {
|
||||
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
|
||||
|
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,7 +115,7 @@ func ReverseProxtInit() {
|
||||
StatisticCollector: statisticCollector,
|
||||
WebDirectory: *path_webserver,
|
||||
AccessController: accessController,
|
||||
AutheliaRouter: autheliaRouter,
|
||||
ForwardAuthRouter: forwardAuthRouter,
|
||||
LoadBalancer: loadBalancer,
|
||||
PluginManager: pluginManager,
|
||||
/* Utilities */
|
||||
@ -584,7 +584,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if authProviderType == 1 {
|
||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
|
||||
} else if authProviderType == 2 {
|
||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
|
||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward
|
||||
} else if authProviderType == 3 {
|
||||
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
|
||||
} else {
|
||||
|
@ -101,7 +101,7 @@ func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath s
|
||||
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
|
||||
relativeFilepath = relativeFilepath + "index.html"
|
||||
}
|
||||
if DEVELOPMENT_BUILD {
|
||||
if *development_build {
|
||||
//Load from disk
|
||||
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
|
||||
content, err = os.ReadFile(targetFilePath)
|
||||
|
45
src/start.go
45
src/start.go
@ -13,7 +13,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"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/dbinc"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
@ -98,7 +98,7 @@ func startupSequence() {
|
||||
})
|
||||
|
||||
//Create a TLS certificate manager
|
||||
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, DEVELOPMENT_BUILD, SystemWideLogger)
|
||||
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -141,11 +141,10 @@ func startupSequence() {
|
||||
}
|
||||
|
||||
//Create authentication providers
|
||||
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
|
||||
UseHTTPS: false, // Automatic populate in router initiation
|
||||
AutheliaURL: "", // Automatic populate in router initiation
|
||||
Logger: SystemWideLogger,
|
||||
Database: sysdb,
|
||||
forwardAuthRouter = forward.NewAuthRouter(&forward.AuthRouterOptions{
|
||||
Address: "",
|
||||
Logger: SystemWideLogger,
|
||||
Database: sysdb,
|
||||
})
|
||||
|
||||
//Create a statistic collector
|
||||
@ -308,23 +307,41 @@ func startupSequence() {
|
||||
pluginFolder := *path_plugin
|
||||
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
|
||||
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
|
||||
PluginDir: pluginFolder,
|
||||
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
|
||||
ZoraxyVersion: SYSTEM_VERSION,
|
||||
ZoraxyUUID: nodeUUID,
|
||||
DevelopmentBuild: DEVELOPMENT_BUILD,
|
||||
},
|
||||
PluginDir: pluginFolder,
|
||||
Database: sysdb,
|
||||
Logger: SystemWideLogger,
|
||||
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
|
||||
CSRFTokenGen: func(r *http.Request) string {
|
||||
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
|
||||
go func() {
|
||||
err = pluginManager.UpdateDownloadablePluginList()
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("plugin-manager", "Failed to sync plugin list from plugin store", err)
|
||||
} else {
|
||||
SystemWideLogger.PrintAndLog("plugin-manager", "Plugin list synced from plugin store", nil)
|
||||
}
|
||||
}()
|
||||
|
||||
err = pluginManager.LoadPluginsFromDisk()
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err)
|
||||
SystemWideLogger.PrintAndLog("plugin-manager", "Failed to load plugins", err)
|
||||
}
|
||||
|
||||
/* Docker UX Optimizer */
|
||||
|
@ -47,8 +47,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small input" style="width: 300px; height: 38px;">
|
||||
<input type="text" id="searchInput" placeholder="Quick Search" onkeydown="handleSearchInput(event);" onchange="handleSearchInput(event);" onblur="handleSearchInput(event);">
|
||||
<div class="ui small input" style="width: 300px; height: 38px;">
|
||||
<!-- Prevent the browser from filling the saved Zoraxy login account into the input searchInput below -->
|
||||
<input type="password" autocomplete="off" hidden/>
|
||||
<input type="text" id="searchInput" placeholder="Quick Search" onkeydown="handleSearchInput(event);" onchange="handleSearchInput(event);" onblur="handleSearchInput(event);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -142,12 +144,23 @@
|
||||
if (subd.Disabled){
|
||||
enableChecked = "";
|
||||
}
|
||||
|
||||
let httpProto = "http://";
|
||||
if ($("#tls").checkbox("is checked")) {
|
||||
httpProto = "https://";
|
||||
} else {
|
||||
httpProto = "http://";
|
||||
}
|
||||
let hostnameRedirectPort = currentListeningPort;
|
||||
if (hostnameRedirectPort == 80 || hostnameRedirectPort == 443){
|
||||
hostnameRedirectPort = "";
|
||||
}else{
|
||||
hostnameRedirectPort = ":" + hostnameRedirectPort;
|
||||
}
|
||||
let aliasDomains = ``;
|
||||
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
|
||||
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
|
||||
subd.MatchingDomainAlias.forEach(alias => {
|
||||
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
|
||||
aliasDomains += `<a href="${httpProto}${alias}${hostnameRedirectPort}" target="_blank">${alias}</a>, `;
|
||||
});
|
||||
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
||||
aliasDomains += `</small><br>`;
|
||||
@ -155,7 +168,7 @@
|
||||
|
||||
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||
<td data-label="" editable="true" datatype="inbound">
|
||||
<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
|
||||
<a href="${httpProto}${subd.RootOrMatchingDomain}${hostnameRedirectPort}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
|
||||
${aliasDomains}
|
||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||
</td>
|
||||
@ -172,8 +185,8 @@
|
||||
</td>
|
||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Forward Auth`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
|
||||
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
||||
@ -379,7 +392,7 @@
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
|
||||
<label>Authelia</label>
|
||||
<label>Forward Auth</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -775,4 +788,4 @@
|
||||
filterProxyList();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@ -185,6 +185,35 @@
|
||||
|
||||
</tbody>
|
||||
</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>
|
||||
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -480,7 +509,10 @@ function initiatePluginList(){
|
||||
<a href="${plugin.Spec.url}" target="_blank">${plugin.Spec.url}</a></td>
|
||||
<td data-label="Category">${plugin.Spec.type==0?"Router":"Utilities"}</td>
|
||||
<td data-label="Action">
|
||||
<button onclick="getPluginInfo('${plugin.Spec.id}', this);" class="ui basic icon button" pluginid="${plugin.Spec.id}">
|
||||
<button onclick="uninstallPlugin('${plugin.Spec.id}', '${plugin.Spec.name}', this);" class="ui basic red icon button">
|
||||
<i class="trash icon"></i>
|
||||
</button>
|
||||
<button onclick="getPluginInfo('${plugin.Spec.id}', this);" class="ui basic icon button" pluginid="${plugin.Spec.id}">
|
||||
<i class="info circle icon"></i>
|
||||
</button>
|
||||
<button onclick="stopPlugin('${plugin.Spec.id}', this);" class="ui basic button pluginEnableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? '' : 'style="display:none;"'}>
|
||||
@ -509,9 +541,6 @@ function initiatePluginList(){
|
||||
|
||||
initiatePluginList();
|
||||
|
||||
/* Tag Assignment */
|
||||
|
||||
|
||||
|
||||
|
||||
/* Plugin Lifecycle */
|
||||
@ -563,6 +592,122 @@ function getPluginInfo(pluginId, btn){
|
||||
showSideWrapper("snippet/pluginInfo.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
function openPluginStore(){
|
||||
//Open plugin store in extended mode
|
||||
showSideWrapper("snippet/pluginstore.html?t=" + Date.now(), true);
|
||||
}
|
||||
|
||||
function uninstallPlugin(pluginId, pluginName, btn=undefined) {
|
||||
if (confirm("Are you sure you want to remove " + pluginName + " plugin?")) {
|
||||
if (btn) {
|
||||
$(btn).html('<i class="spinner loading icon"></i>');
|
||||
$(btn).addClass('disabled');
|
||||
}
|
||||
$.cjax({
|
||||
url: '/api/plugins/store/uninstall',
|
||||
type: 'POST',
|
||||
data: { "pluginID": pluginId },
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
msgbox(data.error, false);
|
||||
} else {
|
||||
msgbox(pluginName + " uninstalled successfully", true);
|
||||
initiatePluginList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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>
|
||||
|
||||
|
||||
|
@ -14,23 +14,59 @@
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<h3>Authelia</h3>
|
||||
<p>Configuration settings for Authelia authentication provider.</p>
|
||||
|
||||
<h3>Forward Auth</h3>
|
||||
<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">
|
||||
<div class="field">
|
||||
<label for="autheliaServerUrl">Authelia Server URL</label>
|
||||
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
|
||||
<small>Example: auth.example.com</small>
|
||||
<label for="forwardAuthAddress">Address</label>
|
||||
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
|
||||
<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 class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="useHttps" name="useHttps">
|
||||
<label for="useHttps">Use HTTPS</label>
|
||||
<small>Check this if your authelia server uses HTTPS</small>
|
||||
<div class="ui basic segment advanceoptions" style="margin-top:0.6em;">
|
||||
<div class="ui advancedSSOForwardAuthOptions accordion">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
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>
|
||||
<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>
|
||||
@ -39,12 +75,15 @@
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$.cjax({
|
||||
url: '/api/sso/Authelia',
|
||||
url: '/api/sso/forward-auth',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$('#autheliaServerUrl').val(data.autheliaURL);
|
||||
$('#useHttps').prop('checked', data.useHTTPS);
|
||||
$('#forwardAuthAddress').val(data.address);
|
||||
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
|
||||
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
|
||||
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
|
||||
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||
@ -52,27 +91,35 @@
|
||||
});
|
||||
});
|
||||
|
||||
function updateAutheliaSettings(){
|
||||
var autheliaServerUrl = $('#autheliaServerUrl').val();
|
||||
var useHttps = $('#useHttps').prop('checked');
|
||||
function updateForwardAuthSettings() {
|
||||
const address = $('#forwardAuthAddress').val();
|
||||
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({
|
||||
url: '/api/sso/Authelia',
|
||||
url: '/api/sso/forward-auth',
|
||||
method: 'POST',
|
||||
data: {
|
||||
autheliaURL: autheliaServerUrl,
|
||||
useHTTPS: useHttps
|
||||
address: address,
|
||||
responseHeaders: responseHeaders,
|
||||
responseClientHeaders: responseClientHeaders,
|
||||
requestHeaders: requestHeaders,
|
||||
requestExcludedCookies: requestExcludedCookies
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
$.msgbox(data.error, false);
|
||||
if (data.error !== undefined) {
|
||||
msgbox(data.error, false);
|
||||
return;
|
||||
}
|
||||
msgbox('Authelia settings updated', true);
|
||||
console.log('Authelia settings updated:', data);
|
||||
msgbox('Forward Auth settings updated', true);
|
||||
console.log('Forward Auth settings updated:', data);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error('Error updating Authelia settings:', textStatus, errorThrown);
|
||||
console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -184,7 +184,46 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<h3>Requested Hostnames</h3>
|
||||
<p>Most requested hostnames from downstream</p>
|
||||
<div>
|
||||
<div style="height: 500px; overflow-y: auto;">
|
||||
<table class="ui unstackable striped celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-sort">Hostname</th>
|
||||
<th class="no-sort">Requests</th>
|
||||
</tr></thead>
|
||||
<tbody id="stats_downstreamTable">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<h3>Forwarded Upstreams</h3>
|
||||
<p>The Top 100 upstreams where the requests are forwarded to</p>
|
||||
<div>
|
||||
<div style="height: 500px; overflow-y: auto;">
|
||||
<table class="ui unstackable striped celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-sort">Upstream Endpoint</th>
|
||||
<th class="no-sort">Requests</th>
|
||||
</tr></thead>
|
||||
<tbody id="stats_upstreamTable">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment" id="trendGraphs">
|
||||
<h3>Visitor Trend Analysis</h3>
|
||||
@ -263,6 +302,22 @@
|
||||
//Render Referer header
|
||||
renderRefererTable(data.Referer);
|
||||
|
||||
if (data.Downstreams == null){
|
||||
//No downstream data to show
|
||||
$("#stats_downstreamTable").html("<tr><td colspan='2'>No data</td></tr>");
|
||||
}else{
|
||||
//Render the downstream table
|
||||
renderDownstreamTable(data.Downstreams);
|
||||
}
|
||||
|
||||
if (data.Upstreams == null){
|
||||
//No upstream data to show
|
||||
$("#stats_upstreamTable").html("<tr><td colspan='2'>No data</td></tr>");
|
||||
}else{
|
||||
//Render the upstream table
|
||||
renderUpstreamTable(data.Upstreams);
|
||||
}
|
||||
|
||||
//Hide the trend graphs
|
||||
$("#trendGraphs").hide();
|
||||
});
|
||||
@ -410,6 +465,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderDownstreamTable(downstreamList){
|
||||
const sortedEntries = Object.entries(downstreamList).sort(([, valueA], [, valueB]) => valueB - valueA);
|
||||
$("#stats_downstreamTable").html("");
|
||||
let endStop = 100;
|
||||
if (sortedEntries.length < 100){
|
||||
endStop = sortedEntries.length;
|
||||
}
|
||||
for (var i = 0; i < endStop; i++) {
|
||||
let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,"");
|
||||
if (sortedEntries[i][0] == ""){
|
||||
//Root
|
||||
referer = `<span style="color: #b5b5b5;">(<i class="eye slash outline icon"></i> Unknown or Hidden)</span>`;
|
||||
}
|
||||
$("#stats_downstreamTable").append(`<tr>
|
||||
<td>${referer}</td>
|
||||
<td>${sortedEntries[i][1]}</td>
|
||||
</tr>`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUpstreamTable(upstreamList){
|
||||
const sortedEntries = Object.entries(upstreamList).sort(([, valueA], [, valueB]) => valueB - valueA);
|
||||
$("#stats_upstreamTable").html("");
|
||||
let endStop = 100;
|
||||
if (sortedEntries.length < 100){
|
||||
endStop = sortedEntries.length;
|
||||
}
|
||||
for (var i = 0; i < endStop; i++) {
|
||||
let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,"");
|
||||
if (sortedEntries[i][0] == ""){
|
||||
//Root
|
||||
referer = `<span style="color: #b5b5b5;">(<i class="eye slash outline icon"></i> Unknown or Hidden)</span>`;
|
||||
}
|
||||
$("#stats_upstreamTable").append(`<tr>
|
||||
<td>${referer}</td>
|
||||
<td>${sortedEntries[i][1]}</td>
|
||||
</tr>`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFileTypeGraph(requestURLs){
|
||||
//Create the device chart
|
||||
let fileExtensions = {};
|
||||
|
@ -524,7 +524,6 @@
|
||||
$("#tls").checkbox("set checked");
|
||||
}else{
|
||||
$(".tlsEnabledOnly").addClass('disabled');
|
||||
$(".tlsEnabledOnly").addClass('disabled');
|
||||
}
|
||||
|
||||
//Initiate the input listener on the checkbox
|
||||
|
@ -197,6 +197,12 @@ body.darkTheme .menubar{
|
||||
max-width: calc(80% - 1em);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 478px) {
|
||||
.sideWrapper.extendedMode {
|
||||
max-width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
|
||||
.sideWrapper .content{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -1,14 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Notes: This should be open in its original path-->
|
||||
<!-- Notes: This should be open in its original path -->
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css" />
|
||||
<script src="../script/jquery-3.6.0.min.js"></script>
|
||||
<script src="../script/semantic/semantic.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<link rel="stylesheet" href="../darktheme.css" />
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br />
|
||||
<div class="ui container">
|
||||
@ -17,48 +17,428 @@
|
||||
<input
|
||||
id="searchbar"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
placeholder="Search Containers ..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</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="ui checkbox">
|
||||
<input type="checkbox" id="showUnexposed" class="hidden" />
|
||||
<label for="showUnexposed"
|
||||
>Show Containers with unexposed ports
|
||||
<br />
|
||||
<small
|
||||
>Please make sure Zoraxy and the target container share a
|
||||
network</small
|
||||
>
|
||||
</label>
|
||||
>Show Containers with Unexposed Ports</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Networked Containers Lists -->
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
List of Docker Containers
|
||||
Containers on Zoraxy's Networks
|
||||
<div class="sub header">
|
||||
Below is a list of all detected Docker containers currently running
|
||||
on the system.
|
||||
These containers share a network with Zoraxy.<br />
|
||||
Your networks must support Docker DNS-based name resolution.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="containersList" class="ui middle aligned divided list active">
|
||||
<div class="ui loader active"></div>
|
||||
<div id="networkedList" class="ui middle aligned divided list">
|
||||
<div class="ui active loader"></div>
|
||||
</div>
|
||||
<div class="ui horizontal divider"></div>
|
||||
<div id="containersAddedListHeader" class="ui header" hidden>
|
||||
Already added containers:
|
||||
<!-- Host Mode Containers List -->
|
||||
<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
|
||||
id="containersAddedList"
|
||||
class="ui middle aligned divided list"
|
||||
></div>
|
||||
<div id="hostmodeList" class="ui middle aligned divided list"></div>
|
||||
<div class="ui horizontal divider"></div>
|
||||
<!-- Other Containers List -->
|
||||
<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>
|
||||
|
||||
<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) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
@ -66,177 +446,6 @@
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
270
src/web/snippet/pluginstore.html
Normal file
270
src/web/snippet/pluginstore.html
Normal file
@ -0,0 +1,270 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Notes: This should be open in its original path-->
|
||||
<meta charset="utf-8">
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<title>Plugin Store</title>
|
||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
||||
<script src="../script/jquery-3.6.0.min.js"></script>
|
||||
<script src="../script/semantic/semantic.min.js"></script>
|
||||
<script src="../script/utils.js"></script>
|
||||
<style>
|
||||
#pluginList{
|
||||
padding: 1em;
|
||||
border: 1px solid #ccc;
|
||||
height: 500px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body.darkTheme #pluginList .header{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.installablePlugin{
|
||||
position: relative;
|
||||
}
|
||||
.installablePlugin .action{
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
right: 0.4em;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#pluginList .item .image {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui warning message">
|
||||
<div class="header">Experimental Feature</div>
|
||||
<p>The Plugin Store is an experimental feature. Use it at your own risk.</p>
|
||||
</div>
|
||||
<div class="ui fluid search">
|
||||
<div class="ui fluid icon input">
|
||||
<input id="searchInput" class="prompt" type="text" placeholder="Search plugins">
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divided items" id="pluginList">
|
||||
|
||||
</div>
|
||||
<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 basic segment advanceoptions">
|
||||
<div class="ui accordion advanceSettings">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Advance Settings
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Plugin Store URLs</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<textarea id="pluginStoreURLs" rows="5"></textarea>
|
||||
<label>Enter plugin store URLs, separating each URL with a new line</label>
|
||||
</div>
|
||||
<button class="ui basic button" onclick="savePluginStoreURLs()">
|
||||
<i class="ui green save icon"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="ui divider"></div>
|
||||
<div class="field" >
|
||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
</div>
|
||||
<script>
|
||||
let availablePlugins = [];
|
||||
let installedPlugins = [];
|
||||
$(".accordion").accordion();
|
||||
|
||||
function initStoreList(){
|
||||
$.get("/api/plugins/list", function(data) {
|
||||
if (data.error != undefined) {
|
||||
parent.msgbox(data.error, false);
|
||||
return;
|
||||
}else{
|
||||
installedPlugins = data || [];
|
||||
console.log(installedPlugins);
|
||||
}
|
||||
|
||||
$.cjax({
|
||||
url: '/api/plugins/store/list',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
availablePlugins = data || [];
|
||||
populatePluginList(availablePlugins);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
initStoreList();
|
||||
|
||||
/* Plugin Search */
|
||||
function searchPlugins() {
|
||||
const query = document.getElementById('searchInput').value.toLowerCase();
|
||||
const items = document.querySelectorAll('#pluginList .item');
|
||||
if (query.trim() === '') {
|
||||
items.forEach(item => {
|
||||
item.style.display = '';
|
||||
});
|
||||
return;
|
||||
}
|
||||
items.forEach(item => {
|
||||
const name = item.querySelector('.header').textContent.toLowerCase();
|
||||
const description = item.querySelector('.description p').textContent.toLowerCase();
|
||||
const authorElement = item.querySelector('.plugin_author');
|
||||
const author = authorElement ? authorElement.textContent.toLowerCase() : '';
|
||||
const id = item.getAttribute('plugin_id').toLowerCase();
|
||||
|
||||
if (name.includes(query) || description.includes(query) || author.includes(query) || id.includes(query)) {
|
||||
item.style.display = '';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Bind search function to input field and Enter key
|
||||
document.getElementById('searchInput').addEventListener('input', searchPlugins);
|
||||
document.getElementById('searchInput').addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchPlugins();
|
||||
}
|
||||
});
|
||||
|
||||
function forceResyncPlugins() {
|
||||
parent.msgbox("Updating plugin list...", true);
|
||||
document.getElementById('searchInput').value = '';
|
||||
$.cjax({
|
||||
url: '/api/plugins/store/resync',
|
||||
type: 'POST',
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
parent.msgbox("Plugin list updated successfully", true);
|
||||
initStoreList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Plugin Store */
|
||||
function populatePluginList(plugins) {
|
||||
const pluginList = document.getElementById('pluginList');
|
||||
pluginList.innerHTML = ''; // Clear existing items
|
||||
plugins.forEach(plugin => {
|
||||
console.log(plugin);
|
||||
let thisPluginIsInstalled = false;
|
||||
installedPlugins.forEach(installedPlugin => {
|
||||
if (installedPlugin.Spec.id == plugin.PluginIntroSpect.id) {
|
||||
thisPluginIsInstalled = true;
|
||||
}
|
||||
});
|
||||
const item = `
|
||||
<div class="item installablePlugin" plugin_id="${plugin.PluginIntroSpect.id}">
|
||||
<div class="ui tiny image">
|
||||
<img src="${plugin.IconPath}" alt="${plugin.PluginIntroSpect.name}">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="header">${plugin.PluginIntroSpect.name} </div> <a class="section" href="${plugin.PluginIntroSpect.url}" target="_blank"><i class="ui linkify icon"></i></a>
|
||||
<div class="meta">
|
||||
<p>v${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch} by <span class="plugin_author">${plugin.PluginIntroSpect.author}</span></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>${plugin.PluginIntroSpect.description}</p>
|
||||
</div>
|
||||
<div class="action">
|
||||
${thisPluginIsInstalled
|
||||
? `<button class="ui basic circular disabled button">Installed</button>`
|
||||
: `<button class="ui basic circular button" onclick="installPlugin('${plugin.PluginIntroSpect.id}', this);",><i class="ui download icon"></i> Install</button>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#pluginList').append(item);
|
||||
});
|
||||
|
||||
// Reapply search filter if there's a query in the search bar
|
||||
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
|
||||
if (searchQuery.trim() !== '') {
|
||||
searchPlugins();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Plugin Actions */
|
||||
function installPlugin(pluginId, btn=undefined) {
|
||||
if (btn !== undefined) {
|
||||
$(btn).addClass('loading').prop('disabled', true);
|
||||
}
|
||||
$.cjax({
|
||||
url: '/api/plugins/store/install',
|
||||
type: 'POST',
|
||||
data: { "pluginID": pluginId },
|
||||
success: function(data) {
|
||||
if (btn !== undefined) {
|
||||
$(btn).removeClass('loading').prop('disabled', false);
|
||||
}
|
||||
if (data.error != undefined) {
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
parent.msgbox("Plugin installed successfully", true);
|
||||
initStoreList();
|
||||
|
||||
//Also reload the parent plugin list
|
||||
parent.initiatePluginList();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
if (btn !== undefined) {
|
||||
$(btn).removeClass('loading').prop('disabled', false);
|
||||
}
|
||||
parent.msgbox("An error occurred while installing the plugin", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeThisWrapper(){
|
||||
parent.hideSideWrapper(true);
|
||||
}
|
||||
|
||||
/* Advanced Options */
|
||||
function savePluginManagerURLs() {
|
||||
const urls = document.getElementById('pluginStoreURLs').value.split('\n').map(url => url.trim()).filter(url => url !== '');
|
||||
console.log('Saving URLs:', urls);
|
||||
// Add your logic to save the URLs here, e.g., send them to the server
|
||||
$.cjax({
|
||||
url: '/api/plugins/store/saveURLs',
|
||||
type: 'POST',
|
||||
data: { urls },
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
parent.msgbox("URLs saved successfully", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -345,7 +345,7 @@ func HandleZoraxyInfo(w http.ResponseWriter, r *http.Request) {
|
||||
info := ZoraxyInfo{
|
||||
Version: SYSTEM_VERSION,
|
||||
NodeUUID: displayUUID,
|
||||
Development: DEVELOPMENT_BUILD,
|
||||
Development: *development_build,
|
||||
BootTime: displayBootTime,
|
||||
EnableSshLoopback: displayAllowSSHLB,
|
||||
}
|
||||
|
Reference in New Issue
Block a user