]> sipb.mit.edu Git - ikiwiki.git/blob - doc/todo/require_CAPTCHA_to_edit.mdwn
web commit by http://willu.myopenid.com/: Add (buggy) code for todo item
[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 Okie - I have a first pass of this.  There are still some issues.
18
19 Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
20 If you get the CAPTCHA wrong then the current code tells formbuilder that
21 one of the fields in invalid.  This stops the login from going through.
22 Unfortunately, formbuilder is caching this validity somewhere, and I haven't
23 found a way around that yet.  This means that if you get the CAPTCHA
24 wrong, it will continue to fail.  You need to load the login page again so
25 it doesn't have the error message on the screen, then it'll work again.
26
27 A second issue is that the OpenID login system resets the 'required' flags
28 of all the other fields, so using OpenID will cause the CAPTCHA to be
29 ignored.
30
31 Instructions
32 =====
33
34 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
35 The keys are added as options.
36
37         reCaptchaPubKey => "LONGPUBLICKEYSTRING",
38         reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
39
40 You can also use "signInSSL" if you're using ssl for your login screen.
41
42
43 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
44
45 ----------
46
47 #!/usr/bin/perl
48 # Ikiwiki password authentication.
49 package IkiWiki::Plugin::recaptcha;
50
51 use warnings;
52 use strict;
53 use IkiWiki 2.00;
54
55 sub import { #{{{
56         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
57 } # }}}
58
59 sub getopt () { #{{{
60         eval q{use Getopt::Long};
61         error($@) if $@;
62         Getopt::Long::Configure('pass_through');
63         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
64         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
65 } #}}}
66
67 sub formbuilder_setup (@) { #{{{
68         my %params=@_;
69
70         my $form=$params{form};
71         my $session=$params{session};
72         my $cgi=$params{cgi};
73         my $pubkey=$config{reCaptchaPubKey};
74         my $privkey=$config{reCaptchaPrivKey};
75         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
76                 unless defined $config{reCaptchaPubKey};
77         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
78                 unless defined $config{reCaptchaPrivKey};
79         my $tagtextPlain=<<EOTAG;
80                 <script type="text/javascript"
81                         src="http://api.recaptcha.net/challenge?k=$pubkey">
82                 </script>
83
84                 <noscript>
85                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
86                                 height="300" width="500" frameborder="0"></iframe><br>
87                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
88                         <input type="hidden" name="recaptcha_response_field" 
89                                 value="manual_challenge">
90                 </noscript>
91 EOTAG
92
93         my $tagtextSSL=<<EOTAGS;
94                 <script type="text/javascript"
95                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
96                 </script>
97
98                 <noscript>
99                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
100                                 height="300" width="500" frameborder="0"></iframe><br>
101                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
102                         <input type="hidden" name="recaptcha_response_field" 
103                                 value="manual_challenge">
104                 </noscript>
105 EOTAGS
106
107         my $tagtext;
108
109         if ($config{signInSSL}) {
110                 $tagtext = $tagtextSSL;
111         } else {
112                 $tagtext = $tagtextPlain;
113         }
114         
115         if ($form->title eq "signin") {
116                 # Give up if module is unavailable to avoid
117                 # needing to depend on it.
118                 eval q{use LWP::UserAgent};
119                 if ($@) {
120                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
121                         return;
122                 }
123
124                 debug("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
125                         unless $pubkey;
126                 debug("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
127                         unless $privkey;
128                 debug("To use reCAPTCHA you must know the remote IP address")
129                         unless $session->remote_addr();
130
131                 my $extras = $form->keepextras();
132                 if ($extras) {
133                         push ( @$extras, qw(recaptcha_challenge_field recaptcha_response_field) );
134                 } else {
135                         $extras = [qw(recaptcha_challenge_field recaptcha_response_field)];
136                 }
137                 $form->keepextras($extras);
138
139                 my $challenge = "invalid";
140                 my $response = "invalid";
141                 my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
142
143                 $form->field(
144                         name => "recaptcha",
145                         label => "",
146                         type => 'static',
147                         comment => $tagtext,
148                         required => 1,
149                         message => "CAPTCHA verification failed",
150                 );
151
152                 # validate the captcha.
153                 if ($form->submitted && $form->submitted eq "Login" &&
154                                 defined $form->cgi_param("recaptcha_challenge_field") && 
155                                 length $form->cgi_param("recaptcha_challenge_field") &&
156                                 defined $form->cgi_param("recaptcha_response_field") && 
157                                 length $form->cgi_param("recaptcha_response_field")) {
158                         
159                         $form->field(name => "recaptcha",
160                                 message => "CAPTCHA verification failed",
161                                 required => 1,
162                                 validate => sub {
163                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
164                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
165                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
166                                                 $response = $form->cgi_param("recaptcha_response_field");
167                                                 warn("Validating: ".$challenge." ".$response);
168                                                 $result = check_answer($privkey,
169                                                                 $session->remote_addr(),
170                                                                 $challenge, $response);
171                                         } else {
172                                                 warn("re-Validating");
173                                         }
174                                         if ($result->{is_valid}) {
175                                                 warn("valid");
176                                                 return 1;
177                                         } else {
178                                                 warn("invalid");
179                                                 return 0;
180                                         }
181                                 });
182                 }
183         }
184 } # }}}
185
186 # The following function is borrowed with modifications from
187 # Captcha::reCAPTCHA by Andy Armstrong and is under the PERL Artistic License
188
189 sub check_answer {
190     my ( $privkey, $remoteip, $challenge, $response ) = @_;
191
192     die
193       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
194       unless $privkey;
195
196     die "For security reasons, you must pass the remote ip to reCAPTCHA"
197       unless $remoteip;
198
199         if (! ($challenge && $response)) {
200                 warn("Challenge or response not set!");
201                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
202         }
203
204         my $ua = LWP::UserAgent->new();
205
206     my $resp = $ua->post(
207         'http://api-verify.recaptcha.net/verify',
208         {
209             privatekey => $privkey,
210             remoteip   => $remoteip,
211             challenge  => $challenge,
212             response   => $response
213         }
214     );
215
216     if ( $resp->is_success ) {
217         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
218         if ( $answer =~ /true/ ) {
219             warn("CAPTCHA valid");
220             return { is_valid => 1 };
221         }
222         else {
223             chomp $message;
224             warn("CAPTCHA failed: ".$message);
225             return { is_valid => 0, error => $message };
226         }
227     }
228     else {
229         warn("Unable to contact reCaptcha verification host!");
230         return { is_valid => 0, error => 'recaptcha-not-reachable' };
231     }
232 }
233
234 1;
235