mirror of
				https://github.com/tobychui/zoraxy.git
				synced 2025-11-04 07:54:12 +01:00 
			
		
		
		
	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
This commit is contained in:
		@@ -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")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/def.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/def.go
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/go.mod
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/go.mod
									
									
									
									
									
								
							@@ -5,11 +5,11 @@ go 1.22.0
 | 
			
		||||
toolchain go1.22.2
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/armon/go-radix v1.0.0
 | 
			
		||||
	github.com/boltdb/bolt v1.3.1
 | 
			
		||||
	github.com/docker/docker v27.0.0+incompatible
 | 
			
		||||
	github.com/go-acme/lego/v4 v4.21.0
 | 
			
		||||
	github.com/go-ping/ping v1.1.0
 | 
			
		||||
	github.com/go-session/session v3.1.2+incompatible
 | 
			
		||||
	github.com/google/uuid v1.6.0
 | 
			
		||||
	github.com/gorilla/sessions v1.2.2
 | 
			
		||||
	github.com/gorilla/websocket v1.5.1
 | 
			
		||||
@@ -19,7 +19,6 @@ require (
 | 
			
		||||
	github.com/shirou/gopsutil/v4 v4.25.1
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	golang.org/x/net v0.33.0
 | 
			
		||||
	golang.org/x/sys v0.28.0
 | 
			
		||||
	golang.org/x/text v0.21.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -27,30 +26,22 @@ require (
 | 
			
		||||
	cloud.google.com/go/auth v0.13.0 // indirect
 | 
			
		||||
	cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
 | 
			
		||||
	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
 | 
			
		||||
	github.com/armon/go-radix v1.0.0 // indirect
 | 
			
		||||
	github.com/benbjohnson/clock v1.3.0 // indirect
 | 
			
		||||
	github.com/ebitengine/purego v0.8.2 // indirect
 | 
			
		||||
	github.com/go-ole/go-ole v1.2.6 // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
 | 
			
		||||
	github.com/golang/snappy v0.0.1 // indirect
 | 
			
		||||
	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
 | 
			
		||||
	github.com/monperrus/crawler-user-agents v1.1.0 // indirect
 | 
			
		||||
	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 | 
			
		||||
	github.com/peterhellberg/link v1.2.0 // indirect
 | 
			
		||||
	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
 | 
			
		||||
	github.com/shopspring/decimal v1.3.1 // indirect
 | 
			
		||||
	github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
 | 
			
		||||
	github.com/tidwall/buntdb v1.1.2 // indirect
 | 
			
		||||
	github.com/tidwall/gjson v1.12.1 // indirect
 | 
			
		||||
	github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
 | 
			
		||||
	github.com/tidwall/match v1.1.1 // indirect
 | 
			
		||||
	github.com/tidwall/pretty v1.2.0 // indirect
 | 
			
		||||
	github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
 | 
			
		||||
	github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
 | 
			
		||||
	github.com/tjfoc/gmsm v1.4.1 // indirect
 | 
			
		||||
	github.com/vultr/govultr/v3 v3.9.1 // indirect
 | 
			
		||||
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 | 
			
		||||
	go.mongodb.org/mongo-driver v1.12.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.28.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
@@ -111,11 +102,9 @@ require (
 | 
			
		||||
	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
 | 
			
		||||
	github.com/go-logr/logr v1.4.2 // indirect
 | 
			
		||||
	github.com/go-logr/stdr v1.2.2 // indirect
 | 
			
		||||
	github.com/go-oauth2/oauth2/v4 v4.5.2
 | 
			
		||||
	github.com/go-resty/resty/v2 v2.16.2 // indirect
 | 
			
		||||
	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 | 
			
		||||
	github.com/goccy/go-json v0.10.4 // indirect
 | 
			
		||||
	github.com/gofrs/uuid v4.4.0+incompatible
 | 
			
		||||
	github.com/gogo/protobuf v1.3.2 // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
 | 
			
		||||
	github.com/google/go-querystring v1.1.0 // indirect
 | 
			
		||||
@@ -187,7 +176,6 @@ require (
 | 
			
		||||
	github.com/transip/gotransip/v6 v6.26.0 // indirect
 | 
			
		||||
	github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
 | 
			
		||||
	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
 | 
			
		||||
	github.com/xlzd/gotp v0.1.0
 | 
			
		||||
	github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
 | 
			
		||||
	github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
 | 
			
		||||
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								src/go.sum
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								src/go.sum
									
									
									
									
									
								
							@@ -76,15 +76,11 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB
 | 
			
		||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
			
		||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
 | 
			
		||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
 | 
			
		||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 | 
			
		||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 | 
			
		||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0=
 | 
			
		||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 | 
			
		||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 | 
			
		||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 | 
			
		||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 | 
			
		||||
@@ -186,7 +182,6 @@ github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 | 
			
		||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
			
		||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
 | 
			
		||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 | 
			
		||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 | 
			
		||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 | 
			
		||||
@@ -202,8 +197,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
 | 
			
		||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 | 
			
		||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
 | 
			
		||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
 | 
			
		||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 | 
			
		||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 | 
			
		||||
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
 | 
			
		||||
@@ -222,16 +215,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 | 
			
		||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 | 
			
		||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 | 
			
		||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 | 
			
		||||
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
 | 
			
		||||
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 | 
			
		||||
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
 | 
			
		||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
 | 
			
		||||
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
 | 
			
		||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
 | 
			
		||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 | 
			
		||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 | 
			
		||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 | 
			
		||||
@@ -242,16 +231,11 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5
 | 
			
		||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
 | 
			
		||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
 | 
			
		||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 | 
			
		||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 | 
			
		||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 | 
			
		||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 | 
			
		||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 | 
			
		||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 | 
			
		||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 | 
			
		||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 | 
			
		||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 | 
			
		||||
@@ -307,7 +291,6 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
 | 
			
		||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 | 
			
		||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
 | 
			
		||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
 | 
			
		||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 | 
			
		||||
@@ -320,7 +303,6 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq
 | 
			
		||||
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
 | 
			
		||||
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
 | 
			
		||||
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
 | 
			
		||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 | 
			
		||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 | 
			
		||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
 | 
			
		||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
 | 
			
		||||
@@ -377,8 +359,6 @@ github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6
 | 
			
		||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 | 
			
		||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E=
 | 
			
		||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
 | 
			
		||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
 | 
			
		||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
			
		||||
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
 | 
			
		||||
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
 | 
			
		||||
@@ -395,11 +375,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
 | 
			
		||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
			
		||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
			
		||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 | 
			
		||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 | 
			
		||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 | 
			
		||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
			
		||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 | 
			
		||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
 | 
			
		||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
 | 
			
		||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 | 
			
		||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
 | 
			
		||||
@@ -408,8 +386,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 | 
			
		||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 | 
			
		||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 | 
			
		||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 | 
			
		||||
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
 | 
			
		||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 | 
			
		||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
 | 
			
		||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
@@ -493,11 +469,11 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 | 
			
		||||
github.com/monperrus/crawler-user-agents v1.1.0 h1:Xy8ZrhizT+y2FONWFFdKOP+3BhH97BDLuG7W/MswoGI=
 | 
			
		||||
github.com/monperrus/crawler-user-agents v1.1.0/go.mod h1:GfRyKbsbxSrRxTPYnVi4U/0stQd6BcFCxDy6i6IxQ0M=
 | 
			
		||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 | 
			
		||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 | 
			
		||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 | 
			
		||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
 | 
			
		||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
 | 
			
		||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
			
		||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
 | 
			
		||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
 | 
			
		||||
@@ -531,7 +507,6 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
 | 
			
		||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 | 
			
		||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
 | 
			
		||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
 | 
			
		||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 | 
			
		||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 | 
			
		||||
@@ -609,10 +584,7 @@ github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D
 | 
			
		||||
github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs=
 | 
			
		||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
 | 
			
		||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
 | 
			
		||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
 | 
			
		||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 | 
			
		||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 | 
			
		||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 | 
			
		||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
 | 
			
		||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
 | 
			
		||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 | 
			
		||||
@@ -628,7 +600,6 @@ github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHei
 | 
			
		||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 | 
			
		||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
 | 
			
		||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
 | 
			
		||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 | 
			
		||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 | 
			
		||||
github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA=
 | 
			
		||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
 | 
			
		||||
@@ -661,7 +632,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
			
		||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 | 
			
		||||
@@ -675,25 +645,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krc
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M=
 | 
			
		||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
 | 
			
		||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
 | 
			
		||||
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
 | 
			
		||||
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
 | 
			
		||||
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
 | 
			
		||||
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
 | 
			
		||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 | 
			
		||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
 | 
			
		||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
 | 
			
		||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
 | 
			
		||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 | 
			
		||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 | 
			
		||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 | 
			
		||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 | 
			
		||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 | 
			
		||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
 | 
			
		||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
 | 
			
		||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
 | 
			
		||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
 | 
			
		||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
 | 
			
		||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
 | 
			
		||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 | 
			
		||||
@@ -705,11 +656,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
 | 
			
		||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 | 
			
		||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
 | 
			
		||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 | 
			
		||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 | 
			
		||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 | 
			
		||||
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
 | 
			
		||||
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
 | 
			
		||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 | 
			
		||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
 | 
			
		||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
 | 
			
		||||
github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8=
 | 
			
		||||
@@ -717,27 +663,12 @@ github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+H
 | 
			
		||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 | 
			
		||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
 | 
			
		||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 | 
			
		||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 | 
			
		||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 | 
			
		||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
 | 
			
		||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 | 
			
		||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 | 
			
		||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 | 
			
		||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 | 
			
		||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
 | 
			
		||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
 | 
			
		||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 | 
			
		||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
 | 
			
		||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg=
 | 
			
		||||
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
 | 
			
		||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw=
 | 
			
		||||
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY=
 | 
			
		||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 | 
			
		||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 | 
			
		||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
 | 
			
		||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 | 
			
		||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
 | 
			
		||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
 | 
			
		||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 | 
			
		||||
@@ -783,7 +714,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 | 
			
		||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
@@ -861,7 +791,6 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
 | 
			
		||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 | 
			
		||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
@@ -903,7 +832,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
@@ -919,7 +847,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
@@ -1009,7 +936,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
 | 
			
		||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
			
		||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
			
		||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 | 
			
		||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
			
		||||
@@ -1071,7 +997,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										169
									
								
								src/mod/auth/sso/authentik/authentik.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/mod/auth/sso/authentik/authentik.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
package authentik
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"imuslab.com/zoraxy/mod/database"
 | 
			
		||||
	"imuslab.com/zoraxy/mod/info/logger"
 | 
			
		||||
	"imuslab.com/zoraxy/mod/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthentikRouterOptions struct {
 | 
			
		||||
	UseHTTPS     bool   //If the Authentik server is using HTTPS
 | 
			
		||||
	AuthentikURL string //The URL of the Authentik server
 | 
			
		||||
	Logger       *logger.Logger
 | 
			
		||||
	Database     *database.Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AuthentikRouter struct {
 | 
			
		||||
	options *AuthentikRouterOptions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAuthentikRouter creates a new AuthentikRouter object
 | 
			
		||||
func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter {
 | 
			
		||||
	options.Database.NewTable("authentik")
 | 
			
		||||
 | 
			
		||||
	//Read settings from database, if exists
 | 
			
		||||
	options.Database.Read("authentik", "authentikURL", &options.AuthentikURL)
 | 
			
		||||
	options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS)
 | 
			
		||||
 | 
			
		||||
	return &AuthentikRouter{
 | 
			
		||||
		options: options,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS
 | 
			
		||||
func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.Method == http.MethodGet {
 | 
			
		||||
		//Return the current settings
 | 
			
		||||
		js, _ := json.Marshal(map[string]interface{}{
 | 
			
		||||
			"useHTTPS":     ar.options.UseHTTPS,
 | 
			
		||||
			"authentikURL": ar.options.AuthentikURL,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		utils.SendJSONResponse(w, string(js))
 | 
			
		||||
		return
 | 
			
		||||
	} else if r.Method == http.MethodPost {
 | 
			
		||||
		//Update the settings
 | 
			
		||||
		AuthentikURL, err := utils.PostPara(r, "authentikURL")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			utils.SendErrorResponse(w, "authentikURL not found")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		useHTTPS, err := utils.PostBool(r, "useHTTPS")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			useHTTPS = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Write changes to runtime
 | 
			
		||||
		ar.options.AuthentikURL = AuthentikURL
 | 
			
		||||
		ar.options.UseHTTPS = useHTTPS
 | 
			
		||||
 | 
			
		||||
		//Write changes to database
 | 
			
		||||
		ar.options.Database.Write("authentik", "authentikURL", AuthentikURL)
 | 
			
		||||
		ar.options.Database.Write("authentik", "useHTTPS", useHTTPS)
 | 
			
		||||
 | 
			
		||||
		utils.SendOK(w)
 | 
			
		||||
	} else {
 | 
			
		||||
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleAuthentikAuth is the internal handler for Authentik authentication
 | 
			
		||||
// Set useHTTPS to true if your Authentik server is using HTTPS
 | 
			
		||||
// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com
 | 
			
		||||
func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
 | 
			
		||||
	const outpostPrefix = "outpost.goauthentik.io"
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
 | 
			
		||||
	if ar.options.AuthentikURL == "" {
 | 
			
		||||
		ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil)
 | 
			
		||||
		w.WriteHeader(500)
 | 
			
		||||
		w.Write([]byte("500 - Internal Server Error"))
 | 
			
		||||
		return errors.New("authentik URL not set")
 | 
			
		||||
	}
 | 
			
		||||
	protocol := "http"
 | 
			
		||||
	if ar.options.UseHTTPS {
 | 
			
		||||
		protocol = "https"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authentikBaseURL := protocol + "://" + ar.options.AuthentikURL
 | 
			
		||||
	//Remove tailing slash if any
 | 
			
		||||
	authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/")
 | 
			
		||||
 | 
			
		||||
	scheme := "http"
 | 
			
		||||
	if r.TLS != nil {
 | 
			
		||||
		scheme = "https"
 | 
			
		||||
	}
 | 
			
		||||
	reqUrl := scheme + "://" + r.Host + r.RequestURI
 | 
			
		||||
	// Pass request to outpost if path matches outpost prefix
 | 
			
		||||
	if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) {
 | 
			
		||||
		req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
 | 
			
		||||
			w.WriteHeader(401)
 | 
			
		||||
			return errors.New("unauthorized")
 | 
			
		||||
		}
 | 
			
		||||
		req.Header.Set("X-Original-URL", reqUrl)
 | 
			
		||||
		req.Header.Set("Host", r.Host)
 | 
			
		||||
		for _, cookie := range r.Cookies() {
 | 
			
		||||
			req.AddCookie(cookie)
 | 
			
		||||
		}
 | 
			
		||||
		if resp, err := client.Do(req); err != nil {
 | 
			
		||||
			ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err)
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
			return errors.New("internal server error")
 | 
			
		||||
		} else {
 | 
			
		||||
			defer resp.Body.Close()
 | 
			
		||||
			for k := range resp.Header {
 | 
			
		||||
				w.Header().Set(k, resp.Header.Get(k))
 | 
			
		||||
			}
 | 
			
		||||
			w.WriteHeader(resp.StatusCode)
 | 
			
		||||
			if _, err = io.Copy(w, resp.Body); err != nil {
 | 
			
		||||
				ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err)
 | 
			
		||||
				w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
				return errors.New("internal server error")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Make a request to Authentik to verify the request
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
 | 
			
		||||
		w.WriteHeader(401)
 | 
			
		||||
		return errors.New("unauthorized")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("X-Original-URL", reqUrl)
 | 
			
		||||
 | 
			
		||||
	// Copy cookies from the incoming request
 | 
			
		||||
	for _, cookie := range r.Cookies() {
 | 
			
		||||
		req.AddCookie(cookie)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Making the verification request
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err)
 | 
			
		||||
		w.WriteHeader(401)
 | 
			
		||||
		return errors.New("unauthorized")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != 200 {
 | 
			
		||||
		redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String())
 | 
			
		||||
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
 | 
			
		||||
		return errors.New("unauthorized")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -48,7 +48,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	//Check if this is a redirection url
 | 
			
		||||
	if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
 | 
			
		||||
		statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
 | 
			
		||||
		h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", "")
 | 
			
		||||
		h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", r.Host, "")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +79,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if sep.RequireRateLimit {
 | 
			
		||||
			err := h.handleRateLimitRouting(w, r, sep)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
 | 
			
		||||
				h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307, r.Host, "")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -110,7 +110,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
 | 
			
		||||
				//Missing tailing slash. Redirect to target proxy endpoint
 | 
			
		||||
				http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
 | 
			
		||||
				h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307)
 | 
			
		||||
				h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307, r.Host, "")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -186,6 +186,9 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Do not log default site requests to avoid flooding the logs
 | 
			
		||||
		//h.Parent.logRequest(r, false, 307, "root", domainOnly, "")
 | 
			
		||||
 | 
			
		||||
		//No vdir match. Route via root router
 | 
			
		||||
		h.hostRequest(w, r, h.Parent.Root)
 | 
			
		||||
	case DefaultSite_Redirect:
 | 
			
		||||
@@ -208,19 +211,19 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
		}
 | 
			
		||||
		hostname := parsedURL.Hostname()
 | 
			
		||||
		if hostname == domainOnly {
 | 
			
		||||
			h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly)
 | 
			
		||||
			h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly, "")
 | 
			
		||||
			http.Error(w, "Loopback redirects due to invalid settings", 500)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly)
 | 
			
		||||
		h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly, "")
 | 
			
		||||
		http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
 | 
			
		||||
	case DefaultSite_NotFoundPage:
 | 
			
		||||
		//Serve the not found page, use template if exists
 | 
			
		||||
		h.serve404PageWithTemplate(w, r)
 | 
			
		||||
	case DefaultSite_NoResponse:
 | 
			
		||||
		//No response. Just close the connection
 | 
			
		||||
		h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly)
 | 
			
		||||
		h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly, "")
 | 
			
		||||
		hijacker, ok := w.(http.Hijacker)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			w.WriteHeader(http.StatusNoContent)
 | 
			
		||||
