34 Commits

Author SHA1 Message Date
0e5550487e Merge pull request #644 from tobychui/v3.2.1
- Merged in authentik forward auth support
- Merged IPv6 whitelist patch
- Added support for basic per host name statistic
- Added experimental plugin store
- Added `$remote_ip` in custom header that filters port number from `$remote_addr`
- Fixed origin is not populated in log bug
- Fixed redirection location rewrite bug
2025-04-27 14:54:27 +08:00
9781735983 Moved dev settings to flag
- Moved internal DEVELOPMENT_MODE flag to start parameters
2025-04-27 13:55:54 +08:00
ffc67ede12 Added working plugin store prototype
- Added plugin install and remove api
2025-04-24 21:19:16 +08:00
6750c7fe3d Added wip plugin store
- Added plugin store snippet
- Added plugin list sync functions
- Work in progress install / uninstall plugin function
2025-04-22 07:15:30 +08:00
36c2c9a00e Merge pull request #641 from james-d-elliott/fix-authelia-headers
fix(authelia): original headers
2025-04-21 18:56:28 +08:00
4f026e8c07 fix(authelia): original headers
This fixes the original headers imparted to authelia.
2025-04-21 13:15:21 +10:00
72b100aab0 Update README.md
Fixed contributor list format
2025-04-19 08:45:57 +08:00
291f12e5ea Update README.md
Added community maintained section contact list
2025-04-19 08:44:22 +08:00
0c8dfd8aa0 Added http proxy list link port
- Added port number and http proto to http proxy list link #635
2025-04-12 11:32:22 +08:00
76e2861fea Merge pull request #633 from WHFo/v3.2.1
Update httprp.html
2025-04-11 20:09:36 +08:00
b23b967165 Update httprp.html
Prevent the browser from filling the saved Zoraxy login account into the  input searchInput
2025-04-11 12:02:28 +08:00
d682d52eb7 Merge pull request #631 from Nirostar/patch-1
Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID
2025-04-08 21:12:07 +08:00
23eeeee701 Move Scope ID handling into CIDR check 2025-04-08 15:06:20 +02:00
e961e52dea Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID 2025-04-08 14:44:11 +02:00
b863a9720f Fixed #629
- Added $remote_ip to remote port number from remote address
2025-04-07 20:02:04 +08:00
ca7cd0476c Updated location rewrite logic
- Updated to a much more relax logic for handling domain with port redirection
2025-04-06 17:05:30 +08:00
a3cccee162 Added "blocks common exploits" module
- Added blocks common exploits prototype
- Added bot detection function (not included in dpcore yet) #615
2025-04-06 16:50:59 +08:00
b9b992a817 Fixed #626
- Added checks for port in hostname redirection in dpcore util
2025-04-06 16:49:44 +08:00
19d5695f1a Merge pull request #627 from andyburri/freebsd
Add FreeBSD amd64 support
2025-04-06 15:21:28 +08:00
bcfc777d15 Feat: Add FreeBSD amd64 support 2025-04-06 07:09:54 +00:00
caa64ada76 Update Dockerfile 2025-04-05 15:41:29 -04:00
ac91a3fef1 Fixed #201 and #608
- Added domain / host name specific statistics
- Added upstream forward request count
2025-04-03 13:35:34 +08:00
05f1743ecd Fixed #681
- Fixed origin is not populated in log bug
2025-04-02 20:11:43 +08:00
d4c1225f75 Merge pull request #568 from JokerQyou/feature/authentik-forward-auth
[WIP] Add Authentik forward auth support
2025-03-31 20:05:52 +08:00
f245a61d32 Merge pull request #616 from SamuelPalubaCZ/main
Create docker-compose.yml
2025-03-31 18:47:40 +08:00
5c2b8e4c31 Create docker-compose.yml
So portainer can use latest compose automaticaly
2025-03-31 01:01:22 +02:00
f6eef46d3f Update README.md
Removed GAN related parameters
2025-03-30 19:18:10 +08:00
3adc669db9 Update main.css
Optimized web css
2025-03-30 19:16:56 +08:00
85201885f0 Update main.css 2025-03-30 12:35:39 +08:00
44b65d1bfa Fixed homepage css bug on mobile view 2025-03-30 12:34:37 +08:00
6cb9e8e427 Fix Docker workflow 2025-03-29 11:34:54 -04:00
d4b1cc8c57 Fixed css bug
- Fixed menu become transport on mobile view
- Fixed css font not correctly loaded on page initiate
2025-03-29 16:09:35 +08:00
6a8057c3a7 fix passing wrong URI to Authentik outpost 2025-03-02 00:04:08 +08:00
ebf6ad6600 Add Authentik forward auth support 2025-03-01 15:29:36 +08:00
48 changed files with 1602 additions and 287 deletions

View File

@ -32,6 +32,7 @@ jobs:
- name: Setup building file structure
run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
cp -lr $GITHUB_WORKSPACE/example/ $GITHUB_WORKSPACE/docker/
- name: Build and push Docker image
uses: docker/build-push-action@v6

View File

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

View File

@ -35,6 +35,8 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
FROM docker.io/golang:bookworm
# 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/

16
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
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
environment:
FASTGEOIP: "true"

View File

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

View File

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

View File

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

View File

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

View File

