Skip to content

Instantly share code, notes, and snippets.

@JamoCA
Last active August 21, 2018 00:25
Show Gist options
  • Save JamoCA/f1af7397d92b93eeea20 to your computer and use it in GitHub Desktop.
Save JamoCA/f1af7397d92b93eeea20 to your computer and use it in GitHub Desktop.
An update to a ColdFusion-based limiter UDF to throttle requests. Using HTTP status 429, CacheGet/Push (for automatic collection flushing) and cookie filters.
<cffunction name="limiter">
<!---
by Charlie Arehart, charlie@carehart.org, in 2009, updated 2012
http://www.carehart.org/blog/client/index.cfm/2010/5/21/throttling_by_ip_address
- Throttles requests made more than "count" times within "duration" seconds from single IP.
- sends 503 status code for bots to consider as well as text for humans to read
- also logs to a new "limiter.log" that is created automatically in cf logs directory, tracking when limits are hit, to help fine tune
- note that since it relies on the application scope, you need to place the call to it AFTER a cfapplication tag in application.cfm
- updated 10/16/12: now adds a test around the actual throttling code, so that it applies only to requests that present no cookie, so should only impact spiders, bots, and other automated requests. A "legit" user in a regular browser will be given a cookie by CF after their first visit and so would no longer be throttled.
- I also tweaked the cflog output to be more like a csv-format output
Rewrite by James Moberg, james@ssmedia.com 2/8/2016
- Remove reliance of application scope.
- Generate request key using server name, IP address & CGI.User_Agent
- Use CacheGet/CachePut to auto-flush expired request data based on time (default: 5 minutes)
- Add Cookie Bypass filter. Set to "" to filter all requests. "*" will ignore any request w/a cookie. "cftoken" will ignore requests with a CFToken cookie.
- Use HTTP Status 429 (based on practices from other rate limiter libraries)
- Sample call: limiter(count, duration, "*");
--->
<cfargument name="count" type="numeric" default="3">
<cfargument name="duration" type="numeric" default="3">
<cfargument name="cookieBypass" type="string" default="*">
<cfset var thisCookieName = "">
<cfset var CurrentLimiter = "">
<cfset var Config = {
browserKey = "#CGI.Server_Name#_#CGI.Remote_Addr#_#CGI.Http_User_Agent#",
tempData = {},
CacheTTL = CreateTimeSpan(0,0,5,0),
CookieBypass = 0,
Message = "Too Many Requests - your IP is being rate limited."
}>
<!--- Alt message: You are making too many requests too fast, please slow down and wait #arguments.duration# seconds --->
<CFIF arguments.cookieBypass IS "*" AND LEN(TRIM(cgi.http_cookie))>
<CFSET Config.CookieBypass = 1>
<CFELSEIF LEN(arguments.cookieBypass) AND LEN(TRIM(cgi.http_cookie))>
<CFLOOP LIST="#arguments.cookieBypass#" INDEX="thisCookieName">
<CFIF StructKeyExists(Cookie, thisCookieName)>
<CFSET Config.CookieBypass = 1>
<CFBREAK>
</CFIF>
</CFLOOP>
</CFIF>
<CFIF VAL(Config.CookieBypass)>
<CFRETURN>
</CFIF>
<cfset CurrentLimiter = cacheGet(config.browserKey)>
<CFIF isNull(CurrentLimiter)>
<cfset config.tempData = StructNew()>
<cfset config.tempData.attempts = 1>
<cfset config.tempData.last_attempt = Now()>
<CFSET cachePut(config.browserKey, config.tempData, config.CacheTTL)>
<CFELSEIF isDate(CurrentLimiter.last_attempt) AND DateDiff("s", CurrentLimiter.last_attempt, Now()) LT arguments.duration>
<cfset CurrentLimiter.attempts = CurrentLimiter.attempts + 1>
<cfset CurrentLimiter.last_attempt = Now()>
<cfset cachePut(config.browserKey, CurrentLimiter, config.CacheTTL)>
<cfif CurrentLimiter.attempts GT arguments.count>
<cflog file="limiter" text="'limiter invoked for:','#cgi.remote_addr#',#CurrentLimiter.attempts#,#cgi.request_method#,'#cgi.SCRIPT_NAME#', '#cgi.QUERY_STRING#','#cgi.http_user_agent#','#CurrentLimiter.last_attempt#',#StructCount(Cookie)#">
<cfheader statuscode="429" statustext="Too Many Requests">
<cfheader name="Retry-After" value="#arguments.duration#">
<cfheader name="X-Rate-Limit-Reset" value="#arguments.duration#">
<cfcontent type="text/html; charset=UTF-8">
<cfoutput><p>#Config.Message#</p></cfoutput>
<cfabort>
</cfif>
<CFELSE>
<cfset config.tempData = StructNew()>
<cfset config.tempData.attempts = 1>
<cfset config.tempData.last_attempt = Now()>
<CFSET cachePut(config.browserKey, config.tempData, config.CacheTTL)>
</CFIF>
</cffunction>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment