diff --git a/.gitignore b/.gitignore index 26006a7..6e386d5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ docker/ImagePublisher.sh src/mod/acme/test/stackoverflow.pem /tools/dns_challenge_update/code-gen/acmedns /tools/dns_challenge_update/code-gen/lego +src/tmp/localhost.key +src/tmp/localhost.pem +src/www/html/index.html +src/sys.uuid +src/zoraxy +src/log/ \ No newline at end of file diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 2771059..f89fa26 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -72,6 +72,14 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Rate Limit Check + if sep.RequireRateLimit { + err := h.handleRateLimitRouting(w, r, sep) + if err != nil { + return + } + } + //Validate basic auth if sep.RequireBasicAuth { err := h.handleBasicAuthRouting(w, r, sep) diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 7532d09..1395734 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -129,6 +129,13 @@ func (router *Router) StartProxyService() error { } } + // Rate Limit Check + // if sep.RequireBasicAuth { + if err := handleRateLimit(w, r, sep); err != nil { + return + } + // } + //Validate basic auth if sep.RequireBasicAuth { err := handleBasicAuth(w, r, sep) diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go new file mode 100644 index 0000000..0ac4e9f --- /dev/null +++ b/src/mod/dynamicproxy/ratelimit.go @@ -0,0 +1,86 @@ +package dynamicproxy + +import ( + "errors" + "log" + "net" + "net/http" + "sync" + "sync/atomic" + "time" +) + +// IpTable is a rate limiter implementation using sync.Map with atomic int64 +type IpTable struct { + table sync.Map +} + +// Increment the count of requests for a given IP +func (t *IpTable) Increment(ip string) { + v, _ := t.table.LoadOrStore(ip, new(int64)) + atomic.AddInt64(v.(*int64), 1) +} + +// Check if the IP is in the table and if it is, check if the count is less than the limit +func (t *IpTable) Exceeded(ip string, limit int64) bool { + v, ok := t.table.Load(ip) + if !ok { + return false + } + count := atomic.LoadInt64(v.(*int64)) + return count >= limit +} + +// Get the count of requests for a given IP +func (t *IpTable) GetCount(ip string) int64 { + v, ok := t.table.Load(ip) + if !ok { + return 0 + } + return atomic.LoadInt64(v.(*int64)) +} + +// Clear the IP table +func (t *IpTable) Clear() { + t.table.Range(func(key, value interface{}) bool { + t.table.Delete(key) + return true + }) +} + +var ipTable = IpTable{} + +func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + err := handleRateLimit(w, r, pe) + if err != nil { + h.logRequest(r, false, 429, "ratelimit", pe.Domain) + } + return err +} + +func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + w.WriteHeader(500) + log.Println("Error resolving remote address", r.RemoteAddr, err) + return errors.New("internal server error") + } + + ipTable.Increment(ip) + + if ipTable.Exceeded(ip, int64(pe.RateLimit)) { + w.WriteHeader(429) + return errors.New("rate limit exceeded") + } + + // log.Println("Rate limit check", ip, ipTable.GetCount(ip)) + + return nil +} + +func InitRateLimit() { + for { + ipTable.Clear() + time.Sleep(time.Second) + } +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 3386aa6..d4f9a8a 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -124,6 +124,10 @@ type ProxyEndpoint struct { BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target + // Rate Limiting + RequireRateLimit bool + RateLimit int64 // Rate limit in requests per second + //Access Control AccessFilterUUID string //Access filter ID diff --git a/src/reverseproxy.go b/src/reverseproxy.go index e2113da..9e7664f 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -146,6 +146,10 @@ func ReverseProxtInit() { SystemWideLogger.Println("Uptime Monitor background service started") }() + // Init Rate Limit + go func() { + dynamicproxy.InitRateLimit() + }() } func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) { @@ -229,6 +233,26 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { requireBasicAuth := (rba == "true") + // Require Rate Limiting? + rl, _ := utils.PostPara(r, "rate") + if rl == "" { + rl = "false" + } + requireRateLimit := (rl == "true") + rlnum, _ := utils.PostPara(r, "ratenum") + if rlnum == "" { + rlnum = "0" + } + proxyRateLimit, err := strconv.ParseInt(rlnum, 10, 64) + if err != nil { + utils.SendErrorResponse(w, "invalid rate limit number") + return + } + if proxyRateLimit <= 0 { + utils.SendErrorResponse(w, "rate limit number must be greater than 0") + return + } + // Bypass WebSocket Origin Check strbpwsorg, _ := utils.PostPara(r, "bpwsorg") if strbpwsorg == "" { @@ -309,6 +333,9 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, DefaultSiteOption: 0, DefaultSiteValue: "", + // Rate Limit + RequireRateLimit: requireRateLimit, + RateLimit: proxyRateLimit, } preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint) @@ -430,6 +457,26 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { requireBasicAuth := (rba == "true") + // Rate Limiting? + rl, _ := utils.PostPara(r, "rate") + if rl == "" { + rl = "false" + } + requireRateLimit := (rl == "true") + rlnum, _ := utils.PostPara(r, "ratenum") + if rlnum == "" { + rlnum = "0" + } + proxyRateLimit, err := strconv.ParseInt(rlnum, 10, 64) + if err != nil { + utils.SendErrorResponse(w, "invalid rate limit number") + return + } + if proxyRateLimit <= 0 { + utils.SendErrorResponse(w, "rate limit number must be greater than 0") + return + } + // Bypass WebSocket Origin Check strbpwsorg, _ := utils.PostPara(r, "bpwsorg") if strbpwsorg == "" { @@ -451,6 +498,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS newProxyEndpoint.SkipCertValidations = skipTlsValidation newProxyEndpoint.RequireBasicAuth = requireBasicAuth + newProxyEndpoint.RequireRateLimit = requireRateLimit + newProxyEndpoint.RateLimit = proxyRateLimit newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck //Prepare to replace the current routing rule diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index b96dedc..c660eb3 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -20,6 +20,7 @@ Destination Virtual Directory Basic Auth + Rate Limit Actions @@ -107,6 +108,9 @@ ${subd.RequireBasicAuth?``:``} + + ${subd.RequireRateLimit?` ${subd.RateLimit}req/s`:``} +
@@ -301,6 +305,23 @@
`); + } else if (datatype == "ratelimit"){ + let requireRateLimit = payload.RequireRateLimit; + let checkstate = ""; + if (requireRateLimit){ + checkstate = "checked"; + } + let rateLimit = payload.RateLimit; + + column.empty().append(`
+ + +
+
+ +
+ `); + }else if (datatype == 'action'){ column.empty().append(` @@ -348,6 +369,8 @@ let requireTLS = $(row).find(".RequireTLS")[0].checked; let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked; let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked; + let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked; + let rateLimit = $(row).find(".RateLimit").val(); let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked; let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked; console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth) @@ -364,6 +387,8 @@ "tlsval": skipCertValidations, "bpwsorg" : bypassWebsocketOrigin, "bauth" :requireBasicAuth, + "rate" :requireRateLimit, + "ratenum" :rateLimit, }, success: function(data){ if (data.error !== undefined){ diff --git a/src/web/components/rules.html b/src/web/components/rules.html index 6a4faad..9a8474e 100644 --- a/src/web/components/rules.html +++ b/src/web/components/rules.html @@ -73,6 +73,17 @@
+
+
+ + +
+
+
+ + + The Rate Limit is applied to the whole proxy endpoint. If the number of requests exceeds the limit, the proxy will return a 429 error code. +
@@ -147,6 +158,8 @@ var skipTLSValidation = $("#skipTLSValidation")[0].checked; var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked; var requireBasicAuth = $("#requireBasicAuth")[0].checked; + var proxyRateLimit = $("#proxyRateLimit").val(); + var requireRateLimit = $("#requireRateLimit")[0].checked; var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked; var accessRuleToUse = $("#newProxyRuleAccessFilter").val(); @@ -176,6 +189,8 @@ bpwsorg: skipWebSocketOriginCheck, bypassGlobalTLS: bypassGlobalTLS, bauth: requireBasicAuth, + rate: requireRateLimit, + ratenum: proxyRateLimit, cred: JSON.stringify(credentials), access: accessRuleToUse, }, @@ -264,6 +279,16 @@ } $("#requireBasicAuth").on('change', toggleBasicAuth); toggleBasicAuth(); + + function toggleRateLimit() { + if ($("#requireRateLimit").parent().checkbox("is checked")) { + $("#proxyRateLimit").parent().removeClass("disabled"); + } else { + $("#proxyRateLimit").parent().addClass("disabled"); + } + } + $("#requireRateLimit").on('change', toggleRateLimit); + toggleRateLimit(); /*