@ -83,6 +83,7 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
// Register the APIs for Authentication handlers like Authelia and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS)
}
// Register the APIs for redirection rules management functions
@ -233,6 +234,11 @@ 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)
}
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
@ -318,7 +324,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")

View File

@ -13,6 +13,8 @@ import (
"net/http"
"time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
@ -41,9 +43,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_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.1"
/* System Constants */
TMP_FOLDER = "./tmp"
@ -99,8 +100,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 +144,8 @@ var (
pluginManager *plugins.Manager //Plugin manager for managing plugins
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -31,16 +31,23 @@ 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 {
requestHostname := r.Host
if sep.AuthenticationProvider.AuthMethod == 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)
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 == AuthMethodAuthentik {
err := h.handleAuthentikAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
}
@ -51,11 +58,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 +79,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,6 +99,7 @@ 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")
}
@ -106,3 +112,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}
func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r)
}

View File

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

View File

@ -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, "/") {

View File

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

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ package dynamicproxy
*/
import (
_ "embed"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"net"
"net/http"
"sync"
@ -63,7 +64,8 @@ type RouterOption struct {
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
/* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets
@ -143,6 +145,7 @@ const (
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
AuthMethodAuthentik
)
type AuthenticationProvider struct {

View File

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

View File

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

View File

@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
/* Plugin Store */

View File

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

View File

@ -58,6 +58,59 @@ func NewPluginManager(options *ManagerOptions) *Manager {
}
}
// 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()
m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil)
// The default state of the plugin is disabled, so no need to start it
}
}
}
// LoadPluginsFromDisk loads all plugins from the plugin directory
func (m *Manager) LoadPluginsFromDisk() error {
// Load all plugins from the plugin directory
@ -82,7 +135,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 {
@ -258,3 +311,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
View 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)
}

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

View File

@ -29,10 +29,16 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

@ -116,6 +116,7 @@ func ReverseProxtInit() {
WebDirectory: *path_webserver,
AccessController: accessController,
AutheliaRouter: autheliaRouter,
AuthentikRouter: authentikRouter,
LoadBalancer: loadBalancer,
PluginManager: pluginManager,
/* Utilities */
@ -587,6 +588,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
} else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else if authProviderType == 4 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik
} else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}

View File

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

View File

@ -9,6 +9,8 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
@ -98,7 +100,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)
}
@ -148,6 +150,13 @@ func startupSequence() {
Database: sysdb,
})
authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AuthentikURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
})
//Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb,
@ -312,7 +321,10 @@ func startupSequence() {
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
ZoraxyVersion: SYSTEM_VERSION,
ZoraxyUUID: nodeUUID,
DevelopmentBuild: DEVELOPMENT_BUILD,
DevelopmentBuild: *development_build,
},
PluginStoreURLs: []string{
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
},
Database: sysdb,
Logger: SystemWideLogger,
@ -322,9 +334,19 @@ func startupSequence() {
},
})
//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 */

View File

@ -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>
@ -174,6 +187,7 @@
${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 == 0x4?`<i class="ui blue key icon"></i> Authentik`:``}
${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>`:""}
@ -382,6 +396,12 @@
<label>Authelia</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="4" name="authProviderType" ${authProvider==0x4?"checked":""}>
<label>Authentik</label>
</div>
</div>
</div>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
@ -775,4 +795,4 @@
filterProxyList();
});
});
</script>
</script>

View File

@ -185,6 +185,8 @@
</tbody>
</table>
<br>
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
</div>
<script>
@ -480,7 +482,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 +514,6 @@ function initiatePluginList(){
initiatePluginList();
/* Tag Assignment */
/* Plugin Lifecycle */
@ -563,6 +565,33 @@ 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();
}
}
});
}
}
</script>

View File

@ -34,6 +34,27 @@
</form>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Authentik</h3>
<p>Configuration settings for Authentik authentication provider.</p>
<form class="ui form">
<div class="field">
<label for="authentikServerUrl">Authentik Server URL</label>
<input type="text" id="authentikServerUrl" name="authentikServerUrl" placeholder="Enter Authentik Server URL">
<small>Example: auth.example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="authentikUseHttps" name="useHttps">
<label for="authentikUseHttps">Use HTTPS</label>
<small>Check this if your Authentik server uses HTTPS</small>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateAuthentikSettings();"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
</div>
<script>
@ -50,6 +71,18 @@
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
$.cjax({
url: '/api/sso/Authentik',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#authentikServerUrl').val(data.authentikURL);
$('#authentikUseHttps').prop('checked', data.useHTTPS);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
});
function updateAutheliaSettings(){
@ -76,4 +109,28 @@
}
});
}
function updateAuthentikSettings(){
var authentikServerUrl = $('#authentikServerUrl').val();
var useHttps = $('#authentikUseHttps').prop('checked');
$.cjax({
url: '/api/sso/Authentik',
method: 'POST',
data: {
authentikURL: authentikServerUrl,
useHTTPS: useHttps
},
success: function(data) {
if (data.error != undefined) {
$.msgbox(data.error, false);
return;
}
msgbox('Authentik settings updated', true);
console.log('Authentik settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authentik settings:', textStatus, errorThrown);
}
});
}
</script>

View File

@ -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 = {};

View File

@ -524,7 +524,6 @@
$("#tls").checkbox("set checked");
}else{
$(".tlsEnabledOnly").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
//Initiate the input listener on the checkbox

View File

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

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

View File

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