@@ -234,11 +237,11 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
		conn.Close()
 | 
			
		||||
	case DefaultSite_TeaPot:
 | 
			
		||||
		//I'm a teapot
 | 
			
		||||
		h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
 | 
			
		||||
		h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly, "")
 | 
			
		||||
		http.Error(w, "I'm a teapot", http.StatusTeapot)
 | 
			
		||||
	default:
 | 
			
		||||
		//Unknown routing option. Send empty response
 | 
			
		||||
		h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
 | 
			
		||||
		h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly, "")
 | 
			
		||||
		http.Error(w, "544 - No Route Defined", 544)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
 | 
			
		||||
 | 
			
		||||
	isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
 | 
			
		||||
	if isBlocked {
 | 
			
		||||
		h.Parent.logRequest(r, false, 403, blockedReason, "")
 | 
			
		||||
		h.Parent.logRequest(r, false, 403, blockedReason, r.Host, "")
 | 
			
		||||
	}
 | 
			
		||||
	return isBlocked
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,16 +31,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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
 | 
			
		||||
	thisTransporter := http.DefaultTransport
 | 
			
		||||
 | 
			
		||||
	//Hack the default transporter to handle more connections
 | 
			
		||||
 | 
			
		||||
	optimalConcurrentConnection := 256
 | 
			
		||||
	if dpcOptions.MaxConcurrentConnection > 0 {
 | 
			
		||||
		optimalConcurrentConnection = dpcOptions.MaxConcurrentConnection
 | 
			
		||||
@@ -137,18 +136,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func singleJoiningSlash(a, b string) string {
 | 
			
		||||
	aslash := strings.HasSuffix(a, "/")
 | 
			
		||||
	bslash := strings.HasPrefix(b, "/")
 | 
			
		||||
	switch {
 | 
			
		||||
	case aslash && bslash:
 | 
			
		||||
		return a + b[1:]
 | 
			
		||||
	case !aslash && !bslash:
 | 
			
		||||
		return a + "/" + b
 | 
			
		||||
	}
 | 
			
		||||
	return a + b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func joinURLPath(a, b *url.URL) (path, rawpath string) {
 | 
			
		||||
	apath, bpath := a.EscapedPath(), b.EscapedPath()
 | 
			
		||||
	aslash, bslash := strings.HasSuffix(apath, "/"), strings.HasPrefix(bpath, "/")
 | 
			
		||||
@@ -352,7 +339,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
 | 
			
		||||
			}
 | 
			
		||||
		} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
 | 
			
		||||
			//Back to the root of this proxy object
 | 
			
		||||
			//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
 | 
			
		||||
			locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation
 | 
			
		||||
		} else {
 | 
			
		||||
			//Relative path. Do not modifiy location header
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,24 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
 | 
			
		||||
		//Do not modify location header
 | 
			
		||||
		return urlString, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Issue #626: Check if the location header is another subdomain with port
 | 
			
		||||
	//E.g. Proxy config: blog.example.com -> 127.0.0.1:80
 | 
			
		||||
	//Check if it is actually redirecting to (*.)blog.example.com:8080 instead of current domain
 | 
			
		||||
	//like Location: http://x.blog.example.com:1234/
 | 
			
		||||
	_, newLocationPort, err := net.SplitHostPort(u.Host)
 | 
			
		||||
	if (newLocationPort == "80" || newLocationPort == "443") && err == nil {
 | 
			
		||||
		//Port 80 or 443, some web server use this to switch between http and https
 | 
			
		||||
		//E.g. http://example.com:80 -> https://example.com:443
 | 
			
		||||
		//E.g. http://example.com:443 -> https://example.com:80
 | 
			
		||||
		//That usually means the user have invalidly configured the web server to use port 80 or 443
 | 
			
		||||
		//for http or https. We should not modify the location header in this case.
 | 
			
		||||
 | 
			
		||||
	} else if strings.Contains(u.Host, ":") && err == nil {
 | 
			
		||||
		//Other port numbers. Do not modify location header
 | 
			
		||||
		return urlString, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	u.Host = rrr.OriginalHost
 | 
			
		||||
 | 
			
		||||
	if strings.Contains(rrr.ProxyDomain, "/") {
 | 
			
		||||
 
 | 
			
		||||
@@ -155,7 +155,7 @@ func (router *Router) StartProxyService() error {
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							http.ServeFile(w, r, "./web/hosterror.html")
 | 
			
		||||
							router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
 | 
			
		||||
							router.logRequest(r, false, 404, "vdir-http", r.Host)
 | 
			
		||||
							router.logRequest(r, false, 404, "vdir-http", r.Host, "")
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								src/mod/dynamicproxy/exploits/exploits.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/mod/dynamicproxy/exploits/exploits.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
package exploits
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	exploits.go
 | 
			
		||||
 | 
			
		||||
	This file is used to define routing rules that blocks common exploits.
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	_ "embed"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
 | 
			
		||||
	agents "github.com/monperrus/crawler-user-agents"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Detector struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewExploitDetector() *Detector {
 | 
			
		||||
	return &Detector{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RequestContainCommonExploits checks if the request contains common exploits
 | 
			
		||||
// such as SQL injection, file injection, and other common attack patterns.
 | 
			
		||||
func (d *Detector) RequestContainCommonExploits(r *http.Request) bool {
 | 
			
		||||
	query := r.URL.RawQuery
 | 
			
		||||
	userAgent := r.UserAgent()
 | 
			
		||||
 | 
			
		||||
	// Block SQL injections
 | 
			
		||||
	sqlInjectionPatterns := []string{
 | 
			
		||||
		`union.*select.*\(`,
 | 
			
		||||
		`union.*all.*select.*`,
 | 
			
		||||
		`concat.*\(`,
 | 
			
		||||
	}
 | 
			
		||||
	for _, pattern := range sqlInjectionPatterns {
 | 
			
		||||
		if match, _ := regexp.MatchString(pattern, query); match {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Block file injections
 | 
			
		||||
	fileInjectionPatterns := []string{
 | 
			
		||||
		`[a-zA-Z0-9_]=http://`,
 | 
			
		||||
		`[a-zA-Z0-9_]=(\.\.//?)+`,
 | 
			
		||||
		`[a-zA-Z0-9_]=/([a-z0-9_.]//?)+`,
 | 
			
		||||
	}
 | 
			
		||||
	for _, pattern := range fileInjectionPatterns {
 | 
			
		||||
		if match, _ := regexp.MatchString(pattern, query); match {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Block common exploits
 | 
			
		||||
	commonExploitPatterns := []string{
 | 
			
		||||
		`(<|%3C).*script.*(>|%3E)`,
 | 
			
		||||
		`GLOBALS(=|\[|\%[0-9A-Z]{0,2})`,
 | 
			
		||||
		`_REQUEST(=|\[|\%[0-9A-Z]{0,2})`,
 | 
			
		||||
		`proc/self/environ`,
 | 
			
		||||
		`mosConfig_[a-zA-Z_]{1,21}(=|\%3D)`,
 | 
			
		||||
		`base64_(en|de)code\(.*\)`,
 | 
			
		||||
	}
 | 
			
		||||
	for _, pattern := range commonExploitPatterns {
 | 
			
		||||
		if match, _ := regexp.MatchString(pattern, query); match {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Block spam
 | 
			
		||||
	spamPatterns := []string{
 | 
			
		||||
		`\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b`,
 | 
			
		||||
		`\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b`,
 | 
			
		||||
		`\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b`,
 | 
			
		||||
		`\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b`,
 | 
			
		||||
	}
 | 
			
		||||
	for _, pattern := range spamPatterns {
 | 
			
		||||
		if match, _ := regexp.MatchString(pattern, query); match {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Block user agents
 | 
			
		||||
	userAgentPatterns := []string{
 | 
			
		||||
		`Indy Library`,
 | 
			
		||||
		`libwww-perl`,
 | 
			
		||||
		`GetRight`,
 | 
			
		||||
		`GetWeb!`,
 | 
			
		||||
		`Go!Zilla`,
 | 
			
		||||
		`Download Demon`,
 | 
			
		||||
		`Go-Ahead-Got-It`,
 | 
			
		||||
		`TurnitinBot`,
 | 
			
		||||
		`GrabNet`,
 | 
			
		||||
	}
 | 
			
		||||
	for _, pattern := range userAgentPatterns {
 | 
			
		||||
		if match, _ := regexp.MatchString(pattern, userAgent); match {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RequestIsMadeByBots checks if the request is made by bots or crawlers
 | 
			
		||||
func (d *Detector) RequestIsMadeByBots(r *http.Request) bool {
 | 
			
		||||
	userAgent := r.UserAgent()
 | 
			
		||||
	return agents.IsCrawler(userAgent)
 | 
			
		||||
}
 | 
			
		||||
@@ -116,13 +116,13 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
 | 
			
		||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
 | 
			
		||||
	r.Header.Set("X-Forwarded-Host", r.Host)
 | 
			
		||||
	r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
 | 
			
		||||
 | 
			
		||||
	reqHostname := r.Host
 | 
			
		||||
	/* Load balancing */
 | 
			
		||||
	selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.ServeFile(w, r, "./web/rperror.html")
 | 
			
		||||
		h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
 | 
			
		||||
		h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
 | 
			
		||||
		h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname(), r.Host)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +144,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
		if selectedUpstream.RequireTLS {
 | 
			
		||||
			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
 | 
			
		||||
		}
 | 
			
		||||
		h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
 | 
			
		||||
		h.Parent.logRequest(r, true, 101, "host-websocket", reqHostname, selectedUpstream.OriginIpOrDomain)
 | 
			
		||||
 | 
			
		||||
		if target.HeaderRewriteRules == nil {
 | 
			
		||||
			target.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
 | 
			
		||||
@@ -161,12 +161,11 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	originalHostHeader := r.Host
 | 
			
		||||
	if r.URL != nil {
 | 
			
		||||
		r.Host = r.URL.Host
 | 
			
		||||
	} else {
 | 
			
		||||
		//Fallback when the upstream proxy screw something up in the header
 | 
			
		||||
		r.URL, _ = url.Parse(originalHostHeader)
 | 
			
		||||
		r.URL, _ = url.Parse(reqHostname)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Populate the user-defined headers with the values from the request
 | 
			
		||||
@@ -188,7 +187,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
	//Handle the request reverse proxy
 | 
			
		||||
	statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
 | 
			
		||||
		ProxyDomain:         selectedUpstream.OriginIpOrDomain,
 | 
			
		||||
		OriginalHost:        originalHostHeader,
 | 
			
		||||
		OriginalHost:        reqHostname,
 | 
			
		||||
		UseTLS:              selectedUpstream.RequireTLS,
 | 
			
		||||
		NoCache:             h.Parent.Option.NoCache,
 | 
			
		||||
		PathPrefix:          "",
 | 
			
		||||
@@ -201,28 +200,28 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
 | 
			
		||||
	//validate the error
 | 
			
		||||
	var dnsError *net.DNSError
 | 
			
		||||
	upstreamHostname := selectedUpstream.OriginIpOrDomain
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.As(err, &dnsError) {
 | 
			
		||||
			http.ServeFile(w, r, "./web/hosterror.html")
 | 
			
		||||
			h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
 | 
			
		||||
			h.Parent.logRequest(r, false, 404, "host-http", reqHostname, upstreamHostname)
 | 
			
		||||
		} else if errors.Is(err, context.Canceled) {
 | 
			
		||||
			//Request canceled by client, usually due to manual refresh before page load
 | 
			
		||||
			http.Error(w, "Request canceled", http.StatusRequestTimeout)
 | 
			
		||||
			h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", r.URL.Hostname())
 | 
			
		||||
			h.Parent.logRequest(r, false, http.StatusRequestTimeout, "host-http", reqHostname, upstreamHostname)
 | 
			
		||||
		} else {
 | 
			
		||||
			http.ServeFile(w, r, "./web/rperror.html")
 | 
			
		||||
			h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
 | 
			
		||||
			h.Parent.logRequest(r, false, 521, "host-http", reqHostname, upstreamHostname)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname())
 | 
			
		||||
	h.Parent.logRequest(r, true, statusCode, "host-http", reqHostname, upstreamHostname)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handle vdir type request
 | 
			
		||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
 | 
			
		||||
	rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
 | 
			
		||||
	r.URL, _ = url.Parse(rewriteURL)
 | 
			
		||||
 | 
			
		||||
	r.Header.Set("X-Forwarded-Host", r.Host)
 | 
			
		||||
	r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
 | 
			
		||||
 | 
			
		||||
@@ -242,7 +241,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
			target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
 | 
			
		||||
		h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain)
 | 
			
		||||
		wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
 | 
			
		||||
			SkipTLSValidation:  target.SkipCertValidations,
 | 
			
		||||
			SkipOriginCheck:    target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
 | 
			
		||||
@@ -254,12 +253,12 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	originalHostHeader := r.Host
 | 
			
		||||
	reqHostname := r.Host
 | 
			
		||||
	if r.URL != nil {
 | 
			
		||||
		r.Host = r.URL.Host
 | 
			
		||||
	} else {
 | 
			
		||||
		//Fallback when the upstream proxy screw something up in the header
 | 
			
		||||
		r.URL, _ = url.Parse(originalHostHeader)
 | 
			
		||||
		r.URL, _ = url.Parse(reqHostname)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Populate the user-defined headers with the values from the request
 | 
			
		||||
@@ -282,7 +281,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
	//Handle the virtual directory reverse proxy request
 | 
			
		||||
	statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
 | 
			
		||||
		ProxyDomain:         target.Domain,
 | 
			
		||||
		OriginalHost:        originalHostHeader,
 | 
			
		||||
		OriginalHost:        reqHostname,
 | 
			
		||||
		UseTLS:              target.RequireTLS,
 | 
			
		||||
		PathPrefix:          target.MatchingPath,
 | 
			
		||||
		UpstreamHeaders:     upstreamHeaders,
 | 
			
		||||
@@ -296,19 +295,19 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 | 
			
		||||
		if errors.As(err, &dnsError) {
 | 
			
		||||
			http.ServeFile(w, r, "./web/hosterror.html")
 | 
			
		||||
			log.Println(err.Error())
 | 
			
		||||
			h.Parent.logRequest(r, false, 404, "vdir-http", target.Domain)
 | 
			
		||||
			h.Parent.logRequest(r, false, 404, "vdir-http", reqHostname, target.Domain)
 | 
			
		||||
		} else {
 | 
			
		||||
			http.ServeFile(w, r, "./web/rperror.html")
 | 
			
		||||
			log.Println(err.Error())
 | 
			
		||||
			h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain)
 | 
			
		||||
			h.Parent.logRequest(r, false, 521, "vdir-http", reqHostname, target.Domain)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain)
 | 
			
		||||
	h.Parent.logRequest(r, true, statusCode, "vdir-http", reqHostname, target.Domain)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This logger collect data for the statistical analysis. For log to file logger, check the Logger and LogHTTPRequest handler
 | 
			
		||||
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
 | 
			
		||||
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, originalHostname string, upstreamHostname string) {
 | 
			
		||||
	if router.Option.StatisticCollector != nil {
 | 
			
		||||
		go func() {
 | 
			
		||||
			requestInfo := statistic.RequestInfo{
 | 
			
		||||
@@ -320,10 +319,11 @@ func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, for
 | 
			
		||||
				Referer:                       r.Referer(),
 | 
			
		||||
				UserAgent:                     r.UserAgent(),
 | 
			
		||||
				RequestURL:                    r.Host + r.RequestURI,
 | 
			
		||||
				Target:                        target,
 | 
			
		||||
				Target:                        originalHostname,
 | 
			
		||||
				Upstream:                      upstreamHostname,
 | 
			
		||||
			}
 | 
			
		||||
			router.Option.StatisticCollector.RecordRequest(requestInfo)
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
	router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode)
 | 
			
		||||
	router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode, originalHostname, upstreamHostname)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ func (t *RequestCountPerIpTable) Clear() {
 | 
			
		||||
func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
 | 
			
		||||
	err := h.Parent.handleRateLimit(w, r, pe)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname())
 | 
			
		||||
		h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname(), "")
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package rewrite
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,6 +15,11 @@ func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
 | 
			
		||||
	// Request-specific variables
 | 
			
		||||
	vars["$host"] = r.Host
 | 
			
		||||
	vars["$remote_addr"] = r.RemoteAddr
 | 
			
		||||
	remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		remoteIP = r.RemoteAddr // Fallback to the full RemoteAddr if parsing fails
 | 
			
		||||
	}
 | 
			
		||||
	vars["$remote_ip"] = remoteIP
 | 
			
		||||
	vars["$request_uri"] = r.RequestURI
 | 
			
		||||
	vars["$request_method"] = r.Method
 | 
			
		||||
	vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// Log HTTP request. Note that this must run in go routine to prevent any blocking
 | 
			
		||||
// in reverse proxy router
 | 
			
		||||
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int) {
 | 
			
		||||
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int, downstreamHostname string, upstreamHostname string) {
 | 
			
		||||
	go func() {
 | 
			
		||||
		l.ValidateAndUpdateLogFilepath()
 | 
			
		||||
		if l.logger == nil || l.file == nil {
 | 
			
		||||
@@ -26,7 +26,9 @@ func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int
 | 
			
		||||
		clientIP := netutils.GetRequesterIP(r)
 | 
			
		||||
		requestURI := r.RequestURI
 | 
			
		||||
		statusCodeString := strconv.Itoa(statusCode)
 | 
			
		||||
		//fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
 | 
			
		||||
		l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString)
 | 
			
		||||
 | 
			
		||||
		//Pretty print for debugging
 | 
			
		||||
		//fmt.Printf("------------\nRequest URL: %s (class: %s) \nUpstream Hostname: %s\nDownstream Hostname: %s\nStatus Code: %s\n", r.URL, reqclass, upstreamHostname, downstreamHostname, statusCodeString)
 | 
			
		||||
		l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + downstreamHostname + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString)
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,11 @@ func MatchIpWildcard(ipAddress, wildcard string) bool {
 | 
			
		||||
 | 
			
		||||
// Match ip address with CIDR
 | 
			
		||||
func MatchIpCIDR(ip string, cidr string) bool {
 | 
			
		||||
	// Trim away scope ID if present in IP (e.g. fe80::1%eth0)
 | 
			
		||||
	if i := strings.Index(ip, "%"); i != -1 {
 | 
			
		||||
		ip = ip[:i]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// parse the CIDR string
 | 
			
		||||
	_, cidrnet, err := net.ParseCIDR(cidr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	utils.SendOK(w)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Plugin Store */
 | 
			
		||||
 
 | 
			
		||||
@@ -274,13 +274,10 @@ func (m *Manager) StopPlugin(pluginID string) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check if the plugin is still running
 | 
			
		||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
 | 
			
		||||
func (m *Manager) PluginIsRunning(pluginID string) bool {
 | 
			
		||||
	plugin, err := m.GetPluginByID(pluginID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if plugin.process == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return plugin.process.ProcessState == nil
 | 
			
		||||
	return plugin.IsRunning()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										356
									
								
								src/mod/plugins/store.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,356 @@
 | 
			
		||||
package plugins
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
 | 
			
		||||
	"imuslab.com/zoraxy/mod/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	Plugin Store
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// See https://github.com/aroz-online/zoraxy-official-plugins/blob/main/directories/index.json for the standard format
 | 
			
		||||
 | 
			
		||||
type Checksums struct {
 | 
			
		||||
	LinuxAmd64   string `json:"linux_amd64"`
 | 
			
		||||
	Linux386     string `json:"linux_386"`
 | 
			
		||||
	LinuxArm     string `json:"linux_arm"`
 | 
			
		||||
	LinuxArm64   string `json:"linux_arm64"`
 | 
			
		||||
	LinuxMipsle  string `json:"linux_mipsle"`
 | 
			
		||||
	LinuxRiscv64 string `json:"linux_riscv64"`
 | 
			
		||||
	WindowsAmd64 string `json:"windows_amd64"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DownloadablePlugin struct {
 | 
			
		||||
	IconPath         string
 | 
			
		||||
	PluginIntroSpect zoraxy_plugin.IntroSpect //Plugin introspect information
 | 
			
		||||
	ChecksumsSHA256  Checksums                //Checksums for the plugin binary
 | 
			
		||||
	DownloadURLs     map[string]string        //Download URLs for different platforms
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Plugin Store Index List Sync */
 | 
			
		||||
//Update the plugin list from the plugin store URLs
 | 
			
		||||
func (m *Manager) UpdateDownloadablePluginList() error {
 | 
			
		||||
	//Get downloadable plugins from each of the plugin store URLS
 | 
			
		||||
	m.Options.DownloadablePluginCache = []*DownloadablePlugin{}
 | 
			
		||||
	for _, url := range m.Options.PluginStoreURLs {
 | 
			
		||||
		pluginList, err := m.getPluginListFromURL(url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to get plugin list from %s: %w", url, err)
 | 
			
		||||
		}
 | 
			
		||||
		m.Options.DownloadablePluginCache = append(m.Options.DownloadablePluginCache, pluginList...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.Options.LastSuccPluginSyncTime = time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get the plugin list from the URL
 | 
			
		||||
func (m *Manager) getPluginListFromURL(url string) ([]*DownloadablePlugin, error) {
 | 
			
		||||
	//Get the plugin list from the URL
 | 
			
		||||
	resp, err := http.Get(url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return nil, fmt.Errorf("failed to get plugin list from %s: %s", url, resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var pluginList []*DownloadablePlugin
 | 
			
		||||
	content, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to read plugin list from %s: %w", url, err)
 | 
			
		||||
	}
 | 
			
		||||
	content = []byte(strings.TrimSpace(string(content)))
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(content, &pluginList)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to unmarshal plugin list from %s: %w", url, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return pluginList, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin {
 | 
			
		||||
	//List all downloadable plugins
 | 
			
		||||
	if len(m.Options.DownloadablePluginCache) == 0 {
 | 
			
		||||
		return []*DownloadablePlugin{}
 | 
			
		||||
	}
 | 
			
		||||
	return m.Options.DownloadablePluginCache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InstallPlugin installs the given plugin by moving it to the PluginDir.
 | 
			
		||||
func (m *Manager) InstallPlugin(plugin *DownloadablePlugin) error {
 | 
			
		||||
	pluginDir := filepath.Join(m.Options.PluginDir, plugin.PluginIntroSpect.Name)
 | 
			
		||||
	pluginFile := plugin.PluginIntroSpect.Name
 | 
			
		||||
	if runtime.GOOS == "windows" {
 | 
			
		||||
		pluginFile += ".exe"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Check if the plugin id already exists in runtime plugin map
 | 
			
		||||
	if _, ok := m.LoadedPlugins[plugin.PluginIntroSpect.ID]; ok {
 | 
			
		||||
		return fmt.Errorf("plugin already installed: %s", plugin.PluginIntroSpect.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create the plugin directory if it doesn't exist
 | 
			
		||||
	err := os.MkdirAll(pluginDir, os.ModePerm)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create plugin directory: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Download the plugin binary
 | 
			
		||||
	downloadURL, ok := plugin.DownloadURLs[runtime.GOOS+"_"+runtime.GOARCH]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("no download URL available for the current platform")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Get(downloadURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to download plugin: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return fmt.Errorf("failed to download plugin: %s", resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write the plugin binary to the plugin directory
 | 
			
		||||
	pluginPath := filepath.Join(pluginDir, pluginFile)
 | 
			
		||||
	out, err := os.Create(pluginPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create plugin file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(out, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		out.Close()
 | 
			
		||||
		return fmt.Errorf("failed to write plugin file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make the plugin executable
 | 
			
		||||
	err = os.Chmod(pluginPath, 0755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		out.Close()
 | 
			
		||||
		return fmt.Errorf("failed to set executable permissions: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Verify the checksum of the downloaded plugin binary
 | 
			
		||||
	checksums, err := plugin.ChecksumsSHA256.GetCurrentPlatformChecksum()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		if !verifyChecksumForFile(pluginPath, checksums) {
 | 
			
		||||
			out.Close()
 | 
			
		||||
			return fmt.Errorf("checksum verification failed for plugin binary")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Ok, also download the icon if exists
 | 
			
		||||
	if plugin.IconPath != "" {
 | 
			
		||||
		iconURL := strings.TrimSpace(plugin.IconPath)
 | 
			
		||||
		if iconURL != "" {
 | 
			
		||||
			resp, err := http.Get(iconURL)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to download plugin icon: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
			//Save the icon to the plugin directory
 | 
			
		||||
			iconPath := filepath.Join(pluginDir, "icon.png")
 | 
			
		||||
			out, err := os.Create(iconPath)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to create plugin icon file: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			defer out.Close()
 | 
			
		||||
 | 
			
		||||
			io.Copy(out, resp.Body)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//Close the plugin exeutable
 | 
			
		||||
	out.Close()
 | 
			
		||||
 | 
			
		||||
	//Reload the plugin list
 | 
			
		||||
	m.ReloadPluginFromDisk()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UninstallPlugin uninstalls the plugin by removing its directory.
 | 
			
		||||
func (m *Manager) UninstallPlugin(pluginID string) error {
 | 
			
		||||
 | 
			
		||||
	//Stop the plugin process if it's running
 | 
			
		||||
	plugin, ok := m.LoadedPlugins[pluginID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("plugin not found: %s", pluginID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if plugin.IsRunning() {
 | 
			
		||||
		err := m.StopPlugin(plugin.Spec.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to stop plugin: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Make sure the plugin process is stopped
 | 
			
		||||
	m.Options.Logger.PrintAndLog("plugin-manager", "Removing plugin in 3 seconds...", nil)
 | 
			
		||||
	time.Sleep(3 * time.Second)
 | 
			
		||||
 | 
			
		||||
	// Remove the plugin directory
 | 
			
		||||
	err := os.RemoveAll(plugin.RootDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to remove plugin directory: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Reload the plugin list
 | 
			
		||||
	m.ReloadPluginFromDisk()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCurrentPlatformChecksum returns the checksum for the current platform
 | 
			
		||||
func (c *Checksums) GetCurrentPlatformChecksum() (string, error) {
 | 
			
		||||
	switch runtime.GOOS {
 | 
			
		||||
	case "linux":
 | 
			
		||||
		switch runtime.GOARCH {
 | 
			
		||||
		case "amd64":
 | 
			
		||||
			return c.LinuxAmd64, nil
 | 
			
		||||
		case "386":
 | 
			
		||||
			return c.Linux386, nil
 | 
			
		||||
		case "arm":
 | 
			
		||||
			return c.LinuxArm, nil
 | 
			
		||||
		case "arm64":
 | 
			
		||||
			return c.LinuxArm64, nil
 | 
			
		||||
		case "mipsle":
 | 
			
		||||
			return c.LinuxMipsle, nil
 | 
			
		||||
		case "riscv64":
 | 
			
		||||
			return c.LinuxRiscv64, nil
 | 
			
		||||
		default:
 | 
			
		||||
			return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
 | 
			
		||||
		}
 | 
			
		||||
	case "windows":
 | 
			
		||||
		switch runtime.GOARCH {
 | 
			
		||||
		case "amd64":
 | 
			
		||||
			return c.WindowsAmd64, nil
 | 
			
		||||
		default:
 | 
			
		||||
			return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VerifyChecksum verifies the checksum of the downloaded plugin binary.
 | 
			
		||||
func verifyChecksumForFile(filePath string, checksum string) bool {
 | 
			
		||||
	file, err := os.Open(filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	hash := sha256.New()
 | 
			
		||||
	if _, err := io.Copy(hash, file); err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	calculatedChecksum := fmt.Sprintf("%x", hash.Sum(nil))
 | 
			
		||||
 | 
			
		||||
	return calculatedChecksum == checksum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	Handlers for Plugin Store
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
func (m *Manager) HandleListDownloadablePlugins(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	//List all downloadable plugins
 | 
			
		||||
	plugins := m.ListDownloadablePlugins()
 | 
			
		||||
	js, _ := json.Marshal(plugins)
 | 
			
		||||
	utils.SendJSONResponse(w, string(js))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleResyncPluginList is the handler for resyncing the plugin list from the plugin store URLs
 | 
			
		||||
func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.Method != http.MethodPost {
 | 
			
		||||
		//Make sure this function require csrf token
 | 
			
		||||
		utils.SendErrorResponse(w, "Method not allowed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Resync the plugin list from the plugin store URLs
 | 
			
		||||
	err := m.UpdateDownloadablePluginList()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "Failed to resync plugin list: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	utils.SendOK(w)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleInstallPlugin is the handler for installing a plugin
 | 
			
		||||
func (m *Manager) HandleInstallPlugin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.Method != http.MethodPost {
 | 
			
		||||
		utils.SendErrorResponse(w, "Method not allowed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pluginID, err := utils.PostPara(r, "pluginID")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "pluginID is required")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Find the plugin info from cache
 | 
			
		||||
	var plugin *DownloadablePlugin
 | 
			
		||||
	for _, p := range m.Options.DownloadablePluginCache {
 | 
			
		||||
		if p.PluginIntroSpect.ID == pluginID {
 | 
			
		||||
			plugin = p
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if plugin == nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "Plugin not found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Install the plugin (implementation depends on your system)
 | 
			
		||||
	err = m.InstallPlugin(plugin)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "Failed to install plugin: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	utils.SendOK(w)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleUninstallPlugin is the handler for uninstalling a plugin
 | 
			
		||||
func (m *Manager) HandleUninstallPlugin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.Method != http.MethodPost {
 | 
			
		||||
		utils.SendErrorResponse(w, "Method not allowed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pluginID, err := utils.PostPara(r, "pluginID")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "pluginID is required")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Uninstall the plugin (implementation depends on your system)
 | 
			
		||||
	err = m.UninstallPlugin(pluginID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.SendErrorResponse(w, "Failed to uninstall plugin: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	utils.SendOK(w)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								src/mod/plugins/store_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/mod/plugins/store_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
package plugins
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestUpdateDownloadablePluginList(t *testing.T) {
 | 
			
		||||
	mockManager := &Manager{
 | 
			
		||||
		Options: &ManagerOptions{
 | 
			
		||||
			DownloadablePluginCache: []*DownloadablePlugin{},
 | 
			
		||||
			PluginStoreURLs:         []string{},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Inject a mock URL for testing
 | 
			
		||||
	mockManager.Options.PluginStoreURLs = []string{"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json"}
 | 
			
		||||
 | 
			
		||||
	err := mockManager.UpdateDownloadablePluginList()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("expected no error, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(mockManager.Options.DownloadablePluginCache) == 0 {
 | 
			
		||||
		t.Fatalf("expected plugin cache to be updated, but it was empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mockManager.Options.LastSuccPluginSyncTime == 0 {
 | 
			
		||||
		t.Fatalf("expected LastSuccPluginSyncTime to be updated, but it was not")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetPluginListFromURL(t *testing.T) {
 | 
			
		||||
	mockManager := &Manager{
 | 
			
		||||
		Options: &ManagerOptions{
 | 
			
		||||
			DownloadablePluginCache: []*DownloadablePlugin{},
 | 
			
		||||
			PluginStoreURLs:         []string{},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pluginList, err := mockManager.getPluginListFromURL("https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("expected no error, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(pluginList) == 0 {
 | 
			
		||||
		t.Fatalf("expected plugin list to be populated, but it was empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, plugin := range pluginList {
 | 
			
		||||
		t.Logf("Plugin: %+v", plugin)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,8 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti
 | 
			
		||||
		Referer:         make(map[string]int),
 | 
			
		||||
		UserAgent:       make(map[string]int),
 | 
			
		||||
		RequestURL:      make(map[string]int),
 | 
			
		||||
		Downstreams:     make(map[string]int),
 | 
			
		||||
		Upstreams:       make(map[string]int),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, export := range exports {
 | 
			
		||||
@@ -66,6 +68,14 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti
 | 
			
		||||
		for key, value := range export.RequestURL {
 | 
			
		||||
			mergedExport.RequestURL[key] += value
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for key, value := range export.Downstreams {
 | 
			
		||||
			mergedExport.Downstreams[key] += value
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for key, value := range export.Upstreams {
 | 
			
		||||
			mergedExport.Upstreams[key] += value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return mergedExport
 | 
			
		||||
 
 | 
			
		||||
@@ -24,12 +24,14 @@ type DailySummary struct {
 | 
			
		||||
	ErrorRequest int64 //Invalid request of the day, including error or not found
 | 
			
		||||
	ValidRequest int64 //Valid request of the day
 | 
			
		||||
	//Type counters
 | 
			
		||||
	ForwardTypes    *sync.Map //Map that hold the forward types
 | 
			
		||||
	RequestOrigin   *sync.Map //Map that hold [country ISO code]: visitor counter
 | 
			
		||||
	RequestClientIp *sync.Map //Map that hold all unique request IPs
 | 
			
		||||
	Referer         *sync.Map //Map that store where the user was refered from
 | 
			
		||||
	UserAgent       *sync.Map //Map that store the useragent of the request
 | 
			
		||||
	RequestURL      *sync.Map //Request URL of the request object
 | 
			
		||||
	ForwardTypes        *sync.Map //Map that hold the forward types
 | 
			
		||||
	RequestOrigin       *sync.Map //Map that hold [country ISO code]: visitor counter
 | 
			
		||||
	RequestClientIp     *sync.Map //Map that hold all unique request IPs
 | 
			
		||||
	Referer             *sync.Map //Map that store where the user was refered from
 | 
			
		||||
	UserAgent           *sync.Map //Map that store the useragent of the request
 | 
			
		||||
	RequestURL          *sync.Map //Request URL of the request object
 | 
			
		||||
	DownstreamHostnames *sync.Map //Request count of downstream hostname
 | 
			
		||||
	UpstreamHostnames   *sync.Map //Forwarded request count of upstream hostname
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequestInfo struct {
 | 
			
		||||
@@ -42,6 +44,7 @@ type RequestInfo struct {
 | 
			
		||||
	UserAgent                     string //UserAgent of the downstream request
 | 
			
		||||
	RequestURL                    string //Request URL
 | 
			
		||||
	Target                        string //Target domain or hostname
 | 
			
		||||
	Upstream                      string ////Upstream domain or hostname, if the request is forwarded to upstream
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CollectorOption struct {
 | 
			
		||||
@@ -233,6 +236,24 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
 | 
			
		||||
		} else {
 | 
			
		||||
			c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Record the downstream hostname
 | 
			
		||||
		//This is the hostname that the user visited, not the target domain
 | 
			
		||||
		ds, ok := c.DailySummary.DownstreamHostnames.Load(ri.Target)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			c.DailySummary.DownstreamHostnames.Store(ri.Target, 1)
 | 
			
		||||
		} else {
 | 
			
		||||
			c.DailySummary.DownstreamHostnames.Store(ri.Target, ds.(int)+1)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Record the upstream hostname
 | 
			
		||||
		//This is the selected load balancer upstream hostname or ip
 | 
			
		||||
		us, ok := c.DailySummary.UpstreamHostnames.Load(ri.Upstream)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			c.DailySummary.UpstreamHostnames.Store(ri.Upstream, 1)
 | 
			
		||||
		} else {
 | 
			
		||||
			c.DailySummary.UpstreamHostnames.Store(ri.Upstream, us.(int)+1)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	//ADD MORE HERE IF NEEDED
 | 
			
		||||
@@ -271,15 +292,17 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
 | 
			
		||||
 | 
			
		||||
func NewDailySummary() *DailySummary {
 | 
			
		||||
	return &DailySummary{
 | 
			
		||||
		TotalRequest:    0,
 | 
			
		||||
		ErrorRequest:    0,
 | 
			
		||||
		ValidRequest:    0,
 | 
			
		||||
		ForwardTypes:    &sync.Map{},
 | 
			
		||||
		RequestOrigin:   &sync.Map{},
 | 
			
		||||
		RequestClientIp: &sync.Map{},
 | 
			
		||||
		Referer:         &sync.Map{},
 | 
			
		||||
		UserAgent:       &sync.Map{},
 | 
			
		||||
		RequestURL:      &sync.Map{},
 | 
			
		||||
		TotalRequest:        0,
 | 
			
		||||
		ErrorRequest:        0,
 | 
			
		||||
		ValidRequest:        0,
 | 
			
		||||
		ForwardTypes:        &sync.Map{},
 | 
			
		||||
		RequestOrigin:       &sync.Map{},
 | 
			
		||||
		RequestClientIp:     &sync.Map{},
 | 
			
		||||
		Referer:             &sync.Map{},
 | 
			
		||||
		UserAgent:           &sync.Map{},
 | 
			
		||||
		RequestURL:          &sync.Map{},
 | 
			
		||||
		DownstreamHostnames: &sync.Map{},
 | 
			
		||||
		UpstreamHostnames:   &sync.Map{},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,21 @@ type DailySummaryExport struct {
 | 
			
		||||
	Referer         map[string]int
 | 
			
		||||
	UserAgent       map[string]int
 | 
			
		||||
	RequestURL      map[string]int
 | 
			
		||||
	Downstreams     map[string]int
 | 
			
		||||
	Upstreams       map[string]int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SyncMapToMapStringInt(syncMap *sync.Map) map[string]int {
 | 
			
		||||
	result := make(map[string]int)
 | 
			
		||||
	syncMap.Range(func(key, value interface{}) bool {
 | 
			
		||||
		strKey, okKey := key.(string)
 | 
			
		||||
		intValue, okValue := value.(int)
 | 
			
		||||
		if okKey && okValue {
 | 
			
		||||
			result[strKey] = intValue
 | 
			
		||||
		}
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DailySummaryToExport(summary DailySummary) DailySummaryExport {
 | 
			
		||||
@@ -26,77 +41,53 @@ func DailySummaryToExport(summary DailySummary) DailySummaryExport {
 | 
			
		||||
		Referer:         make(map[string]int),
 | 
			
		||||
		UserAgent:       make(map[string]int),
 | 
			
		||||
		RequestURL:      make(map[string]int),
 | 
			
		||||
		Downstreams:     make(map[string]int),
 | 
			
		||||
		Upstreams:       make(map[string]int),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	summary.ForwardTypes.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.ForwardTypes[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	summary.RequestOrigin.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.RequestOrigin[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	summary.RequestClientIp.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.RequestClientIp[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	summary.Referer.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.Referer[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	summary.UserAgent.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.UserAgent[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	summary.RequestURL.Range(func(key, value interface{}) bool {
 | 
			
		||||
		export.RequestURL[key.(string)] = value.(int)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
	export.ForwardTypes = SyncMapToMapStringInt(summary.ForwardTypes)
 | 
			
		||||
	export.RequestOrigin = SyncMapToMapStringInt(summary.RequestOrigin)
 | 
			
		||||
	export.RequestClientIp = SyncMapToMapStringInt(summary.RequestClientIp)
 | 
			
		||||
	export.Referer = SyncMapToMapStringInt(summary.Referer)
 | 
			
		||||
	export.UserAgent = SyncMapToMapStringInt(summary.UserAgent)
 | 
			
		||||
	export.RequestURL = SyncMapToMapStringInt(summary.RequestURL)
 | 
			
		||||
	export.Downstreams = SyncMapToMapStringInt(summary.DownstreamHostnames)
 | 
			
		||||
	export.Upstreams = SyncMapToMapStringInt(summary.UpstreamHostnames)
 | 
			
		||||
 | 
			
		||||
	return export
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MapStringIntToSyncMap(m map[string]int) *sync.Map {
 | 
			
		||||
	syncMap := &sync.Map{}
 | 
			
		||||
	for k, v := range m {
 | 
			
		||||
		syncMap.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
	return syncMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DailySummaryExportToSummary(export DailySummaryExport) DailySummary {
 | 
			
		||||
	summary := DailySummary{
 | 
			
		||||
		TotalRequest:    export.TotalRequest,
 | 
			
		||||
		ErrorRequest:    export.ErrorRequest,
 | 
			
		||||
		ValidRequest:    export.ValidRequest,
 | 
			
		||||
		ForwardTypes:    &sync.Map{},
 | 
			
		||||
		RequestOrigin:   &sync.Map{},
 | 
			
		||||
		RequestClientIp: &sync.Map{},
 | 
			
		||||
		Referer:         &sync.Map{},
 | 
			
		||||
		UserAgent:       &sync.Map{},
 | 
			
		||||
		RequestURL:      &sync.Map{},
 | 
			
		||||
		TotalRequest:        export.TotalRequest,
 | 
			
		||||
		ErrorRequest:        export.ErrorRequest,
 | 
			
		||||
		ValidRequest:        export.ValidRequest,
 | 
			
		||||
		ForwardTypes:        &sync.Map{},
 | 
			
		||||
		RequestOrigin:       &sync.Map{},
 | 
			
		||||
		RequestClientIp:     &sync.Map{},
 | 
			
		||||
		Referer:             &sync.Map{},
 | 
			
		||||
		UserAgent:           &sync.Map{},
 | 
			
		||||
		RequestURL:          &sync.Map{},
 | 
			
		||||
		DownstreamHostnames: &sync.Map{},
 | 
			
		||||
		UpstreamHostnames:   &sync.Map{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.ForwardTypes {
 | 
			
		||||
		summary.ForwardTypes.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.RequestOrigin {
 | 
			
		||||
		summary.RequestOrigin.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.RequestClientIp {
 | 
			
		||||
		summary.RequestClientIp.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.Referer {
 | 
			
		||||
		summary.Referer.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.UserAgent {
 | 
			
		||||
		summary.UserAgent.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range export.RequestURL {
 | 
			
		||||
		summary.RequestURL.Store(k, v)
 | 
			
		||||
	}
 | 
			
		||||
	summary.ForwardTypes = MapStringIntToSyncMap(export.ForwardTypes)
 | 
			
		||||
	summary.RequestOrigin = MapStringIntToSyncMap(export.RequestOrigin)
 | 
			
		||||
	summary.RequestClientIp = MapStringIntToSyncMap(export.RequestClientIp)
 | 
			
		||||
	summary.Referer = MapStringIntToSyncMap(export.Referer)
 | 
			
		||||
	summary.UserAgent = MapStringIntToSyncMap(export.UserAgent)
 | 
			
		||||
	summary.RequestURL = MapStringIntToSyncMap(export.RequestURL)
 | 
			
		||||
	summary.DownstreamHostnames = MapStringIntToSyncMap(export.Downstreams)
 | 
			
		||||
	summary.UpstreamHostnames = MapStringIntToSyncMap(export.Upstreams)
 | 
			
		||||
 | 
			
		||||
	return summary
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/start.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								src/start.go
									
									
									
									
									
								
							@@ -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 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -184,7 +184,46 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>  
 | 
			
		||||
        </div> 
 | 
			
		||||
        <div class="ui divider"></div>
 | 
			
		||||
        <div class="ui stackable grid">
 | 
			
		||||
            <div class="eight wide column">
 | 
			
		||||
                <h3>Requested Hostnames</h3>
 | 
			
		||||
                <p>Most requested hostnames from downstream</p>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div style="height: 500px; overflow-y: auto;">
 | 
			
		||||
                        <table class="ui unstackable striped celled table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                              <tr>
 | 
			
		||||
                                <th class="no-sort">Hostname</th>
 | 
			
		||||
                                <th class="no-sort">Requests</th>
 | 
			
		||||
                            </tr></thead>
 | 
			
		||||
                            <tbody id="stats_downstreamTable">
 | 
			
		||||
                             
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="eight wide column">
 | 
			
		||||
                <h3>Forwarded Upstreams</h3>
 | 
			
		||||
                <p>The Top 100 upstreams where the requests are forwarded to</p>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div style="height: 500px; overflow-y: auto;">
 | 
			
		||||
                        <table class="ui unstackable striped celled table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                              <tr>
 | 
			
		||||
                                <th class="no-sort">Upstream Endpoint</th>
 | 
			
		||||
                                <th class="no-sort">Requests</th>
 | 
			
		||||
                            </tr></thead>
 | 
			
		||||
                            <tbody id="stats_upstreamTable">
 | 
			
		||||
                             
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div> 
 | 
			
		||||
        <div class="ui divider"></div>
 | 
			
		||||
        <div class="ui basic segment" id="trendGraphs">
 | 
			
		||||
            <h3>Visitor Trend Analysis</h3>
 | 
			
		||||
@@ -263,6 +302,22 @@
 | 
			
		||||
            //Render Referer header
 | 
			
		||||
            renderRefererTable(data.Referer);
 | 
			
		||||
 | 
			
		||||
            if (data.Downstreams == null){
 | 
			
		||||
                //No downstream data to show
 | 
			
		||||
                $("#stats_downstreamTable").html("<tr><td colspan='2'>No data</td></tr>");
 | 
			
		||||
            }else{
 | 
			
		||||
                //Render the downstream table
 | 
			
		||||
                renderDownstreamTable(data.Downstreams);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (data.Upstreams == null){
 | 
			
		||||
                //No upstream data to show
 | 
			
		||||
                $("#stats_upstreamTable").html("<tr><td colspan='2'>No data</td></tr>");
 | 
			
		||||
            }else{
 | 
			
		||||
                //Render the upstream table
 | 
			
		||||
                renderUpstreamTable(data.Upstreams);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Hide the trend graphs
 | 
			
		||||
            $("#trendGraphs").hide();
 | 
			
		||||
        });
 | 
			
		||||
@@ -410,6 +465,46 @@
 | 
			
		||||
        }
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
   function renderDownstreamTable(downstreamList){
 | 
			
		||||
        const sortedEntries = Object.entries(downstreamList).sort(([, valueA], [, valueB]) => valueB - valueA);
 | 
			
		||||
        $("#stats_downstreamTable").html("");
 | 
			
		||||
        let endStop = 100;
 | 
			
		||||
        if (sortedEntries.length < 100){
 | 
			
		||||
            endStop = sortedEntries.length;
 | 
			
		||||
        }
 | 
			
		||||
        for (var i = 0; i < endStop; i++) {
 | 
			
		||||
            let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,"");
 | 
			
		||||
            if (sortedEntries[i][0] == ""){
 | 
			
		||||
                //Root
 | 
			
		||||
                referer = `<span style="color: #b5b5b5;">(<i class="eye slash outline icon"></i> Unknown or Hidden)</span>`;
 | 
			
		||||
            }
 | 
			
		||||
            $("#stats_downstreamTable").append(`<tr>
 | 
			
		||||
                    <td>${referer}</td>
 | 
			
		||||
                    <td>${sortedEntries[i][1]}</td>
 | 
			
		||||
                </tr>`);
 | 
			
		||||
        }
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
    function renderUpstreamTable(upstreamList){
 | 
			
		||||
          const sortedEntries = Object.entries(upstreamList).sort(([, valueA], [, valueB]) => valueB - valueA);
 | 
			
		||||
          $("#stats_upstreamTable").html("");
 | 
			
		||||
          let endStop = 100;
 | 
			
		||||
          if (sortedEntries.length < 100){
 | 
			
		||||
                endStop = sortedEntries.length;
 | 
			
		||||
          }
 | 
			
		||||
          for (var i = 0; i < endStop; i++) {
 | 
			
		||||
                let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,"");
 | 
			
		||||
                if (sortedEntries[i][0] == ""){
 | 
			
		||||
                 //Root
 | 
			
		||||
                 referer = `<span style="color: #b5b5b5;">(<i class="eye slash outline icon"></i> Unknown or Hidden)</span>`;
 | 
			
		||||
                }
 | 
			
		||||
                $("#stats_upstreamTable").append(`<tr>
 | 
			
		||||
                      <td>${referer}</td>
 | 
			
		||||
                      <td>${sortedEntries[i][1]}</td>
 | 
			
		||||
                 </tr>`);
 | 
			
		||||
          }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
   function renderFileTypeGraph(requestURLs){
 | 
			
		||||
     //Create the device chart
 | 
			
		||||
     let fileExtensions = {};
 | 
			
		||||
 
 | 
			
		||||
@@ -524,7 +524,6 @@
 | 
			
		||||
                $("#tls").checkbox("set checked");
 | 
			
		||||
            }else{
 | 
			
		||||
                $(".tlsEnabledOnly").addClass('disabled');
 | 
			
		||||
                $(".tlsEnabledOnly").addClass('disabled');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Initiate the input listener on the checkbox
 | 
			
		||||
 
 | 
			
		||||
@@ -197,6 +197,12 @@ body.darkTheme .menubar{
 | 
			
		||||
    max-width: calc(80% - 1em);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 478px) {
 | 
			
		||||
    .sideWrapper.extendedMode {
 | 
			
		||||
        max-width: calc(100% - 1em);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sideWrapper .content{
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										270
									
								
								src/web/snippet/pluginstore.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/web/snippet/pluginstore.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <!-- Notes: This should be open in its original path-->
 | 
			
		||||
        <meta charset="utf-8">
 | 
			
		||||
        <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
 | 
			
		||||
        <title>Plugin Store</title>
 | 
			
		||||
        <link rel="stylesheet" href="../script/semantic/semantic.min.css">
 | 
			
		||||
        <script src="../script/jquery-3.6.0.min.js"></script>
 | 
			
		||||
        <script src="../script/semantic/semantic.min.js"></script>
 | 
			
		||||
        <script src="../script/utils.js"></script>
 | 
			
		||||
        <style>
 | 
			
		||||
            #pluginList{
 | 
			
		||||
                padding: 1em;
 | 
			
		||||
                border: 1px solid #ccc;
 | 
			
		||||
                height: 500px;
 | 
			
		||||
                overflow-y: scroll;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            body.darkTheme #pluginList .header{
 | 
			
		||||
                color: #fff;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .installablePlugin{
 | 
			
		||||
                position: relative;
 | 
			
		||||
            }
 | 
			
		||||
            .installablePlugin .action{
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                top: 0.4em;
 | 
			
		||||
                right: 0.4em;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            @media screen and (max-width: 768px) {
 | 
			
		||||
                #pluginList .item .image {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        </style>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <link rel="stylesheet" href="../darktheme.css">
 | 
			
		||||
        <script src="../script/darktheme.js"></script>
 | 
			
		||||
        <br>
 | 
			
		||||
        <div class="ui container">
 | 
			
		||||
            <div class="ui warning message">
 | 
			
		||||
                <div class="header">Experimental Feature</div>
 | 
			
		||||
                <p>The Plugin Store is an experimental feature. Use it at your own risk.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="ui fluid search">
 | 
			
		||||
                <div class="ui fluid icon input">
 | 
			
		||||
                    <input id="searchInput" class="prompt" type="text" placeholder="Search plugins">
 | 
			
		||||
                    <i class="search icon"></i>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="ui divided items" id="pluginList">
 | 
			
		||||
               
 | 
			
		||||
            </div>
 | 
			
		||||
            <button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
 | 
			
		||||
            <!-- <div class="ui divider"></div>
 | 
			
		||||
            <div class="ui basic segment advanceoptions">
 | 
			
		||||
                <div class="ui accordion advanceSettings">
 | 
			
		||||
                    <div class="title">
 | 
			
		||||
                      <i class="dropdown icon"></i>
 | 
			
		||||
                        Advance Settings
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="content">
 | 
			
		||||
                        <p>Plugin Store URLs</p>
 | 
			
		||||
                        <div class="ui form">
 | 
			
		||||
                          <div class="field">
 | 
			
		||||
                            <textarea id="pluginStoreURLs" rows="5"></textarea>
 | 
			
		||||
                            <label>Enter plugin store URLs, separating each URL with a new line</label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <button class="ui basic button" onclick="savePluginStoreURLs()">
 | 
			
		||||
                            <i class="ui green save icon"></i>Save
 | 
			
		||||
                          </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        -->
 | 
			
		||||
            <div class="ui divider"></div>
 | 
			
		||||
            <div class="field" >
 | 
			
		||||
                <button class="ui basic button"  style="float: right;" onclick="closeThisWrapper();">Close</button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <br><br><br><br>
 | 
			
		||||
        </div>
 | 
			
		||||
        <script>
 | 
			
		||||
            let availablePlugins = [];
 | 
			
		||||
            let installedPlugins = [];
 | 
			
		||||
            $(".accordion").accordion();
 | 
			
		||||
 | 
			
		||||
            function initStoreList(){
 | 
			
		||||
                $.get("/api/plugins/list", function(data) {
 | 
			
		||||
                    if (data.error != undefined) {
 | 
			
		||||
                        parent.msgbox(data.error, false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }else{
 | 
			
		||||
                        installedPlugins = data || [];
 | 
			
		||||
                        console.log(installedPlugins);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $.cjax({
 | 
			
		||||
                        url: '/api/plugins/store/list',
 | 
			
		||||
                        type: 'GET',
 | 
			
		||||
                        success: function(data) {
 | 
			
		||||
                            if (data.error != undefined) {
 | 
			
		||||
                                parent.msgbox(data.error, false);
 | 
			
		||||
                            }else{
 | 
			
		||||
                                availablePlugins = data || [];
 | 
			
		||||
                                populatePluginList(availablePlugins);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            initStoreList();
 | 
			
		||||
 | 
			
		||||
            /* Plugin Search */
 | 
			
		||||
            function searchPlugins() {
 | 
			
		||||
                const query = document.getElementById('searchInput').value.toLowerCase();
 | 
			
		||||
                const items = document.querySelectorAll('#pluginList .item');
 | 
			
		||||
                if (query.trim() === '') {
 | 
			
		||||
                    items.forEach(item => {
 | 
			
		||||
                        item.style.display = '';
 | 
			
		||||
                    });
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                items.forEach(item => {
 | 
			
		||||
                    const name = item.querySelector('.header').textContent.toLowerCase();
 | 
			
		||||
                    const description = item.querySelector('.description p').textContent.toLowerCase();
 | 
			
		||||
                    const authorElement = item.querySelector('.plugin_author');
 | 
			
		||||
                    const author = authorElement ? authorElement.textContent.toLowerCase() : '';
 | 
			
		||||
                    const id = item.getAttribute('plugin_id').toLowerCase();
 | 
			
		||||
 | 
			
		||||
                    if (name.includes(query) || description.includes(query) || author.includes(query) || id.includes(query)) {
 | 
			
		||||
                        item.style.display = '';
 | 
			
		||||
                    } else {
 | 
			
		||||
                        item.style.display = 'none';
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Bind search function to input field and Enter key
 | 
			
		||||
            document.getElementById('searchInput').addEventListener('input', searchPlugins);
 | 
			
		||||
            document.getElementById('searchInput').addEventListener('keydown', function(event) {
 | 
			
		||||
                if (event.key === 'Enter') {
 | 
			
		||||
                    searchPlugins();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            function forceResyncPlugins() {
 | 
			
		||||
                parent.msgbox("Updating plugin list...", true);
 | 
			
		||||
                document.getElementById('searchInput').value = '';
 | 
			
		||||
                $.cjax({
 | 
			
		||||
                    url: '/api/plugins/store/resync',
 | 
			
		||||
                    type: 'POST',
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
                        if (data.error != undefined) {
 | 
			
		||||
                            parent.msgbox(data.error, false);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            parent.msgbox("Plugin list updated successfully", true);
 | 
			
		||||
                            initStoreList();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /* Plugin Store */
 | 
			
		||||
            function populatePluginList(plugins) {
 | 
			
		||||
                const pluginList = document.getElementById('pluginList');
 | 
			
		||||
                pluginList.innerHTML = ''; // Clear existing items
 | 
			
		||||
                plugins.forEach(plugin => {
 | 
			
		||||
                    console.log(plugin);
 | 
			
		||||
                    let thisPluginIsInstalled = false;
 | 
			
		||||
                    installedPlugins.forEach(installedPlugin => {
 | 
			
		||||
                        if (installedPlugin.Spec.id == plugin.PluginIntroSpect.id) {
 | 
			
		||||
                            thisPluginIsInstalled = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    const item = `
 | 
			
		||||
                        <div class="item installablePlugin" plugin_id="${plugin.PluginIntroSpect.id}">
 | 
			
		||||
                            <div class="ui tiny image">
 | 
			
		||||
                                <img src="${plugin.IconPath}" alt="${plugin.PluginIntroSpect.name}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="content">
 | 
			
		||||
                                <div class="header">${plugin.PluginIntroSpect.name} </div> <a class="section" href="${plugin.PluginIntroSpect.url}" target="_blank"><i class="ui linkify icon"></i></a>
 | 
			
		||||
                                <div class="meta">
 | 
			
		||||
                                    <p>v${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch} by <span class="plugin_author">${plugin.PluginIntroSpect.author}</span></p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="description">
 | 
			
		||||
                                    <p>${plugin.PluginIntroSpect.description}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="action">
 | 
			
		||||
                                    ${thisPluginIsInstalled 
 | 
			
		||||
                                        ? `<button class="ui basic circular disabled button">Installed</button>` 
 | 
			
		||||
                                        : `<button class="ui basic circular button" onclick="installPlugin('${plugin.PluginIntroSpect.id}', this);",><i class="ui download icon"></i> Install</button>`}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    `;
 | 
			
		||||
                    $('#pluginList').append(item);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Reapply search filter if there's a query in the search bar
 | 
			
		||||
                const searchQuery = document.getElementById('searchInput').value.toLowerCase();
 | 
			
		||||
                if (searchQuery.trim() !== '') {
 | 
			
		||||
                    searchPlugins();
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /* Plugin Actions */
 | 
			
		||||
            function installPlugin(pluginId, btn=undefined) {
 | 
			
		||||
                if (btn !== undefined) {
 | 
			
		||||
                    $(btn).addClass('loading').prop('disabled', true);
 | 
			
		||||
                }
 | 
			
		||||
                $.cjax({
 | 
			
		||||
                    url: '/api/plugins/store/install',
 | 
			
		||||
                    type: 'POST',
 | 
			
		||||
                    data: { "pluginID": pluginId },
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
                        if (btn !== undefined) {
 | 
			
		||||
                            $(btn).removeClass('loading').prop('disabled', false);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (data.error != undefined) {
 | 
			
		||||
                            parent.msgbox(data.error, false);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            parent.msgbox("Plugin installed successfully", true);
 | 
			
		||||
                            initStoreList();
 | 
			
		||||
 | 
			
		||||
                            //Also reload the parent plugin list
 | 
			
		||||
                            parent.initiatePluginList();
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    error: function() {
 | 
			
		||||
                        if (btn !== undefined) {
 | 
			
		||||
                            $(btn).removeClass('loading').prop('disabled', false);
 | 
			
		||||
                        }
 | 
			
		||||
                        parent.msgbox("An error occurred while installing the plugin", false);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function closeThisWrapper(){
 | 
			
		||||
                parent.hideSideWrapper(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /* Advanced Options */                                
 | 
			
		||||
            function savePluginManagerURLs() {
 | 
			
		||||
                const urls = document.getElementById('pluginStoreURLs').value.split('\n').map(url => url.trim()).filter(url => url !== '');
 | 
			
		||||
                console.log('Saving URLs:', urls);
 | 
			
		||||
                // Add your logic to save the URLs here, e.g., send them to the server
 | 
			
		||||
                $.cjax({
 | 
			
		||||
                    url: '/api/plugins/store/saveURLs',
 | 
			
		||||
                    type: 'POST',
 | 
			
		||||
                    data: { urls },
 | 
			
		||||
                    success: function(data) {
 | 
			
		||||
                        if (data.error != undefined) {
 | 
			
		||||
                            parent.msgbox(data.error, false);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            parent.msgbox("URLs saved successfully", true);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        </script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -345,7 +345,7 @@ func HandleZoraxyInfo(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	info := ZoraxyInfo{
 | 
			
		||||
		Version:           SYSTEM_VERSION,
 | 
			
		||||
		NodeUUID:          displayUUID,
 | 
			
		||||
		Development:       DEVELOPMENT_BUILD,
 | 
			
		||||
		Development:       *development_build,
 | 
			
		||||
		BootTime:          displayBootTime,
 | 
			
		||||
		EnableSshLoopback: displayAllowSSHLB,
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user