mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
Merge pull request #544 from tobychui/v3.1.8
- Exposed timeout value from dpcore to UI - Added active load balancing (if uptime monitor is enabled on that rule) - Refactorized io stats and remove dependencies over wmic - Removed SMTP input validation - Fixed sticky session bug - Fixed passive load balancer bug - Fixed dockerfile bug
This commit is contained in:
commit
895ee1e53f
@ -42,7 +42,7 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.1.7"
|
||||
SYSTEM_VERSION = "3.1.8"
|
||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||
|
||||
/* System Constants */
|
||||
|
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=
|
||||
|
@ -2,7 +2,6 @@ package dpcore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@ -12,8 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/modh2c"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
@ -84,9 +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)
|
||||
UseH2CRoundTripper bool //Use H2C RoundTripper for HTTP/2.0 connection
|
||||
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 {
|
||||
@ -104,30 +106,38 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
||||
}
|
||||
|
||||
thisTransporter := http.DefaultTransport
|
||||
if dpcOptions.UseH2CRoundTripper {
|
||||
thisTransporter = &http2.Transport{
|
||||
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
},
|
||||
AllowHTTP: true,
|
||||
}
|
||||
}
|
||||
|
||||
//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,
|
||||
|
@ -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"
|
||||
@ -198,14 +200,21 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -28,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
|
||||
@ -193,7 +194,7 @@ 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
|
||||
}
|
||||
|
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
|
||||
}
|
||||
|
||||
|
@ -163,10 +163,11 @@ func ReverseProxtInit() {
|
||||
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")
|
||||
@ -594,6 +595,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
loadBalancer.ResetSessions()
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
|
||||
//Save it to file
|
||||
|
@ -79,6 +79,25 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendErrorResponse(w, "upstream origin not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Response timeout in seconds, set to 0 for default
|
||||
respTimeout, err := utils.PostInt(r, "respt")
|
||||
if err != nil {
|
||||
respTimeout = 0
|
||||
}
|
||||
|
||||
//Idle timeout in seconds, set to 0 for default
|
||||
idleTimeout, err := utils.PostInt(r, "idlet")
|
||||
if err != nil {
|
||||
idleTimeout = 0
|
||||
}
|
||||
|
||||
//Max concurrent connection to dpcore instance, set to 0 for default
|
||||
maxConn, err := utils.PostInt(r, "maxconn")
|
||||
if err != nil {
|
||||
maxConn = 0
|
||||
}
|
||||
|
||||
requireTLS, _ := utils.PostBool(r, "tls")
|
||||
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
|
||||
bpwsorg, _ := utils.PostBool(r, "bpwsorg")
|
||||
@ -91,7 +110,9 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
SkipWebSocketOriginCheck: bpwsorg,
|
||||
Weight: 1,
|
||||
MaxConn: 0,
|
||||
MaxConn: maxConn,
|
||||
RespTimeout: int64(respTimeout),
|
||||
IdleTimeout: int64(idleTimeout),
|
||||
}
|
||||
|
||||
//Add the new upstream to endpoint
|
||||
|
@ -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",
|
||||
|
@ -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">
|
||||
|
@ -57,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>
|
||||
@ -121,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>
|
||||
@ -172,6 +215,8 @@
|
||||
renderUpstreamEntryToTable(upstream, false);
|
||||
});
|
||||
|
||||
$(".advanceUpstreamOptions.accordion").accordion();
|
||||
|
||||
let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
|
||||
if (totalUpstreams == 1){
|
||||
$(".lowPriorityButton").addClass('disabled');
|
||||
@ -227,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;">
|
||||
@ -262,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 -->
|
||||
@ -316,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",
|
||||
@ -332,6 +432,9 @@
|
||||
"tlsval": skipVerification,
|
||||
"bpwsorg":skipWebSocketOriginCheck,
|
||||
"active": activateLoadbalancer,
|
||||
"maxconn": maxConn,
|
||||
"respt": respTimeout,
|
||||
"idlet": idleTimeout,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
@ -340,6 +443,9 @@
|
||||
parent.msgbox("New upstream origin added");
|
||||
initOriginList();
|
||||
$("#originURL").val("");
|
||||
$("#maxConn").val("0");
|
||||
$("#respTimeout").val("0");
|
||||
$("#idleTimeout").val("0");
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -356,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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user