private bool RateLimit(string hostname, string channel) { if (string.IsNullOrEmpty(hostname) || string.IsNullOrEmpty(channel)) { // sanity check - this probably chanserv. this.logger.Error("JoinMessage ratelimiting called with null channel or null hostname!"); return(true); } try { // TODO: rate limiting needs to be tidyed up a bit lock (this.rateLimitCache) { if (!this.rateLimitCache.ContainsKey(channel)) { this.rateLimitCache.Add(channel, new Cache()); } var channelCache = this.rateLimitCache[channel]; if (channelCache.ContainsKey(hostname)) { this.logger.Debug("Rate limit key found."); var cacheEntry = channelCache[hostname]; if (cacheEntry.Expiry.AddMinutes(this.configuration.RateLimitDuration) >= DateTime.Now) { this.logger.Debug("Rate limit key NOT expired."); if (cacheEntry.Counter >= this.configuration.RateLimitMax) { this.logger.Debug("Rate limit HIT"); // RATE LIMITED! return(true); } this.logger.Debug("Rate limit incremented."); // increment counter cacheEntry.Counter++; } else { this.logger.Debug("Rate limit key is expired, resetting to new value."); // Cache expired cacheEntry.Expiry = DateTime.Now; cacheEntry.Counter = 1; } } else { this.logger.Debug("Rate limit not found, creating key."); // Not in cache. var cacheEntry = new RateLimitCacheEntry { Expiry = DateTime.Now, Counter = 1 }; channelCache.Add(hostname, cacheEntry); } // Clean up the channel's cache. foreach (var key in channelCache.Keys.ToList()) { if (channelCache[key].Expiry.AddMinutes(this.configuration.RateLimitDuration) < DateTime.Now) { // Expired. channelCache.Remove(key); } } } } catch (Exception ex) { // ooh dear. Something went wrong with rate limiting. this.logger.Error("Unknown error during rate limit processing.", ex); return(false); } return(false); }
private void CreateOrUpdateCacheEntry(string key, RateLimitCacheEntry entry, TimeSpan span) { var cacheOpts = new MemoryCacheEntryOptions().SetPriority(CacheItemPriority.High).SetAbsoluteExpiration(span).RegisterPostEvictionCallback(EvictionNotice); memoryCache.Set <RateLimitCacheEntry>(key, entry, cacheOpts); }
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) { RateLimitCacheEntry cacheRec = null; string path = context.ActionDescriptor.ViewEnginePath; string method = context.HttpContext.Request.Method; // string method = context.HandlerMethod.HttpMethod; string key = RateLimitRule.MakeRuleKey(path, method); if (rateLimits.RateLimitRuleMatches(key)) { logger?.LogDebug($"Limit rule for page: {path} and method: {method} matched"); string filterKey = rateLimits.FilterKey.BuildKey(context); string filterKeyName = rateLimits.FilterKey.GetFilterKeyName(); if (rateLimits.IsWhitelisted(filterKey)) { logger?.LogDebug($"Rate ignored for whitelisted {filterKeyName}: {filterKey}"); } else { var rule = rateLimits.GetRateLimitByKey(key); var ruleLimits = rule.RequestLimits; var cacheKeys = GetTimeUnitCacheKeys(path, method, filterKey, filterKeyName).ToArray(); DateTime now = DateTime.UtcNow; // limit for this page and method is in effect, check cache entries for each time unit (minute, hour, day) for (int z = 0; z < 3; z++) { if (ruleLimits[z] == 0) { // zero means don't do limiting for this time unit logger?.LogDebug($"Rate-limiting disabled for per-{rule.GetTimeUnitName(z)}"); } else { bool exists = memoryCache.TryGetValue <RateLimitCacheEntry>(cacheKeys[z], out cacheRec); if (exists) { if (cacheRec.IsExpired(now)) { // there's no guarantee the system will auto-expire entries on time, so we have to manually check them logger?.LogDebug($"Manual delete of expired cache key for {rule.GetRuleDesc(z)} for {filterKeyName} {filterKey}"); memoryCache.Remove(cacheKeys[z]); // dump the old cache entry // create a new cache entry cacheRec = new RateLimitCacheEntry(spans[z]); CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, spans[z]); } else { TimeSpan timeRemaining = cacheRec.CalcRemainingTime(now); int totSecs = Convert.ToInt32(timeRemaining.TotalSeconds) + 1; string timeRemainingStr = cacheRec.BuildTimeCountdownStringFromSpan(timeRemaining); if (cacheRec.ReqCnt + 1 > ruleLimits[z]) { // cache limit has been exceeded - return 429 logger?.LogDebug($"Rule limit ({cacheRec.ReqCnt}) exceeded for {rule.GetRuleDesc(z)} for Filter: {filterKeyName}, Key: {filterKey}"); context.HttpContext.Response.Headers.Add("Retry-After", totSecs.ToString()); context.Result = new ContentResult { // StatusCode = (int) HttpStatusCode.TooManyRequests, StatusCode = 429, ContentType = "text/html", Content = $"Request quota ({rule.GetRuleDesc(z)}) exceeded. Try again in {timeRemainingStr}." }; return; } else { logger?.LogDebug($"Time remaining on cache entry per {rule.GetTimeUnitName(z)}: {timeRemainingStr} for Filter: {filterKeyName}, Key: {filterKey}"); logger?.LogDebug($"Adding 1 to count ({cacheRec.ReqCnt}) per {rule.GetTimeUnitName(z)} for Filter: {filterKeyName}, Key: {filterKey}"); cacheRec.ReqCnt += 1; // pass in new time left based on original starting time and span CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, cacheRec.CalcRemainingTime(now)); } } } else { // doesn't exist in cache, so we'll create it fresh cacheRec = new RateLimitCacheEntry(spans[z]); CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, spans[z]); } } } } } else { logger?.LogDebug($"No limit rule for page: {path} and method: {method}"); } await next.Invoke(); }
private bool RateLimit(IUser source, int id) { if (source == null || source.Hostname == null) { this.Logger.Error("Rate limiting called with no source or no source hostname!"); return(true); } var cacheKey = string.Format("{1}|{0}", source.Hostname, id); lock (RateLimitCache) { if (RateLimitCache.ContainsKey(cacheKey)) { this.Logger.Debug("Rate limit key found."); var cacheEntry = RateLimitCache[cacheKey]; if (cacheEntry.Expiry.AddMinutes(this.config.RateLimitDuration) >= DateTime.Now) { this.Logger.Debug("Rate limit key NOT expired."); if (cacheEntry.Counter >= this.config.RateLimitMax) { this.Logger.Debug("Rate limit HIT"); // RATE LIMITED! return(true); } this.Logger.Debug("Rate limit incremented."); // increment counter cacheEntry.Counter++; } else { this.Logger.Debug("Rate limit key is expired, resetting to new value."); // Cache expired cacheEntry.Expiry = DateTime.Now; cacheEntry.Counter = 1; } } else { this.Logger.Debug("Rate limit not found, creating key."); // Not in cache. var cacheEntry = new RateLimitCacheEntry { Expiry = DateTime.Now, Counter = 1 }; RateLimitCache.Add(cacheKey, cacheEntry); } // Clean up the cache foreach (var key in RateLimitCache.Keys.ToList()) { if (RateLimitCache[key].Expiry.AddMinutes(this.config.RateLimitDuration) < DateTime.Now) { // Expired. RateLimitCache.Remove(key); } } } return(false); }