mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-28 18:31:45 +02:00
Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
895ee1e53f | |||
caf4ab331b | |||
36c1f149e6 | |||
b0dc4d6670 | |||
5d8bec7f24 | |||
32f60dfba6 | |||
0abe4c12cf | |||
7555611ba5 | |||
e624227dae | |||
27695584ab | |||
e47a7a8357 | |||
3246f8ea2c | |||
ccbda6d7c2 | |||
a7285438af | |||
693dba07b7 | |||
9b64278200 | |||
d04eff2bda | |||
3320b56b19 | |||
99728144b3 | |||
05511ed4ca | |||
70abfe6fcf | |||
6ab91c377f | |||
1863af0d63 | |||
2a9d87787d | |||
f753becd66 | |||
bb2d0d5b46 | |||
07dc63a82c | |||
97a6cf016a | |||
8df68f1f4e | |||
e4ad505f2a | |||
a402c4f326 | |||
791fbfa1b4 | |||
c49f2fd1db | |||
7d9f240d56 | |||
e20f816080 | |||
eeb438eb18 | |||
bfd64a885e | |||
45f61b3053 | |||
0d4c71d0f6 | |||
d1e5581eea | |||
be5797c8a5 | |||
ebd316a7f1 | |||
84aec4387a | |||
30dfb9cb65 | |||
0b1768ab5b | |||
ad4721820b | |||
1d4c275db3 | |||
b3ad97743c | |||
1a6a87e79b | |||
749fd4b7af |
7
.gitignore
vendored
7
.gitignore
vendored
@ -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
|
33
CHANGELOG.md
33
CHANGELOG.md
@ -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)
|
||||
|
14
README.md
14
README.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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())))
|
||||
}
|
||||
|
||||
|
16
src/def.go
16
src/def.go
@ -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")
|
||||
)
|
||||
|
11
src/go.mod
11
src/go.mod
@ -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
|
||||
|
20
src/go.sum
20
src/go.sum
@ -176,6 +176,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-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=
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
65
src/mod/dynamicproxy/default.go
Normal file
65
src/mod/dynamicproxy/default.go
Normal 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: "",
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -17,5 +17,6 @@ func IsProxmox(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
|
45
src/mod/dynamicproxy/modh2c/modh2c.go
Normal file
45
src/mod/dynamicproxy/modh2c/modh2c.go
Normal file
@ -0,0 +1,45 @@
|
||||
package modh2c
|
||||
|
||||
/*
|
||||
modh2c.go
|
||||
|
||||
This module is a simple h2c roundtripper for dpcore
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type H2CRoundTripper struct {
|
||||
}
|
||||
|
||||
func NewH2CRoundTripper() *H2CRoundTripper {
|
||||
return &H2CRoundTripper{}
|
||||
}
|
||||
|
||||
// Example from https://github.com/thrawn01/h2c-golang-example/blob/master/cmd/client/main.go
|
||||
func (h2c *H2CRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, req.Method, req.RequestURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
},
|
||||
}
|
||||
|
||||
return tr.RoundTrip(req)
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package dynamicproxy
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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
@ -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
|
||||
}
|
||||
|
@ -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 == "" {
|
||||
|
@ -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
58
src/mod/uptime/typedef.go
Normal file
@ -0,0 +1,58 @@
|
||||
package uptime
|
||||
|
||||
import "imuslab.com/zoraxy/mod/info/logger"
|
||||
|
||||
const (
|
||||
logModuleName = "uptime-monitor"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
Timestamp int64
|
||||
ID string
|
||||
Name string
|
||||
URL string
|
||||
Protocol string
|
||||
Online bool
|
||||
StatusCode int
|
||||
Latency int64
|
||||
}
|
||||
|
||||
type ProxyType string
|
||||
|
||||
const (
|
||||
ProxyType_Host ProxyType = "Origin Server"
|
||||
ProxyType_Vdir ProxyType = "Virtual Directory"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
ID string
|
||||
Name string
|
||||
URL string
|
||||
Protocol string
|
||||
ProxyType ProxyType
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Targets []*Target
|
||||
Interval int
|
||||
MaxRecordsStore int
|
||||
OnlineStateNotify func(upstreamIP string, isOnline bool)
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
Config *Config
|
||||
OnlineStatusLog map[string][]*Record
|
||||
}
|
||||
|
||||
// Default configs
|
||||
var exampleTarget = Target{
|
||||
ID: "example",
|
||||
Name: "Example",
|
||||
URL: "example.com",
|
||||
Protocol: "https",
|
||||
}
|
||||
|
||||
func defaultNotify(upstreamIP string, isOnline bool) {
|
||||
// Do nothing
|
||||
}
|
@ -14,56 +14,6 @@ import (
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 @@
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
260
src/web/snippet/tagEditor.html
Normal file
260
src/web/snippet/tagEditor.html
Normal file
@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
||||
<script src="../script/jquery-3.6.0.min.js"></script>
|
||||
<script src="../script/semantic/semantic.min.js"></script>
|
||||
<script src="../script/utils.js"></script>
|
||||
<style>
|
||||
.ui.circular.label.tag-color{
|
||||
min-width: 5px !important;
|
||||
min-height: 5px !important;
|
||||
margin-right: .4em;
|
||||
margin-bottom: -0.2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
Edit Tags
|
||||
<div class="sub header" id="epname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<p>Tags currently applied to this host name / proxy rule</p>
|
||||
<div style="max-height: 300px; overflow-y: scroll;">
|
||||
<table class="ui compact basic unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag Name</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tagsTableBody">
|
||||
<!-- Rows will be dynamically added here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Add New Tags</h4>
|
||||
<p>Create new tag or add this proxy rule to an existing tag</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>New Tags</label>
|
||||
<input type="text" id="tagsInput" placeholder="e.g. mediaserver, management">
|
||||
</div>
|
||||
<button class="ui basic button" onclick="addSelectedTags();"><i class="ui blue plus icon"></i> Add tag</button>
|
||||
<div class="ui horizontal divider">
|
||||
Or
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Join Existing Tag Groups</label>
|
||||
<div class="ui fluid multiple search selection dropdown" id="existingTagsDropdown">
|
||||
<input type="hidden" id="existingTagsInput">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Select Tags</div>
|
||||
<div class="menu" id="existingTagsMenu">
|
||||
<!-- Options will be dynamically added here -->
|
||||
</div>
|
||||
</div>
|
||||
<small id="notagwarning" style="display:none;"><i class="ui green circle check icon"></i> All tags has already been included in this host</small>
|
||||
</div>
|
||||
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<!-- <button class="ui basic button" onclick="saveTags();"><i class="ui green save icon"></i> Save Changes</button> -->
|
||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||
</div>
|
||||
<script>
|
||||
let editingEndpoint = {};
|
||||
if (window.location.hash.length > 1){
|
||||
let payloadHash = window.location.hash.substr(1);
|
||||
try{
|
||||
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||
$("#epname").text(payloadHash.ep);
|
||||
editingEndpoint = payloadHash;
|
||||
loadTags();
|
||||
}catch(ex){
|
||||
console.log("Unable to load endpoint data from hash")
|
||||
}
|
||||
}
|
||||
|
||||
function loadTags(){
|
||||
$.get("/api/proxy/detail", { type: "host", epname: editingEndpoint.ep }, function(data){
|
||||
if (data.error == undefined){
|
||||
//Render the tags to the table
|
||||
$("#tagsTableBody").empty();
|
||||
data.Tags.forEach(tag => {
|
||||
addTagRow(tag);
|
||||
});
|
||||
|
||||
if (data.Tags.length == 0){
|
||||
appendNoTagNotice();
|
||||
}
|
||||
} else {
|
||||
parent.msgbox(data.error, false);
|
||||
}
|
||||
|
||||
//Populate the dropdown with all tags created in the system
|
||||
populateTagsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
//Append or remove a notice to the table when no tags are applied
|
||||
function appendNoTagNotice(){
|
||||
$("#tagsTableBody").append(`<tr class="notagNotice" style="opacity: 0.5; pointer-events: none; user-select: none;"><td colspan="2"><i class="ui green circle check icon"></i> No tags applied to this host</td></tr>`);
|
||||
}
|
||||
|
||||
function removeNoTagNotice(){
|
||||
$("#tagsTableBody .notagNotice").remove();
|
||||
}
|
||||
|
||||
//Load all tags created in this system
|
||||
function loadAllCreatedTags(callback){
|
||||
$.get("/api/proxy/list?type=host", function(data){
|
||||
if (data.error !== undefined){
|
||||
//No existsing rule created yet. Fresh install?
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
let tags = {};
|
||||
data.forEach(host => {
|
||||
host.Tags.forEach(tag => {
|
||||
tags[tag] = true;
|
||||
});
|
||||
});
|
||||
|
||||
let uniqueTags = Object.keys(tags);
|
||||
callback(uniqueTags);
|
||||
});
|
||||
}
|
||||
|
||||
//Populate the dropdown with all tags created in the system
|
||||
function populateTagsDropdown(){
|
||||
loadAllCreatedTags(function(tags) {
|
||||
let existingTags = new Set();
|
||||
$('#tagsTableBody tr').each(function() {
|
||||
existingTags.add($(this).attr('value'));
|
||||
});
|
||||
tags = tags.filter(tag => !existingTags.has(tag));
|
||||
$('#existingTagsMenu').empty();
|
||||
tags.forEach(tag => {
|
||||
$('#existingTagsMenu').append(`<div class="item" data-value="${tag}">${tag}</div>`);
|
||||
});
|
||||
$('#existingTagsDropdown').dropdown();
|
||||
|
||||
if (tags.length == 0){
|
||||
$('#notagwarning').show();
|
||||
}else{
|
||||
$('#notagwarning').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tagAlreadyExistsInTable(tag) {
|
||||
return $(`#tagsTableBody .tagEntry[value="${tag}"]`).length > 0;
|
||||
}
|
||||
|
||||
function addSelectedTags() {
|
||||
let tags = $('#tagsInput').val().split(',').map(tag => tag.trim());
|
||||
tags.forEach(tag => {
|
||||
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||
addTagRow(tag);
|
||||
}
|
||||
});
|
||||
console.log(tags);
|
||||
populateTagsDropdown();
|
||||
$('#tagsInput').val('');
|
||||
saveTags();
|
||||
}
|
||||
|
||||
function joinSelectedTagGroups() {
|
||||
if ($('#existingTagsInput').val() == ""){
|
||||
parent.msgbox("Please select at least one tag group to join", false);
|
||||
return;
|
||||
}
|
||||
let selectedTags = $('#existingTagsInput').val().split(',');
|
||||
selectedTags.forEach(tag => {
|
||||
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||
addTagRow(tag);
|
||||
}
|
||||
});
|
||||
populateTagsDropdown();
|
||||
$('#existingTagsDropdown').dropdown('clear');
|
||||
saveTags();
|
||||
}
|
||||
|
||||
|
||||
// Function to generate a color based on a tag name
|
||||
function getTagColorByName(tagName) {
|
||||
function hashCode(str) {
|
||||
return str.split('').reduce((prevHash, currVal) =>
|
||||
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
||||
}
|
||||
let hash = hashCode(tagName);
|
||||
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
||||
return color;
|
||||
}
|
||||
|
||||
//Add a tag row to the table
|
||||
function addTagRow(tag) {
|
||||
const row = `<tr class="tagEntry" value="${tag}">
|
||||
<td><div class="ui circular label tag-color" style="background-color: ${getTagColorByName(tag)};"></div> ${tag}</td>
|
||||
<td>
|
||||
<button title="Delete Tag" class="ui circular mini red basic icon button" onclick="removeTag('${tag}')">
|
||||
<i class="trash icon"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
$("#tagsTableBody").append(row);
|
||||
removeNoTagNotice();
|
||||
}
|
||||
|
||||
function removeTag(tag) {
|
||||
$(`#tagsTableBody .tagEntry[value="${tag}"]`).remove();
|
||||
populateTagsDropdown();
|
||||
saveTags();
|
||||
|
||||
if ($('#tagsTableBody tr').length == 0){
|
||||
appendNoTagNotice();
|
||||
}
|
||||
}
|
||||
|
||||
function saveTags(){
|
||||
let tags = [];
|
||||
$('#tagsTableBody tr').each(function() {
|
||||
tags.push($(this).attr('value'));
|
||||
});
|
||||
console.log(tags);
|
||||
$.cjax({
|
||||
url: "/api/proxy/edit",
|
||||
method: "POST",
|
||||
data: {
|
||||
type: "host",
|
||||
rootname: editingEndpoint.ep,
|
||||
tags: tags.join(",")
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
parent.msgbox("Tags updated");
|
||||
//Update the preview on parent page
|
||||
parent.renderTagsPreview(editingEndpoint.ep, tags);
|
||||
//parent.hideSideWrapper();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -36,6 +36,10 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
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
3
tools/benchmark/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module imuslab.com/zoraxy/benchmark
|
||||
|
||||
go 1.23.2
|
51
tools/benchmark/main.go
Normal file
51
tools/benchmark/main.go
Normal 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
42
tools/benchmark/server.go
Normal 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
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
5
tools/websocket_echo/go.mod
Normal file
5
tools/websocket_echo/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module aroz.org/zoraxy/websocket-echo
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
2
tools/websocket_echo/go.sum
Normal file
2
tools/websocket_echo/go.sum
Normal 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=
|
44
tools/websocket_echo/main.go
Normal file
44
tools/websocket_echo/main.go
Normal 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))
|
||||
}
|
67
tools/websocket_echo/test.html
Normal file
67
tools/websocket_echo/test.html
Normal 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>
|
Reference in New Issue
Block a user