50 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
d1e5581eea Merge pull request #449 from tobychui/v3.1.6
- Exposed log file, sys.uuid and static web server path to start flag
- Optimized connection close implementation
- Added toggle for uptime monitor
- Added optional copy HTTP custom headers to websocket connection
2024-12-31 21:49:41 +08:00
be5797c8a5 Updated geodb and minor instructions 2024-12-31 21:47:19 +08:00
ebd316a7f1 Exposed log and db filepath setting 2024-12-31 21:14:37 +08:00
84aec4387a Added CF and Fastly IP in access list
Added CF and Fastly Client IP passthrough header for access control ip resolver
2024-12-31 20:30:36 +08:00
30dfb9cb65 Added new UI feature
- Added toggle for uptime monitor
- Added toggle for enable custom header passthrough to websocket
2024-12-30 21:41:15 +08:00
0b1768ab5b Added manual toggle for websocket header copy
- Added setting for toggling websocket header copy
- Added close connection in TLS mode
2024-12-30 21:07:29 +08:00
ad4721820b Added websocket header test and benchmark tool 2024-12-30 21:01:45 +08:00
1d4c275db3 Fixed nil pointer exception in new setups 2024-12-29 16:11:00 +08:00
b3ad97743c Fixed #444
- Restored legacy behavior if proxmox cookie is detected in request
2024-12-29 15:09:24 +08:00
1a6a87e79b Merge pull request #443 from Morethanevil/main
Update CHANGELOG.md
2024-12-28 15:19:43 +08:00
749fd4b7af Update CHANGELOG.md 2024-12-28 05:25:00 +01:00
59 changed files with 41540 additions and 31566 deletions

7
.gitignore vendored
View File

@ -39,4 +39,9 @@ src/tmp/localhost.pem
src/www/html/index.html
src/sys.uuid
src/zoraxy
src/log/
src/log/
# dev-tags
/Dockerfile
/Entrypoint.sh

View File

@ -1,3 +1,36 @@
# 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
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
+ Added levelDB as database implementation (not currently used)
+ Added external GeoIP db loading support
+ Restructured a lot of modules
# v3.1.4 24 Nov 2024
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)

View File

@ -101,12 +101,20 @@ Usage of zoraxy:
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-cfgupgrade
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
Run Zoraxy in docker compatibility mode
-earlyrenew int
Number of days to early renew a soon expiring certificate (days) (default 30)
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-log string
Log folder path (default "./log")
-mdns
Enable mDNS scanner and transponder (default true)
-mdnsname string
@ -117,12 +125,16 @@ Usage of zoraxy:
Management web interface listening port (default ":8000")
-sshlb
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
Show version of this server
-webfm
Enable web file manager for static web server root folder (default true)
-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
ZeroTier authtoken for the local node
-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
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 --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy

View File

@ -1,5 +1,14 @@
#!/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
echo "CA certificates updated."
@ -11,12 +20,13 @@ if [ "$ZEROTIER" = "true" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d
zerotier-one -d &
zerotierpid=$!
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
exec zoraxy \
zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
@ -33,5 +43,10 @@ exec zoraxy \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
-ztauth="$ZTAUTH" \
-ztport="$ZTPORT"
-ztport="$ZTPORT" \
&
zoraxypid=$!
wait $zoraxypid
wait $zerotierpid

View File

@ -59,6 +59,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior)
/* Reverse proxy auth related */
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
@ -87,6 +88,7 @@ func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
}

View File

@ -48,12 +48,17 @@ func LoadReverseProxyConfig(configFilepath string) error {
}
//Parse it into dynamic proxy endpoint
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
thisConfigEndpoint := dynamicproxy.GetDefaultProxyEndpoint()
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
if err != nil {
return err
}
//Make sure the tags are not nil
if thisConfigEndpoint.Tags == nil {
thisConfigEndpoint.Tags = []string{}
}
//Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
@ -129,31 +134,23 @@ func RemoveReverseProxyConfig(endpoint string) error {
// Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default Authentication Provider
defaultAuth := &dynamicproxy.AuthenticationProvider{
AuthMethod: dynamicproxy.AuthMethodNone,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyTypeRoot,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
//Get the default proxy endpoint
rootProxyEndpointConfig := dynamicproxy.GetDefaultProxyEndpoint()
rootProxyEndpointConfig.ProxyType = dynamicproxy.ProxyTypeRoot
rootProxyEndpointConfig.RootOrMatchingDomain = "/"
rootProxyEndpointConfig.ActiveOrigins = []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
AuthenticationProvider: defaultAuth,
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
})
}
rootProxyEndpointConfig.DefaultSiteOption = dynamicproxy.DefaultSite_InternalStaticWebServer
rootProxyEndpointConfig.DefaultSiteValue = ""
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&rootProxyEndpointConfig)
if err != nil {
return nil, err
}
@ -175,12 +172,16 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
}
// Specify the folder path to be zipped
folderPath := "./conf/"
if !utils.FileExists("./conf") {
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
return
}
folderPath := "./conf"
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
// Set the Content-Disposition header to specify the file name
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
// Set the Content-Disposition header to specify the file name, add timestamp to the filename
w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"")
// Create a zip writer
zipWriter := zip.NewWriter(w)
@ -230,7 +231,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
}
// Open the file on disk
file, err := os.Open("sys.db")
file, err := os.Open("./sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return
@ -279,6 +280,8 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
//os.Rename(*path_conf, backupPath)
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}

View File

