/// <summary> /// Process and track REST request tracking. /// </summary> /// <example> /// One line of code will enforce rate limiting and blacklist a repeated bad request offender, /// returning a class object that contains the HttpStatusCode and messaging. /// <code> /// var jsonData = RestSecurity.RateLimit("169.128.25.23", "2018-12-01T23:59:00Z", "member/signin", restApiSettings); /// if (jsonData.StatusCode == HttpStatusCode.OK) /// { /// // Do stuff and set the status code and messaging properties in jsonData. /// // After all processing, execute ProcessRestSecurityTracking() so subsequent /// // calls to RateLimit() are aware of the ultimate result of this request. /// RestSecurity.ProcessRestSecurityTracking(jsonData, "member/signin", restApiSettings); /// } /// </code> /// </example> /// <param name="result">REST request result to process and track</param> /// <param name="restPath">REST path to use as an identifier so that each path has its own rate limiting, etc. (e.g. "/member/signin")</param> /// <param name="config">REST security configuration</param> public static void ProcessRestSecurityTracking(RestSecurityResult result, string restPath, RestSecurityConfig config) { // Failure, track bad requests for potential blacklisting if (result.StatusCode != HttpStatusCode.OK) { int failures = 0; var _x = HttpContext.Current.Cache; if (CacheHelpers.CacheExists("FAILURES" + GetWanIpCacheName(result.WanIp, restPath))) { failures = CacheHelpers.Cache <int>("FAILURES" + GetWanIpCacheName(result.WanIp, restPath)); } failures++; if (failures > config.RestApiBlacklistMaxFailureCount) { CacheHelpers.CacheDelete("FAILURES" + GetWanIpCacheName(result.WanIp, restPath)); CacheHelpers.CacheAdd("BLACKLISTED" + GetWanIpCacheName(result.WanIp, restPath), 0, expirationSeconds: config.RestApiBlacklistSeconds); } else { CacheHelpers.CacheAdd("FAILURES" + GetWanIpCacheName(result.WanIp, restPath), failures, expirationSeconds: config.RestApiBlacklistMaxFailureSeconds); } } }
/// <summary> /// Simple way to rate limit and blacklist abusers for anonymous REST requests. /// Uses WAN IP address to identify requesters and track their bad requests in order to blacklist them. /// </summary> /// <example> /// One line of code will enforce rate limiting and blacklist a repeated bad request offender, /// returning a class object that contains the HttpStatusCode and messaging. /// <code> /// var jsonData = RestSecurity.RateLimit("169.128.25.23", "2018-12-01T23:59:00Z", "member/signin", restApiSettings); /// if (jsonData.StatusCode == HttpStatusCode.OK) /// { /// // Do stuff and set the status code and messaging properties in jsonData. /// // After all processing, execute ProcessRestSecurityTracking() so subsequent /// // calls to RateLimit() are aware of the ultimate result of this request. /// RestSecurity.ProcessRestSecurityTracking(jsonData, "member/signin", restApiSettings); /// } /// </code> /// </example> /// <param name="wanIpAddress">Optional WAN IP address of the caller (provided by the caller) to compare against the actual address used to make the request</param> /// <param name="timestamp">Optional UTC timestamp of the REST call (provided by the caller) in the format 2018-12-01T23:59:00Z</param> /// <param name="restPath">REST path to use as an identifier so that each path has its own rate limiting, etc. (e.g. "/member/signin")</param> /// <param name="config">RestSecurityConfig object</param> /// <param name="context">Optional HttpContext object (uses Current if not provided)</param> /// <returns>RestSecurityResult object with response information</returns> public static RestSecurityResult RateLimit(string wanIpAddress, string timestamp, string restPath, RestSecurityConfig config, HttpContext context = null) { var wanip = ContextHelpers.EnsureAppContext(context).Request.UserHostAddress; var result = new RestSecurityResult { WanIp = wanip, StatusCode = HttpStatusCode.BadGateway, StatusText = "Bad Gateway", Message = "Invalid request" }; try { if (CacheHelpers.CacheExists("BLACKLISTED" + GetWanIpCacheName(wanip, restPath))) { result.StatusCode = HttpStatusCode.Forbidden; result.StatusText = "Forbidden"; result.Message = "You appear to be up to no good."; } else { if (CacheHelpers.CacheExists(GetWanIpCacheName(wanip, restPath)) == false) { var newRateData = new RateLimitingCounter { Counter = 0, Expiration = DateTime.Now.AddSeconds(config.RestApiRateLimitSeconds) }; CacheHelpers.CacheAdd(GetWanIpCacheName(wanip, restPath), newRateData, expirationDateTime: newRateData.Expiration); } var rateData = CacheHelpers.Cache <RateLimitingCounter>(GetWanIpCacheName(wanip, restPath)); rateData.Counter += 1; CacheHelpers.CacheDelete(GetWanIpCacheName(wanip, restPath)); CacheHelpers.CacheAdd(GetWanIpCacheName(wanip, restPath), rateData, expirationDateTime: rateData.Expiration); if (rateData.Counter <= config.RestApiRateLimitCount) { // Validate WAN IP address matches actual if (string.IsNullOrEmpty(wanIpAddress) || (string.IsNullOrEmpty(wanIpAddress) == false && wanip == wanIpAddress)) { if (string.IsNullOrEmpty(timestamp)) { // Timestamp not used, request is good... result.StatusCode = HttpStatusCode.OK; result.StatusText = "OK"; result.Message = ""; } else { // Enforce UTC timestamp format of 2018-12-01T23:59:00Z if (timestamp.Length > 19 && timestamp.Contains("T") && timestamp.EndsWith("Z")) { if (string.IsNullOrWhiteSpace(timestamp) == false && timestamp.IsDate() && DateTime.Parse(timestamp, null, System.Globalization.DateTimeStyles.AdjustToUniversal).DateDiff <int>(DateTime.Now.ToUniversalTime(), DateDiffComparisonType.Minutes) <= config.RestApiTimestampMinutes) { // Validate timestamp, request is good... result.StatusCode = HttpStatusCode.OK; result.StatusText = "OK"; result.Message = ""; } } } } else { result.StatusCode = HttpStatusCode.BadRequest; result.StatusText = "Bad Request"; result.Message = "You are not who you claim to be."; } } else { result.StatusCode = (HttpStatusCode)429; result.StatusText = "Too Many Requests"; result.Message = "Slow down there partner."; } } } catch (Exception e) { result.StatusCode = HttpStatusCode.InternalServerError; result.StatusText = "Internal Server Error"; result.Message = e.Message; } ProcessRestSecurityTracking(result, restPath, config); return(result); }