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 <[email protected]>
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(({256,512,1024,4096,8192});;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." 

Reply via email to