My first own CFML tag: <cfhoneypot>

If you are a CFML (Coldfusion) programmer like me, you know the power of all those <cf...> tags which we use everyday. Every new release of Adobe Coldfusion gives us new tags, after which the opensource engines like Railo include them in their products a.s.a.p.
But did you know that you can also create your own <cf...> tags with Railo? And that they can be written in CFML? Difficult? No, not at all!

For example, Railo did not yet include some ajax tags which became available in Adobe Coldfusion 8, so they were created by Railo user and contributor Andrea Campolonghi. Just cfc's and supporting files, written in cfml, which were easily installed with the Railo Extension Provider system. Now, they are even in the Railo core, so you do not even need to install them separately.

My own tag

Off course, I needed to check this out, to get a proof of concept. And since I recently wrote some code and plugins for Project Honeypot, it was an easy choice to try and create <cfhoneypot>. Would this tag ever pass the serious guys at the CFML Advisory Committee? Off course not. Then it would be called something like <cfprotect>, <cfspam>, or <cfantispam>. But hey, it's just a proof of concept!

How-to

The first thing I did was looking at the documentation that Railo provided (page 1, page 2, and page 3). That gave me an idea of what to do, but it was much simpler to just download the code Andrea created, and take a good look at it ;-)

So I copied one of his cfc's, cleaned it, and started to think what I actually wanted the cf tag to do.
I came up with the following:

<!--- if key 'variable' is ommitted, the link html will be directly outputted to stdout --->
<cfhoneypot action="createLink"
url="http://www.fff.nl/dfff.cfm"
variable="linkhtml"
/>

<!---
Optional attributes:
- variable; default: 'cfhoneypot'
--->
<cfhoneypot action="getThreatRating"
httpblkey="ascv"
ip="123.123.123.123"
variable="structname"
/>

<!---
Optional attributes:
- variable; default: 'cfhoneypot'
- threatnrs; default: 4,5,6,7
- maxdaysago; default: 20
- minimalThreatIndex; default: 5
--->
<cfhoneypot action="isThreat"
httpblkey="ascv"
ip="123.123.123.123"
variable="structname"
threatnrs="4,5,6,7"
maxdaysago="20"
minimalThreatIndex="5"
/>

<!---
Optional attributes:
- threatnrs; default: 4,5,6,7
- maxdaysago; default: 20
- minimalThreatIndex; default: 5
- blocktext; default: "<h1>Blocked due to spam check</h1>
<p>You are not allowed to visit this website due to spamming on this and/or other websites</p>"
--->
<cfhoneypot action="BlockThreat"
httpblkey="ascv"
ip="123.123.123.123"
threatnrs="4,5,6,7"
maxdaysago="20"
minimumThreatIndex="5"
blocktext="you are locked out of this website due to spamming"
/>

The CreateLink action creates a random link to a honeypot page, as is described here.
The GetThreatRating action checks an IP address against the Project Honeypot database via the HTTP:BL DNS functionality. See the blog post about my mangoBlog Spam Blocker plugin.
The IsThreat action returns the same structure as the GetThreatRating action, but adds a key "isThreat" with a boolean value True or False. This value is off course based on the settings you provided. For now, see the source code for more explanation about the settings.
The BlockThreat action is handled the same as the IsThreat action, but if the key "IsThreat" is True, it completely stops the page execution, after outputting the text you entered in the attribute "blocktext". This action does not return any value, it is all or nothing (blocked or continue).

Download and install

If you want to try the code, then download the zip, and unzip it into {railo-install path}/railo-server/context/library/tag/
Then, restart Railo (can be done via the Railo admin).
Also, take the file test.cfm from the zip file, and put it somewhere in a web root, to test the functionality.

The code

I already created most of the code for other projects, so I just had to create one new file, Honeypot.cfc. As I said before, I borrowed and rewrote one, to this:

<cfcomponent name="Honeypot">

<!--- Meta data --->
<cfset this.metadata.attributetype="fixed" />
<cfset this.metadata.attributes={
action: {required:true,type:"string"}

, variable: {required:false,type:"string"}
, url: {required:false,type:"string"}
, httpblkey: {required:false,type:"string"}
, ip: {required:false,type:"string"}
, threatnrs: {required:false,type:"string"}
, maxdaysago: {required:false,type:"number"}
, minimumThreatIndex: {required:false,type:"number"}
, blocktext: {required:false,type:"string"}
}>


<cffunction name="init" output="no" returntype="void"
hint="invoked after tag is constructed">
<cfargument name="hasEndTag" type="boolean" required="yes" />
<cfargument name="parent" type="component" required="no" hint="the parent cfc custom tag, if there is one" />
<cfset variables.hasEndTag = arguments.hasEndTag />
<cfset variables.parent = arguments.parent />
</cffunction>

<cffunction name="onStartTag" output="yes" returntype="boolean">
<cfargument name="attributes" type="struct" />
<cfargument name="caller" type="struct" />
<cfset variables.attributes = arguments.attributes />
<cfset var key = "" />
<cfset var action = getAttribute('action') />


<!--- check type --->
<cfif action eq "createlink">
<cfif not attributeExists('url')>
<cfthrow message="cfhoneypot: when action is '#action#', the atribute [url] is required!" />
</cfif>
<cfelseif action eq "isThreat" or action eq "BlockThreat" or action eq "getThreatRating">
<cfloop list="httpblkey,ip" index="key">
<cfif not attributeExists(key)>
<cfthrow message="cfhoneypot: when action is '#action#', the atribute [#key#] is required!" />
</cfif>
</cfloop>
<cfelse>
<cfthrow message="cfhoneypot does not have an action '#htmleditformat(type)#'!" />
</cfif>

