/ lib / captcha-test.php
captcha-test.php
  1  <?
  2  
  3  require_once "lib/rpc.php";
  4  require_once 'lib/ini.php';
  5  load_ini_file('captcha_config.ini');
  6  
  7  $recaptcha_public_key  = RECAPTCHA_API_KEY_PUBLIC;
  8  $recaptcha_private_key = RECAPTCHA_API_KEY_PRIVATE;
  9  
 10  $hcaptcha_public_key  = HCAPTCHA_API_KEY_PUBLIC;
 11  $hcaptcha_private_key = HCAPTCHA_API_KEY_PRIVATE;
 12  
 13  // Parameter formats and other checks must much the formatting in /captcha
 14  function is_twister_captcha_valid($memcached, $ip, $userpwd, $board = '!', $thread_id = 0, &$unsolved_count = null) {
 15    if (!$memcached) {
 16      return false;
 17    }
 18    
 19    if (defined('TEST_BOARD') && TEST_BOARD) {
 20      require_once 'lib/twister_captcha-test.php';
 21    }
 22    else {
 23      require_once 'lib/twister_captcha.php';
 24    }
 25    
 26    if (!isset($_POST['t-challenge']) || !$_POST['t-challenge']) {
 27      return false;
 28    }
 29    
 30    if (!isset($_POST['t-response']) || !$_POST['t-response']) {
 31      return false;
 32    }
 33    
 34    if (strlen($_POST['t-response']) > 24) {
 35      return false;
 36    }
 37    
 38    if (strlen($_POST['t-challenge']) > 255) {
 39      return false;
 40    }
 41    
 42    // User agent
 43    if (!isset($_SERVER['HTTP_USER_AGENT'])) {
 44      $user_agent = '!';
 45    }
 46    else {
 47      $user_agent = md5($_SERVER['HTTP_USER_AGENT']);
 48    }
 49    
 50    // Password
 51    $password = '!';
 52    
 53    if ($userpwd && !$userpwd->isNew()) {
 54      $password = $userpwd->getPwd();
 55    }
 56    
 57    // ---
 58    
 59    list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']);
 60    
 61    $long_ip = ip2long($ip);
 62    
 63    if (!$uniq_id || !$challenge_hash || !$long_ip) {
 64      return false;
 65    }
 66    
 67    $challenge_key = "ch$long_ip";
 68    
 69    $params = [$ip, $password, $user_agent, $board, $thread_id];
 70    
 71    $response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']);
 72    
 73    $is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params);
 74    
 75    if ($is_valid) {
 76      $active_uniq_id = $memcached->get($challenge_key);
 77      
 78      // Delete challenge
 79      $memcached->delete($challenge_key);
 80      
 81      if (!$active_uniq_id || $uniq_id !== $active_uniq_id) {
 82        return false;
 83      }
 84      
 85      // Return and decrement the unsolved session count
 86      $us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null);
 87      
 88      if ($unsolved_count !== null) {
 89        $unsolved_count = $us;
 90      }
 91      
 92      return true;
 93    }
 94    
 95    // Delete challenge
 96    $memcached->delete($challenge_key);
 97    
 98    return false;
 99  }
