39 Commits

Author SHA1 Message Date
895ee1e53f Merge pull request #544 from tobychui/v3.1.8
- Exposed timeout value from dpcore to UI
- Added active load balancing (if uptime monitor is enabled on that rule)
- Refactorized io stats and remove dependencies over wmic
- Removed SMTP input validation
- Fixed sticky session bug
- Fixed passive load balancer bug
- Fixed dockerfile bug
2025-02-16 17:13:37 +08:00
caf4ab331b Exposed dpcore timeout options
- Exposed idle timeout and response timeout option
- Updated upstream edit UI to use the new API
- Updated geodb
2025-02-16 16:58:25 +08:00
36c1f149e6 Fixed #497
- Removed SMTP input validations
- Updated version no.
- Added todo for removing SMTP all together in future revisions
2025-02-16 09:08:08 +08:00
b0dc4d6670 Fix #542 2025-02-15 16:26:10 -05:00
5d8bec7f24 Fixed sticky session bug
- Fixed sticky session bug in new active fallback lb implementation
2025-02-14 22:53:29 +08:00
32f60dfba6 Fixed #523
- Fixed passive fallback logic
- Added active fallback setting notify from uptime monitor
2025-02-14 22:04:51 +08:00
0abe4c12cf Fixed #526
- Fixed typos
2025-02-12 20:58:22 +08:00
7555611ba5 Fixed h2c enable crash bug
- Moved h2c roundtripper to a dedicated module
- Fixed h2c enable crash bug
2025-02-11 21:53:21 +08:00
e624227dae Merge pull request #520 from eyerrock/wmic-refactor
Remove WMIC dependency and unify network stats retrieval
2025-02-11 19:38:56 +08:00
27695584ab Update README.md
Added new start flags into README
2025-02-09 13:44:55 +08:00
e47a7a8357 Merge pull request #525 from Morethanevil/main
Update CHANGELOG.md
2025-02-09 10:53:13 +08:00
3246f8ea2c Update CHANGELOG.md
:)
2025-02-08 23:52:37 +01:00
ccbda6d7c2 refactored io stats 2025-02-08 16:11:47 +01:00
a7285438af Merge pull request #522 from tobychui/v3.1.7
- Merged and added new tagging system for HTTP Proxy rules
- Added inline editing for redirection rules
- Added uptime monitor status dot detail info (now clickable)
- Added close connection support to port 80 listener
- Optimized port collision check on startup
- Optimized dark theme color scheme (Free consultation by [3S Design studio](https://www.3sdesign.io/))
- Fixed capital letter rule unable to delete bug
2025-02-08 18:40:15 +08:00
693dba07b7 Updated tag filtering
- Added automatic empty tag removal when creating new proxy rule
2025-02-08 17:07:26 +08:00
9b64278200 Merge pull request #521 from PassiveLemon/docker-term-fix
Refactor: Launch services in background and trap Docker TERM signal
2025-02-08 16:09:38 +08:00
d04eff2bda Updated geodb
- Updated geoip database
2025-02-08 16:08:33 +08:00
3320b56b19 Update tagEditor.html
- Optimized UX for tag editor
- Finished integration of tag system
2025-02-08 15:19:36 +08:00
99728144b3 Refactor: Launch services in background and trap Docker TERM signal 2025-02-08 01:37:03 -05:00
05511ed4ca Updated tag system design
- Added search-able tag dropdown
- Implemented realtime quick search
- Added better tag coloring
2025-02-07 22:08:56 +08:00
70abfe6fcf Restore dockerfile
- The docker file change shd be included in another PR
2025-02-06 20:36:23 +08:00
6ab91c377f Merge pull request #509 from adoolaard/dev-tags
Add Tagging Feature for Reverse Proxy Hosts + Search & Filter
2025-02-06 20:35:32 +08:00
1863af0d63 Minor css update
- Changed inline edit button for redirection rule to circular to match http proxy rule page
2025-02-05 20:33:38 +08:00
2a9d87787d Fixed #510
- Added inline edit for redirection rule
2025-02-05 20:24:42 +08:00
f753becd66 The proxy hosts broke on import, because the tags were missing. This is now fixed. 2025-02-03 15:10:13 +01:00
bb2d0d5b46 Fixed #507 2025-02-03 21:10:24 +08:00
07dc63a82c Added H2C (experimental)
- Added experimental H2C transporter
- Exposed default listening port and web server listen state to start parameters #474
2025-02-03 20:36:34 +08:00
97a6cf016a Point on the I 2025-01-31 00:17:10 +01:00
8df68f1f4e Zoeken en filteren werkt ook! 2025-01-30 22:48:48 +01:00
e4ad505f2a Tags editor works! 2025-01-30 22:42:06 +01:00
a402c4f326 Tags are working, just not yet editable 2025-01-30 22:22:42 +01:00
791fbfa1b4 Updated gitignore 2025-01-30 21:48:40 +01:00
c49f2fd1db Changed dockerfile to better cache 2025-01-30 21:22:19 +01:00
7d9f240d56 Updated Close Conn resp for TLS
- Use No Resp instead of 200 for close connection mode default site settings
2025-01-18 22:10:45 +08:00
e20f816080 Fixed #467
- Added status dot info in uptime monitor
- simplified the no response record to no_resp in default site
2025-01-18 21:49:35 +08:00
eeb438eb18 Fixed #474
- Added automatic port check and reminder for beginners
2025-01-18 15:19:55 +08:00
bfd64a885e Removed confirm from access
- Removed troublesome confirm popup from black / whitelist
- Minor fix to checkbox css
2025-01-15 20:59:09 +08:00
45f61b3053 Optimized dark theme mode
- Make dark theme mode less dark
2025-01-15 20:44:20 +08:00
0d4c71d0f6 Fixed #450 2024-12-31 22:56:51 +08:00
44 changed files with 39056 additions and 30068 deletions

7
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,5 +17,6 @@ func IsProxmox(r *http.Request) bool {
return true return true
} }
} }
return false return false
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@ -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 @@
} }
}); });
} //}
} }
/* /*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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