]> sipb.mit.edu Git - ikiwiki.git/blob - doc/todo/require_CAPTCHA_to_edit.mdwn
64f0a38d8179fd4c497ceccd0b109042954260c1
[ikiwiki.git] / doc / todo / require_CAPTCHA_to_edit.mdwn
1 I don't necessarily trust all OpenID providers to stop bots.  I note that ikiwiki allows [[banned_users]], and that there are other todos such as [[todo/openid_user_filtering]] that would extend this.  However, it might be nice to have a CAPTCHA system.
2
3 I imagine a plugin that modifies the login screen to use <http://recaptcha.net/>.  You would then be required to fill in the captcha as well as log in in the normal way.
4
5 > I hate CAPTCHAs with a passion. Someone else is welcome to write such a
6 > plugin.
7 >
8 > If spam via openid (which I have never ever seen yet) becomes
9 > a problem, a provider whitelist/blacklist seems like a much nicer
10 > solution than a CAPTCHA. --[[Joey]]
11
12 >> Apparently there has been openid spam (you can google for it).  But as for
13 >> white/black lists, were you thinking of listing the openids, or the content?
14 >> Something like the moinmoin global <http://master.moinmo.in/BadContent>
15 >> list?
16
17 >>> OpenID can be thought of as pushing the problem of determining if
18 >>> someone is a human or a spambot back from the openid consumer to the
19 >>> openid provider. So, providers that make it possible for spambots to
20 >>> use their openids, or that are even set up explicitly for use in
21 >>> spamming, would be the ones to block. Or, providers that are known to
22 >>> use very good screening for humans would be the ones to allow.
23 >>> (Openid delegation makes it a bit harder than just looking at the
24 >>> openid url though.) --[[Joey]]
25
26 Okie - I have a first pass of this.  There are still some issues.
27
28 Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
29 If you get the CAPTCHA wrong then the current code tells formbuilder that
30 one of the fields is invalid.  This stops the login from going through.
31 Unfortunately, formbuilder is caching this validity somewhere, and I haven't
32 found a way around that yet.  This means that if you get the CAPTCHA
33 wrong, it will continue to fail.  You need to load the login page again so
34 it doesn't have the error message on the screen, then it'll work again.
35
36 > fixed this - updated code is attached.
37
38 A second issue is that the OpenID login system resets the 'required' flags
39 of all the other fields, so using OpenID will cause the CAPTCHA to be
40 ignored.
41
42 > This is still not fixed.  I would have thought the following patch would
43 > have fixed this second issue, but it doesn't.
44
45 --- a/IkiWiki/Plugin/openid.pm
46 +++ b/IkiWiki/Plugin/openid.pm
47 @@ -61,6 +61,7 @@ sub formbuilder_setup (@) { #{{{
48                         # Skip all other required fields in this case.
49                         foreach my $field ($form->field) {
50                                 next if $field eq "openid_url";
51 +                               next if $field eq "recaptcha";
52                                 $form->field(name => $field, required => 0,
53                                         validate => '/.*/');
54                         }
55
56 >> What seems to be happing here is that the openid plugin defines a
57 >> validate hook for openid_url that calls validate(). validate() in turn
58 >> redirects the user to the openid server for validation, and exits. If
59 >> the openid plugins' validate hook is called before your recaptcha
60 >> validator, your code never gets a chance to run. I don't know how to
61 >> control the other that FormBuilder validates fields, but the only fix I
62 >> can see is to somehow influence that order. 
63 >>
64 >> Hmm, maybe you need to move your own validation code out of the validate
65 >> hook. Instead, just validate the captcha in the formbuilder_setup hook.
66 >> The problem with this approach is that if validation fails, you can't
67 >> just flag it as invalid and let formbuilder handle that. Instead, you'd
68 >> have to hack something in to redisplay the captcha by hand. --[[Joey]]
69
70 Instructions
71 =====
72
73 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
74 The keys are added as options.
75
76         reCaptchaPubKey => "LONGPUBLICKEYSTRING",
77         reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
78
79 You can also use "signInSSL" if you're using ssl for your login screen.
80
81
82 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
83
84 ----------
85
86 #!/usr/bin/perl
87 # Ikiwiki password authentication.
88 package IkiWiki::Plugin::recaptcha;
89
90 use warnings;
91 use strict;
92 use IkiWiki 2.00;
93
94 sub import { #{{{
95         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
96 } # }}}
97
98 sub getopt () { #{{{
99         eval q{use Getopt::Long};
100         error($@) if $@;
101         Getopt::Long::Configure('pass_through');
102         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
103         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
104 } #}}}
105
106 sub formbuilder_setup (@) { #{{{
107         my %params=@_;
108
109         my $form=$params{form};
110         my $session=$params{session};
111         my $cgi=$params{cgi};
112         my $pubkey=$config{reCaptchaPubKey};
113         my $privkey=$config{reCaptchaPrivKey};
114         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
115                 unless defined $config{reCaptchaPubKey};
116         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
117                 unless defined $config{reCaptchaPrivKey};
118         my $tagtextPlain=<<EOTAG;
119                 <script type="text/javascript"
120                         src="http://api.recaptcha.net/challenge?k=$pubkey">
121                 </script>
122
123                 <noscript>
124                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
125                                 height="300" width="500" frameborder="0"></iframe><br>
126                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
127                         <input type="hidden" name="recaptcha_response_field" 
128                                 value="manual_challenge">
129                 </noscript>
130 EOTAG
131
132         my $tagtextSSL=<<EOTAGS;
133                 <script type="text/javascript"
134                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
135                 </script>
136
137                 <noscript>
138                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
139                                 height="300" width="500" frameborder="0"></iframe><br>
140                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
141                         <input type="hidden" name="recaptcha_response_field" 
142                                 value="manual_challenge">
143                 </noscript>
144 EOTAGS
145
146         my $tagtext;
147
148         if ($config{signInSSL}) {
149                 $tagtext = $tagtextSSL;
150         } else {
151                 $tagtext = $tagtextPlain;
152         }
153         
154         if ($form->title eq "signin") {
155                 # Give up if module is unavailable to avoid
156                 # needing to depend on it.
157                 eval q{use LWP::UserAgent};
158                 if ($@) {
159                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
160                         return;
161                 }
162
163                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
164                         unless $pubkey;
165                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
166                         unless $privkey;
167                 die("To use reCAPTCHA you must know the remote IP address")
168                         unless $session->remote_addr();
169
170                 $form->field(
171                         name => "recaptcha",
172                         label => "",
173                         type => 'static',
174                         comment => $tagtext,
175                         required => 1,
176                         message => "CAPTCHA verification failed",
177                 );
178
179                 # validate the captcha.
180                 if ($form->submitted && $form->submitted eq "Login" &&
181                                 defined $form->cgi_param("recaptcha_challenge_field") && 
182                                 length $form->cgi_param("recaptcha_challenge_field") &&
183                                 defined $form->cgi_param("recaptcha_response_field") && 
184                                 length $form->cgi_param("recaptcha_response_field")) {
185
186                         my $challenge = "invalid";
187                         my $response = "invalid";
188                         my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
189
190                         $form->field(name => "recaptcha",
191                                 message => "CAPTCHA verification failed",
192                                 required => 1,
193                                 validate => sub {
194                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
195                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
196                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
197                                                 $response = $form->cgi_param("recaptcha_response_field");
198                                                 debug("Validating: ".$challenge." ".$response);
199                                                 $result = check_answer($privkey,
200                                                                 $session->remote_addr(),
201                                                                 $challenge, $response);
202                                         } else {
203                                                 debug("re-Validating");
204                                         }
205
206                                         if ($result->{is_valid}) {
207                                                 debug("valid");
208                                                 return 1;
209                                         } else {
210                                                 debug("invalid");
211                                                 return 0;
212                                         }
213                                 });
214                 }
215         }
216 } # }}}
217
218 # The following function is borrowed from
219 # Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
220
221 sub check_answer {
222     my ( $privkey, $remoteip, $challenge, $response ) = @_;
223
224     die
225       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
226       unless $privkey;
227
228     die "For security reasons, you must pass the remote ip to reCAPTCHA"
229       unless $remoteip;
230
231         if (! ($challenge && $response)) {
232                 debug("Challenge or response not set!");
233                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
234         }
235
236         my $ua = LWP::UserAgent->new();
237
238     my $resp = $ua->post(
239         'http://api-verify.recaptcha.net/verify',
240         {
241             privatekey => $privkey,
242             remoteip   => $remoteip,
243             challenge  => $challenge,
244             response   => $response
245         }
246     );
247
248     if ( $resp->is_success ) {
249         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
250         if ( $answer =~ /true/ ) {
251             debug("CAPTCHA valid");
252             return { is_valid => 1 };
253         }
254         else {
255             chomp $message;
256             debug("CAPTCHA failed: ".$message);
257             return { is_valid => 0, error => $message };
258         }
259     }
260     else {
261         debug("Unable to contact reCaptcha verification host!");
262         return { is_valid => 0, error => 'recaptcha-not-reachable' };
263     }
264 }
265
266 1;
267