@ -42,11 +42,10 @@ import (
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.1.5"
SYSTEM_VERSION = "3.1.8"
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
/* System Constants */
DATABASE_PATH = "sys.db"
TMP_FOLDER = "./tmp"
WEBSERV_DEFAULT_PORT = 5487
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
@ -59,7 +58,6 @@ const (
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
CSRF_COOKIENAME = "zoraxy_csrf"
LOG_PREFIX = "zr"
LOG_FOLDER = "./log"
LOG_EXTENSION = ".log"
/* Configuration Folder Storage Path Constants */
@ -86,10 +84,20 @@ var (
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
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")
/* 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_database = flag.String("dbpath", "./sys.db", "Database path")
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
path_logFile = flag.String("log", "./log", "Log folder path")
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
/* Maintaince Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
)

View File

@ -16,8 +16,10 @@ require (
github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1
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/sys v0.25.0
golang.org/x/sys v0.28.0
golang.org/x/text v0.18.0
)
@ -26,13 +28,15 @@ require (
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/benbjohnson/clock v1.3.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // 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/syndtr/goleveldb v1.0.0 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect
github.com/tidwall/gjson v1.12.1 // indirect
@ -43,6 +47,7 @@ require (
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
)
@ -175,7 +180,7 @@ require (
github.com/softlayer/softlayer-go v1.1.5 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // 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/dnspod v1.0.1002 // 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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
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-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-resty/resty/v2 v2.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
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=
@ -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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
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-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-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-20191005200804-aed5e4c7ecf9/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-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-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-20210315160823-c6e025ad8005/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.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -86,7 +86,7 @@ func main() {
SetupCloseHandler()
//Read or create the system uuid
uuidRecord := "./sys.uuid"
uuidRecord := *path_uuid
if !utils.FileExists(uuidRecord) {
newSystemUUID := uuid.New().String()
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)

View File

@ -209,23 +209,18 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
//Serve the not found page, use template if exists
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)
}
h.serve404PageWithTemplate(w, r)
case DefaultSite_NoResponse:
//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)
if !ok {
w.WriteHeader(http.StatusNoContent)
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
w.WriteHeader(http.StatusNoContent)
return
}
conn.Close()
@ -239,3 +234,15 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
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

@ -0,0 +1,65 @@
package dynamicproxy
import (
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
/*
Default Provider
This script provide the default options for all datatype
provided by dynamicproxy module
*/
// GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
}
}
// GetDefaultHeaderRewriteRules return a default header rewrite rules
func GetDefaultHeaderRewriteRules() *HeaderRewriteRules {
return &HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
RequestHostOverwrite: "",
HSTSMaxAge: 0,
EnablePermissionPolicyHeader: false,
PermissionPolicy: nil,
DisableHopByHopHeaderRemoval: false,
}
}
// GetDefaultProxyEndpoint return a default proxy endpoint
func GetDefaultProxyEndpoint() ProxyEndpoint {
randomPrefix := uuid.New().String()
return ProxyEndpoint{
ProxyType: ProxyTypeHost,
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
}

View File

@ -99,6 +99,23 @@ func DomainUsesTLS(targetURL string) bool {
return false
}
/*
WebSocket Header Sniff
*/
// Check if the requst is a special case where
// user defined header shall not be passed over
func RequireWebsocketHeaderCopy(r *http.Request) bool {
//Return false for proxmox
if IsProxmox(r) {
return false
}
//Add more edge cases here
return true
}
/*
Request Handlers
*/

View File

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

View File

@ -12,6 +12,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/modh2c"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
@ -82,8 +83,12 @@ type requestCanceler interface {
}
type DpcoreOptions struct {
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)
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)
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 {
@ -100,22 +105,39 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
}
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
//Hack the default transporter to handle more connections
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).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
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 {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
}
if dpcOptions.UseH2CRoundTripper {
//Use H2C RoundTripper for HTTP/2.0 connection
thisTransporter = modh2c.NewH2CRoundTripper()
}
return &ReverseProxy{
Director: director,
Prepender: prepender,

View File

@ -157,12 +157,18 @@ func (router *Router) StartProxyService() error {
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
router.logRequest(r, false, 404, "vdir-http", r.Host)
}
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if sep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = sep.HeaderRewriteRules
}
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.HeaderRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: sep.HeaderRewriteRules.DisableHopByHopHeaderRemoval,
HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
})
@ -185,7 +191,24 @@ func (router *Router) StartProxyService() error {
w.Write([]byte("400 - Bad Request"))
} else {
//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)
}
}
}
@ -331,7 +354,7 @@ func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
return true
}
if key == matchingDomain {
if key == strings.ToLower(matchingDomain) {
targetProxyEndpoint = v
}
return true

View File

@ -27,7 +27,12 @@ import (
// Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if ep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = ep.HeaderRewriteRules
}
for _, header := range endpointProxyRewriteRules.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) {
return true
}
@ -38,6 +43,9 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*rewrite.UserDefinedHeader{}
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header)
@ -55,6 +63,9 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefined
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
}
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.HeaderRewriteRules.UserDefinedHeaders = append(ep.HeaderRewriteRules.UserDefinedHeaders, newHeaderRule)
return nil
@ -106,7 +117,7 @@ func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath s
return errors.New("target virtual directory routing rule not found")
}
// Delete a vdir rule by its matching path
// Add a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
@ -256,7 +267,8 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Delete(lookupHostname)
return nil
}

View File

