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<=%d && %d>%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. + &client.ip;.&client.Fullname;.&client.accept;.&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."
