Decorates an ASP.NET MVC route that needs to have client requests limited over time. Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Caching;
using System.Net;
using System.Web.Routing;
using System.Globalization;
//
// Usage:
// [AllowXRequestsEveryXSeconds(Name = "CtReporting", ContentName = "TooManyRequests", Requests = 15, Seconds = 60)]
//
namespace SecurityEssentials.Core.Attributes
{
/// <summary>
/// SECURE: Decorates any MVC route that needs to have client requests limited over time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class AllowXRequestsEveryXSecondsAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// The number of requests to allow per client in the given number of seconds
/// </summary>
public int Requests { get; set; }
/// <summary>
/// A text message (not themed) that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "You have performed this action more than {x} times in the last {n} seconds.".
/// </summary>
public string Message { get; set; }
/// <summary>
/// The content name (themed and from SiteContent) to show upon throttling. If this is present, the Message parameter will not be used.
/// </summary>
public string ContentName { get; set; }
// Used to get around weird cache behavior with value types
public class Int32Value
{
public Int32Value()
{
Value = 1;
}
public int Value { get; set; }
}
public override void OnActionExecuting(ActionExecutingContext c)
{
if (c == null) { throw new ArgumentException("ActionExecutingContext not spcecified"); }
var key = string.Concat("AllowXRequestsEveryXSeconds-", Name, "-", c.HttpContext.Request.UserHostAddress);
var allowExecute = false;
var currentCacheValue = HttpRuntime.Cache[key];
if (currentCacheValue == null)
{
HttpRuntime.Cache.Add(key,
new Int32Value(),
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
else
{
var value = (Int32Value)currentCacheValue;
value.Value++;
if (value.Value <= Requests)
{
allowExecute = true;
}
}
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
{
Message = "You have performed this action more than {x} times in the last {n} seconds.";
}
if (!string.IsNullOrEmpty(ContentName))
{
//use SiteContent
c.Result = new RedirectToRouteResult(new RouteValueDictionary { { "Controller", "WebPageContent" }, { "Action", ContentName } });
}
else
{
//just send a message (not themed)
c.Result = new ContentResult { Content = Message.Replace("{x}", Requests.ToString(CultureInfo.CurrentCulture)).Replace("{n}", Seconds.ToString(CultureInfo.CurrentCulture)) };
}
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.TrySkipIisCustomErrors = true; //to prevent iis from showing default 409 page
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
}