<!--- do action --->
<cfif action eq "createLink">
<cfset _createLink(caller=arguments.caller) />
<cfelseif action eq "getThreatRating">
<cfset _getIPThreat(caller=arguments.caller) />
<cfelseif action eq "BlockThreat">
<cfset _blockThreatIP(caller=arguments.caller) />
<cfelseif action eq "isThreat">
<cfset _isIPThreat(caller=arguments.caller) />
</cfif>

<cfreturn true />
</cffunction>


<cffunction name="_blockThreatIP" access="private" returntype="void">
<cfargument name="caller" type="struct" required="yes" />
<cfset var SpamBlocker = _getInitializedSpamBlockerCFC() />
<cfset SpamBlocker.blockThreatUser(getAttribute('IP')) />
</cffunction>


<cffunction name="_isIPThreat" access="private" returntype="void">
<cfargument name="caller" type="struct" required="yes" />
<cfset var SpamBlocker = _getInitializedSpamBlockerCFC() />
<cfset var stCheck = SpamBlocker.getUserThreat(getAttribute('IP')) />
<cfset _insertValueIntoCaller(caller, stCheck) />
</cffunction>


<cffunction name="_getIPThreat" access="private" returntype="void">
<cfargument name="caller" type="struct" required="yes" />
<cfset var SpamBlocker = _getInitializedSpamBlockerCFC() />
<cfset var stCheck = SpamBlocker.getThreatData(getAttribute('IP')) />
<cfset _insertValueIntoCaller(caller, stCheck) />
</cffunction>


<cffunction name="_createLink" access="private" returntype="void">
<cfargument name="caller" type="struct" required="yes" />
<cfset var LinkGenerator = createObject("component", "components.LinkGenerator").init(honeyPotURL=getAttribute('url')) />
<cfset var linkHtml = LinkGenerator.getHTML() />
<cfset _insertValueIntoCaller(caller=caller, value=linkHtml, optionalToSTDOUT=true) />
</cffunction>


<cffunction name="onEndTag" output="yes" returntype="boolean">
<cfargument name="attributes" type="struct">
<cfargument name="caller" type="struct">
<cfargument name="generatedContent" type="string">
<cfreturn false/>
</cffunction>


<cffunction name="_getInitializedSpamBlockerCFC" access="private" returntype="components.SpamBlocker">
<cfset var SpamBlocker = createObject("component", "components.SpamBlocker") />
<cfset SpamBlocker.honeypotKey = getAttribute('httpblkey') />
<cfif attributeExists('threatnrs')>
<cfset SpamBlocker.lsAbsoluteThreatNrs = getAttribute('threatnrs') />
</cfif>
<cfif attributeExists('maxdaysago')>
<cfset SpamBlocker.nIsNoThreatAfterDayNum = getAttribute('maxdaysago') />
</cfif>
<cfif attributeExists('minimumThreatIndex')>
<cfset SpamBlocker.nMinimumThreatScore = getAttribute('minimumThreatIndex') />
</cfif>
<cfif attributeExists('blocktext')>
<cfset SpamBlocker.sBlockText = getAttribute('blocktext') />
</cfif>
<cfreturn SpamBlocker />
</cffunction>


<cffunction name="_insertValueIntoCaller" access="private" returntype="void">
<cfargument name="caller" type="struct" required="yes" />
<cfargument name="value" type="any" required="yes" hint="The value to insert into the caller page" />
<cfargument name="optionalToSTDOUT" type="boolean" required="no" default="false" hint="If no 'variable' attr. is given, should we output the value to STDOUT or to a variable called 'cfhoneypot'" />
<cfif attributeExists('variable')>
<cfset arguments.caller[getAttribute('variable')] = arguments.value />
<cfelseif arguments.optionalToSTDOUT>
<cfoutput>#arguments.value#</cfoutput>
<cfelse>
<cfset arguments.caller["cfhoneypot"] = arguments.value />
</cfif>
</cffunction>


<!--- attributes --->
<cffunction name="getAttribute" output="false" access="public" returntype="any">
<cfargument name="key" required="true" type="String" />
<cfreturn variables.attributes[key] />
</cffunction>

<cffunction name="attributeExists" output="false" access="public" returntype="boolean">
<cfargument name="key" required="true" type="String" />
<cfreturn structKeyExists(variables.attributes, key) />
</cffunction>

</cfcomponent>

I also did some rewriting of the cfc's I created on earlier projects, you can check the code of these cfc's in the svn repository browser.

As you can see in the code above, I am practically only using the onTagStart function, and the metadata functionality, which makes sure that the required parameters are given with correct type.

For me, this is a great start, which will allow me to write <cfchart type="flash" />, <cfform> and others. Hope I will find time to do that!

del.icio.us Digg StumbleUpon Facebook Technorati Fav reddit Google Bookmarks
| Viewed 4187 times
  1. jbuda

    #1 by jbuda - July 26, 2010 at 9:15 AM

    Nice post, always looking for a way to reuse code and this will be a brilliant starting / reference point.

    Thanks
(will not be published)
Leave this field empty