From c98576cf5130cf1353882c5e77b285e23a5049cd Mon Sep 17 00:00:00 2001 From: Sushant Date: Mon, 11 Nov 2024 07:46:45 +0530 Subject: [PATCH] integrated sliding window rate limit on frontend client --- rate_shield/api/index.html | 174 +++++++++++++++++++++++++ rate_shield/api/server.go | 23 +++- rate_shield/limiter/limiter.go | 2 +- web/src/api/rules.tsx | 7 + web/src/components/AddOrUpdateRule.tsx | 134 ++++++------------- web/src/pages/APIConfiguration.tsx | 2 +- web/src/utils/validators.tsx | 125 ++++++++++++++++++ 7 files changed, 371 insertions(+), 96 deletions(-) create mode 100644 rate_shield/api/index.html create mode 100644 web/src/utils/validators.tsx diff --git a/rate_shield/api/index.html b/rate_shield/api/index.html new file mode 100644 index 0000000..8931f24 --- /dev/null +++ b/rate_shield/api/index.html @@ -0,0 +1,174 @@ + + + + + + + + + RateShield Status + + + + +
+

RateShield is Running

+

Welcome to the RateShield rate limiter. The server is currently active on this port.

+ Default Frontend Port: 5173 +

If the frontend is not accessible, ensure the React application is running. Use:

+ npm run dev +
+ + + diff --git a/rate_shield/api/server.go b/rate_shield/api/server.go index 0e32f82..7475025 100644 --- a/rate_shield/api/server.go +++ b/rate_shield/api/server.go @@ -3,6 +3,7 @@ package api import ( "fmt" "net/http" + "os" "github.com/rs/zerolog/log" "github.com/x-sushant-x/RateShield/limiter" @@ -23,11 +24,12 @@ func NewServer(port int, limiter limiter.Limiter) Server { } func (s Server) StartServer() error { - log.Info().Msg("Setting Up API Endpoints ✅") + log.Info().Msg("Setting Up API endpoints ✅") mux := http.NewServeMux() s.rulesRoutes(mux) s.registerRateLimiterRoutes(mux) + s.setupHome(mux) corsMux := s.setupCORS(mux) @@ -36,7 +38,7 @@ func (s Server) StartServer() error { Handler: corsMux, } - log.Info().Msg("Rate Shield Running ✅") + log.Info().Msg("Rate Shield running on port: " + fmt.Sprintf("%d", s.port) + " ✅") err := server.ListenAndServe() if err != nil { @@ -79,6 +81,21 @@ func (s Server) rulesRoutes(mux *http.ServeMux) { func (s Server) registerRateLimiterRoutes(mux *http.ServeMux) { rateLimiterHandler := NewRateLimitHandler(s.limiter) - mux.HandleFunc("/check-limit", rateLimiterHandler.CheckRateLimit) } + +func (s Server) setupHome(mux *http.ServeMux) { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + + wd, wdError := os.Getwd() + + homepage, err := os.ReadFile(wd + "/api/" + "index.html") + if err != nil || wdError != nil { + fmt.Println(err) + w.Write([]byte("Rate Shield is running. Open frontend client on port 5173. If it does not work make sure react application is running.")) + } + + fmt.Fprint(w, string(homepage)) + }) +} diff --git a/rate_shield/limiter/limiter.go b/rate_shield/limiter/limiter.go index 9143b4a..9c07556 100644 --- a/rate_shield/limiter/limiter.go +++ b/rate_shield/limiter/limiter.go @@ -103,7 +103,7 @@ func (l *Limiter) GetRule(key string) (*models.Rule, bool, error) { } func (l *Limiter) StartRateLimiter() { - log.Info().Msg("Starting Limiter Service ✅") + log.Info().Msg("Starting limiter service ✅") l.cachedRules = l.redisRuleSvc.CacheRulesLocally() l.tokenBucket.startAddTokenJob() go l.listenToRulesUpdate() diff --git a/web/src/api/rules.tsx b/web/src/api/rules.tsx index 5515f06..3a69366 100644 --- a/web/src/api/rules.tsx +++ b/web/src/api/rules.tsx @@ -5,6 +5,7 @@ export interface rule { endpoint: string; http_method: string; fixed_window_counter_rule: fixedWindowCounterRule | null; + sliding_window_counter_rule: slidingWindowCounterRule | null; token_bucket_rule: tokenBucketRule | null; allow_on_error: boolean; } @@ -27,6 +28,12 @@ export interface fixedWindowCounterRule { window: number; } +export interface slidingWindowCounterRule { + max_requests: number; + window: number; +} + + export interface tokenBucketRule { bucket_capacity: number; token_add_rate: number; diff --git a/web/src/components/AddOrUpdateRule.tsx b/web/src/components/AddOrUpdateRule.tsx index b1f80dc..a675ff9 100644 --- a/web/src/components/AddOrUpdateRule.tsx +++ b/web/src/components/AddOrUpdateRule.tsx @@ -7,9 +7,11 @@ import { deleteRule, fixedWindowCounterRule, rule, + slidingWindowCounterRule, tokenBucketRule, } from "../api/rules"; import { customToastStyle } from "../utils/toast_styles"; +import { validateNewFixedWindowCounterRule, validateNewRule, validateNewSlidingWindowCounterRule, validateNewTokenBucketRule } from "../utils/validators"; interface Props { closeAddNewRule: () => void; @@ -19,6 +21,7 @@ interface Props { httpMethod?: string; fixed_window_counter_rule: fixedWindowCounterRule | null; token_bucket_rule: tokenBucketRule | null; + sliding_window_counter_rule: slidingWindowCounterRule | null; allow_on_error: boolean; } @@ -30,15 +33,15 @@ const AddOrUpdateRule: React.FC = ({ httpMethod, token_bucket_rule, fixed_window_counter_rule, + sliding_window_counter_rule, allow_on_error, }) => { const [apiEndpoint, setApiEndpoint] = useState(endpoint || ""); const [limitStrategy, setLimitStrategy] = useState(strategy); const [method, setHttpMethod] = useState(httpMethod || "GET"); const [tokenBucket, setTokenBucketRule] = useState(token_bucket_rule); - const [fixedWindowCounter, setFixedWindowCounterRule] = useState( - fixed_window_counter_rule, - ); + const [fixedWindowCounter, setFixedWindowCounterRule] = useState(fixed_window_counter_rule); + const [slidingWindowCounter, setSlidingWindowCounterRule] = useState(sliding_window_counter_rule) const [allowOnError, setAllowOnError] = useState(allow_on_error || false); const addOrUpdateRule = async () => { @@ -48,95 +51,29 @@ const AddOrUpdateRule: React.FC = ({ strategy: limitStrategy, fixed_window_counter_rule: fixedWindowCounter, token_bucket_rule: tokenBucket, + sliding_window_counter_rule: slidingWindowCounter, allow_on_error: allowOnError, }; + - console.log("New Rule: ", newRule); - - if (newRule.endpoint === "" || newRule.endpoint === undefined) { - toast.error("API Endpoint can't be null.", { - style: customToastStyle, - }); + if(!validateNewRule(newRule)) { + console.log("validateNewRule") return; } - if ( - newRule.strategy === "" || - newRule.strategy == undefined || - newRule.strategy === "UNDEFINED" - ) { - toast.error("API limit strategy can't be null.", { - style: customToastStyle, - }); + if(!validateNewTokenBucketRule(newRule)) { + console.log("validateNewTokenBucketRule") return; } - if (newRule.http_method === "" || newRule.http_method === undefined) { - toast.error("API HTTP Method can't be null.", { - style: customToastStyle, - }); + if(!validateNewFixedWindowCounterRule(newRule)) { + console.log("validateNewFixedWindowCounterRule") return; } - - if (newRule.strategy == "TOKEN BUCKET") { - if ( - newRule.token_bucket_rule?.bucket_capacity === 0 || - !newRule.token_bucket_rule?.bucket_capacity || - newRule.token_bucket_rule.bucket_capacity <= 0 - ) { - toast.error("Invalid value for bucket capacity.", { - style: customToastStyle, - }); - return; - } - - if ( - newRule.token_bucket_rule?.token_add_rate === 0 || - !newRule.token_bucket_rule?.token_add_rate || - newRule.token_bucket_rule.token_add_rate <= 0 - ) { - toast.error("Invalid value for bucket capacity.", { - style: customToastStyle, - }); - return; - } - - if ( - newRule.token_bucket_rule?.token_add_rate > - newRule.token_bucket_rule.bucket_capacity - ) { - toast.error( - "Token add rate should not be more than bucket capacity.", - { - style: customToastStyle, - }, - ); - return; - } - } - - if (newRule.strategy == "FIXED WINDOW COUNTER") { - if ( - newRule.fixed_window_counter_rule?.max_requests === 0 || - !newRule.fixed_window_counter_rule?.max_requests || - newRule.fixed_window_counter_rule?.max_requests <= 0 - ) { - toast.error("Invalid value for maximum requests.", { - style: customToastStyle, - }); - return; - } - - if ( - newRule.fixed_window_counter_rule?.window === 0 || - !newRule.fixed_window_counter_rule?.window || - newRule.fixed_window_counter_rule?.window <= 0 - ) { - toast.error("Invalid value for window time.", { - style: customToastStyle, - }); - return; - } + + if(!validateNewSlidingWindowCounterRule(newRule)) { + console.log("validateNewSlidingWindowCounterRule") + return; } try { @@ -267,7 +204,7 @@ const AddOrUpdateRule: React.FC = ({ }} /> - ) : limitStrategy === "FIXED WINDOW COUNTER" ? ( + ) : limitStrategy === "FIXED WINDOW COUNTER" || limitStrategy === "SLIDING WINDOW COUNTER" ? (

Maximum Requests

= ({ placeholder="Ex: - 10000" value={fixedWindowCounter?.max_requests} onChange={(e) => { - setFixedWindowCounterRule({ - max_requests: Number.parseInt(e.target.value), - window: fixedWindowCounter?.window || 0, - }); + if(limitStrategy === "FIXED WINDOW COUNTER") { + setFixedWindowCounterRule({ + max_requests: Number.parseInt(e.target.value), + window: fixedWindowCounter?.window || 0, + }); + } else if(limitStrategy === "SLIDING WINDOW COUNTER") { + setSlidingWindowCounterRule({ + max_requests: Number.parseInt(e.target.value), + window: fixedWindowCounter?.window || 0, + }); + } }} /> @@ -288,11 +232,19 @@ const AddOrUpdateRule: React.FC = ({ placeholder="Ex: - 100" value={fixedWindowCounter?.window} onChange={(e) => { - setFixedWindowCounterRule({ - max_requests: - fixedWindowCounter?.max_requests || 0, - window: Number.parseInt(e.target.value) || 0, - }); + if(limitStrategy === "FIXED WINDOW COUNTER") { + setFixedWindowCounterRule({ + max_requests: + fixedWindowCounter?.max_requests || 0, + window: Number.parseInt(e.target.value) || 0, + }); + } else if(limitStrategy === "SLIDING WINDOW COUNTER") { + setSlidingWindowCounterRule({ + max_requests: + fixedWindowCounter?.max_requests || 0, + window: Number.parseInt(e.target.value) || 0, + }); + } }} />
diff --git a/web/src/pages/APIConfiguration.tsx b/web/src/pages/APIConfiguration.tsx index babf2f4..3ef221b 100644 --- a/web/src/pages/APIConfiguration.tsx +++ b/web/src/pages/APIConfiguration.tsx @@ -33,7 +33,6 @@ export default function APIConfiguration() { useEffect(() => { fetchRules(); - console.log("Page Number: " + pageNumber); }, [pageNumber]); useEffect(() => { @@ -88,6 +87,7 @@ export default function APIConfiguration() { fixed_window_counter_rule={ selectedRule?.fixed_window_counter_rule || null } + sliding_window_counter_rule={selectedRule?.sliding_window_counter_rule || null} token_bucket_rule={selectedRule?.token_bucket_rule || null} allow_on_error={selectedRule?.allow_on_error || false} /> diff --git a/web/src/utils/validators.tsx b/web/src/utils/validators.tsx new file mode 100644 index 0000000..ed3f374 --- /dev/null +++ b/web/src/utils/validators.tsx @@ -0,0 +1,125 @@ +import toast from "react-hot-toast"; +import { rule } from "../api/rules"; +import { customToastStyle } from "./toast_styles"; + +export function validateNewRule(newRule: rule) { + if (newRule.endpoint === "" || newRule.endpoint === undefined) { + toast.error("API Endpoint can't be null.", { + style: customToastStyle, + }); + return false; + } + + if ( + newRule.strategy === "" || + newRule.strategy === undefined || + newRule.strategy === "UNDEFINED" + ) { + toast.error("API limit strategy can't be null.", { + style: customToastStyle, + }); + return false; + } + + if (newRule.http_method === "" || newRule.http_method === undefined) { + toast.error("API HTTP Method can't be null.", { + style: customToastStyle, + }); + return false; + } + return true +} + +export function validateNewTokenBucketRule(newRule: rule) { + if (newRule.strategy === "TOKEN BUCKET") { + if ( + newRule.token_bucket_rule?.bucket_capacity === 0 || + !newRule.token_bucket_rule?.bucket_capacity || + newRule.token_bucket_rule.bucket_capacity <= 0 + ) { + toast.error("Invalid value for bucket capacity.", { + style: customToastStyle, + }); + return false; + } + + if ( + newRule.token_bucket_rule?.token_add_rate === 0 || + !newRule.token_bucket_rule?.token_add_rate || + newRule.token_bucket_rule.token_add_rate <= 0 + ) { + toast.error("Invalid value for bucket capacity.", { + style: customToastStyle, + }); + return false; + } + + if ( + newRule.token_bucket_rule?.token_add_rate > + newRule.token_bucket_rule.bucket_capacity + ) { + toast.error( + "Token add rate should not be more than bucket capacity.", + { + style: customToastStyle, + }, + ); + return false; + } + } + return true +} + +export function validateNewFixedWindowCounterRule(newRule: rule) { + if (newRule.strategy === "FIXED WINDOW COUNTER") { + if ( + newRule.fixed_window_counter_rule?.max_requests === 0 || + !newRule.fixed_window_counter_rule?.max_requests || + newRule.fixed_window_counter_rule?.max_requests <= 0 + ) { + toast.error("Invalid value for maximum requests.", { + style: customToastStyle, + }); + return false; + } + + if ( + newRule.fixed_window_counter_rule?.window === 0 || + !newRule.fixed_window_counter_rule?.window || + newRule.fixed_window_counter_rule?.window <= 0 + ) { + toast.error("Invalid value for window time.", { + style: customToastStyle, + }); + return false; + } + } + return true +} + +export function validateNewSlidingWindowCounterRule(newRule: rule) { + if (newRule.strategy === "FIXED WINDOW COUNTER") { + if ( + newRule.sliding_window_counter_rule?.max_requests === 0 || + !newRule.sliding_window_counter_rule?.max_requests || + newRule.sliding_window_counter_rule?.max_requests <= 0 + ) { + toast.error("Invalid value for maximum requests.", { + style: customToastStyle, + }); + return false; + } + + if ( + newRule.sliding_window_counter_rule?.window === 0 || + !newRule.sliding_window_counter_rule?.window || + newRule.sliding_window_counter_rule?.window <= 0 + ) { + toast.error("Invalid value for window time.", { + style: customToastStyle, + }); + return false; + } + } + return true +} \ No newline at end of file