roxen.lists.roxen.general

Subject Author Date
Update (Re: RFC: Automated CAPTCHA tag in vform) Stephen R. van den Berg <srb[at]cuci[dot]nl> 30-01-2009
Newer version, small but essential fixes.

commit 6566dd88b46a79a46c8085711337159f0e6c617f
Author: Stephen R. van den Berg <<srb[at]cuci.nl>>
Date:   Fri Jan 30 02:15:22 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..9a351ba 100644
--- a/server/modules/tags/vform.pike
+++ b/server/modules/tags/vform.pike
@@ -376,6 +376,159 @@ 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 (!ok)
+	  {
+	    id->misc->vform_ok = 0;
+	    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 +597,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 +616,7 @@ class TagVForm {
   RXML.TagSet internal = RXML.TagSet (this_module(), "internal",
 				      ({ TagVInput(),
 					 TagReload(),
+					 TagCaptcha(),
 					 TagClear(),
 					 TagVSelect(),
 					 TagIfVFailed(),
@@ -542,6 +695,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 +754,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."