roxen.lists.roxen.general

Subject Author Date
RFC: Automated CAPTCHA tag in vform Stephen R. van den Berg <srb[at]cuci[dot]nl> 30-01-2009
I'm using this currently, it seems to work rather well.
Any comments about improvements are welcome.
You're invited to patch it into Roxen if it looks good enough.

commit e51ce5e1c6a5fb97e5334f9694a8315bf93ca6b8
Author: Stephen R. van den Berg <<srb[at]cuci.nl>>
Date:   Fri Jan 30 00:55:00 2009 +0100

    Implement an automated CAPTCHA tag as part of vform

diff --git a/server/modules/tags/vform.pike b/server/modules/tags/vform.pike
index 4a52c66..5a5fc7a 100644
--- a/server/modules/tags/vform.pike
+++ b/server/modules/tags/vform.pike
@@ -376,6 +376,156 @@ class TagVForm {
     }
   }
 
+  class TagCaptcha {
+    inherit RXML.Tag;
+    constant name = "captcha";
+    constant flags = RXML.FLAG_EMPTY_ELEMENT;
+
+    class Frame {
+      inherit RXML.Frame;
+
+      array do_return(RequestID id) {
+	int debug = args->debug && 1;
+	int t,tdiff;
+#define LPRIME		1151
+#define RANDRANGE	(((1<<16)/2-LPRIME)/2)
+	int prim1=random(RANDRANGE)*2+LPRIME,prim2=random(RANDRANGE)*2+LPRIME,
+	 prim3=random(RANDRANGE*2)+LPRIME;
+
+	string timetosess(int t,void|string seed)
+	{ return MIME.encode_base64(Crypto.MD5.hash((args->seed||"")
+	   +"."+(string)t+"."+(seed||"")));
+	};
+
+	string getvarname(int t)
+	{ return sprintf("%s%.*s%s",args->prefix||"",args->namewidth||8,
+	    timetosess(t),args->postfix||"");
+	};
+
+	int primiterate(int i)
+	{ return 1<<16<i?i:primiterate((i*prim1+prim2)%(1<<16|prim1+prim2|1));
+	};
+
+        array formulas =
+         ({
+ #if 0		// These two formula's are too simple
+           lambda(string challenge)
+            { return sprintf("%d",challenge^primiterate(prim3));
+            },
+           lambda(string challenge)
+            { int i=(int)challenge^primiterate(prim3);
+              int j=random((1<<31)-2)+1;
+              return sprintf("%d^0%o",j^i,j);
+            },
+#endif
+           lambda(string challenge)
+            { int i=(int)challenge^primiterate(prim3);
+              int j=random((1<<31)-2)+1;
+              int s=random(29)+1;
+              int a=j^i;
+              return sprintf("(%d^0%o)<<0%o^0%o^0%o",
+               a>>s,j>>s,s,a&(1<<s)-1,j&(1<<s)-1);
+            },
+           lambda(string challenge)
+            { int i=(int)challenge^primiterate(prim3);
+              int j=random((1<<31)-2)+1;
+              int k=random((1<<31)-2)+1;
+              return sprintf("%d%%0%o^0%o",j,k,i^(j%k));
+            },
+         });
+	
+        NOCACHE();
+        t = time(1);
+        tdiff = Roxen.time_dequantifier(args,
+	 args["unix-time"] ? (int)args["unix-time"] : t) - t;
+	if(tdiff <= 0)
+	   tdiff = 32;
+
+	string v2, session;
+	{
+	  int t1;
+	  int obfuscinterval = tdiff*4;
+	  foreach(();;int ntd)
+	    if(ntd>=tdiff)
+	    {
+	      obfuscinterval = ntd;
+	      break;
+	    }
+	  t1 = t - t%(tdiff*obfuscinterval);
+	  session = getvarname(t1);
+        
+	  v2 = getvarname(t1 + (tdiff*obfuscinterval)/2);
+	}
+
+	string challenge;
+	mapping sessvar;
+
+	result = "";
+
+	{
+	  mixed vold;
+	  vold = RXML.user_get_var(session, "form")
+	      || RXML.user_get_var(v2, "form");
+
+	  int ok = 0;
+
+	  if(stringp(vold))
+	  {
+#define SESSIONPREFIX	"captcha."
+#define SESSIONWIDTH	16
+	    sscanf(vold,"%[^|]|%s",session,challenge);
+	    if(session && sizeof(session))
+	    {
+	      session = SESSIONPREFIX + session;
+	      if(sessvar = cache.get_session_data(session))
+	      {
+		// Clear the session immediately, so we do not allow
+		// retries; they get exactly one shot at the challenge
+		// within the allotted timeframe.
+
+	        cache.clear_session(session);
+	        int ntdiff = t-sessvar->t;
+	        ok = ntdiff<=tdiff && ntdiff>=(int)args->minsecs
+		  && sessvar->challenge==challenge;
+	        if(debug)
+		  result += sprintf(
+		   "<br />%d&lt;=%d && %d&gt;%d && %s==%s<br />Next session: ",
+		   ntdiff,tdiff,ntdiff,(int)args->minsecs,sessvar->challenge,
+		   challenge);
+	      }
+	    }
+	  }
+	  if (!(id->misc->vform_ok = ok))
+	    id->misc->vform_failed[name]=1;
+	  else
+	    id->misc->vform_verified[name]=1;
+	}
+	session = timetosess(t,roxen.create_unique_id())[..SESSIONWIDTH-1];
+	challenge = sprintf("%d",random(1<<31-1));
+	sessvar = (["t":t, "challenge":challenge]);
+      	cache.set_session_data(sessvar, SESSIONPREFIX+session, t+tdiff);
+	challenge = random(formulas)(challenge);
+        result +=
+	  RXML.t_xml->format_tag("script",
+	   ([
+	    "type":"text/javascript"
+	   ]), "var s='"+session+"|';"
+	   "var c="+challenge+";"
+	   "function g(l){"
+	   +sprintf("return 1<<16<l?l:g((l*0%o+0%o)%%(8<<13|0%o|1));",
+	    prim1,prim2,prim1+prim2)+
+	   "};"
+	   "document.write('<input name=\""+v2+"\""
+	   " type=\""+(debug?"text\" size=\"32":"hidden")
+	   +sprintf("\" value=\"'+s+(c^g(0%o))+'\" />",prim3)
+	   +(debug?sprintf("<pre>%s^%d</pre><br />",
+	    challenge,primiterate(prim3)):"")+"');")
+	 ;
+	return 0;
+      }
+    }
+  }
+
   class TagVerifyFail {
     inherit RXML.Tag;
     constant name = "verify-fail";
@@ -444,7 +594,6 @@ class TagVForm {
 
     int eval(string ind, RequestID id) {
       if(!ind || !sizeof(ind)) return !id->misc->vform_ok;
-      if(!id->real_variables[ind]) return 0;
       return id->misc->vform_failed[ind];
     }
   }
@@ -464,6 +613,7 @@ class TagVForm {
   RXML.TagSet internal = RXML.TagSet (this_module(), "internal",
 				      ({ TagVInput(),
 					 TagReload(),
+					 TagCaptcha(),
 					 TagClear(),
 					 TagVSelect(),
 					 TagIfVFailed(),
@@ -542,6 +692,9 @@ and <tag>roxen-automatic-charset-variable</tag>.</p>
 <ex-box>
 <vform>
   <vinput name='mail' type='email'>&_.warning;</vinput>
+  <captcha
+   seed='&client.ip;.&client.Fullname;.&client.accept;.&client.accept-charset;'
+   minsecs='1' minutes='32' />
   <input type='hidden' name='user' value='&form.userid;' />
   <input type='submit' />
 </vform>
@@ -598,6 +751,68 @@ and <tag>roxen-automatic-charset-variable</tag>.</p>
  verified.
 </p></desc>",
 
+"captcha":#"<desc type='tag'><p><short>
+ Creates a fully automated CAPTCHA widget.  It currently relies
+ on the fact that robots are bad at evaluating complex embedded
+ Javascript.</short>
+</p></desc>
+
+<attr name='seed'><p>
+ Extra seed used to generate the session key, it is recommended to
+ include as much information about the client as possible (e.g.
+
&amp;client.ip;.&amp;client.Fullname;.&amp;client.accept;.&amp;client.accept-charset;);
the session key already uses the current time as seed.</p>
+</attr>
+
+<attr name='minsecs'><p>
+ The minimum time in seconds that has to have passed before we accept
+ a submission of the form.</p>
+</attr>
+
+<attr name='prefix'><p>
+ Optional prefix of the name of the hidden captcha variable.</p>
+</attr>
+
+<attr name='namewidth' value='number'><p>
+ Optionally specify how many characters should be used for the
+ random part of the hidden captcha variable; defaults to 8.</p>
+</attr>
+
+<attr name='postfix'><p>
+ Optional suffix of the name of the hidden captcha variable.</p>
+</attr>
+
+<attr name='unix-time' value='number'>
+ <p>The exact time of expiration, expressed as a posix time integer.</p>
+</attr>
+
+<attr name='seconds' value='number'>
+ <p>Add this number of seconds to the time the user has to answer.</p>
+</attr>
+
+<attr name='minutes' value='number'>
+ <p>Add this number of minutes to the time the user has to answer.</p>
+</attr>
+
+<attr name='hours' value='number'>
+ <p>Add this number of hours to the time the user has to answer.</p>
+</attr>
+
+<attr name='days' value='number'>
+ <p>Add this number of days to the time the user has to answer.</p>
+</attr>
+
+<attr name='weeks' value='number'>
+ <p>Add this number of weeks to the time the user has to answer.</p>
+</attr>
+
+<attr name='months' value='number'>
+ <p>Add this number of months to the time the user has to answer.</p>
+</attr>
+
+<attr name='years' value='number'>
+ <p>Add this number of years to the time the user has to answer.</p>
+</attr>",
+
 "vinput":({ #"<desc type='cont'><p><short>
  Creates a self-verifying input widget.</short>
 </p></desc>
-- 
Sincerely,
           Stephen R. van den Berg.
"If you make people think they're thinking, they'll love you;
 but if you really make them think, they'll hate you."