mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-05 22:01:44 +02:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
895ee1e53f | |||
caf4ab331b | |||
36c1f149e6 | |||
b0dc4d6670 | |||
5d8bec7f24 | |||
32f60dfba6 | |||
0abe4c12cf | |||
7555611ba5 | |||
e624227dae | |||
27695584ab | |||
e47a7a8357 | |||
3246f8ea2c | |||
ccbda6d7c2 | |||
a7285438af | |||
693dba07b7 | |||
9b64278200 | |||
d04eff2bda | |||
3320b56b19 | |||
99728144b3 | |||
05511ed4ca | |||
70abfe6fcf | |||
6ab91c377f | |||
1863af0d63 | |||
2a9d87787d | |||
f753becd66 | |||
bb2d0d5b46 | |||
07dc63a82c | |||
97a6cf016a | |||
8df68f1f4e | |||
e4ad505f2a | |||
a402c4f326 | |||
791fbfa1b4 | |||
c49f2fd1db | |||
7d9f240d56 | |||
e20f816080 | |||
eeb438eb18 | |||
bfd64a885e | |||
45f61b3053 | |||
0d4c71d0f6 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -39,4 +39,9 @@ src/tmp/localhost.pem
|
|||||||
src/www/html/index.html
|
src/www/html/index.html
|
||||||
src/sys.uuid
|
src/sys.uuid
|
||||||
src/zoraxy
|
src/zoraxy
|
||||||
src/log/
|
src/log/
|
||||||
|
|
||||||
|
|
||||||
|
# dev-tags
|
||||||
|
/Dockerfile
|
||||||
|
/Entrypoint.sh
|
20
CHANGELOG.md
20
CHANGELOG.md
@ -1,3 +1,23 @@
|
|||||||
|
# v3.1.7 08 Feb 2025
|
||||||
|
|
||||||
|
+ Merged and added new tagging system for HTTP Proxy rules [by @adoolaard](https://github.com/adoolaard)
|
||||||
|
+ Added inline editing for redirection rules [#510](https://github.com/tobychui/zoraxy/issues/510)
|
||||||
|
+ Added uptime monitor status dot detail info (now clickable) [#467](https://github.com/tobychui/zoraxy/issues/467)
|
||||||
|
+ Added close connection support to port 80 listener [#405](https://github.com/tobychui/zoraxy/issues/450)
|
||||||
|
+ Optimized port collision check on startup
|
||||||
|
+ Optimized dark theme color scheme (Free consultation by 3S Design studio)
|
||||||
|
+ Fixed capital letter rule unable to delete bug [#507](https://github.com/tobychui/zoraxy/issues/507)
|
||||||
|
+ Fixed docker statistic not save bug [by @PassiveLemon](https://github.com/PassiveLemon) [#505](https://github.com/tobychui/zoraxy/issues/505)
|
||||||
|
|
||||||
|
|
||||||
|
# v3.1.6 31 Dec 2024
|
||||||
|
|
||||||
|
|
||||||
|
+ Exposed log file, sys.uuid and static web server path to start flag (customizable conf and sys.db path is still wip)
|
||||||
|
+ Optimized connection close implementation
|
||||||
|
+ Added toggle for uptime monitor
|
||||||
|
+ Added optional copy HTTP custom headers to websocket connection [#444](https://github.com/tobychui/zoraxy/issues/444)
|
||||||
|
|
||||||
# v3.1.5 28 Dec 2024
|
# v3.1.5 28 Dec 2024
|
||||||
|
|
||||||
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
|
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
|
||||||
|
14
README.md
14
README.md
@ -101,12 +101,20 @@ Usage of zoraxy:
|
|||||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||||
-cfgupgrade
|
-cfgupgrade
|
||||||
Enable auto config upgrade if breaking change is detected (default true)
|
Enable auto config upgrade if breaking change is detected (default true)
|
||||||
|
-db string
|
||||||
|
Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto")
|
||||||
|
-default_inbound_enabled
|
||||||
|
If web server is enabled by default (default true)
|
||||||
|
-default_inbound_port int
|
||||||
|
Default web server listening port (default 443)
|
||||||
-docker
|
-docker
|
||||||
Run Zoraxy in docker compatibility mode
|
Run Zoraxy in docker compatibility mode
|
||||||
-earlyrenew int
|
-earlyrenew int
|
||||||
Number of days to early renew a soon expiring certificate (days) (default 30)
|
Number of days to early renew a soon expiring certificate (days) (default 30)
|
||||||
-fastgeoip
|
-fastgeoip
|
||||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||||
|
-log string
|
||||||
|
Log folder path (default "./log")
|
||||||
-mdns
|
-mdns
|
||||||
Enable mDNS scanner and transponder (default true)
|
Enable mDNS scanner and transponder (default true)
|
||||||
-mdnsname string
|
-mdnsname string
|
||||||
@ -117,12 +125,16 @@ Usage of zoraxy:
|
|||||||
Management web interface listening port (default ":8000")
|
Management web interface listening port (default ":8000")
|
||||||
-sshlb
|
-sshlb
|
||||||
Allow loopback web ssh connection (DANGER)
|
Allow loopback web ssh connection (DANGER)
|
||||||
|
-update_geoip
|
||||||
|
Download the latest GeoIP data and exit
|
||||||
|
-uuid string
|
||||||
|
sys.uuid file path (default "./sys.uuid")
|
||||||
-version
|
-version
|
||||||
Show version of this server
|
Show version of this server
|
||||||
-webfm
|
-webfm
|
||||||
Enable web file manager for static web server root folder (default true)
|
Enable web file manager for static web server root folder (default true)
|
||||||
-webroot string
|
-webroot string
|
||||||
Static web server root folder. Only allow chnage in start paramters (default "./www")
|
Static web server root folder. Only allow change in start paramters (default "./www")
|
||||||
-ztauth string
|
-ztauth string
|
||||||
ZeroTier authtoken for the local node
|
ZeroTier authtoken for the local node
|
||||||
-ztport int
|
-ztport int
|
||||||
|
@ -32,7 +32,7 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
|||||||
FROM docker.io/ubuntu:latest
|
FROM docker.io/ubuntu:latest
|
||||||
|
|
||||||
RUN apt-get update -y &&\
|
RUN apt-get update -y &&\
|
||||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
|
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
||||||
|
|
||||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
trap cleanup TERM INT
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "Shutting down..."
|
||||||
|
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
||||||
|
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
update-ca-certificates
|
update-ca-certificates
|
||||||
echo "CA certificates updated."
|
echo "CA certificates updated."
|
||||||
|
|
||||||
@ -11,12 +20,13 @@ if [ "$ZEROTIER" = "true" ]; then
|
|||||||
mkdir -p /opt/zoraxy/config/zerotier/
|
mkdir -p /opt/zoraxy/config/zerotier/
|
||||||
fi
|
fi
|
||||||
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
|
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
|
||||||
zerotier-one -d
|
zerotier-one -d &
|
||||||
|
zerotierpid=$!
|
||||||
echo "ZeroTier daemon started."
|
echo "ZeroTier daemon started."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting Zoraxy..."
|
echo "Starting Zoraxy..."
|
||||||
exec zoraxy \
|
zoraxy \
|
||||||
-autorenew="$AUTORENEW" \
|
-autorenew="$AUTORENEW" \
|
||||||
-cfgupgrade="$CFGUPGRADE" \
|
-cfgupgrade="$CFGUPGRADE" \
|
||||||
-db="$DB" \
|
-db="$DB" \
|
||||||
@ -33,5 +43,10 @@ exec zoraxy \
|
|||||||
-webfm="$WEBFM" \
|
-webfm="$WEBFM" \
|
||||||
-webroot="$WEBROOT" \
|
-webroot="$WEBROOT" \
|
||||||
-ztauth="$ZTAUTH" \
|
-ztauth="$ZTAUTH" \
|
||||||
-ztport="$ZTPORT"
|
-ztport="$ZTPORT" \
|
||||||
|
&
|
||||||
|
|
||||||
|
zoraxypid=$!
|
||||||
|
wait $zoraxypid
|
||||||
|
wait $zerotierpid
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
|
|||||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||||
|
authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule)
|
||||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,11 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Make sure the tags are not nil
|
||||||
|
if thisConfigEndpoint.Tags == nil {
|
||||||
|
thisConfigEndpoint.Tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
//Matching domain not set. Assume root
|
//Matching domain not set. Assume root
|
||||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||||
@ -175,8 +180,8 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Set the Content-Type header to indicate it's a zip file
|
// Set the Content-Type header to indicate it's a zip file
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
// Set the Content-Disposition header to specify the file name
|
// Set the Content-Disposition header to specify the file name, add timestamp to the filename
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
|
w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"")
|
||||||
|
|
||||||
// Create a zip writer
|
// Create a zip writer
|
||||||
zipWriter := zip.NewWriter(w)
|
zipWriter := zip.NewWriter(w)
|
||||||
|
@ -42,7 +42,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.1.6"
|
SYSTEM_VERSION = "3.1.8"
|
||||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
@ -87,6 +87,10 @@ var (
|
|||||||
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||||
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||||
|
|
||||||
|
/* Default Configuration Flags */
|
||||||
|
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
|
||||||
|
defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default")
|
||||||
|
|
||||||
/* Path Configuration Flags */
|
/* Path Configuration Flags */
|
||||||
//path_database = flag.String("dbpath", "./sys.db", "Database path")
|
//path_database = flag.String("dbpath", "./sys.db", "Database path")
|
||||||
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
|
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
|
||||||
|
11
src/go.mod
11
src/go.mod
@ -16,8 +16,10 @@ require (
|
|||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/likexian/whois v1.15.1
|
github.com/likexian/whois v1.15.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1
|
||||||
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
golang.org/x/net v0.29.0
|
golang.org/x/net v0.29.0
|
||||||
golang.org/x/sys v0.25.0
|
golang.org/x/sys v0.28.0
|
||||||
golang.org/x/text v0.18.0
|
golang.org/x/text v0.18.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,13 +28,15 @@ require (
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.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 v3.2.2+incompatible // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/shopspring/decimal v1.3.1 // indirect
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
|
||||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
||||||
github.com/tidwall/buntdb v1.1.2 // indirect
|
github.com/tidwall/buntdb v1.1.2 // indirect
|
||||||
github.com/tidwall/gjson v1.12.1 // indirect
|
github.com/tidwall/gjson v1.12.1 // indirect
|
||||||
@ -43,6 +47,7 @@ require (
|
|||||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.9.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
|
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -175,7 +180,7 @@ require (
|
|||||||
github.com/softlayer/softlayer-go v1.1.5 // indirect
|
github.com/softlayer/softlayer-go v1.1.5 // indirect
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
|
20
src/go.sum
20
src/go.sum
@ -176,6 +176,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
|||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
@ -221,6 +223,8 @@ 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-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 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
|
||||||
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
|
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 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
||||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||||
@ -570,6 +574,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
@ -609,6 +615,8 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ
|
|||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
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 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
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=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
@ -661,8 +669,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
@ -737,6 +745,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.2.1/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=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
|
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
|
||||||
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
|
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
|
||||||
@ -893,6 +903,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -907,6 +918,7 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -927,8 +939,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
@ -209,25 +209,18 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
|||||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||||
case DefaultSite_NotFoundPage:
|
case DefaultSite_NotFoundPage:
|
||||||
//Serve the not found page, use template if exists
|
//Serve the not found page, use template if exists
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
h.serve404PageWithTemplate(w, r)
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
|
||||||
if err != nil {
|
|
||||||
w.Write(page_hosterror)
|
|
||||||
} else {
|
|
||||||
w.Write(template)
|
|
||||||
}
|
|
||||||
case DefaultSite_NoResponse:
|
case DefaultSite_NoResponse:
|
||||||
//No response. Just close the connection
|
//No response. Just close the connection
|
||||||
h.Parent.logRequest(r, false, 444, "root-noresponse", domainOnly)
|
h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly)
|
||||||
hijacker, ok := w.(http.Hijacker)
|
hijacker, ok := w.(http.Hijacker)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("Connection", "close")
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn, _, err := hijacker.Hijack()
|
conn, _, err := hijacker.Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Set("Connection", "close")
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -241,3 +234,15 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
|||||||
http.Error(w, "544 - No Route Defined", 544)
|
http.Error(w, "544 - No Route Defined", 544)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve 404 page with template if exists
|
||||||
|
func (h *ProxyHandler) serve404PageWithTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||||
|
if err != nil {
|
||||||
|
w.Write(page_hosterror)
|
||||||
|
} else {
|
||||||
|
w.Write(template)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,5 +17,6 @@ func IsProxmox(r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||||
|
"imuslab.com/zoraxy/mod/dynamicproxy/modh2c"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -82,8 +83,12 @@ type requestCanceler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DpcoreOptions struct {
|
type DpcoreOptions struct {
|
||||||
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
|
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
|
||||||
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
|
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
|
||||||
|
MaxConcurrentConnection int //Maxmium concurrent requests to this server
|
||||||
|
ResponseHeaderTimeout int64 //Timeout for response header, set to 0 for default
|
||||||
|
IdleConnectionTimeout int64 //Idle connection timeout, set to 0 for default
|
||||||
|
UseH2CRoundTripper bool //Use H2C RoundTripper for HTTP/2.0 connection
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
|
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
|
||||||
@ -100,22 +105,39 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Hack the default transporter to handle more connections
|
|
||||||
thisTransporter := http.DefaultTransport
|
thisTransporter := http.DefaultTransport
|
||||||
|
|
||||||
|
//Hack the default transporter to handle more connections
|
||||||
optimalConcurrentConnection := 32
|
optimalConcurrentConnection := 32
|
||||||
|
if dpcOptions.MaxConcurrentConnection > 0 {
|
||||||
|
optimalConcurrentConnection = dpcOptions.MaxConcurrentConnection
|
||||||
|
}
|
||||||
|
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
|
||||||
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
||||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
||||||
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
|
|
||||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||||
thisTransporter.(*http.Transport).DisableCompression = true
|
thisTransporter.(*http.Transport).DisableCompression = true
|
||||||
|
|
||||||
//TODO: Add user adjustable timeout option here
|
if dpcOptions.ResponseHeaderTimeout > 0 {
|
||||||
|
//Set response header timeout
|
||||||
|
thisTransporter.(*http.Transport).ResponseHeaderTimeout = time.Duration(dpcOptions.ResponseHeaderTimeout) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
if dpcOptions.IdleConnectionTimeout > 0 {
|
||||||
|
//Set idle connection timeout
|
||||||
|
thisTransporter.(*http.Transport).IdleConnTimeout = time.Duration(dpcOptions.IdleConnectionTimeout) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
if dpcOptions.IgnoreTLSVerification {
|
if dpcOptions.IgnoreTLSVerification {
|
||||||
//Ignore TLS certificate validation error
|
//Ignore TLS certificate validation error
|
||||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dpcOptions.UseH2CRoundTripper {
|
||||||
|
//Use H2C RoundTripper for HTTP/2.0 connection
|
||||||
|
thisTransporter = modh2c.NewH2CRoundTripper()
|
||||||
|
}
|
||||||
|
|
||||||
return &ReverseProxy{
|
return &ReverseProxy{
|
||||||
Director: director,
|
Director: director,
|
||||||
Prepender: prepender,
|
Prepender: prepender,
|
||||||
|
@ -191,7 +191,24 @@ func (router *Router) StartProxyService() error {
|
|||||||
w.Write([]byte("400 - Bad Request"))
|
w.Write([]byte("400 - Bad Request"))
|
||||||
} else {
|
} else {
|
||||||
//No defined sub-domain
|
//No defined sub-domain
|
||||||
http.NotFound(w, r)
|
if router.Root.DefaultSiteOption == DefaultSite_NoResponse {
|
||||||
|
//No response. Just close the connection
|
||||||
|
hijacker, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
w.Header().Set("Connection", "close")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn, _, err := hijacker.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Connection", "close")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
} else {
|
||||||
|
//Default behavior
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -337,7 +354,7 @@ func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == matchingDomain {
|
if key == strings.ToLower(matchingDomain) {
|
||||||
targetProxyEndpoint = v
|
targetProxyEndpoint = v
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -267,7 +267,8 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
|
|||||||
|
|
||||||
// Remove this proxy endpoint from running proxy endpoint list
|
// Remove this proxy endpoint from running proxy endpoint list
|
||||||
func (ep *ProxyEndpoint) Remove() error {
|
func (ep *ProxyEndpoint) Remove() error {
|
||||||
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
|
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
|
||||||
|
ep.parent.ProxyEndpoints.Delete(lookupHostname)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package loadbalance
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
@ -25,11 +26,12 @@ type Options struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RouteManager struct {
|
type RouteManager struct {
|
||||||
SessionStore *sessions.CookieStore
|
SessionStore *sessions.CookieStore
|
||||||
LoadBalanceMap sync.Map //Sync map to store the last load balance state of a given node
|
OnlineStatus sync.Map //Store the online status notify by uptime monitor
|
||||||
OnlineStatusMap sync.Map //Sync map to store the online status of a given ip address or domain name
|
Options Options //Options for the load balancer
|
||||||
onlineStatusTickerStop chan bool //Stopping channel for the online status pinger
|
|
||||||
Options Options //Options for the load balancer
|
cacheTicker *time.Ticker //Ticker for cache cleanup
|
||||||
|
cacheTickerStop chan bool //Stop the cache cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Upstream or Origin Server */
|
/* Upstream or Origin Server */
|
||||||
@ -41,8 +43,12 @@ type Upstream struct {
|
|||||||
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
|
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
|
||||||
|
|
||||||
//Load balancing configs
|
//Load balancing configs
|
||||||
Weight int //Random weight for round robin, 0 for fallback only
|
Weight int //Random weight for round robin, 0 for fallback only
|
||||||
MaxConn int //TODO: Maxmium connection to this server, 0 for unlimited
|
|
||||||
|
//HTTP Transport Config
|
||||||
|
MaxConn int //Maxmium concurrent requests to this upstream dpcore instance
|
||||||
|
RespTimeout int64 //Response header timeout in milliseconds
|
||||||
|
IdleTimeout int64 //Idle connection timeout in milliseconds
|
||||||
|
|
||||||
//currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
|
//currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
|
||||||
proxy *dpcore.ReverseProxy
|
proxy *dpcore.ReverseProxy
|
||||||
@ -55,14 +61,31 @@ func NewLoadBalancer(options *Options) *RouteManager {
|
|||||||
options.SystemUUID = uuid.New().String()
|
options.SystemUUID = uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Create a ticker for cache cleanup every 12 hours
|
||||||
|
cacheTicker := time.NewTicker(12 * time.Hour)
|
||||||
|
cacheTickerStop := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
options.Logger.PrintAndLog("LoadBalancer", "Upstream state cache ticker started", nil)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cacheTickerStop:
|
||||||
|
return
|
||||||
|
case <-cacheTicker.C:
|
||||||
|
//Clean up the cache
|
||||||
|
options.Logger.PrintAndLog("LoadBalancer", "Cleaning up upstream state cache", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
//Generate a session store for stickySession
|
//Generate a session store for stickySession
|
||||||
store := sessions.NewCookieStore([]byte(options.SystemUUID))
|
store := sessions.NewCookieStore([]byte(options.SystemUUID))
|
||||||
return &RouteManager{
|
return &RouteManager{
|
||||||
SessionStore: store,
|
SessionStore: store,
|
||||||
LoadBalanceMap: sync.Map{},
|
OnlineStatus: sync.Map{},
|
||||||
OnlineStatusMap: sync.Map{},
|
Options: *options,
|
||||||
onlineStatusTickerStop: nil,
|
|
||||||
Options: *options,
|
cacheTicker: cacheTicker,
|
||||||
|
cacheTickerStop: cacheTickerStop,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,11 +113,20 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
|
|||||||
return strings.Join(targets, ", ")
|
return strings.Join(targets, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RouteManager) Close() {
|
// Reset the current session store and clear all previous sessions
|
||||||
if m.onlineStatusTickerStop != nil {
|
func (m *RouteManager) ResetSessions() {
|
||||||
m.onlineStatusTickerStop <- true
|
m.SessionStore = sessions.NewCookieStore([]byte(m.Options.SystemUUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *RouteManager) Close() {
|
||||||
|
//Close the session store
|
||||||
|
m.SessionStore.MaxAge(0)
|
||||||
|
|
||||||
|
//Stop the cache cleanup
|
||||||
|
if m.cacheTicker != nil {
|
||||||
|
m.cacheTicker.Stop()
|
||||||
|
}
|
||||||
|
close(m.cacheTickerStop)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log Println, replace all log.Println or fmt.Println with this
|
// Log Println, replace all log.Println or fmt.Println with this
|
||||||
|
@ -1,39 +1,72 @@
|
|||||||
package loadbalance
|
package loadbalance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return the last ping status to see if the target is online
|
// Return if the target host is online
|
||||||
func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
|
func (m *RouteManager) IsTargetOnline(upstreamIP string) bool {
|
||||||
value, ok := m.LoadBalanceMap.Load(matchingDomainOrIp)
|
value, ok := m.OnlineStatus.Load(upstreamIP)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
// Assume online if not found, also update the map
|
||||||
|
m.OnlineStatus.Store(upstreamIP, true)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
isOnline, ok := value.(bool)
|
isOnline, ok := value.(bool)
|
||||||
return ok && isOnline
|
return ok && isOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping a target to see if it is online
|
// Notify the host online state, should be called from uptime monitor
|
||||||
func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
|
func (m *RouteManager) NotifyHostOnlineState(upstreamIP string, isOnline bool) {
|
||||||
client := &http.Client{
|
//if the upstream IP contains http or https, strip it
|
||||||
Timeout: 10 * time.Second,
|
upstreamIP = strings.TrimPrefix(upstreamIP, "http://")
|
||||||
|
upstreamIP = strings.TrimPrefix(upstreamIP, "https://")
|
||||||
|
|
||||||
|
//Check previous state and update
|
||||||
|
if m.IsTargetOnline(upstreamIP) == isOnline {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
url := targetMatchingDomainOrIp
|
m.OnlineStatus.Store(upstreamIP, isOnline)
|
||||||
if requireTLS {
|
m.println("Updating upstream "+upstreamIP+" online state to "+strconv.FormatBool(isOnline), nil)
|
||||||
url = "https://" + url
|
}
|
||||||
} else {
|
|
||||||
url = "http://" + url
|
// Set this host unreachable for a given amount of time defined in timeout
|
||||||
}
|
// this shall be used in passive fallback. The uptime monitor should call to NotifyHostOnlineState() instead
|
||||||
|
func (m *RouteManager) NotifyHostUnreachableWithTimeout(upstreamIp string, timeout int64) {
|
||||||
resp, err := client.Get(url)
|
//if the upstream IP contains http or https, strip it
|
||||||
if err != nil {
|
upstreamIp = strings.TrimPrefix(upstreamIp, "http://")
|
||||||
return false
|
upstreamIp = strings.TrimPrefix(upstreamIp, "https://")
|
||||||
}
|
if timeout <= 0 {
|
||||||
defer resp.Body.Close()
|
//Set to the default timeout
|
||||||
|
timeout = 60
|
||||||
return resp.StatusCode >= 200 && resp.StatusCode <= 600
|
}
|
||||||
|
|
||||||
|
if !m.IsTargetOnline(upstreamIp) {
|
||||||
|
//Already offline
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.OnlineStatus.Store(upstreamIp, false)
|
||||||
|
m.println("Setting upstream "+upstreamIp+" unreachable for "+strconv.FormatInt(timeout, 10)+"s", nil)
|
||||||
|
go func() {
|
||||||
|
//Set the upstream back to online after the timeout
|
||||||
|
<-time.After(time.Duration(timeout) * time.Second)
|
||||||
|
m.NotifyHostOnlineState(upstreamIp, true)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterOfflineOrigins return only online origins from a list of origins
|
||||||
|
func (m *RouteManager) FilterOfflineOrigins(origins []*Upstream) []*Upstream {
|
||||||
|
var onlineOrigins []*Upstream
|
||||||
|
for _, origin := range origins {
|
||||||
|
if m.IsTargetOnline(origin.OriginIpOrDomain) {
|
||||||
|
onlineOrigins = append(onlineOrigins, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return onlineOrigins
|
||||||
}
|
}
|
||||||
|
@ -19,39 +19,62 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
|
|||||||
if len(origins) == 0 {
|
if len(origins) == 0 {
|
||||||
return nil, errors.New("no upstream is defined for this host")
|
return nil, errors.New("no upstream is defined for this host")
|
||||||
}
|
}
|
||||||
var targetOrigin = origins[0]
|
|
||||||
|
//Pick the origin
|
||||||
if useStickySession {
|
if useStickySession {
|
||||||
//Use stick session, check which origins this request previously used
|
//Use stick session, check which origins this request previously used
|
||||||
targetOriginId, err := m.getSessionHandler(r, origins)
|
targetOriginId, err := m.getSessionHandler(r, origins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//No valid session found. Assign a new upstream
|
// No valid session found or origin is offline
|
||||||
|
// Filter the offline origins
|
||||||
|
origins = m.FilterOfflineOrigins(origins)
|
||||||
|
if len(origins) == 0 {
|
||||||
|
return nil, errors.New("no online upstream is available for origin: " + r.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get a random origin
|
||||||
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
|
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.println("Unable to get random upstream", err)
|
m.println("Unable to get random upstream", err)
|
||||||
targetOrigin = origins[0]
|
targetOrigin = origins[0]
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//fmt.Println("DEBUG: (Sticky Session) Registering session origin " + origins[index].OriginIpOrDomain)
|
||||||
m.setSessionHandler(w, r, targetOrigin.OriginIpOrDomain, index)
|
m.setSessionHandler(w, r, targetOrigin.OriginIpOrDomain, index)
|
||||||
return targetOrigin, nil
|
return targetOrigin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Valid session found. Resume the previous session
|
//Valid session found and origin is online
|
||||||
|
//fmt.Println("DEBUG: (Sticky Session) Picking origin " + origins[targetOriginId].OriginIpOrDomain)
|
||||||
return origins[targetOriginId], nil
|
return origins[targetOriginId], nil
|
||||||
} else {
|
}
|
||||||
//Do not use stick session. Get a random one
|
//No sticky session, get a random origin
|
||||||
var err error
|
m.clearSessionHandler(w, r) //Clear the session
|
||||||
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
|
|
||||||
if err != nil {
|
|
||||||
m.println("Failed to get next origin", err)
|
|
||||||
targetOrigin = origins[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//Filter the offline origins
|
||||||
|
origins = m.FilterOfflineOrigins(origins)
|
||||||
|
if len(origins) == 0 {
|
||||||
|
return nil, errors.New("no online upstream is available for origin: " + r.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get a random origin
|
||||||
|
targetOrigin, _, err := getRandomUpstreamByWeight(origins)
|
||||||
|
if err != nil {
|
||||||
|
m.println("Failed to get next origin", err)
|
||||||
|
targetOrigin = origins[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Println("DEBUG: Picking origin " + targetOrigin.OriginIpOrDomain)
|
//fmt.Println("DEBUG: Picking origin " + targetOrigin.OriginIpOrDomain)
|
||||||
return targetOrigin, nil
|
return targetOrigin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsableUpstreamCounts return the number of usable upstreams
|
||||||
|
func (m *RouteManager) GetUsableUpstreamCounts(origins []*Upstream) int {
|
||||||
|
origins = m.FilterOfflineOrigins(origins)
|
||||||
|
return len(origins)
|
||||||
|
}
|
||||||
|
|
||||||
/* Features related to session access */
|
/* Features related to session access */
|
||||||
//Set a new origin for this connection by session
|
//Set a new origin for this connection by session
|
||||||
func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, originIpOrDomain string, index int) error {
|
func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, originIpOrDomain string, index int) error {
|
||||||
@ -70,6 +93,20 @@ func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *RouteManager) clearSessionHandler(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
session, err := m.SessionStore.Get(r, "STICKYSESSION")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session.Options.MaxAge = -1
|
||||||
|
session.Options.Path = "/"
|
||||||
|
err = session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get the previous connected origin from session
|
// Get the previous connected origin from session
|
||||||
func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) {
|
func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) {
|
||||||
// Get existing session
|
// Get existing session
|
||||||
@ -86,15 +123,22 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream)
|
|||||||
return -1, errors.New("no session has been set")
|
return -1, errors.New("no session has been set")
|
||||||
}
|
}
|
||||||
originDomain := originDomainRaw.(string)
|
originDomain := originDomainRaw.(string)
|
||||||
originID := originIDRaw.(int)
|
//originID := originIDRaw.(int)
|
||||||
|
|
||||||
//Check if it has been modified
|
//Check if the upstream still exists
|
||||||
if len(upstreams) < originID || upstreams[originID].OriginIpOrDomain != originDomain {
|
for i, upstream := range upstreams {
|
||||||
//Mismatch or upstreams has been updated
|
if upstream.OriginIpOrDomain == originDomain {
|
||||||
return -1, errors.New("upstreams has been changed")
|
if !m.IsTargetOnline(originDomain) {
|
||||||
|
//Origin is offline
|
||||||
|
return -1, errors.New("origin is offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ok, the origin is still online
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return originID, nil
|
return -1, errors.New("origin is no longer exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Functions related to random upstream picking */
|
/* Functions related to random upstream picking */
|
||||||
|
@ -39,8 +39,11 @@ func (u *Upstream) StartProxy() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
|
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
|
||||||
IgnoreTLSVerification: u.SkipCertValidations,
|
IgnoreTLSVerification: u.SkipCertValidations,
|
||||||
FlushInterval: 100 * time.Millisecond,
|
FlushInterval: 100 * time.Millisecond,
|
||||||
|
ResponseHeaderTimeout: u.RespTimeout,
|
||||||
|
IdleConnectionTimeout: u.IdleTimeout,
|
||||||
|
MaxConcurrentConnection: u.MaxConn,
|
||||||
})
|
})
|
||||||
|
|
||||||
u.proxy = proxy
|
u.proxy = proxy
|
||||||
|
45
src/mod/dynamicproxy/modh2c/modh2c.go
Normal file
45
src/mod/dynamicproxy/modh2c/modh2c.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package modh2c
|
||||||
|
|
||||||
|
/*
|
||||||
|
modh2c.go
|
||||||
|
|
||||||
|
This module is a simple h2c roundtripper for dpcore
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type H2CRoundTripper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewH2CRoundTripper() *H2CRoundTripper {
|
||||||
|
return &H2CRoundTripper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example from https://github.com/thrawn01/h2c-golang-example/blob/master/cmd/client/main.go
|
||||||
|
func (h2c *H2CRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, req.Method, req.RequestURI, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
var d net.Dialer
|
||||||
|
return d.DialContext(ctx, network, addr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.RoundTrip(req)
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package dynamicproxy
|
package dynamicproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -198,14 +200,21 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
Version: target.parent.Option.HostVersion,
|
Version: target.parent.Option.HostVersion,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//validate the error
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.As(err, &dnsError) {
|
if errors.As(err, &dnsError) {
|
||||||
http.ServeFile(w, r, "./web/hosterror.html")
|
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", r.URL.Hostname())
|
||||||
|
} 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())
|
||||||
} else {
|
} else {
|
||||||
|
//Notify the load balancer that the host is unreachable
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
h.Parent.loadBalancer.NotifyHostUnreachableWithTimeout(selectedUpstream.OriginIpOrDomain, PassiveLoadBalanceNotifyTimeout)
|
||||||
http.ServeFile(w, r, "./web/rperror.html")
|
http.ServeFile(w, r, "./web/rperror.html")
|
||||||
//TODO: Take this upstream offline automatically
|
|
||||||
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
|
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package redirection
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -111,6 +110,42 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit an existing redirection rule, the oldRedirectURL is used to find the rule to be edited
|
||||||
|
func (t *RuleTable) EditRedirectRule(oldRedirectURL string, newRedirectURL string, destURL string, forwardPathname bool, statusCode int) error {
|
||||||
|
newRule := &RedirectRules{
|
||||||
|
RedirectURL: newRedirectURL,
|
||||||
|
TargetURL: destURL,
|
||||||
|
ForwardChildpath: forwardPathname,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove the old rule
|
||||||
|
t.DeleteRedirectRule(oldRedirectURL)
|
||||||
|
|
||||||
|
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||||
|
filename := utils.ReplaceSpecialCharacters(newRedirectURL) + ".json"
|
||||||
|
filepath := path.Join(t.configPath, filename)
|
||||||
|
|
||||||
|
// Create a new file for writing the JSON data
|
||||||
|
file, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
t.log("Error creating file "+filepath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = json.NewEncoder(file).Encode(newRule)
|
||||||
|
if err != nil {
|
||||||
|
t.log("Error encoding JSON to file "+filepath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the runtime map
|
||||||
|
t.rules.Store(newRedirectURL, newRule)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||||
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
|
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
|
||||||
@ -118,7 +153,6 @@ func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
|||||||
// Create the full file path by joining the t.configPath with the filename
|
// Create the full file path by joining the t.configPath with the filename
|
||||||
filepath := path.Join(t.configPath, filename)
|
filepath := path.Join(t.configPath, filename)
|
||||||
|
|
||||||
fmt.Println(redirectURL, filename, filepath)
|
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||||
return nil // File doesn't exist, nothing to delete
|
return nil // File doesn't exist, nothing to delete
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<h1>What happend?</h1>
|
<h1>What happened?</h1>
|
||||||
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
|
|
||||||
type ProxyType int
|
type ProxyType int
|
||||||
|
|
||||||
|
const PassiveLoadBalanceNotifyTimeout = 60 //Time to assume a passive load balance is unreachable, in seconds
|
||||||
const (
|
const (
|
||||||
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
|
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
|
||||||
ProxyTypeHost //Host Proxy, match by host (domain) name
|
ProxyTypeHost //Host Proxy, match by host (domain) name
|
||||||
@ -193,7 +194,8 @@ type ProxyEndpoint struct {
|
|||||||
DefaultSiteValue string //Fallback routing target, optional
|
DefaultSiteValue string //Fallback routing target, optional
|
||||||
|
|
||||||
//Internal Logic Elements
|
//Internal Logic Elements
|
||||||
parent *Router `json:"-"`
|
parent *Router `json:"-"`
|
||||||
|
Tags []string // Tags for the proxy endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,14 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@ -202,144 +197,21 @@ func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r
|
|||||||
|
|
||||||
// Get network interface stats, return accumulated rx bits, tx bits and error if any
|
// Get network interface stats, return accumulated rx bits, tx bits and error if any
|
||||||
func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
|
||||||
if runtime.GOOS == "windows" {
|
// Get aggregated network I/O stats for all interfaces
|
||||||
//Windows wmic sometime freeze and not respond.
|
counters, err := net.IOCounters(false)
|
||||||
//The safer way is to make a bypass mechanism
|
if err != nil {
|
||||||
//when timeout with channel
|
return 0, 0, err
|
||||||
|
}
|
||||||
type wmicResult struct {
|
if len(counters) == 0 {
|
||||||
RX int64
|
return 0, 0, errors.New("no network interfaces found")
|
||||||
TX int64
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackChan := make(chan wmicResult)
|
|
||||||
cmd := exec.Command("wmic", "path", "Win32_PerfRawData_Tcpip_NetworkInterface", "Get", "BytesReceivedPersec,BytesSentPersec,BytesTotalPersec")
|
|
||||||
//Execute the cmd in goroutine
|
|
||||||
go func() {
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
callbackChan <- wmicResult{0, 0, err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Filter out the first line
|
|
||||||
lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n")
|
|
||||||
if len(lines) >= 2 && len(lines[1]) >= 0 {
|
|
||||||
dataLine := lines[1]
|
|
||||||
for strings.Contains(dataLine, " ") {
|
|
||||||
dataLine = strings.ReplaceAll(dataLine, " ", " ")
|
|
||||||
}
|
|
||||||
dataLine = strings.TrimSpace(dataLine)
|
|
||||||
info := strings.Split(dataLine, " ")
|
|
||||||
if len(info) != 3 {
|
|
||||||
callbackChan <- wmicResult{0, 0, errors.New("invalid wmic results length")}
|
|
||||||
}
|
|
||||||
rxString := info[0]
|
|
||||||
txString := info[1]
|
|
||||||
|
|
||||||
rx := int64(0)
|
|
||||||
tx := int64(0)
|
|
||||||
if s, err := strconv.ParseInt(rxString, 10, 64); err == nil {
|
|
||||||
rx = s
|
|
||||||
}
|
|
||||||
|
|
||||||
if s, err := strconv.ParseInt(txString, 10, 64); err == nil {
|
|
||||||
tx = s
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
callbackChan <- wmicResult{rx * 4, tx * 4, nil}
|
|
||||||
} else {
|
|
||||||
//Invalid data
|
|
||||||
callbackChan <- wmicResult{0, 0, errors.New("invalid wmic results")}
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
//Spawn a timer to terminate the cmd process if timeout
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
if cmd != nil && cmd.Process != nil {
|
|
||||||
cmd.Process.Kill()
|
|
||||||
callbackChan <- wmicResult{0, 0, errors.New("wmic execution timeout")}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := wmicResult{}
|
|
||||||
result = <-callbackChan
|
|
||||||
cmd = nil
|
|
||||||
if result.Err != nil {
|
|
||||||
n.logger.PrintAndLog("netstat", "Unable to extract NIC info from wmic", result.Err)
|
|
||||||
}
|
|
||||||
return result.RX, result.TX, result.Err
|
|
||||||
} else if runtime.GOOS == "linux" {
|
|
||||||
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
|
|
||||||
if err != nil {
|
|
||||||
//Permission denied
|
|
||||||
return 0, 0, errors.New("access denied")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allIfaceRxByteFiles) == 0 {
|
|
||||||
return 0, 0, errors.New("no valid iface found")
|
|
||||||
}
|
|
||||||
|
|
||||||
rxSum := int64(0)
|
|
||||||
txSum := int64(0)
|
|
||||||
for _, rxByteFile := range allIfaceRxByteFiles {
|
|
||||||
rxBytes, err := os.ReadFile(rxByteFile)
|
|
||||||
if err == nil {
|
|
||||||
rxBytesInt, err := strconv.Atoi(strings.TrimSpace(string(rxBytes)))
|
|
||||||
if err == nil {
|
|
||||||
rxSum += int64(rxBytesInt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Usually the tx_bytes file is nearby it. Read it as well
|
|
||||||
txByteFile := filepath.Join(filepath.Dir(rxByteFile), "tx_bytes")
|
|
||||||
txBytes, err := os.ReadFile(txByteFile)
|
|
||||||
if err == nil {
|
|
||||||
txBytesInt, err := strconv.Atoi(strings.TrimSpace(string(txBytes)))
|
|
||||||
if err == nil {
|
|
||||||
txSum += int64(txBytesInt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return value as bits
|
|
||||||
return rxSum * 8, txSum * 8, nil
|
|
||||||
|
|
||||||
} else if runtime.GOOS == "darwin" {
|
|
||||||
cmd := exec.Command("netstat", "-ib") //get data from netstat -ib
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
outStrs := string(out) //byte array to multi-line string
|
|
||||||
for _, outStr := range strings.Split(strings.TrimSuffix(outStrs, "\n"), "\n") { //foreach multi-line string
|
|
||||||
if strings.HasPrefix(outStr, "en") { //search for ethernet interface
|
|
||||||
if strings.Contains(outStr, "<Link#") { //search for the link with <Link#?>
|
|
||||||
outStrSplit := strings.Fields(outStr) //split by white-space
|
|
||||||
|
|
||||||
rxSum, errRX := strconv.Atoi(outStrSplit[6]) //received bytes sum
|
|
||||||
if errRX != nil {
|
|
||||||
return 0, 0, errRX
|
|
||||||
}
|
|
||||||
|
|
||||||
txSum, errTX := strconv.Atoi(outStrSplit[9]) //transmitted bytes sum
|
|
||||||
if errTX != nil {
|
|
||||||
return 0, 0, errTX
|
|
||||||
}
|
|
||||||
|
|
||||||
return int64(rxSum) * 8, int64(txSum) * 8, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, 0, nil //no ethernet adapters with en*/<Link#*>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, 0, errors.New("platform not supported")
|
var totalRx, totalTx uint64
|
||||||
|
for _, counter := range counters {
|
||||||
|
totalRx += counter.BytesRecv
|
||||||
|
totalTx += counter.BytesSent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert bytes to bits
|
||||||
|
return int64(totalRx * 8), int64(totalTx * 8), nil
|
||||||
}
|
}
|
||||||
|
@ -157,3 +157,13 @@ func resolveIpFromDomain(targetIpOrDomain string) string {
|
|||||||
|
|
||||||
return targetIpAddrString
|
return targetIpAddrString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the given port is already used by another process
|
||||||
|
func CheckIfPortOccupied(portNumber int) bool {
|
||||||
|
listener, err := net.Listen("tcp", ":"+strconv.Itoa(portNumber))
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
58
src/mod/uptime/typedef.go
Normal file
58
src/mod/uptime/typedef.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package uptime
|
||||||
|
|
||||||
|
import "imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
|
||||||
|
const (
|
||||||
|
logModuleName = "uptime-monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
Timestamp int64
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Protocol string
|
||||||
|
Online bool
|
||||||
|
StatusCode int
|
||||||
|
Latency int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyType_Host ProxyType = "Origin Server"
|
||||||
|
ProxyType_Vdir ProxyType = "Virtual Directory"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Target struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Protocol string
|
||||||
|
ProxyType ProxyType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Targets []*Target
|
||||||
|
Interval int
|
||||||
|
MaxRecordsStore int
|
||||||
|
OnlineStateNotify func(upstreamIP string, isOnline bool)
|
||||||
|
Logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
Config *Config
|
||||||
|
OnlineStatusLog map[string][]*Record
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default configs
|
||||||
|
var exampleTarget = Target{
|
||||||
|
ID: "example",
|
||||||
|
Name: "Example",
|
||||||
|
URL: "example.com",
|
||||||
|
Protocol: "https",
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultNotify(upstreamIP string, isOnline bool) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
@ -14,56 +14,6 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
logModuleName = "uptime-monitor"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Record struct {
|
|
||||||
Timestamp int64
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
Protocol string
|
|
||||||
Online bool
|
|
||||||
StatusCode int
|
|
||||||
Latency int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProxyType_Host ProxyType = "Origin Server"
|
|
||||||
ProxyType_Vdir ProxyType = "Virtual Directory"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Target struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
Protocol string
|
|
||||||
ProxyType ProxyType
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Targets []*Target
|
|
||||||
Interval int
|
|
||||||
MaxRecordsStore int
|
|
||||||
Logger *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
type Monitor struct {
|
|
||||||
Config *Config
|
|
||||||
OnlineStatusLog map[string][]*Record
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default configs
|
|
||||||
var exampleTarget = Target{
|
|
||||||
ID: "example",
|
|
||||||
Name: "Example",
|
|
||||||
URL: "example.com",
|
|
||||||
Protocol: "https",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new uptime monitor
|
// Create a new uptime monitor
|
||||||
func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
||||||
//Create new monitor object
|
//Create new monitor object
|
||||||
@ -77,6 +27,11 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
|||||||
config.Logger, _ = logger.NewFmtLogger()
|
config.Logger, _ = logger.NewFmtLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.OnlineStateNotify == nil {
|
||||||
|
//Use default notify function if not provided
|
||||||
|
config.OnlineStateNotify = defaultNotify
|
||||||
|
}
|
||||||
|
|
||||||
//Start the endpoint listener
|
//Start the endpoint listener
|
||||||
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
@ -218,6 +173,7 @@ func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
|||||||
end := time.Now().UnixNano() / int64(time.Millisecond)
|
end := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
|
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
|
||||||
|
m.Config.OnlineStateNotify(url, false)
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
} else {
|
} else {
|
||||||
diff := end - start
|
diff := end - start
|
||||||
@ -231,7 +187,7 @@ func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
|||||||
} else {
|
} else {
|
||||||
succ = false
|
succ = false
|
||||||
}
|
}
|
||||||
|
m.Config.OnlineStateNotify(url, true)
|
||||||
return succ, diff, statusCode
|
return succ, diff, statusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +78,49 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleEditRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
originalRedirectUrl, err := utils.PostPara(r, "originalRedirectUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "original redirect url cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRedirectUrl, err := utils.PostPara(r, "newRedirectUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "redirect url cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
destUrl, err := utils.PostPara(r, "destUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "destination url cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardChildpath, err := utils.PostPara(r, "forwardChildpath")
|
||||||
|
if err != nil {
|
||||||
|
//Assume true
|
||||||
|
forwardChildpath = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectTypeString, err := utils.PostPara(r, "redirectType")
|
||||||
|
if err != nil {
|
||||||
|
redirectTypeString = "307"
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectionStatusCode, err := strconv.Atoi(redirectTypeString)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid status code number")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = redirectTable.EditRedirectRule(originalRedirectUrl, newRedirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle redirection regex support. Note that this cost another O(n) time complexity to each page load
|
// Toggle redirection regex support. Note that this cost another O(n) time complexity to each page load
|
||||||
func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
|
func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
|
||||||
enabled, err := utils.PostPara(r, "enable")
|
enabled, err := utils.PostPara(r, "enable")
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||||
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
"imuslab.com/zoraxy/mod/uptime"
|
"imuslab.com/zoraxy/mod/uptime"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@ -27,11 +28,23 @@ func ReverseProxtInit() {
|
|||||||
/*
|
/*
|
||||||
Load Reverse Proxy Global Settings
|
Load Reverse Proxy Global Settings
|
||||||
*/
|
*/
|
||||||
inboundPort := 443
|
inboundPort := *defaultInboundPort
|
||||||
|
autoStartReverseProxy := *defaultEnableInboundTraffic
|
||||||
if sysdb.KeyExists("settings", "inbound") {
|
if sysdb.KeyExists("settings", "inbound") {
|
||||||
|
//Read settings from database
|
||||||
sysdb.Read("settings", "inbound", &inboundPort)
|
sysdb.Read("settings", "inbound", &inboundPort)
|
||||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
if netutils.CheckIfPortOccupied(inboundPort) {
|
||||||
|
autoStartReverseProxy = false
|
||||||
|
SystemWideLogger.Println("Inbound port ", inboundPort, " is occupied. Change the listening port in the webmin panel and press \"Start Service\" to start reverse proxy service")
|
||||||
|
} else {
|
||||||
|
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//Default port
|
||||||
|
if netutils.CheckIfPortOccupied(inboundPort) {
|
||||||
|
autoStartReverseProxy = false
|
||||||
|
SystemWideLogger.Println("Port 443 is occupied. Change the listening port in the webmin panel and press \"Start Service\" to start reverse proxy service")
|
||||||
|
}
|
||||||
SystemWideLogger.Println("Inbound port not set. Using default (443)")
|
SystemWideLogger.Println("Inbound port not set. Using default (443)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +73,9 @@ func ReverseProxtInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listenOnPort80 := true
|
listenOnPort80 := true
|
||||||
|
if netutils.CheckIfPortOccupied(80) {
|
||||||
|
listenOnPort80 = false
|
||||||
|
}
|
||||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||||
if listenOnPort80 {
|
if listenOnPort80 {
|
||||||
SystemWideLogger.Println("Port 80 listener enabled")
|
SystemWideLogger.Println("Port 80 listener enabled")
|
||||||
@ -136,19 +152,22 @@ func ReverseProxtInit() {
|
|||||||
//Start Service
|
//Start Service
|
||||||
//Not sure why but delay must be added if you have another
|
//Not sure why but delay must be added if you have another
|
||||||
//reverse proxy server in front of this service
|
//reverse proxy server in front of this service
|
||||||
time.Sleep(300 * time.Millisecond)
|
if autoStartReverseProxy {
|
||||||
dynamicProxyRouter.StartProxyService()
|
time.Sleep(300 * time.Millisecond)
|
||||||
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
dynamicProxyRouter.StartProxyService()
|
||||||
|
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
||||||
|
}
|
||||||
|
|
||||||
//Add all proxy services to uptime monitor
|
//Add all proxy services to uptime monitor
|
||||||
//Create a uptime monitor service
|
//Create a uptime monitor service
|
||||||
go func() {
|
go func() {
|
||||||
//This must be done in go routine to prevent blocking on system startup
|
//This must be done in go routine to prevent blocking on system startup
|
||||||
uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
|
uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
|
||||||
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
|
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
|
||||||
Interval: 300, //5 minutes
|
Interval: 300, //5 minutes
|
||||||
MaxRecordsStore: 288, //1 day
|
MaxRecordsStore: 288, //1 day
|
||||||
Logger: SystemWideLogger, //Logger
|
OnlineStateNotify: loadBalancer.NotifyHostOnlineState, //Notify the load balancer for online state
|
||||||
|
Logger: SystemWideLogger, //Logger
|
||||||
})
|
})
|
||||||
|
|
||||||
SystemWideLogger.Println("Uptime Monitor background service started")
|
SystemWideLogger.Println("Uptime Monitor background service started")
|
||||||
@ -287,6 +306,23 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagStr, _ := utils.PostPara(r, "tags")
|
||||||
|
tags := []string{}
|
||||||
|
if tagStr != "" {
|
||||||
|
tags = strings.Split(tagStr, ",")
|
||||||
|
for i := range tags {
|
||||||
|
tags[i] = strings.TrimSpace(tags[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove empty tags
|
||||||
|
filteredTags := []string{}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag != "" {
|
||||||
|
filteredTags = append(filteredTags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags = filteredTags
|
||||||
|
|
||||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||||
if eptype == "host" {
|
if eptype == "host" {
|
||||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||||
@ -357,6 +393,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Rate Limit
|
// Rate Limit
|
||||||
RequireRateLimit: requireRateLimit,
|
RequireRateLimit: requireRateLimit,
|
||||||
RateLimit: int64(proxyRateLimit),
|
RateLimit: int64(proxyRateLimit),
|
||||||
|
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||||
@ -515,6 +553,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagStr, _ := utils.PostPara(r, "tags")
|
||||||
|
tags := []string{}
|
||||||
|
if tagStr != "" {
|
||||||
|
tags = strings.Split(tagStr, ",")
|
||||||
|
for i := range tags {
|
||||||
|
tags[i] = strings.TrimSpace(tags[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Generate a new proxyEndpoint from the new config
|
//Generate a new proxyEndpoint from the new config
|
||||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||||
@ -539,6 +586,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
newProxyEndpoint.RateLimit = proxyRateLimit
|
newProxyEndpoint.RateLimit = proxyRateLimit
|
||||||
newProxyEndpoint.UseStickySession = useStickySession
|
newProxyEndpoint.UseStickySession = useStickySession
|
||||||
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
|
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
|
||||||
|
newProxyEndpoint.Tags = tags
|
||||||
|
|
||||||
//Prepare to replace the current routing rule
|
//Prepare to replace the current routing rule
|
||||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||||
@ -547,6 +595,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetProxyEntry.Remove()
|
targetProxyEntry.Remove()
|
||||||
|
loadBalancer.ResetSessions()
|
||||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||||
|
|
||||||
//Save it to file
|
//Save it to file
|
||||||
|
@ -79,6 +79,25 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendErrorResponse(w, "upstream origin not set")
|
utils.SendErrorResponse(w, "upstream origin not set")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Response timeout in seconds, set to 0 for default
|
||||||
|
respTimeout, err := utils.PostInt(r, "respt")
|
||||||
|
if err != nil {
|
||||||
|
respTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//Idle timeout in seconds, set to 0 for default
|
||||||
|
idleTimeout, err := utils.PostInt(r, "idlet")
|
||||||
|
if err != nil {
|
||||||
|
idleTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//Max concurrent connection to dpcore instance, set to 0 for default
|
||||||
|
maxConn, err := utils.PostInt(r, "maxconn")
|
||||||
|
if err != nil {
|
||||||
|
maxConn = 0
|
||||||
|
}
|
||||||
|
|
||||||
requireTLS, _ := utils.PostBool(r, "tls")
|
requireTLS, _ := utils.PostBool(r, "tls")
|
||||||
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
|
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
|
||||||
bpwsorg, _ := utils.PostBool(r, "bpwsorg")
|
bpwsorg, _ := utils.PostBool(r, "bpwsorg")
|
||||||
@ -91,7 +110,9 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
SkipCertValidations: skipTlsValidation,
|
SkipCertValidations: skipTlsValidation,
|
||||||
SkipWebSocketOriginCheck: bpwsorg,
|
SkipWebSocketOriginCheck: bpwsorg,
|
||||||
Weight: 1,
|
Weight: 1,
|
||||||
MaxConn: 0,
|
MaxConn: maxConn,
|
||||||
|
RespTimeout: int64(respTimeout),
|
||||||
|
IdleTimeout: int64(idleTimeout),
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add the new upstream to endpoint
|
//Add the new upstream to endpoint
|
||||||
|
@ -1174,7 +1174,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeIpBlacklist(ipaddr){
|
function removeIpBlacklist(ipaddr){
|
||||||
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
|
//if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/blacklist/ip/remove",
|
url: "/api/blacklist/ip/remove",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@ -1191,7 +1191,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1318,7 +1318,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeIpWhitelist(ipaddr){
|
function removeIpWhitelist(ipaddr){
|
||||||
if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
|
//if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/whitelist/ip/remove",
|
url: "/api/whitelist/ip/remove",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@ -1335,7 +1335,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,7 +11,47 @@
|
|||||||
.subdEntry td:not(.ignoremw){
|
.subdEntry td:not(.ignoremw){
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.httpProxyListTools{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-select{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-select:hover{
|
||||||
|
text-decoration: underline;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<div class="httpProxyListTools" style="margin-bottom: 1em;">
|
||||||
|
<div id="tagFilterDropdown" class="ui floating basic dropdown labeled icon button" style="min-width: 150px;">
|
||||||
|
<i class="filter icon"></i>
|
||||||
|
<span class="text">Filter by tags</span>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="search icon"></i>
|
||||||
|
<input type="text" placeholder="Search tags...">
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="scrolling menu tagList">
|
||||||
|
<!--
|
||||||
|
Example:
|
||||||
|
<div class="item">
|
||||||
|
<div class="ui red empty circular label"></div>
|
||||||
|
Important
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<!-- Add more tag options dynamically -->
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
|
||||||
<table class="ui celled sortable unstackable compact table">
|
<table class="ui celled sortable unstackable compact table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -19,6 +59,7 @@
|
|||||||
<th>Host</th>
|
<th>Host</th>
|
||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>Virtual Directory</th>
|
<th>Virtual Directory</th>
|
||||||
|
<th>Tags</th>
|
||||||
<th style="max-width: 300px;">Advanced Settings</th>
|
<th style="max-width: 300px;">Advanced Settings</th>
|
||||||
<th class="no-sort" style="min-width:150px;">Actions</th>
|
<th class="no-sort" style="min-width:150px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -124,6 +165,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
|
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
|
||||||
|
<td data-label="tags" payload="${encodeURIComponent(JSON.stringify(subd.Tags))}" datatype="tags">
|
||||||
|
<div class="tags-list">
|
||||||
|
${subd.Tags.length >0 ? subd.Tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join(""):"<small style='opacity: 0.3; pointer-events: none; user-select: none;'>No Tags</small>"}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
||||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
|
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
|
||||||
@ -142,6 +188,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
});
|
});
|
||||||
|
populateTagFilterDropdown(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveAccessRuleNameOnHostRPlist();
|
resolveAccessRuleNameOnHostRPlist();
|
||||||
@ -285,7 +332,11 @@
|
|||||||
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
|
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
|
||||||
<i class="ui yellow folder icon"></i> Edit Virtual Directories
|
<i class="ui yellow folder icon"></i> Edit Virtual Directories
|
||||||
</button>`);
|
</button>`);
|
||||||
|
}else if (datatype == "tags"){
|
||||||
|
column.append(`
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<button class="ui basic compact fluid tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editTags('${uuid}');"><i class="ui purple tag icon"></i> Edit tags</button>
|
||||||
|
`);
|
||||||
}else if (datatype == "advanced"){
|
}else if (datatype == "advanced"){
|
||||||
let authProvider = payload.AuthenticationProvider.AuthMethod;
|
let authProvider = payload.AuthenticationProvider.AuthMethod;
|
||||||
|
|
||||||
@ -457,7 +508,12 @@
|
|||||||
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
|
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
|
||||||
let rateLimit = $(row).find(".RateLimit").val();
|
let rateLimit = $(row).find(".RateLimit").val();
|
||||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||||
|
let tags = getTagsArrayFromEndpoint(uuid);
|
||||||
|
if (tags.length > 0){
|
||||||
|
tags = tags.join(",");
|
||||||
|
}else{
|
||||||
|
tags = "";
|
||||||
|
}
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/edit",
|
url: "/api/proxy/edit",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -470,6 +526,7 @@
|
|||||||
"authprovider" :authProviderType,
|
"authprovider" :authProviderType,
|
||||||
"rate" :requireRateLimit,
|
"rate" :requireRateLimit,
|
||||||
"ratenum" :rateLimit,
|
"ratenum" :rateLimit,
|
||||||
|
"tags": tags,
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error !== undefined){
|
if (data.error !== undefined){
|
||||||
@ -609,4 +666,110 @@
|
|||||||
tabSwitchEventBind["httprp"] = function(){
|
tabSwitchEventBind["httprp"] = function(){
|
||||||
listProxyEndpoints();
|
listProxyEndpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tags & Search */
|
||||||
|
function handleSearchInput(event){
|
||||||
|
if (event.key == "Escape"){
|
||||||
|
$("#searchInput").val("");
|
||||||
|
}
|
||||||
|
filterProxyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to filter the proxy list
|
||||||
|
function filterProxyList() {
|
||||||
|
let searchInput = $("#searchInput").val().toLowerCase();
|
||||||
|
let selectedTag = $("#tagFilterDropdown").dropdown('get value');
|
||||||
|
$("#httpProxyList tr").each(function() {
|
||||||
|
let host = $(this).find("td[data-label='']").text().toLowerCase();
|
||||||
|
let tagElements = $(this).find("td[data-label='tags']");
|
||||||
|
let tags = tagElements.attr("payload");
|
||||||
|
tags = JSON.parse(decodeURIComponent(tags));
|
||||||
|
if ((host.includes(searchInput) || searchInput === "") && (tags.includes(selectedTag) || selectedTag === "")) {
|
||||||
|
$(this).show();
|
||||||
|
} else {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a color based on a tag name
|
||||||
|
function getTagColorByName(tagName) {
|
||||||
|
function hashCode(str) {
|
||||||
|
return str.split('').reduce((prevHash, currVal) =>
|
||||||
|
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
||||||
|
}
|
||||||
|
let hash = hashCode(tagName);
|
||||||
|
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
||||||
|
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
||||||
|
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagTextColor(tagName){
|
||||||
|
let color = getTagColorByName(tagName);
|
||||||
|
let r = parseInt(color.substr(1, 2), 16);
|
||||||
|
let g = parseInt(color.substr(3, 2), 16);
|
||||||
|
let b = parseInt(color.substr(5, 2), 16);
|
||||||
|
let brightness = Math.round(((r * 299) + (g * 587) + (b * 114)) / 1000);
|
||||||
|
return brightness > 125 ? "#000000" : "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the tag filter dropdown
|
||||||
|
function populateTagFilterDropdown(data) {
|
||||||
|
let tags = new Set();
|
||||||
|
data.forEach(subd => {
|
||||||
|
subd.Tags.forEach(tag => tags.add(tag));
|
||||||
|
});
|
||||||
|
tags = Array.from(tags).sort((a, b) => a.localeCompare(b));
|
||||||
|
let dropdownMenu = $("#tagFilterDropdown .tagList");
|
||||||
|
dropdownMenu.html(`<div class="item tag-select" data-value="">
|
||||||
|
<div class="ui grey empty circular label"></div>
|
||||||
|
Show all
|
||||||
|
</div>`);
|
||||||
|
tags.forEach(tag => {
|
||||||
|
let thisTagColor = getTagColorByName(tag);
|
||||||
|
dropdownMenu.append(`<div class="item tag-select" data-value="${tag}">
|
||||||
|
<div class="ui empty circular label" style="background-color: ${thisTagColor}; border-color: ${thisTagColor};" ></div>
|
||||||
|
${tag}
|
||||||
|
</div>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit tags for a specific endpoint
|
||||||
|
function editTags(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/tagEditor.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the tags preview from tag editing snippet
|
||||||
|
function renderTagsPreview(endpoint, tags){
|
||||||
|
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
||||||
|
//Update the tag DOM
|
||||||
|
let newTagDOM = tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join("");
|
||||||
|
$(targetProxyRuleEle).find(".tags-list").html(newTagDOM);
|
||||||
|
|
||||||
|
//Update the tag payload
|
||||||
|
$(targetProxyRuleEle).attr("payload", encodeURIComponent(JSON.stringify(tags)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagsArrayFromEndpoint(endpoint){
|
||||||
|
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
||||||
|
let tags = $(targetProxyRuleEle).attr("payload");
|
||||||
|
return JSON.parse(decodeURIComponent(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the proxy list on page load
|
||||||
|
$(document).ready(function() {
|
||||||
|
listProxyEndpoints();
|
||||||
|
|
||||||
|
// Event listener for clicking on tags
|
||||||
|
$(document).on('click', '.tag-select', function() {
|
||||||
|
let tag = $(this).text().trim();
|
||||||
|
$('#tagFilterDropdown').dropdown('set selected', tag);
|
||||||
|
filterProxyList();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
@ -13,7 +13,7 @@
|
|||||||
<th>Destination URL</th>
|
<th>Destination URL</th>
|
||||||
<th class="no-sort">Copy Pathname</th>
|
<th class="no-sort">Copy Pathname</th>
|
||||||
<th class="no-sort">Status Code</th>
|
<th class="no-sort">Status Code</th>
|
||||||
<th class="no-sort">Remove</th>
|
<th class="no-sort">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="redirectionRuleList">
|
<tbody id="redirectionRuleList">
|
||||||
@ -163,13 +163,21 @@
|
|||||||
$("#redirectionRuleList").html("");
|
$("#redirectionRuleList").html("");
|
||||||
$.get("/api/redirect/list", function(data){
|
$.get("/api/redirect/list", function(data){
|
||||||
data.forEach(function(entry){
|
data.forEach(function(entry){
|
||||||
$("#redirectionRuleList").append(`<tr>
|
let encodedEntry = encodeURIComponent(JSON.stringify(entry));
|
||||||
<td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></td>
|
let hrefURL = entry.RedirectURL;
|
||||||
<td>${entry.TargetURL}</td>
|
if (!hrefURL.startsWith("http")){
|
||||||
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
|
hrefURL = "https://" + hrefURL;
|
||||||
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
}
|
||||||
<td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
|
$("#redirectionRuleList").append(`<tr>
|
||||||
</tr>`);
|
<td><a href="${hrefURL}" target="_blank">${entry.RedirectURL}</a></td>
|
||||||
|
<td>${entry.TargetURL}</td>
|
||||||
|
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
|
||||||
|
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
||||||
|
<td>
|
||||||
|
<button onclick="editRule(this);" payload="${encodedEntry}" title="Edit redirection rule" class="ui mini circular icon basic button redirectEditBtn"><i class="edit icon"></i></button>
|
||||||
|
<button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red circular icon basic button"><i class="trash icon"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length == 0){
|
if (data.length == 0){
|
||||||
@ -180,6 +188,68 @@
|
|||||||
}
|
}
|
||||||
initRedirectionRuleList();
|
initRedirectionRuleList();
|
||||||
|
|
||||||
|
function editRule(obj){
|
||||||
|
$(".redirectEditBtn").addClass("disabled");
|
||||||
|
let payload = JSON.parse(decodeURIComponent($(obj).attr("payload")));
|
||||||
|
let row = $(obj).closest("tr");
|
||||||
|
let redirectUrl = payload.RedirectURL;
|
||||||
|
let destUrl = payload.TargetURL;
|
||||||
|
let forwardChildpath = payload.ForwardChildpath;
|
||||||
|
let statusCode = payload.StatusCode;
|
||||||
|
|
||||||
|
row.html(`
|
||||||
|
<td>
|
||||||
|
<div class="ui small input">
|
||||||
|
<input type="text" value="${redirectUrl}" id="editRedirectUrl">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui small input">
|
||||||
|
<input type="text" value="${destUrl}" id="editDestUrl">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><div class="ui toggle checkbox"><input type="checkbox" ${forwardChildpath ? "checked" : ""} id="editForwardChildpath"><label></label></div></td>
|
||||||
|
<td>
|
||||||
|
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="307" ${statusCode == 307 ? "checked" : ""}><label>Temporary Redirect (307)</label></div><br>
|
||||||
|
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="301" ${statusCode == 301 ? "checked" : ""}><label>Moved Permanently (301)</label></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button onclick="saveEditRule(this);" payload="${encodeURIComponent(JSON.stringify(payload))}" class="ui small circular green icon basic button"><i class="save icon"></i></button>
|
||||||
|
<button onclick="initRedirectionRuleList();" class="ui small circular icon basic button"><i class="cancel icon"></i></button>
|
||||||
|
</td>
|
||||||
|
`);
|
||||||
|
|
||||||
|
$(".checkbox").checkbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditRule(obj){
|
||||||
|
let payload = JSON.parse(decodeURIComponent($(obj).attr("payload")));
|
||||||
|
let redirectUrl = $("#editRedirectUrl").val();
|
||||||
|
let destUrl = $("#editDestUrl").val();
|
||||||
|
let forwardChildpath = $("#editForwardChildpath").is(":checked");
|
||||||
|
let statusCode = parseInt($("input[name='editStatusCode']:checked").val());
|
||||||
|
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/redirect/edit",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
originalRedirectUrl: payload.RedirectURL,
|
||||||
|
newRedirectUrl: redirectUrl,
|
||||||
|
destUrl: destUrl,
|
||||||
|
forwardChildpath: forwardChildpath,
|
||||||
|
redirectType: statusCode,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Redirection rule updated", true);
|
||||||
|
initRedirectionRuleList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initRegexpSupportToggle(){
|
function initRegexpSupportToggle(){
|
||||||
$.get("/api/redirect/regex", function(data){
|
$.get("/api/redirect/regex", function(data){
|
||||||
//Set the checkbox initial state
|
//Set the checkbox initial state
|
||||||
|
@ -63,6 +63,11 @@
|
|||||||
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
|
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Tags</label>
|
||||||
|
<input type="text" id="proxyTags" placeholder="e.g. mediaserver, management">
|
||||||
|
<small>Comma-separated list of tags for this proxy host.</small>
|
||||||
|
</div>
|
||||||
<div class="ui horizontal divider">
|
<div class="ui horizontal divider">
|
||||||
<i class="ui green lock icon"></i>
|
<i class="ui green lock icon"></i>
|
||||||
Security
|
Security
|
||||||
@ -198,6 +203,7 @@
|
|||||||
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
|
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
|
||||||
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
||||||
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
|
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
|
||||||
|
let tags = $("#proxyTags").val().trim();
|
||||||
|
|
||||||
if (rootname.trim() == ""){
|
if (rootname.trim() == ""){
|
||||||
$("#rootname").parent().addClass("error");
|
$("#rootname").parent().addClass("error");
|
||||||
@ -231,6 +237,7 @@
|
|||||||
cred: JSON.stringify(credentials),
|
cred: JSON.stringify(credentials),
|
||||||
access: accessRuleToUse,
|
access: accessRuleToUse,
|
||||||
stickysess: useStickySessionLB,
|
stickysess: useStickySessionLB,
|
||||||
|
tags: tags,
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
@ -239,6 +246,7 @@
|
|||||||
//Clear old data
|
//Clear old data
|
||||||
$("#rootname").val("");
|
$("#rootname").val("");
|
||||||
$("#proxyDomain").val("");
|
$("#proxyDomain").val("");
|
||||||
|
$("#proxyTags").val("");
|
||||||
credentials = [];
|
credentials = [];
|
||||||
updateTable();
|
updateTable();
|
||||||
reloadUptimeList();
|
reloadUptimeList();
|
||||||
|
@ -108,6 +108,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showStatusDotInfo(targetDot){
|
||||||
|
$(".statusbar .selectedDotInfo").hide();
|
||||||
|
let payload = $(targetDot).attr("payload");
|
||||||
|
let statusData = JSON.parse(decodeURIComponent(payload));
|
||||||
|
let statusDotInfoEle = $(targetDot).parent().parent().find(".selectedDotInfo");
|
||||||
|
let statusInfoEle = $(statusDotInfoEle).find(".status_dot_status_info");
|
||||||
|
//Fill in the data to the info box
|
||||||
|
$(statusDotInfoEle).find(".status_dot_timestamp").text(format_time(statusData.Timestamp));
|
||||||
|
$(statusDotInfoEle).find(".status_dot_latency").text(statusData.Latency + "ms");
|
||||||
|
$(statusDotInfoEle).find(".status_dot_status_code").text(statusData.StatusCode);
|
||||||
|
|
||||||
|
|
||||||
|
//Set the class of the info box if status code is 5xx
|
||||||
|
$(statusDotInfoEle).removeClass("yellow");
|
||||||
|
$(statusDotInfoEle).removeClass("red");
|
||||||
|
$(statusDotInfoEle).removeClass("green");
|
||||||
|
if (statusData.StatusCode >= 500 && statusData.StatusCode < 600){
|
||||||
|
$(statusDotInfoEle).addClass("yellow");
|
||||||
|
$(statusInfoEle).text(httpErrorStatusCodeToText(statusData.StatusCode));
|
||||||
|
}else if (statusData.StatusCode == 0 && !statusData.Online){
|
||||||
|
$(statusDotInfoEle).addClass("red");
|
||||||
|
$(statusInfoEle).text("Upstream is offline");
|
||||||
|
}else{
|
||||||
|
$(statusDotInfoEle).addClass("green");
|
||||||
|
$(statusInfoEle).text("Upstream Online");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(statusDotInfoEle).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderUptimeData(key, value){
|
function renderUptimeData(key, value){
|
||||||
if (value.length == 0){
|
if (value.length == 0){
|
||||||
@ -132,6 +162,7 @@
|
|||||||
let thisStatus = value[i];
|
let thisStatus = value[i];
|
||||||
let dotType = "";
|
let dotType = "";
|
||||||
let statusCode = thisStatus.StatusCode;
|
let statusCode = thisStatus.StatusCode;
|
||||||
|
let statusDotPayload = encodeURIComponent(JSON.stringify(thisStatus));
|
||||||
|
|
||||||
if (!thisStatus.Online && statusCode == 0){
|
if (!thisStatus.Online && statusCode == 0){
|
||||||
dotType = "offline";
|
dotType = "offline";
|
||||||
@ -159,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let datetime = format_time(thisStatus.Timestamp);
|
let datetime = format_time(thisStatus.Timestamp);
|
||||||
statusDotList += `<div title="${datetime}" class="${dotType} statusDot"></div>`
|
statusDotList += `<div title="${datetime}" class="${dotType} statusDot" payload="${statusDotPayload}" onclick="showStatusDotInfo(this);"></div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
ontimeRate = ontimeRate / value.length * 100;
|
ontimeRate = ontimeRate / value.length * 100;
|
||||||
@ -207,7 +238,7 @@
|
|||||||
|
|
||||||
onlineStatusCss = `color: #f38020;`;
|
onlineStatusCss = `color: #f38020;`;
|
||||||
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
|
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
|
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
|
||||||
onlineStatusCss = `color: #df484a;`;
|
onlineStatusCss = `color: #df484a;`;
|
||||||
@ -233,8 +264,71 @@
|
|||||||
${statusDotList}
|
${statusDotList}
|
||||||
</div>
|
</div>
|
||||||
${reminderEle}
|
${reminderEle}
|
||||||
|
<div class="ui basic segment selectedDotInfo" style="display:none; border: 0.4em;">
|
||||||
|
<div class="ui list">
|
||||||
|
<div class="item"><b>Timestamp</b>: <span class="status_dot_timestamp"></span></div>
|
||||||
|
<div class="item"><b>Latency</b>: <span class="status_dot_latency"></span></div>
|
||||||
|
<div class="item"><b>Status Code</b>: <span class="status_dot_status_code"></span></div>
|
||||||
|
<div class="item"><b>Status Info</b>: <span class="status_dot_status_info"></span></div>
|
||||||
|
</div>
|
||||||
|
<button onclick="$(this).parent().hide();" style="position: absolute; right: 0.4em; top: 0.6em;" class="ui basic tiny circular icon button"><i class="ui times icon"></i></button>
|
||||||
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
</div>`);
|
</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function httpErrorStatusCodeToText(statusCode){
|
||||||
|
switch(statusCode){
|
||||||
|
case 400:
|
||||||
|
return "Bad Request";
|
||||||
|
case 401:
|
||||||
|
return "Unauthorized";
|
||||||
|
case 403:
|
||||||
|
return "Forbidden";
|
||||||
|
case 404:
|
||||||
|
return "Not Found";
|
||||||
|
case 405:
|
||||||
|
return "Method Not Allowed";
|
||||||
|
case 500:
|
||||||
|
return "Internal Server Error";
|
||||||
|
case 501:
|
||||||
|
return "Not Implemented";
|
||||||
|
case 502:
|
||||||
|
return "Bad Gateway";
|
||||||
|
case 503:
|
||||||
|
return "Service Unavailable";
|
||||||
|
case 504:
|
||||||
|
return "Gateway Timeout";
|
||||||
|
case 505:
|
||||||
|
return "HTTP Version Not Supported";
|
||||||
|
case 506:
|
||||||
|
return "Variant Also Negotiates";
|
||||||
|
case 507:
|
||||||
|
return "Insufficient Storage";
|
||||||
|
case 508:
|
||||||
|
return "Loop Detected";
|
||||||
|
case 510:
|
||||||
|
return "Not Extended";
|
||||||
|
case 511:
|
||||||
|
return "Network Authentication Required";
|
||||||
|
case 520:
|
||||||
|
return "Web Server Returned an Unknown Error (Cloudflare)";
|
||||||
|
case 521:
|
||||||
|
return "Web Server is Down (Cloudflare)";
|
||||||
|
case 522:
|
||||||
|
return "Connection Timed Out (Cloudflare)";
|
||||||
|
case 523:
|
||||||
|
return "Origin is Unreachable (Cloudflare)";
|
||||||
|
case 524:
|
||||||
|
return "A Timeout Occurred (Cloudflare)";
|
||||||
|
case 525:
|
||||||
|
return "SSL Handshake Failed (Cloudflare)";
|
||||||
|
case 526:
|
||||||
|
return "Invalid SSL Certificate (Cloudflare)";
|
||||||
|
case 527:
|
||||||
|
return "Railgun Error (Cloudflare)";
|
||||||
|
default:
|
||||||
|
return "Unknown Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
@ -259,6 +259,8 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
SMTP Settings
|
SMTP Settings
|
||||||
|
|
||||||
|
TODO: Remove SMTP support in future versions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//Bind events to the form
|
//Bind events to the form
|
||||||
@ -273,11 +275,13 @@
|
|||||||
adminAddr: $('input[name=recvAddr]').val()
|
adminAddr: $('input[name=recvAddr]').val()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
var inputValid = validateSMTPInputs();
|
var inputValid = validateSMTPInputs();
|
||||||
if (!inputValid){
|
if (!inputValid){
|
||||||
msgbox("SMTP input not valid", false, 5000);
|
msgbox("SMTP input not valid", false, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
|
@ -23,6 +23,10 @@ body:not(.darkTheme){
|
|||||||
--text_color_inverted: #fcfcfc;
|
--text_color_inverted: #fcfcfc;
|
||||||
--button_text_color: #878787;
|
--button_text_color: #878787;
|
||||||
--button_border_color: #dedede;
|
--button_border_color: #dedede;
|
||||||
|
--buttom_toggle_active: #01dc64;
|
||||||
|
--buttom_toggle_disabled: #f2f2f2;
|
||||||
|
--table_bg_default: transparent;
|
||||||
|
--status_dot_bg: #e8e8e8;
|
||||||
|
|
||||||
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
|
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
|
||||||
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||||
@ -31,10 +35,10 @@ body:not(.darkTheme){
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme{
|
body.darkTheme{
|
||||||
--theme_bg: #0a090e;
|
--theme_bg: #1e1e1e;
|
||||||
--theme_bg_primary: #060912;
|
--theme_bg_primary: #151517;
|
||||||
--theme_bg_secondary:#172a41;
|
--theme_bg_secondary:#1b3572;
|
||||||
--theme_highlight: #4380b0;
|
--theme_highlight: #6a7792;
|
||||||
--theme_bg_active: #020101;
|
--theme_bg_active: #020101;
|
||||||
--theme_bg_inverted: #f8f8f9;
|
--theme_bg_inverted: #f8f8f9;
|
||||||
--theme_advance: #000000;
|
--theme_advance: #000000;
|
||||||
@ -47,8 +51,12 @@ body.darkTheme{
|
|||||||
--text_color_inverted: #414141;
|
--text_color_inverted: #414141;
|
||||||
--button_text_color: #e9e9e9;
|
--button_text_color: #e9e9e9;
|
||||||
--button_border_color: #646464;
|
--button_border_color: #646464;
|
||||||
|
--buttom_toggle_active: #01dc64;
|
||||||
|
--buttom_toggle_disabled: #2b2b2b;
|
||||||
|
--table_bg_default: #121214;
|
||||||
|
--status_dot_bg: #232323;
|
||||||
|
|
||||||
--theme_background: linear-gradient(214deg, rgba(3,1,70,1) 17%, rgb(1, 55, 80) 78%);
|
--theme_background: linear-gradient(23deg, rgba(2,74,106,1) 17%, rgba(46,12,136,1) 86%);
|
||||||
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||||
--theme_green: linear-gradient(214deg, rgba(25,128,94,1) 17%, rgba(62,76,111,1) 78%);
|
--theme_green: linear-gradient(214deg, rgba(25,128,94,1) 17%, rgba(62,76,111,1) 78%);
|
||||||
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
|
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
|
||||||
@ -113,6 +121,9 @@ body.darkTheme .ui.basic.button:not(.red) {
|
|||||||
body.darkTheme .ui.basic.button:not(.red):hover {
|
body.darkTheme .ui.basic.button:not(.red):hover {
|
||||||
border: 1px solid var(--button_border_color) !important;
|
border: 1px solid var(--button_border_color) !important;
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--theme_bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme .ui.basic.button:not(.red):not(.dropdown):hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,10 +206,14 @@ body.darkTheme textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .ui.toggle.checkbox input ~ label::before{
|
body.darkTheme .ui.toggle.checkbox input ~ label::before{
|
||||||
background-color: var(--theme_bg_secondary) !important;
|
background-color: var(--buttom_toggle_disabled) !important;
|
||||||
}
|
}
|
||||||
body.darkTheme .ui.toggle.checkbox input:checked ~ label::before{
|
body.darkTheme .ui.toggle.checkbox input:checked ~ label::before{
|
||||||
background-color: var(--theme_highlight) !important;
|
background-color: var(--buttom_toggle_active) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme .ui.checkbox:not(.toggle) input[type="checkbox"]{
|
||||||
|
opacity: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidemenuBtn{
|
#sidemenuBtn{
|
||||||
@ -444,7 +459,7 @@ body.darkTheme .ui.table{
|
|||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table thead th,
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table thead th,
|
||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td,
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td,
|
||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tfoot td {
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table tfoot td {
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--table_bg_default) !important;
|
||||||
color: var(--text_color) !important;
|
color: var(--text_color) !important;
|
||||||
border-color: var(--divider_color) !important;
|
border-color: var(--divider_color) !important;
|
||||||
}
|
}
|
||||||
@ -476,11 +491,11 @@ body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input ~ label::before {
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input ~ label::before {
|
||||||
background-color: var(--theme_bg_secondary) !important;
|
background-color: var(--buttom_toggle_disabled) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input:checked ~ label::before {
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input:checked ~ label::before {
|
||||||
background-color: var(--theme_highlight) !important;
|
background-color: var(--buttom_toggle_active) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.circular.mini.basic.icon.button {
|
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.circular.mini.basic.icon.button {
|
||||||
@ -537,6 +552,18 @@ body.darkTheme .RateLimit input {
|
|||||||
border-color: var(--theme_highlight) !important;
|
border-color: var(--theme_highlight) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.darkTheme .menu.transition{
|
||||||
|
background-color: var(--theme_bg) !important;
|
||||||
|
color: var(--text_color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme .ui.dropdown .menu{
|
||||||
|
background: var(--theme_bg_primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.darkTheme .ui.dropdown .menu .item{
|
||||||
|
color: var(--text_color) !important;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
Virtual Directorie Table
|
Virtual Directorie Table
|
||||||
*/
|
*/
|
||||||
@ -714,7 +741,7 @@ body.darkTheme #redirectset .ui.sortable.unstackable.celled.table thead th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme #redirectset .ui.sortable.unstackable.celled.table tbody tr td {
|
body.darkTheme #redirectset .ui.sortable.unstackable.celled.table tbody tr td {
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--table_bg_default) !important;
|
||||||
color: var(--text_color) !important;
|
color: var(--text_color) !important;
|
||||||
border-color: var(--divider_color) !important;
|
border-color: var(--divider_color) !important;
|
||||||
}
|
}
|
||||||
@ -833,7 +860,7 @@ body.darkTheme #access .ui.unstackable.basic.celled.table thead th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme #access .ui.unstackable.basic.celled.table tbody tr td {
|
body.darkTheme #access .ui.unstackable.basic.celled.table tbody tr td {
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--table_bg_default) !important;
|
||||||
color: var(--text_color) !important;
|
color: var(--text_color) !important;
|
||||||
border-color: var(--divider_color) !important;
|
border-color: var(--divider_color) !important;
|
||||||
}
|
}
|
||||||
@ -985,8 +1012,8 @@ body.darkTheme #utm .standardContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme #utm .standardContainer .padding.statusDot {
|
body.darkTheme #utm .standardContainer .padding.statusDot {
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--status_dot_bg) !important;
|
||||||
border: 0.2px solid var(--text_color_inverted) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .ui.utmloading.segment {
|
body.darkTheme .ui.utmloading.segment {
|
||||||
@ -1116,7 +1143,7 @@ body.darkTheme .statistic .label {
|
|||||||
/* Other Tables */
|
/* Other Tables */
|
||||||
|
|
||||||
body.darkTheme .ui.celled.compact.table {
|
body.darkTheme .ui.celled.compact.table {
|
||||||
background-color: var(--theme_bg) !important;
|
background-color: var(--table_bg_default) !important;
|
||||||
color: var(--text_color) !important;
|
color: var(--text_color) !important;
|
||||||
border-color: var(--divider_color) !important;
|
border-color: var(--divider_color) !important;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<h1>What happend?</h1>
|
<h1>What happened?</h1>
|
||||||
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui stackable grid">
|
<div class="ui stackable grid">
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
<h1>What happend?</h1>
|
<h1>What happened?</h1>
|
||||||
<p>The web server reported a bad gateway error.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
<p>The web server reported a bad gateway error.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
|
260
src/web/snippet/tagEditor.html
Normal file
260
src/web/snippet/tagEditor.html
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||||
|
<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>
|
||||||
|
.ui.circular.label.tag-color{
|
||||||
|
min-width: 5px !important;
|
||||||
|
min-height: 5px !important;
|
||||||
|
margin-right: .4em;
|
||||||
|
margin-bottom: -0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<link rel="stylesheet" href="../darktheme.css">
|
||||||
|
<script src="../script/darktheme.js"></script>
|
||||||
|
<br>
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui header">
|
||||||
|
<div class="content">
|
||||||
|
Edit Tags
|
||||||
|
<div class="sub header" id="epname"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<p>Tags currently applied to this host name / proxy rule</p>
|
||||||
|
<div style="max-height: 300px; overflow-y: scroll;">
|
||||||
|
<table class="ui compact basic unstackable celled table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tag Name</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tagsTableBody">
|
||||||
|
<!-- Rows will be dynamically added here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h4>Add New Tags</h4>
|
||||||
|
<p>Create new tag or add this proxy rule to an existing tag</p>
|
||||||
|
<div class="ui form">
|
||||||
|
<div class="field">
|
||||||
|
<label>New Tags</label>
|
||||||
|
<input type="text" id="tagsInput" placeholder="e.g. mediaserver, management">
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" onclick="addSelectedTags();"><i class="ui blue plus icon"></i> Add tag</button>
|
||||||
|
<div class="ui horizontal divider">
|
||||||
|
Or
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Join Existing Tag Groups</label>
|
||||||
|
<div class="ui fluid multiple search selection dropdown" id="existingTagsDropdown">
|
||||||
|
<input type="hidden" id="existingTagsInput">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="default text">Select Tags</div>
|
||||||
|
<div class="menu" id="existingTagsMenu">
|
||||||
|
<!-- Options will be dynamically added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small id="notagwarning" style="display:none;"><i class="ui green circle check icon"></i> All tags has already been included in this host</small>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<!-- <button class="ui basic button" onclick="saveTags();"><i class="ui green save icon"></i> Save Changes</button> -->
|
||||||
|
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let editingEndpoint = {};
|
||||||
|
if (window.location.hash.length > 1){
|
||||||
|
let payloadHash = window.location.hash.substr(1);
|
||||||
|
try{
|
||||||
|
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||||
|
$("#epname").text(payloadHash.ep);
|
||||||
|
editingEndpoint = payloadHash;
|
||||||
|
loadTags();
|
||||||
|
}catch(ex){
|
||||||
|
console.log("Unable to load endpoint data from hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTags(){
|
||||||
|
$.get("/api/proxy/detail", { type: "host", epname: editingEndpoint.ep }, function(data){
|
||||||
|
if (data.error == undefined){
|
||||||
|
//Render the tags to the table
|
||||||
|
$("#tagsTableBody").empty();
|
||||||
|
data.Tags.forEach(tag => {
|
||||||
|
addTagRow(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.Tags.length == 0){
|
||||||
|
appendNoTagNotice();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent.msgbox(data.error, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Populate the dropdown with all tags created in the system
|
||||||
|
populateTagsDropdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Append or remove a notice to the table when no tags are applied
|
||||||
|
function appendNoTagNotice(){
|
||||||
|
$("#tagsTableBody").append(`<tr class="notagNotice" style="opacity: 0.5; pointer-events: none; user-select: none;"><td colspan="2"><i class="ui green circle check icon"></i> No tags applied to this host</td></tr>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNoTagNotice(){
|
||||||
|
$("#tagsTableBody .notagNotice").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load all tags created in this system
|
||||||
|
function loadAllCreatedTags(callback){
|
||||||
|
$.get("/api/proxy/list?type=host", function(data){
|
||||||
|
if (data.error !== undefined){
|
||||||
|
//No existsing rule created yet. Fresh install?
|
||||||
|
callback([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tags = {};
|
||||||
|
data.forEach(host => {
|
||||||
|
host.Tags.forEach(tag => {
|
||||||
|
tags[tag] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniqueTags = Object.keys(tags);
|
||||||
|
callback(uniqueTags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Populate the dropdown with all tags created in the system
|
||||||
|
function populateTagsDropdown(){
|
||||||
|
loadAllCreatedTags(function(tags) {
|
||||||
|
let existingTags = new Set();
|
||||||
|
$('#tagsTableBody tr').each(function() {
|
||||||
|
existingTags.add($(this).attr('value'));
|
||||||
|
});
|
||||||
|
tags = tags.filter(tag => !existingTags.has(tag));
|
||||||
|
$('#existingTagsMenu').empty();
|
||||||
|
tags.forEach(tag => {
|
||||||
|
$('#existingTagsMenu').append(`<div class="item" data-value="${tag}">${tag}</div>`);
|
||||||
|
});
|
||||||
|
$('#existingTagsDropdown').dropdown();
|
||||||
|
|
||||||
|
if (tags.length == 0){
|
||||||
|
$('#notagwarning').show();
|
||||||
|
}else{
|
||||||
|
$('#notagwarning').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagAlreadyExistsInTable(tag) {
|
||||||
|
return $(`#tagsTableBody .tagEntry[value="${tag}"]`).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSelectedTags() {
|
||||||
|
let tags = $('#tagsInput').val().split(',').map(tag => tag.trim());
|
||||||
|
tags.forEach(tag => {
|
||||||
|
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||||
|
addTagRow(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(tags);
|
||||||
|
populateTagsDropdown();
|
||||||
|
$('#tagsInput').val('');
|
||||||
|
saveTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinSelectedTagGroups() {
|
||||||
|
if ($('#existingTagsInput').val() == ""){
|
||||||
|
parent.msgbox("Please select at least one tag group to join", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let selectedTags = $('#existingTagsInput').val().split(',');
|
||||||
|
selectedTags.forEach(tag => {
|
||||||
|
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||||
|
addTagRow(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
populateTagsDropdown();
|
||||||
|
$('#existingTagsDropdown').dropdown('clear');
|
||||||
|
saveTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to generate a color based on a tag name
|
||||||
|
function getTagColorByName(tagName) {
|
||||||
|
function hashCode(str) {
|
||||||
|
return str.split('').reduce((prevHash, currVal) =>
|
||||||
|
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
||||||
|
}
|
||||||
|
let hash = hashCode(tagName);
|
||||||
|
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
||||||
|
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
||||||
|
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add a tag row to the table
|
||||||
|
function addTagRow(tag) {
|
||||||
|
const row = `<tr class="tagEntry" value="${tag}">
|
||||||
|
<td><div class="ui circular label tag-color" style="background-color: ${getTagColorByName(tag)};"></div> ${tag}</td>
|
||||||
|
<td>
|
||||||
|
<button title="Delete Tag" class="ui circular mini red basic icon button" onclick="removeTag('${tag}')">
|
||||||
|
<i class="trash icon"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
$("#tagsTableBody").append(row);
|
||||||
|
removeNoTagNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTag(tag) {
|
||||||
|
$(`#tagsTableBody .tagEntry[value="${tag}"]`).remove();
|
||||||
|
populateTagsDropdown();
|
||||||
|
saveTags();
|
||||||
|
|
||||||
|
if ($('#tagsTableBody tr').length == 0){
|
||||||
|
appendNoTagNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTags(){
|
||||||
|
let tags = [];
|
||||||
|
$('#tagsTableBody tr').each(function() {
|
||||||
|
tags.push($(this).attr('value'));
|
||||||
|
});
|
||||||
|
console.log(tags);
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/proxy/edit",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
type: "host",
|
||||||
|
rootname: editingEndpoint.ep,
|
||||||
|
tags: tags.join(",")
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
parent.msgbox(data.error, false);
|
||||||
|
} else {
|
||||||
|
parent.msgbox("Tags updated");
|
||||||
|
//Update the preview on parent page
|
||||||
|
parent.renderTagsPreview(editingEndpoint.ep, tags);
|
||||||
|
//parent.hideSideWrapper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -36,6 +36,10 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.darkTheme #upstreamTable{
|
||||||
|
border: 1px solid var(--button_border_color);
|
||||||
|
}
|
||||||
|
|
||||||
.upstreamEntry.inactive{
|
.upstreamEntry.inactive{
|
||||||
background-color: #f3f3f3 !important;
|
background-color: #f3f3f3 !important;
|
||||||
}
|
}
|
||||||
@ -53,6 +57,17 @@
|
|||||||
margin-bottom: 0.4em;
|
margin-bottom: 0.4em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.advanceUpstreamOptions{
|
||||||
|
padding: 0.6em;
|
||||||
|
background-color: var(--theme_advance);
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanceUpstreamOptions.ui.accordion .content{
|
||||||
|
padding: 1em !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -117,6 +132,38 @@
|
|||||||
<label>Skip WebSocket Origin Check<br>
|
<label>Skip WebSocket Origin Check<br>
|
||||||
<small>Check this to allow cross-origin websocket requests</small></label>
|
<small>Check this to allow cross-origin websocket requests</small></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui advanceUpstreamOptions accordion" style="margin-top:0.6em;">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Advanced Options
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Max Concurrent Connections</p>
|
||||||
|
<div class="ui mini fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" id="maxConn" value="0">
|
||||||
|
</div>
|
||||||
|
<small>Set to 0 for default value (32 connections)</small>
|
||||||
|
<br><br>
|
||||||
|
<p>Response Timeout</p>
|
||||||
|
<div class="ui mini right labeled fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" id="respTimeout" value="0">
|
||||||
|
<div class="ui basic label">
|
||||||
|
Seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small>Maximum waiting time for server header response, set to 0 for default</small>
|
||||||
|
<br><br>
|
||||||
|
<p>Idle Timeout</p>
|
||||||
|
<div class="ui mini right labeled fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" id="idleTimeout" value="0">
|
||||||
|
<div class="ui basic label">
|
||||||
|
Seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small>Maximum allowed keep-alive time forcefully closes the connection, set to 0 for default</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
|
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
|
||||||
</div>
|
</div>
|
||||||
@ -168,6 +215,8 @@
|
|||||||
renderUpstreamEntryToTable(upstream, false);
|
renderUpstreamEntryToTable(upstream, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".advanceUpstreamOptions.accordion").accordion();
|
||||||
|
|
||||||
let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
|
let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
|
||||||
if (totalUpstreams == 1){
|
if (totalUpstreams == 1){
|
||||||
$(".lowPriorityButton").addClass('disabled');
|
$(".lowPriorityButton").addClass('disabled');
|
||||||
@ -223,6 +272,8 @@
|
|||||||
let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
|
let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
|
||||||
let payload = encodeURIComponent(JSON.stringify(upstream));
|
let payload = encodeURIComponent(JSON.stringify(upstream));
|
||||||
let domUID = newUID();
|
let domUID = newUID();
|
||||||
|
|
||||||
|
//Timeout values are stored as ms in the backend
|
||||||
$("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"inactive"} basic segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}">
|
$("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"inactive"} basic segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}">
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<div class="ui toggle checkbox" style="display:inline-block;">
|
<div class="ui toggle checkbox" style="display:inline-block;">
|
||||||
@ -258,6 +309,39 @@
|
|||||||
<label>Skip WebSocket Origin Check<br>
|
<label>Skip WebSocket Origin Check<br>
|
||||||
<small>Check this to allow cross-origin websocket requests</small></label>
|
<small>Check this to allow cross-origin websocket requests</small></label>
|
||||||
</div><br>
|
</div><br>
|
||||||
|
<!-- Advance Settings -->
|
||||||
|
<div class="ui advanceUpstreamOptions accordion" style="margin-top:0.6em;">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Advanced Options
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Max Concurrent Connections</p>
|
||||||
|
<div class="ui mini fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" class="maxConn" value="${upstream.MaxConn}">
|
||||||
|
</div>
|
||||||
|
<small>Set to 0 for default value (32 connections)</small>
|
||||||
|
<br>
|
||||||
|
<p style="margin-top: 0.6em;">Response Timeout</p>
|
||||||
|
<div class="ui mini right labeled fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" class="respTimeout" value="${upstream.RespTimeout/1000}">
|
||||||
|
<div class="ui basic label">
|
||||||
|
Seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small>Maximum waiting time before Zoraxy receive server header response, set to 0 for default</small>
|
||||||
|
<br>
|
||||||
|
<p style="margin-top: 0.6em;">Idle Timeout</p>
|
||||||
|
<div class="ui mini right labeled fluid input" style="margin-top: -0.6em;">
|
||||||
|
<input type="number" min="0" class="idleTimeout" value="${upstream.IdleTimeout/1000}">
|
||||||
|
<div class="ui basic label">
|
||||||
|
Seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small>Maximum allowed keep-alive time before Zoraxy forcefully close the connection, set to 0 for default</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="upstreamActions">
|
<div class="upstreamActions">
|
||||||
<!-- Change Priority -->
|
<!-- Change Priority -->
|
||||||
@ -312,12 +396,32 @@
|
|||||||
let skipVerification = $("#skipTlsVerification")[0].checked;
|
let skipVerification = $("#skipTlsVerification")[0].checked;
|
||||||
let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[0].checked;
|
let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[0].checked;
|
||||||
let activateLoadbalancer = $("#activateNewUpstreamCheckbox")[0].checked;
|
let activateLoadbalancer = $("#activateNewUpstreamCheckbox")[0].checked;
|
||||||
|
let maxConn = $("#maxConn").val();
|
||||||
|
let respTimeout = $("#respTimeout").val();
|
||||||
|
let idleTimeout = $("#idleTimeout").val();
|
||||||
|
|
||||||
|
if (maxConn == "" || isNaN(maxConn)){
|
||||||
|
maxConn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (respTimeout == "" || isNaN(respTimeout)){
|
||||||
|
respTimeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idleTimeout == "" || isNaN(idleTimeout)){
|
||||||
|
idleTimeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (origin == ""){
|
if (origin == ""){
|
||||||
parent.msgbox("Upstream origin cannot be empty", false);
|
parent.msgbox("Upstream origin cannot be empty", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Convert seconds to ms
|
||||||
|
respTimeout = parseInt(respTimeout) * 1000;
|
||||||
|
idleTimeout = parseInt(idleTimeout) * 1000;
|
||||||
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/proxy/upstream/add",
|
url: "/api/proxy/upstream/add",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -328,6 +432,9 @@
|
|||||||
"tlsval": skipVerification,
|
"tlsval": skipVerification,
|
||||||
"bpwsorg":skipWebSocketOriginCheck,
|
"bpwsorg":skipWebSocketOriginCheck,
|
||||||
"active": activateLoadbalancer,
|
"active": activateLoadbalancer,
|
||||||
|
"maxconn": maxConn,
|
||||||
|
"respt": respTimeout,
|
||||||
|
"idlet": idleTimeout,
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
@ -336,6 +443,9 @@
|
|||||||
parent.msgbox("New upstream origin added");
|
parent.msgbox("New upstream origin added");
|
||||||
initOriginList();
|
initOriginList();
|
||||||
$("#originURL").val("");
|
$("#originURL").val("");
|
||||||
|
$("#maxConn").val("0");
|
||||||
|
$("#respTimeout").val("0");
|
||||||
|
$("#idleTimeout").val("0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -352,11 +462,34 @@
|
|||||||
let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[0].checked;
|
let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[0].checked;
|
||||||
let skipWebSocketOriginCheck = $(upstream).find(".SkipWebSocketOriginCheck")[0].checked;
|
let skipWebSocketOriginCheck = $(upstream).find(".SkipWebSocketOriginCheck")[0].checked;
|
||||||
|
|
||||||
|
//Advance options
|
||||||
|
let maxConn = $(upstream).find(".maxConn").val();
|
||||||
|
let respTimeout = $(upstream).find(".respTimeout").val();
|
||||||
|
let idleTimeout = $(upstream).find(".idleTimeout").val();
|
||||||
|
|
||||||
|
if (maxConn == "" || isNaN(maxConn)){
|
||||||
|
maxConn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (respTimeout == "" || isNaN(respTimeout)){
|
||||||
|
respTimeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idleTimeout == "" || isNaN(idleTimeout)){
|
||||||
|
idleTimeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
respTimeout = parseInt(respTimeout) * 1000;
|
||||||
|
idleTimeout = parseInt(idleTimeout) * 1000;
|
||||||
|
|
||||||
//Update the original setting with new one just applied
|
//Update the original setting with new one just applied
|
||||||
originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
|
originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
|
||||||
originalSettings.RequireTLS = requireTLS;
|
originalSettings.RequireTLS = requireTLS;
|
||||||
originalSettings.SkipCertValidations = skipTLSVerification;
|
originalSettings.SkipCertValidations = skipTLSVerification;
|
||||||
originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
|
originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
|
||||||
|
originalSettings.MaxConn = parseInt(maxConn);
|
||||||
|
originalSettings.RespTimeout = respTimeout;
|
||||||
|
originalSettings.IdleTimeout = idleTimeout;
|
||||||
|
|
||||||
//console.log(originalSettings);
|
//console.log(originalSettings);
|
||||||
return originalSettings;
|
return originalSettings;
|
||||||
|
Reference in New Issue
Block a user