100  
101  // Decrements the unsolved count by 2 and returns the old count
102  function decrement_twister_captcha_session($memcached, $ip, $return_old = true) {
103    if (!$memcached) {
104      return false;
105    }
106    
107    $long_ip = ip2long($ip);
108    
109    if (!$long_ip) {
110      return false;
111    }
112    
113    $key = "us$long_ip";
114    
115    if ($return_old) {
116      $val = $memcached->get($key);
117      
118      if ($val === false) {
119        $val = 0;
120      }
121    }
122    else {
123      $val = 0;
124    }
125    
126    $memcached->decrement($key, 2);
127    
128    return $val;
129  }
130  
131  // FIXME: The IP arg isn't used for now
132  function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) {
133    if (!$memcached || !$userpwd) {
134      return false;
135    }
136    
137    $current_time = (int)$current_time;
138    
139    if ($current_time <= 0) {
140      return false;
141    }
142    
143    //$long_ip = ip2long($ip);
144    
145    //if (!$long_ip) {
146      //return false;
147    //}
148    
149    $credits = 0;
150    
151    // Config
152    // Stage 1 should match the check in use_twister_captcha_credit()
153    // and captcha.php for optimisation purposes
154    
155    // Stage 1
156    $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
157    $noop_post_count_1 = 5; // required post count
158    $noop_credits_1 = 1; // credits given
159    $noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds)
160    
161    // Stage 2
162    $noop_known_ttl_2 = 21600; // 15 days
163    $noop_post_count_2 = 20;
164    $noop_credits_2 = 2;
165    $noop_duration_2 = 7200; // 2 hours
166    
167    // Stage 3
168    $noop_known_ttl_3 = 129600; // 90 days
169    $noop_post_count_3 = 100;
170    $noop_credits_3 = 3;
171    $noop_duration_3 = 10800; // 3 hours
172    
173    // ---
174    
175    // The IP changed too recently
176    if ($userpwd->ipLifetime() < 60) {
177      return false;
178    }
179    
180    // Stage 3
181    if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) {
182      $credits = $noop_credits_3;
183      $duration = $noop_duration_3;
184    }
185    // Stage 2
186    else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) {
187      $credits = $noop_credits_2;
188      $duration = $noop_duration_2;
189    }
190    // Stage 1
191    else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) {
192      $credits = $noop_credits_1;
193      $duration = $noop_duration_1;
194    }
195    else {
196      return false;
197    }
198    
199    if (!$credits || $credits > 3) {
200      return false;
201    }
202    
203    $expiration_ts = $current_time + $duration;
204    
205    // Require no more than 5 actions in the past 8 minutes
206    /*
207    $query = <<<SQL
208  SELECT SQL_NO_CACHE COUNT(*) FROM user_actions
209  WHERE ip = $long_ip
210  AND time >= DATE_SUB(NOW(), INTERVAL 8 MINUTE)
211  SQL;
212    
213    $res = mysql_global_call($query);
214    
215    if (!$res) {
216      return false;
217    }
218    
219    $count = (int)mysql_fetch_row($res)[0];
220    
221    if ($count > 5) {
222      return false;
223    }
224    */
225    // Set credits
226    
227    $pwd = $userpwd->getPwd();
228    
229    if (!$pwd) {
230      return false;
231    }
232    
233    $key = "cr-$pwd";
234    $val = "$credits.$expiration_ts";
235    
236    $res = $memcached->replace($key, $val, $expiration_ts);
237    
238    if ($res === false) {
239      if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
240        return $memcached->set($key, $val, $expiration_ts);
241      }
242      else {
243        return false;
244      }
245    }
246    
247    return true;
248  }
249  
250  // FIXME: The IP arg isn't used for now
251  function use_twister_captcha_credit($memcached, $ip, $userpwd) {
252    if (!$memcached || !$userpwd) {
253      return false;
254    }
255    
256    //$long_ip = ip2long($ip);
257    
258    //if (!$long_ip) {
259      //return false;
260    //}
261    
262    // Must match the check in set_twister_captcha_credits()
263    $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
264    $noop_post_count_1 = 5; // required post count
265    
266    if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) {
267      return false;
268    }
269    
270    $pwd = $userpwd->getPwd();
271    
272    if (!$pwd) {
273      return false;
274    }
275    
276    $key = "cr-$pwd";
277    $credits = $memcached->get($key);
278    
279    if ($credits === false) {
280      return false;
281    }
282    
283    list($count, $ts) = explode('.', $credits);
284    
285    $count = (int)$count;
286    $ts = (int)$ts;
287    
288    // No credits left
289    if ($count <= 0 || $ts <= 0) {
290      $memcached->delete($key);
291      return false;
292    }
293    
294    $count -= 1;
295    
296    $res = $memcached->replace($key, "$count.$ts", $ts);
297    
298    if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) {
299      return false;
300    }
301    
302    return true;
303  }
304  
305  function twister_captcha_form() {
306    return '<div id="t-root"></div>';
307  }
308  
309  function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) {
310    $data = [
311      'board' => $board,
312      'thread_id' => $thread_id,
313    ];
314    
315    if ($userpwd) {
316      $data['arg_num'] = $userpwd->pwdLifetime();
317      $data['pwd'] = $userpwd->getPwd();
318    }
319    
320    if ($meta) {
321      $data['meta'] = $meta;
322    }
323    
324    if ($is_quiet) {
325      $type = 'failed_captcha_quiet';
326    }
327    else {
328      $type = 'failed_captcha';
329    }
330    
331    write_to_event_log($type, $ip, $data);
332  }
333  
334  function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
335    global $hcaptcha_public_key;
336    
337    $js_tag = '<script src="https://hcaptcha.com/1/api.js'
338      . (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
339    
340    if ($autoload) {
341      $attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"';
342      
343      if ($dark) {
344        $attrs .= ' data-theme="dark"';
345      }
346    }
347    else {
348      $attrs = '';
349    }
350    
351    $container_tag = '<script>window.hcaptchaKey = "' . $hcaptcha_public_key
352        . '";</script><div id="g-recaptcha"'
353        . $attrs . '></div>';
354    
355    return $js_tag.$container_tag;
356  }
357  
358  // Moves css out of the form for html validation
359  function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
360    global $recaptcha_public_key;
361    
362    $js_tag = '<script src="https://www.google.com/recaptcha/api.js'
363      . (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
364    
365    if ($autoload) {
366      $attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"';
367      
368      if ($dark) {
369        $attrs .= ' data-theme="dark"';
370      }
371    }
372    else {
373      $attrs = '';
374    }
375    
376    $container_tag = '<script>window.recaptchaKey = "' . $recaptcha_public_key
377        . '";</script><div id="g-recaptcha"'
378        . $attrs . '></div>';
379    
380    $noscript_tag =<<<HTML
381  <noscript>
382    <div style="width: 302px;">
383      <div style="width: 302px; position: relative;">
384        <div style="width: 302px; height: 422px;">
385          <iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
386        </div>
387        <div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
388          <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
389        </div>
390      </div>
391    </div>
392  </noscript>
393  HTML;
394    
395    if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) {
396      return $container_tag.$noscript_tag;
397    }
398    
399    return $js_tag.$container_tag.$noscript_tag;
400  }
401  
402  // Legacy captcha
403  // Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently.
404  function captcha_form_alt() {
405    global $recaptcha_public_key;
406    
407    $html = <<<HTML
408  <div id="captchaContainerAlt"></div>
409  <script>
410  function onAltCaptchaClick() {
411    Recaptcha.reload('t');
412  }
413  function onAltCaptchaReady() {
414    var el;
415    
416    if (el = document.getElementById('recaptcha_image')) {
417      el.title = 'Reload';
418      el.addEventListener('click', onAltCaptchaClick, false);
419    }
420  }
421  if (!window.passEnabled) {
422    var el = document.createElement('script');
423    el.type = 'text/javascript';
424    el.src = '//www.google.com/recaptcha/api/js/recaptcha_ajax.js';
425    el.onload = function() {
426      Recaptcha.create('$recaptcha_public_key',
427        'captchaContainerAlt',
428        {
429          theme: 'clean',
430          tabindex: 3,
431          callback: onAltCaptchaReady
432        }
433      );
434    }
435    document.head.appendChild(el);
436  }</script>
437  <noscript>
438    <div style="width: 302px;">
439      <div style="width: 302px; position: relative;">
440        <div style="width: 302px; height: 422px;">
441          <iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
442        </div>
443        <div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
444          <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
445        </div>
446      </div>
447    </div>
448  </noscript>
449  HTML;
450    
451    return $html;
452  }
453  
454  function recaptcha_ban($n, $time, $return_error = 0, $length = 1)
455  {
456    auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification.");
457    if( $return_error == 1 ) {
458      return S_GENERICERROR;
459    }
460    error(S_GENERICERROR);
461  }
462  
463  /**
464   * Works for both recaptcha and hcaptcha
465   */
466  function recaptcha_bad_captcha($return_error = false, $codes = null) {
467    $error = S_BADCAPTCHA;
468    
469    if (is_array($codes)) {
470      if (in_array('missing-input-response', $codes)) {
471        $error = S_NOCAPTCHA;
472      }
473      
474      if ($return_error) {
475        return $error;
476      }
477      else {
478        error($error);
479      }
480    }
481    else {
482      if ($return_error) {
483        return $error;
484      }
485      else {
486        error($error);
487      }
488    }
489  }
490  
491  // -----------
492  // hCaptcha
493  // -----------
494  function start_hcaptcha_verify($return_error = false) {
495    global $hcaptcha_private_key, $hcaptcha_ch;
496    
497    $response = $_POST["h-captcha-response"];
498    
499    if (!$response) {
500      if ($return_error == false) {
501        error(S_NOCAPTCHA);
502      }
503      return S_NOCAPTCHA;
504    }
505    
506    $response = urlencode($response);
507    
508    $rlen = strlen($response);
509    
510    if ($rlen > 32768) {
511      return recaptcha_bad_captcha($return_error);
512    }
513    
514    $api_url = 'https://hcaptcha.com/siteverify';
515    
516    $post = array(
517      'secret' => $hcaptcha_private_key,
518      'response' => $response
519    );
520    
521    $hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
522  }
523  
524  function end_hcaptcha_verify($return_error = false) {
525    global $hcaptcha_ch;
526    
527    if (!$hcaptcha_ch) {
528      return;
529    }
530    
531    $ret = rpc_finish_request($hcaptcha_ch, $error, $httperror);
532    
533    // BAD
534    // 413 Request Too Large is bad; it was caused intentionally by the user.
535    if ($httperror == 413) {
536      return recaptcha_bad_captcha($return_error);
537    }
538    
539    // BAD
540    if ($ret == null) {
541      return recaptcha_bad_captcha($return_error);
542    }
543    
544    $resp = json_decode($ret, true);
545    
546    // BAD
547    // Malformed JSON response from Google
548    if (json_last_error() !== JSON_ERROR_NONE) {
549      return recaptcha_bad_captcha($return_error);
550    }
551    
552    // GOOD
553    if ($resp['success']) {
554      return $resp;
555    }
556    
557    // BAD
558    return recaptcha_bad_captcha($return_error, $resp['error-codes']);
559  }
560  
561  // -----------
562  // reCaptcha V2
563  // -----------
564  // FIXME $challenge_field is no longer used
565  function start_recaptcha_verify($return_error = false, $challenge_field = '') {
566    global $recaptcha_private_key, $recaptcha_ch;
567    
568    $response = $_POST["g-recaptcha-response"];
569    
570    if (!$response) {
571      if ($return_error == false) {
572        error(S_NOCAPTCHA);
573      }
574      return S_NOCAPTCHA;
575    }
576    
577    $response = urlencode($response);
578    
579    $rlen = strlen($response);
580    
581    if ($rlen > 4096) {
582      return recaptcha_bad_captcha($return_error);
583    }
584    
585    $api_url = 'https://www.google.com/recaptcha/api/siteverify';
586    
587    $post = array(
588      'secret' => $recaptcha_private_key,
589      'response' => $response
590    );
591    
592    $recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
593  }
594  
595  function end_recaptcha_verify($return_error = false) {
596    global $recaptcha_ch;
597    
598    if (!$recaptcha_ch) {
599      return;
600    }
601    
602    $ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
603    
604    // BAD
605    // 413 Request Too Large is bad; it was caused intentionally by the user.
606    if ($httperror == 413) {
607      return recaptcha_bad_captcha($return_error);
608    }
609    
610    // BAD
611    if ($ret == null) {
612      return recaptcha_bad_captcha($return_error);
613    }
614    
615    $resp = json_decode($ret, true);
616    
617    // BAD
618    // Malformed JSON response from Google
619    if (json_last_error() !== JSON_ERROR_NONE) {
620      return recaptcha_bad_captcha($return_error);
621    }
622    
623    // GOOD
624    if ($resp['success']) {
625      return $resp;
626    }
627    
628    // BAD
629    return recaptcha_bad_captcha($return_error, $resp['error-codes']);
630  }
631  
632  // -----------
633  // reCaptcha V1
634  // -----------
635  function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') {
636    global $recaptcha_private_key, $recaptcha_ch;
637    
638    $challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field;
639    $response  = $_POST["recaptcha_response_field"];
640    if (!$challenge || !$response) {
641      if( $return_error == false ) {
642        error(S_NOCAPTCHA);
643      }
644      return S_NOCAPTCHA;
645    }
646    
647    $num_words = 1 + preg_match_all('/\\s/', $response);
648    $rlen = strlen($response);
649    if ($num_words > 3 || $rlen > 128) {
650      return recaptcha_bad_captcha($return_error);
651    }
652    
653    $post = array(
654      "privatekey" => $recaptcha_private_key,
655      "challenge"  => $challenge,
656      "remoteip"   => $_SERVER["REMOTE_ADDR"],
657      "response"   => $response
658    );
659    
660    $recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false);
661  }
662  
663  function end_recaptcha_verify_alt($return_error = false) {
664    global $recaptcha_ch;
665    
666    if (!$recaptcha_ch) return;
667    
668    $ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
669    
670    if ($httperror == 413) {
671      return recaptcha_bad_captcha($return_error);
672    }
673    
674    if ($ret) {
675      $lines = explode("\n", $ret);
676      if ($lines[0] === "true") {
677        // GOOD
678        return;
679      }
680    }
681    
682    // BAD
683    return recaptcha_bad_captcha($return_error);
684  }
685  
686  ?>