@ -3,6 +3,7 @@ package loadbalance
import (
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/sessions"
@ -25,11 +26,12 @@ type Options struct {
}
type RouteManager struct {
SessionStore *sessions.CookieStore
LoadBalanceMap sync.Map //Sync map to store the last load balance state of a given node
OnlineStatusMap sync.Map //Sync map to store the online status of a given ip address or domain name
onlineStatusTickerStop chan bool //Stopping channel for the online status pinger
Options Options //Options for the load balancer
SessionStore *sessions.CookieStore
OnlineStatus sync.Map //Store the online status notify by uptime monitor
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 */
@ -41,8 +43,12 @@ type Upstream struct {
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Load balancing configs
Weight int //Random weight for round robin, 0 for fallback only
MaxConn int //TODO: Maxmium connection to this server, 0 for unlimited
Weight int //Random weight for round robin, 0 for fallback only
//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
proxy *dpcore.ReverseProxy
@ -55,14 +61,31 @@ func NewLoadBalancer(options *Options) *RouteManager {
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
store := sessions.NewCookieStore([]byte(options.SystemUUID))
return &RouteManager{
SessionStore: store,
LoadBalanceMap: sync.Map{},
OnlineStatusMap: sync.Map{},
onlineStatusTickerStop: nil,
Options: *options,
SessionStore: store,
OnlineStatus: sync.Map{},
Options: *options,
cacheTicker: cacheTicker,
cacheTickerStop: cacheTickerStop,
}
}
@ -90,11 +113,20 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
return strings.Join(targets, ", ")
}
func (m *RouteManager) Close() {
if m.onlineStatusTickerStop != nil {
m.onlineStatusTickerStop <- true
}
// Reset the current session store and clear all previous sessions
func (m *RouteManager) ResetSessions() {
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

View File

@ -1,39 +1,72 @@
package loadbalance
import (
"net/http"
"strconv"
"strings"
"time"
)
// Return the last ping status to see if the target is online
func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
value, ok := m.LoadBalanceMap.Load(matchingDomainOrIp)
// Return if the target host is online
func (m *RouteManager) IsTargetOnline(upstreamIP string) bool {
value, ok := m.OnlineStatus.Load(upstreamIP)
if !ok {
return false
// Assume online if not found, also update the map
m.OnlineStatus.Store(upstreamIP, true)
return true
}
isOnline, ok := value.(bool)
return ok && isOnline
}
// Ping a target to see if it is online
func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
client := &http.Client{
Timeout: 10 * time.Second,
// Notify the host online state, should be called from uptime monitor
func (m *RouteManager) NotifyHostOnlineState(upstreamIP string, isOnline bool) {
//if the upstream IP contains http or https, strip it
upstreamIP = strings.TrimPrefix(upstreamIP, "http://")
upstreamIP = strings.TrimPrefix(upstreamIP, "https://")
//Check previous state and update
if m.IsTargetOnline(upstreamIP) == isOnline {
return
}
url := targetMatchingDomainOrIp
if requireTLS {
url = "https://" + url
} else {
url = "http://" + url
}
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode <= 600
m.OnlineStatus.Store(upstreamIP, isOnline)
m.println("Updating upstream "+upstreamIP+" online state to "+strconv.FormatBool(isOnline), nil)
}
// 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) {
//if the upstream IP contains http or https, strip it
upstreamIp = strings.TrimPrefix(upstreamIp, "http://")
upstreamIp = strings.TrimPrefix(upstreamIp, "https://")
if timeout <= 0 {
//Set to the default timeout
timeout = 60
}
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 {
return nil, errors.New("no upstream is defined for this host")
}
var targetOrigin = origins[0]
//Pick the origin
if useStickySession {
//Use stick session, check which origins this request previously used
targetOriginId, err := m.getSessionHandler(r, origins)
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)
if err != nil {
m.println("Unable to get random upstream", err)
targetOrigin = origins[0]
index = 0
}
//fmt.Println("DEBUG: (Sticky Session) Registering session origin " + origins[index].OriginIpOrDomain)
m.setSessionHandler(w, r, targetOrigin.OriginIpOrDomain, index)
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
} else {
//Do not use stick session. Get a random one
var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Failed to get next origin", err)
targetOrigin = origins[0]
}
}
//No sticky session, get a random origin
m.clearSessionHandler(w, r) //Clear the session
//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)
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 */
//Set a new origin for this connection by session
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
}
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
func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) {
// 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")
}
originDomain := originDomainRaw.(string)
originID := originIDRaw.(int)
//originID := originIDRaw.(int)
//Check if it has been modified
if len(upstreams) < originID || upstreams[originID].OriginIpOrDomain != originDomain {
//Mismatch or upstreams has been updated
return -1, errors.New("upstreams has been changed")
//Check if the upstream still exists
for i, upstream := range upstreams {
if upstream.OriginIpOrDomain == originDomain {
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 */

View File

@ -39,8 +39,11 @@ func (u *Upstream) StartProxy() error {
}
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
IgnoreTLSVerification: u.SkipCertValidations,
FlushInterval: 100 * time.Millisecond,
IgnoreTLSVerification: u.SkipCertValidations,
FlushInterval: 100 * time.Millisecond,
ResponseHeaderTimeout: u.RespTimeout,
IdleConnectionTimeout: u.IdleTimeout,
MaxConcurrentConnection: u.MaxConn,
})
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
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
@ -10,6 +12,7 @@ import (
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
@ -143,10 +146,15 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
}
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
if target.HeaderRewriteRules == nil {
target.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
CopyAllHeaders: true,
CopyAllHeaders: target.EnableWebsocketCustomHeaders,
UserDefinedHeaders: target.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
})
@ -163,15 +171,19 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
}
//Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.HeaderRewriteRules.UserDefinedHeaders)
headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.HeaderRewriteRules != nil {
headerRewriteOptions = target.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.HeaderRewriteRules.HSTSMaxAge,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.HeaderRewriteRules.EnablePermissionPolicyHeader,
PermissionPolicy: target.HeaderRewriteRules.PermissionPolicy,
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the request reverse proxy
@ -183,19 +195,26 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.HeaderRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: target.HeaderRewriteRules.DisableHopByHopHeaderRemoval,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion,
})
//validate the error
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
} 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 {
//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")
//TODO: Take this upstream offline automatically
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
}
}
@ -222,11 +241,16 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
}
if target.parent.HeaderRewriteRules != nil {
target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: true,
SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: domainsniff.RequireWebsocketHeaderCopy(r), //Left this as default to prevent nginx user setting / as vdir
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
})
@ -243,15 +267,20 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
}
//Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.HeaderRewriteRules.UserDefinedHeaders)
headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.parent.HeaderRewriteRules != nil {
headerRewriteOptions = target.parent.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.parent.HeaderRewriteRules.HSTSMaxAge,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.parent.HeaderRewriteRules.EnablePermissionPolicyHeader,
PermissionPolicy: target.parent.HeaderRewriteRules.PermissionPolicy,
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the virtual directory reverse proxy request
@ -262,7 +291,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.parent.HeaderRewriteRules.RequestHostOverwrite,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion,
})

View File

@ -2,7 +2,6 @@ package redirection
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
@ -111,6 +110,42 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
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 {
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
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
filepath := path.Join(t.configPath, filename)
fmt.Println(redirectURL, filename, filepath)
// Check if the file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to delete

View File

@ -123,7 +123,7 @@
<div class="ui container">
<div class="ui stackable grid">
<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>
</div>
<div class="eight wide column">

View File

@ -1,5 +1,12 @@
package dynamicproxy
/*
typdef.go
This script handle the type definition for dynamic proxy and endpoints
If you are looking for the default object initailization, please refer to default.go
*/
import (
_ "embed"
"net"
@ -21,6 +28,7 @@ import (
type ProxyType int
const PassiveLoadBalanceNotifyTimeout = 60 //Time to assume a passive load balance is unreachable, in seconds
const (
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
@ -165,7 +173,8 @@ type ProxyEndpoint struct {
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
HeaderRewriteRules *HeaderRewriteRules
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
//Authentication
AuthenticationProvider *AuthenticationProvider
@ -185,7 +194,8 @@ type ProxyEndpoint struct {
DefaultSiteValue string //Fallback routing target, optional
//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"
"errors"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/shirou/gopsutil/v4/net"
"imuslab.com/zoraxy/mod/info/logger"
"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
func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
if runtime.GOOS == "windows" {
//Windows wmic sometime freeze and not respond.
//The safer way is to make a bypass mechanism
//when timeout with channel
type wmicResult struct {
RX int64
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#*>
// Get aggregated network I/O stats for all interfaces
counters, err := net.IOCounters(false)
if err != nil {
return 0, 0, err
}
if len(counters) == 0 {
return 0, 0, errors.New("no network interfaces found")
}
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

@ -16,6 +16,15 @@ import (
func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {
CF_Connecting_IP := r.Header.Get("CF-Connecting-IP")
Fastly_Client_IP := r.Header.Get("Fastly-Client-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
return CF_Connecting_IP
} else if Fastly_Client_IP != "" {
//Use Fastly Client IP
return Fastly_Client_IP
}
ip = r.Header.Get("X-Forwarded-For")
}
if ip == "" {

View File

@ -157,3 +157,13 @@ func resolveIpFromDomain(targetIpOrDomain string) string {
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"
)
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
func NewUptimeMonitor(config *Config) (*Monitor, error) {
//Create new monitor object
@ -77,6 +27,11 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
config.Logger, _ = logger.NewFmtLogger()
}
if config.OnlineStateNotify == nil {
//Use default notify function if not provided
config.OnlineStateNotify = defaultNotify
}
//Start the endpoint listener
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
done := make(chan bool)
@ -218,6 +173,7 @@ func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
end := time.Now().UnixNano() / int64(time.Millisecond)
if err != nil {
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
m.Config.OnlineStateNotify(url, false)
return false, 0, 0
} else {
diff := end - start
@ -231,7 +187,7 @@ func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
} else {
succ = false
}
m.Config.OnlineStateNotify(url, true)
return succ, diff, statusCode
}

View File

@ -172,6 +172,11 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Host != "" {
requestHeader.Set("Host", req.Host)
}
if userAgent := req.Header.Get("User-Agent"); userAgent != "" {
requestHeader.Set("User-Agent", userAgent)
} else {
requestHeader.Set("User-Agent", "zoraxy-wsproxy/1.1")
}
// Pass X-Forwarded-For headers too, code below is a part of
// httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For
@ -195,19 +200,29 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
requestHeader.Set("X-Forwarded-Proto", "https")
}
// Enable the director to copy any additional headers it desires for
// forwarding to the remote server.
if w.Director != nil {
w.Director(req, requestHeader)
}
// Replace header variables and copy user-defined headers
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(req, w.Options.UserDefinedHeaders)
upstreamHeaders, _ := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
requestHeader.Set(headerValuePair[0], headerValuePair[1])
if w.Options.CopyAllHeaders {
// Rewrite the user defined headers
// This is reported to be not compatible with Proxmox and Home Assistant
// but required by some other projects like MeshCentral
// we will make this optional
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(req, w.Options.UserDefinedHeaders)
upstreamHeaders, _ := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
//Do not copy Upgrade and Connection headers, it will be handled by the upgrader
if strings.EqualFold(headerValuePair[0], "Upgrade") || strings.EqualFold(headerValuePair[0], "Connection") {
continue
}
requestHeader.Set(headerValuePair[0], headerValuePair[1])
}
// Enable the director to copy any additional headers it desires for
// forwarding to the remote server.
if w.Director != nil {
w.Director(req, requestHeader)
}
}
// Connect to the backend URL, also pass the headers we get from the requst

View File

@ -78,6 +78,49 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
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
func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.PostPara(r, "enable")

View File

@ -14,6 +14,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
)
@ -27,11 +28,23 @@ func ReverseProxtInit() {
/*
Load Reverse Proxy Global Settings
*/
inboundPort := 443
inboundPort := *defaultInboundPort
autoStartReverseProxy := *defaultEnableInboundTraffic
if sysdb.KeyExists("settings", "inbound") {
//Read settings from database
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 {
//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)")
}
@ -60,6 +73,9 @@ func ReverseProxtInit() {
}
listenOnPort80 := true
if netutils.CheckIfPortOccupied(80) {
listenOnPort80 = false
}
sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 {
SystemWideLogger.Println("Port 80 listener enabled")
@ -96,7 +112,7 @@ func ReverseProxtInit() {
RedirectRuleTable: redirectTable,
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
WebDirectory: *path_webserver,
AccessController: accessController,
AutheliaRouter: autheliaRouter,
LoadBalancer: loadBalancer,
@ -136,19 +152,22 @@ func ReverseProxtInit() {
//Start Service
//Not sure why but delay must be added if you have another
//reverse proxy server in front of this service
time.Sleep(300 * time.Millisecond)
dynamicProxyRouter.StartProxyService()
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
if autoStartReverseProxy {
time.Sleep(300 * time.Millisecond)
dynamicProxyRouter.StartProxyService()
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
}
//Add all proxy services to uptime monitor
//Create a uptime monitor service
go func() {
//This must be done in go routine to prevent blocking on system startup
uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
Logger: SystemWideLogger, //Logger
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
OnlineStateNotify: loadBalancer.NotifyHostOnlineState, //Notify the load balancer for online state
Logger: SystemWideLogger, //Logger
})
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
if eptype == "host" {
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
@ -320,10 +356,6 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
thisCustomHeaderRules := dynamicproxy.HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
}
//Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O
@ -353,7 +385,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
AuthenticationProvider: &thisAuthenticationProvider,
//Header Rewrite
HeaderRewriteRules: &thisCustomHeaderRules,
HeaderRewriteRules: dynamicproxy.GetDefaultHeaderRewriteRules(),
//Default Site
DefaultSiteOption: 0,
@ -361,6 +393,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
// Rate Limit
RequireRateLimit: requireRateLimit,
RateLimit: int64(proxyRateLimit),
Tags: tags,
}
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
@ -471,6 +505,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
}
bypassGlobalTLS := (bpgtls == "true")
//Disable uptime monitor
disbleUtm, err := utils.PostBool(r, "dutm")
if err != nil {
disbleUtm = false
}
// Auth Provider
authProviderTypeStr, _ := utils.PostPara(r, "authprovider")
if authProviderTypeStr == "" {
@ -513,6 +553,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
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
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
@ -536,6 +585,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
newProxyEndpoint.Tags = tags
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@ -544,6 +595,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
targetProxyEntry.Remove()
loadBalancer.ResetSessions()
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
//Save it to file
@ -1557,3 +1609,39 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
func HandleWsHeaderBehavior(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
domain, err = utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
}
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
if r.Method == http.MethodGet {
js, _ := json.Marshal(targetProxyEndpoint.EnableWebsocketCustomHeaders)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
enableWsHeader, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable state given")
return
}
targetProxyEndpoint.EnableWebsocketCustomHeaders = enableWsHeader
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}

View File

@ -54,14 +54,14 @@ var (
func startupSequence() {
//Start a system wide logger and log viewer
l, err := logger.NewLogger(LOG_PREFIX, LOG_FOLDER)
l, err := logger.NewLogger(LOG_PREFIX, *path_logFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
RootFolder: LOG_FOLDER,
RootFolder: *path_logFile,
Extension: LOG_EXTENSION,
})
@ -73,7 +73,7 @@ func startupSequence() {
backendType = dbinc.BackendBoltDB
}
l.PrintAndLog("database", "Using "+backendType.String()+" as the database backend", nil)
db, err := database.NewDatabase(DATABASE_PATH, backendType)
db, err := database.NewDatabase("./sys.db", backendType)
if err != nil {
log.Fatal(err)
}
@ -158,7 +158,7 @@ func startupSequence() {
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb,
Port: strconv.Itoa(WEBSERV_DEFAULT_PORT), //Default Port
WebRoot: *staticWebServerRoot,
WebRoot: *path_webserver,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
Logger: SystemWideLogger,

View File

@ -79,6 +79,25 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "upstream origin not set")
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")
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
bpwsorg, _ := utils.PostBool(r, "bpwsorg")
@ -91,7 +110,9 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bpwsorg,
Weight: 1,
MaxConn: 0,
MaxConn: maxConn,
RespTimeout: int64(respTimeout),
IdleTimeout: int64(idleTimeout),
}
//Add the new upstream to endpoint

View File

@ -1174,7 +1174,7 @@
}
function removeIpBlacklist(ipaddr){
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
//if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
$.cjax({
url: "/api/blacklist/ip/remove",
type: "POST",
@ -1191,7 +1191,7 @@
}
});
}
//}
}
/*
@ -1318,7 +1318,7 @@
}
function removeIpWhitelist(ipaddr){
if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
//if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
$.cjax({
url: "/api/whitelist/ip/remove",
type: "POST",
@ -1335,7 +1335,7 @@
}
});
}
//}
}
/*

View File

@ -11,7 +11,47 @@
.subdEntry td:not(.ignoremw){
min-width: 200px;
}
.httpProxyListTools{
width: 100%;
}
.tag-select{
cursor: pointer;
}
.tag-select:hover{
text-decoration: underline;
opacity: 0.8;
}
</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;">
<table class="ui celled sortable unstackable compact table">
<thead>
@ -19,6 +59,7 @@
<th>Host</th>
<th>Destination</th>
<th>Virtual Directory</th>
<th>Tags</th>
<th style="max-width: 300px;">Advanced Settings</th>
<th class="no-sort" style="min-width:150px;">Actions</th>
</tr>
@ -124,6 +165,11 @@
</div>
</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;">
${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`:``}
@ -142,6 +188,7 @@
</td>
</tr>`);
});
populateTagFilterDropdown(data);
}
resolveAccessRuleNameOnHostRPlist();
@ -258,6 +305,13 @@
if (payload.UseStickySession){
useStickySessionChecked = "checked";
}
let enableUptimeMonitor = "";
//Note the config file store the uptime monitor as disable, so we need to reverse the logic
if (!payload.DisableUptimeMonitor){
enableUptimeMonitor = "checked";
}
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
@ -265,7 +319,11 @@
<label>Use Sticky Session<br>
<small>Enable stick session on load balancing</small></label>
</div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="EnableUptimeMonitor" ${enableUptimeMonitor}>
<label>Monitor Uptime<br>
<small>Enable active uptime monitor</small></label>
</div>
`;
column.append(input);
$(column).find(".upstreamList").addClass("editing");
@ -274,7 +332,11 @@
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
</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"){
let authProvider = payload.AuthenticationProvider.AuthMethod;
@ -441,11 +503,17 @@
var epttype = "host";
let useStickySession = $(row).find(".UseStickySession")[0].checked;
let DisableUptimeMonitor = !$(row).find(".EnableUptimeMonitor")[0].checked;
let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val();
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
let rateLimit = $(row).find(".RateLimit").val();
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
let tags = getTagsArrayFromEndpoint(uuid);
if (tags.length > 0){
tags = tags.join(",");
}else{
tags = "";
}
$.cjax({
url: "/api/proxy/edit",
method: "POST",
@ -453,10 +521,12 @@
"type": epttype,
"rootname": uuid,
"ss":useStickySession,
"dutm": DisableUptimeMonitor,
"bpgtls": bypassGlobalTLS,
"authprovider" :authProviderType,
"rate" :requireRateLimit,
"ratenum" :rateLimit,
"tags": tags,
},
success: function(data){
if (data.error !== undefined){
@ -596,4 +666,110 @@
tabSwitchEventBind["httprp"] = function(){
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>

View File

@ -13,7 +13,7 @@
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
<th class="no-sort">Actions</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
@ -163,13 +163,21 @@
$("#redirectionRuleList").html("");
$.get("/api/redirect/list", function(data){
data.forEach(function(entry){
$("#redirectionRuleList").append(`<tr>
<td><a href="${entry.RedirectURL}" 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="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>
</tr>`);
let encodedEntry = encodeURIComponent(JSON.stringify(entry));
let hrefURL = entry.RedirectURL;
if (!hrefURL.startsWith("http")){
hrefURL = "https://" + hrefURL;
}
$("#redirectionRuleList").append(`<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){
@ -180,6 +188,68 @@
}
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(){
$.get("/api/redirect/regex", function(data){
//Set the checkbox initial state

View File

@ -41,7 +41,7 @@
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="closeresp">
<label>Close Connection<br>
<small>Close the connection without any response or in TLS mode, send an empty response</small>
<small>Close the connection without any response</small>
</label>
</div>
</div>

View File

@ -63,6 +63,11 @@
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
</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">
<i class="ui green lock icon"></i>
Security
@ -198,6 +203,7 @@
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
let tags = $("#proxyTags").val().trim();
if (rootname.trim() == ""){
$("#rootname").parent().addClass("error");
@ -231,6 +237,7 @@
cred: JSON.stringify(credentials),
access: accessRuleToUse,
stickysess: useStickySessionLB,
tags: tags,
},
success: function(data){
if (data.error != undefined){
@ -239,6 +246,7 @@
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
$("#proxyTags").val("");
credentials = [];
updateTable();
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){
if (value.length == 0){
@ -132,6 +162,7 @@
let thisStatus = value[i];
let dotType = "";
let statusCode = thisStatus.StatusCode;
let statusDotPayload = encodeURIComponent(JSON.stringify(thisStatus));
if (!thisStatus.Online && statusCode == 0){
dotType = "offline";
@ -159,7 +190,7 @@
}
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;
@ -207,7 +238,7 @@
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
@ -233,8 +264,71 @@
${statusDotList}
</div>
${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>`);
}
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>

View File

@ -259,6 +259,8 @@
/*
SMTP Settings
TODO: Remove SMTP support in future versions
*/
//Bind events to the form
@ -273,11 +275,13 @@
adminAddr: $('input[name=recvAddr]').val()
};
/*
var inputValid = validateSMTPInputs();
if (!inputValid){
msgbox("SMTP input not valid", false, 5000);
return;
}
*/
$.cjax({
type: "POST",

View File

@ -23,6 +23,10 @@ body:not(.darkTheme){
--text_color_inverted: #fcfcfc;
--button_text_color: #878787;
--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_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{
--theme_bg: #0a090e;
--theme_bg_primary: #060912;
--theme_bg_secondary:#172a41;
--theme_highlight: #4380b0;
--theme_bg: #1e1e1e;
--theme_bg_primary: #151517;
--theme_bg_secondary:#1b3572;
--theme_highlight: #6a7792;
--theme_bg_active: #020101;
--theme_bg_inverted: #f8f8f9;
--theme_advance: #000000;
@ -47,8 +51,12 @@ body.darkTheme{
--text_color_inverted: #414141;
--button_text_color: #e9e9e9;
--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_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%);
@ -113,6 +121,9 @@ body.darkTheme .ui.basic.button:not(.red) {
body.darkTheme .ui.basic.button:not(.red):hover {
border: 1px solid var(--button_border_color) !important;
background-color: var(--theme_bg) !important;
}
body.darkTheme .ui.basic.button:not(.red):not(.dropdown):hover {
opacity: 0.8;
}
@ -195,10 +206,14 @@ body.darkTheme textarea:focus {
}
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{
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{
@ -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 tbody 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;
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 {
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 {
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 {
@ -537,6 +552,18 @@ body.darkTheme .RateLimit input {
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
*/
@ -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 {
background-color: var(--theme_bg) !important;
background-color: var(--table_bg_default) !important;
color: var(--text_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 {
background-color: var(--theme_bg) !important;
background-color: var(--table_bg_default) !important;
color: var(--text_color) !important;
border-color: var(--divider_color) !important;
}
@ -985,8 +1012,8 @@ body.darkTheme #utm .standardContainer {
}
body.darkTheme #utm .standardContainer .padding.statusDot {
background-color: var(--theme_bg) !important;
border: 0.2px solid var(--text_color_inverted) !important;
background-color: var(--status_dot_bg) !important;
}
body.darkTheme .ui.utmloading.segment {
@ -1116,7 +1143,7 @@ body.darkTheme .statistic .label {
/* Other Tables */
body.darkTheme .ui.celled.compact.table {
background-color: var(--theme_bg) !important;
background-color: var(--table_bg_default) !important;
color: var(--text_color) !important;
border-color: var(--divider_color) !important;
}

View File

@ -123,7 +123,7 @@
<div class="ui container">
<div class="ui stackable grid">
<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>
</div>
<div class="eight wide column">

View File

@ -124,7 +124,7 @@
<div class="ui container">
<div class="ui stackable grid">
<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>
</div>
<div class="eight wide column">

View File

@ -117,6 +117,16 @@
<label>Remove Hop-by-hop Header<br>
<small>This should be ON by default</small></label>
</div>
<div class="ui divider"></div>
<h4> WebSocket Custom Headers</h4>
<p>Copy custom headers from HTTP requests to WebSocket connections.
Might be required by some projects like MeshCentral.</p>
<div class="ui toggle checkbox">
<input type="checkbox" id="copyCustomHeadersWS" name="">
<label>Enable WebSocket Custom Header<br>
<small>This should be OFF by default</small></label>
</div>
<div class="ui yellow message">
<p><i class="exclamation triangle icon"></i>Settings in this section are for advanced users. Invalid settings might cause werid, unexpected behavior.</p>
</div>
@ -597,6 +607,7 @@
})
}
/* Manual Hostname overwrite */
function initManualHostOverwriteValue(){
$.get("/api/proxy/header/handleHostOverwrite?domain=" + editingEndpoint.ep, function(data){
if (data.error != undefined){
@ -643,6 +654,42 @@
})
}
initHopByHopRemoverState();
/* WebSocket Custom Headers */
function initWebSocketCustomHeaderState(){
$.get("/api/proxy/header/handleWsHeaderBehavior?domain=" + editingEndpoint.ep, function(data){
if (data.error != undefined){
parent.msgbox(data.error);
}else{
if (data == true){
$("#copyCustomHeadersWS").parent().checkbox("set checked");
}else{
$("#copyCustomHeadersWS").parent().checkbox("set unchecked");
}
//Bind event to the checkbox
$("#copyCustomHeadersWS").on("change", function(evt){
let isChecked = $(this)[0].checked;
$.cjax({
url: "/api/proxy/header/handleWsHeaderBehavior",
method: "POST",
data: {
"domain": editingEndpoint.ep,
"enable": isChecked,
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("WebSocket Custom Header rule updated");
}
}
})
})
}
})
}
initWebSocketCustomHeaderState();
</script>
</body>
</html>

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;
}
body.darkTheme #upstreamTable{
border: 1px solid var(--button_border_color);
}
.upstreamEntry.inactive{
background-color: #f3f3f3 !important;
}
@ -53,6 +57,17 @@
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>
</head>
<body>
@ -117,6 +132,38 @@
<label>Skip WebSocket Origin Check<br>
<small>Check this to allow cross-origin websocket requests</small></label>
</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>
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
</div>
@ -168,6 +215,8 @@
renderUpstreamEntryToTable(upstream, false);
});
$(".advanceUpstreamOptions.accordion").accordion();
let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
if (totalUpstreams == 1){
$(".lowPriorityButton").addClass('disabled');
@ -223,6 +272,8 @@
let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
let payload = encodeURIComponent(JSON.stringify(upstream));
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}">
<h4 class="ui header">
<div class="ui toggle checkbox" style="display:inline-block;">
@ -258,6 +309,39 @@
<label>Skip WebSocket Origin Check<br>
<small>Check this to allow cross-origin websocket requests</small></label>
</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 class="upstreamActions">
<!-- Change Priority -->
@ -312,12 +396,32 @@
let skipVerification = $("#skipTlsVerification")[0].checked;
let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[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 == ""){
parent.msgbox("Upstream origin cannot be empty", false);
return;
}
//Convert seconds to ms
respTimeout = parseInt(respTimeout) * 1000;
idleTimeout = parseInt(idleTimeout) * 1000;
$.cjax({
url: "/api/proxy/upstream/add",
method: "POST",
@ -328,6 +432,9 @@
"tlsval": skipVerification,
"bpwsorg":skipWebSocketOriginCheck,
"active": activateLoadbalancer,
"maxconn": maxConn,
"respt": respTimeout,
"idlet": idleTimeout,
},
success: function(data){
if (data.error != undefined){
@ -336,6 +443,9 @@
parent.msgbox("New upstream origin added");
initOriginList();
$("#originURL").val("");
$("#maxConn").val("0");
$("#respTimeout").val("0");
$("#idleTimeout").val("0");
}
}
})
@ -352,11 +462,34 @@
let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[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
originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
originalSettings.RequireTLS = requireTLS;
originalSettings.SkipCertValidations = skipTLSVerification;
originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
originalSettings.MaxConn = parseInt(maxConn);
originalSettings.RespTimeout = respTimeout;
originalSettings.IdleTimeout = idleTimeout;
//console.log(originalSettings);
return originalSettings;

3
tools/benchmark/go.mod Normal file
View File

@ -0,0 +1,3 @@
module imuslab.com/zoraxy/benchmark
go 1.23.2

51
tools/benchmark/main.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
var (
//Global variables
stopchan chan bool
//Runtime flags
benchmarkWebserverListeningPort int
)
func init() {
flag.IntVar(&benchmarkWebserverListeningPort, "port", 8123, "Port to listen on")
flag.Parse()
}
/* SIGTERM handler, do shutdown sequences before closing */
func SetupCloseHandler() {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
//Stop all request loops
fmt.Println("Stopping request generators")
if stopchan != nil {
stopchan <- true
}
// Wait for all goroutines to finish
time.Sleep(1 * time.Second)
os.Exit(0)
}()
}
func main() {
//Setup the SIGTERM handler
SetupCloseHandler()
//Start the web server
fmt.Println("Starting web server on port", benchmarkWebserverListeningPort)
fmt.Println("In Zoraxy, point your test proxy rule to this server at the given port")
startWebServer()
select {}
}

42
tools/benchmark/server.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"fmt"
"net/http"
"time"
)
// Start the web server for reciving test request
// in Zoraxy, point test.localhost to this server at the given port in the start variables
func startWebServer() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Print the request details to console
fmt.Printf("Timestamp: %s\n", time.Now().Format(time.RFC1123))
fmt.Printf("Request type: %s\n", r.Method)
fmt.Printf("Payload size: %d bytes\n", r.ContentLength)
fmt.Printf("Request URI: %s\n", r.RequestURI)
fmt.Printf("User Agent: %s\n", r.UserAgent())
fmt.Printf("Remote Address: %s\n", r.RemoteAddr)
fmt.Println("----------------------------------------")
//Set header to text
w.Header().Set("Content-Type", "text/plain")
// Send response, print the request details to web page
w.Write([]byte("----------------------------------------\n"))
w.Write([]byte("Request type: " + r.Method + "\n"))
w.Write([]byte(fmt.Sprintf("Payload size: %d bytes\n", r.ContentLength)))
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
w.Write([]byte("User Agent: " + r.UserAgent() + "\n"))
w.Write([]byte("Remote Address: " + r.RemoteAddr + "\n"))
w.Write([]byte("----------------------------------------\n"))
})
go func() {
err := http.ListenAndServe(fmt.Sprintf(":%d", benchmarkWebserverListeningPort), nil)
if err != nil {
fmt.Printf("Failed to start server: %v\n", err)
stopchan <- true
}
}()
}

View File

@ -0,0 +1,5 @@
module aroz.org/zoraxy/websocket-echo
go 1.23.2
require github.com/gorilla/websocket v1.5.3

View File

@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View File

@ -0,0 +1,44 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func echo(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
defer conn.Close()
for key, values := range r.Header {
for _, value := range values {
message := fmt.Sprintf("%s: %s", key, value)
if err := conn.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
log.Println("WriteMessage error:", err)
return
}
}
}
if err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
log.Println("CloseMessage error:", err)
return
}
}
func main() {
http.HandleFunc("/echo", echo)
log.Fatal(http.ListenAndServe(":8888", nil))
}

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Echo Test</title>
</head>
<body>
<h1>WebSocket Echo Test</h1>
<p>1. Go run main.go</p>
<p>2. Create a Zoraxy proxy rule (and add to hosts file) from ws.localhost (port 80) to 127.0.0.1:8888</p>
<p>3. Click the Connect button below to test if headers are correctly sent over</p>
<button id="connectBtn">Connect</button>
<button id="disconnectBtn" disabled>Disconnect</button>
<input type="text" id="messageInput" placeholder="Enter message">
<button id="sendBtn" disabled>Send</button>
<div id="output"></div>
<script>
let socket;
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const sendBtn = document.getElementById('sendBtn');
const messageInput = document.getElementById('messageInput');
const output = document.getElementById('output');
connectBtn.addEventListener('click', () => {
output.innerHTML = '';
//socket = new WebSocket('ws://localhost:8888/echo');
socket = new WebSocket('ws://ws.localhost/echo');
socket.onopen = () => {
output.innerHTML += '<p>Connected to WebSocket server</p>';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
sendBtn.disabled = false;
};
socket.onmessage = (event) => {
output.innerHTML += `<p>Received: ${event.data}</p>`;
};
socket.onclose = () => {
output.innerHTML += '<p>Disconnected from WebSocket server</p>';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
sendBtn.disabled = true;
};
socket.onerror = (error) => {
output.innerHTML += `<p>Error: ${error.message}</p>`;
};
});
disconnectBtn.addEventListener('click', () => {
socket.close();
});
sendBtn.addEventListener('click', () => {
const message = messageInput.value;
socket.send(message);
output.innerHTML += `<p>Sent: ${message}</p>`;
messageInput.value = '';
});
</script>
</body>
</html>