686 lines
16 KiB
PHP
686 lines
16 KiB
PHP
<?
|
|
|
|
require_once "lib/rpc.php";
|
|
require_once 'lib/ini.php';
|
|
load_ini_file('captcha_config.ini');
|
|
|
|
$recaptcha_public_key = RECAPTCHA_API_KEY_PUBLIC;
|
|
$recaptcha_private_key = RECAPTCHA_API_KEY_PRIVATE;
|
|
|
|
$hcaptcha_public_key = HCAPTCHA_API_KEY_PUBLIC;
|
|
$hcaptcha_private_key = HCAPTCHA_API_KEY_PRIVATE;
|
|
|
|
// Parameter formats and other checks must much the formatting in /captcha
|
|
function is_twister_captcha_valid($memcached, $ip, $userpwd, $board = '!', $thread_id = 0, &$unsolved_count = null) {
|
|
if (!$memcached) {
|
|
return false;
|
|
}
|
|
|
|
if (defined('TEST_BOARD') && TEST_BOARD) {
|
|
require_once 'lib/twister_captcha-test.php';
|
|
}
|
|
else {
|
|
require_once 'lib/twister_captcha.php';
|
|
}
|
|
|
|
if (!isset($_POST['t-challenge']) || !$_POST['t-challenge']) {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($_POST['t-response']) || !$_POST['t-response']) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($_POST['t-response']) > 24) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($_POST['t-challenge']) > 255) {
|
|
return false;
|
|
}
|
|
|
|
// User agent
|
|
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
|
|
$user_agent = '!';
|
|
}
|
|
else {
|
|
$user_agent = md5($_SERVER['HTTP_USER_AGENT']);
|
|
}
|
|
|
|
// Password
|
|
$password = '!';
|
|
|
|
if ($userpwd && !$userpwd->isNew()) {
|
|
$password = $userpwd->getPwd();
|
|
}
|
|
|
|
// ---
|
|
|
|
list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']);
|
|
|
|
$long_ip = ip2long($ip);
|
|
|
|
if (!$uniq_id || !$challenge_hash || !$long_ip) {
|
|
return false;
|
|
}
|
|
|
|
$challenge_key = "ch$long_ip";
|
|
|
|
$params = [$ip, $password, $user_agent, $board, $thread_id];
|
|
|
|
$response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']);
|
|
|
|
$is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params);
|
|
|
|
if ($is_valid) {
|
|
$active_uniq_id = $memcached->get($challenge_key);
|
|
|
|
// Delete challenge
|
|
$memcached->delete($challenge_key);
|
|
|
|
if (!$active_uniq_id || $uniq_id !== $active_uniq_id) {
|
|
return false;
|
|
}
|
|
|
|
// Return and decrement the unsolved session count
|
|
$us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null);
|
|
|
|
if ($unsolved_count !== null) {
|
|
$unsolved_count = $us;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Delete challenge
|
|
$memcached->delete($challenge_key);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Decrements the unsolved count by 2 and returns the old count
|
|
function decrement_twister_captcha_session($memcached, $ip, $return_old = true) {
|
|
if (!$memcached) {
|
|
return false;
|
|
}
|
|
|
|
$long_ip = ip2long($ip);
|
|
|
|
if (!$long_ip) {
|
|
return false;
|
|
}
|
|
|
|
$key = "us$long_ip";
|
|
|
|
if ($return_old) {
|
|
$val = $memcached->get($key);
|
|
|
|
if ($val === false) {
|
|
$val = 0;
|
|
}
|
|
}
|
|
else {
|
|
$val = 0;
|
|
}
|
|
|
|
$memcached->decrement($key, 2);
|
|
|
|
return $val;
|
|
}
|
|
|
|
// FIXME: The IP arg isn't used for now
|
|
function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) {
|
|
if (!$memcached || !$userpwd) {
|
|
return false;
|
|
}
|
|
|
|
$current_time = (int)$current_time;
|
|
|
|
if ($current_time <= 0) {
|
|
return false;
|
|
}
|
|
|
|
//$long_ip = ip2long($ip);
|
|
|
|
//if (!$long_ip) {
|
|
//return false;
|
|
//}
|
|
|
|
$credits = 0;
|
|
|
|
// Config
|
|
// Stage 1 should match the check in use_twister_captcha_credit()
|
|
// and captcha.php for optimisation purposes
|
|
|
|
// Stage 1
|
|
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
|
|
$noop_post_count_1 = 5; // required post count
|
|
$noop_credits_1 = 1; // credits given
|
|
$noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds)
|
|
|
|
// Stage 2
|
|
$noop_known_ttl_2 = 21600; // 15 days
|
|
$noop_post_count_2 = 20;
|
|
$noop_credits_2 = 2;
|
|
$noop_duration_2 = 7200; // 2 hours
|
|
|
|
// Stage 3
|
|
$noop_known_ttl_3 = 129600; // 90 days
|
|
$noop_post_count_3 = 100;
|
|
$noop_credits_3 = 3;
|
|
$noop_duration_3 = 10800; // 3 hours
|
|
|
|
// ---
|
|
|
|
// The IP changed too recently
|
|
if ($userpwd->ipLifetime() < 60) {
|
|
return false;
|
|
}
|
|
|
|
// Stage 3
|
|
if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) {
|
|
$credits = $noop_credits_3;
|
|
$duration = $noop_duration_3;
|
|
}
|
|
// Stage 2
|
|
else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) {
|
|
$credits = $noop_credits_2;
|
|
$duration = $noop_duration_2;
|
|
}
|
|
// Stage 1
|
|
else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) {
|
|
$credits = $noop_credits_1;
|
|
$duration = $noop_duration_1;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
if (!$credits || $credits > 3) {
|
|
return false;
|
|
}
|
|
|
|
$expiration_ts = $current_time + $duration;
|
|
|
|
// Require no more than 5 actions in the past 8 minutes
|
|
/*
|
|
$query = <<<SQL
|
|
SELECT SQL_NO_CACHE COUNT(*) FROM user_actions
|
|
WHERE ip = $long_ip
|
|
AND time >= DATE_SUB(NOW(), INTERVAL 8 MINUTE)
|
|
SQL;
|
|
|
|
$res = mysql_global_call($query);
|
|
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
|
|
$count = (int)mysql_fetch_row($res)[0];
|
|
|
|
if ($count > 5) {
|
|
return false;
|
|
}
|
|
*/
|
|
// Set credits
|
|
|
|
$pwd = $userpwd->getPwd();
|
|
|
|
if (!$pwd) {
|
|
return false;
|
|
}
|
|
|
|
$key = "cr-$pwd";
|
|
$val = "$credits.$expiration_ts";
|
|
|
|
$res = $memcached->replace($key, $val, $expiration_ts);
|
|
|
|
if ($res === false) {
|
|
if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
|
|
return $memcached->set($key, $val, $expiration_ts);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// FIXME: The IP arg isn't used for now
|
|
function use_twister_captcha_credit($memcached, $ip, $userpwd) {
|
|
if (!$memcached || !$userpwd) {
|
|
return false;
|
|
}
|
|
|
|
//$long_ip = ip2long($ip);
|
|
|
|
//if (!$long_ip) {
|
|
//return false;
|
|
//}
|
|
|
|
// Must match the check in set_twister_captcha_credits()
|
|
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
|
|
$noop_post_count_1 = 5; // required post count
|
|
|
|
if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) {
|
|
return false;
|
|
}
|
|
|
|
$pwd = $userpwd->getPwd();
|
|
|
|
if (!$pwd) {
|
|
return false;
|
|
}
|
|
|
|
$key = "cr-$pwd";
|
|
$credits = $memcached->get($key);
|
|
|
|
if ($credits === false) {
|
|
return false;
|
|
}
|
|
|
|
list($count, $ts) = explode('.', $credits);
|
|
|
|
$count = (int)$count;
|
|
$ts = (int)$ts;
|
|
|
|
// No credits left
|
|
if ($count <= 0 || $ts <= 0) {
|
|
$memcached->delete($key);
|
|
return false;
|
|
}
|
|
|
|
$count -= 1;
|
|
|
|
$res = $memcached->replace($key, "$count.$ts", $ts);
|
|
|
|
if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function twister_captcha_form() {
|
|
return '<div id="t-root"></div>';
|
|
}
|
|
|
|
function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) {
|
|
$data = [
|
|
'board' => $board,
|
|
'thread_id' => $thread_id,
|
|
];
|
|
|
|
if ($userpwd) {
|
|
$data['arg_num'] = $userpwd->pwdLifetime();
|
|
$data['pwd'] = $userpwd->getPwd();
|
|
}
|
|
|
|
if ($meta) {
|
|
$data['meta'] = $meta;
|
|
}
|
|
|
|
if ($is_quiet) {
|
|
$type = 'failed_captcha_quiet';
|
|
}
|
|
else {
|
|
$type = 'failed_captcha';
|
|
}
|
|
|
|
write_to_event_log($type, $ip, $data);
|
|
}
|
|
|
|
function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
|
|
global $hcaptcha_public_key;
|
|
|
|
$js_tag = '<script src="https://hcaptcha.com/1/api.js'
|
|
. (!$autoload ? "?onload=$cb&render=explicit" : '') . '" async defer></script>';
|
|
|
|
if ($autoload) {
|
|
$attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"';
|
|
|
|
if ($dark) {
|
|
$attrs .= ' data-theme="dark"';
|
|
}
|
|
}
|
|
else {
|
|
$attrs = '';
|
|
}
|
|
|
|
$container_tag = '<script>window.hcaptchaKey = "' . $hcaptcha_public_key
|
|
. '";</script><div id="g-recaptcha"'
|
|
. $attrs . '></div>';
|
|
|
|
return $js_tag.$container_tag;
|
|
}
|
|
|
|
// Moves css out of the form for html validation
|
|
function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
|
|
global $recaptcha_public_key;
|
|
|
|
$js_tag = '<script src="https://www.google.com/recaptcha/api.js'
|
|
. (!$autoload ? "?onload=$cb&render=explicit" : '') . '" async defer></script>';
|
|
|
|
if ($autoload) {
|
|
$attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"';
|
|
|
|
if ($dark) {
|
|
$attrs .= ' data-theme="dark"';
|
|
}
|
|
}
|
|
else {
|
|
$attrs = '';
|
|
}
|
|
|
|
$container_tag = '<script>window.recaptchaKey = "' . $recaptcha_public_key
|
|
. '";</script><div id="g-recaptcha"'
|
|
. $attrs . '></div>';
|
|
|
|
$noscript_tag =<<<HTML
|
|
<noscript>
|
|
<div style="width: 302px;">
|
|
<div style="width: 302px; position: relative;">
|
|
<div style="width: 302px; height: 422px;">
|
|
<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>
|
|
</div>
|
|
<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;">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</noscript>
|
|
HTML;
|
|
|
|
if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) {
|
|
return $container_tag.$noscript_tag;
|
|
}
|
|
|
|
return $js_tag.$container_tag.$noscript_tag;
|
|
}
|
|
|
|
// Legacy captcha
|
|
// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently.
|
|
function captcha_form_alt() {
|
|
global $recaptcha_public_key;
|
|
|
|
$html = <<<HTML
|
|
<div id="captchaContainerAlt"></div>
|
|
<script>
|
|
function onAltCaptchaClick() {
|
|
Recaptcha.reload('t');
|
|
}
|
|
function onAltCaptchaReady() {
|
|
var el;
|
|
|
|
if (el = document.getElementById('recaptcha_image')) {
|
|
el.title = 'Reload';
|
|
el.addEventListener('click', onAltCaptchaClick, false);
|
|
}
|
|
}
|
|
if (!window.passEnabled) {
|
|
var el = document.createElement('script');
|
|
el.type = 'text/javascript';
|
|
el.src = '//www.google.com/recaptcha/api/js/recaptcha_ajax.js';
|
|
el.onload = function() {
|
|
Recaptcha.create('$recaptcha_public_key',
|
|
'captchaContainerAlt',
|
|
{
|
|
theme: 'clean',
|
|
tabindex: 3,
|
|
callback: onAltCaptchaReady
|
|
}
|
|
);
|
|
}
|
|
document.head.appendChild(el);
|
|
}</script>
|
|
<noscript>
|
|
<div style="width: 302px;">
|
|
<div style="width: 302px; position: relative;">
|
|
<div style="width: 302px; height: 422px;">
|
|
<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>
|
|
</div>
|
|
<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;">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</noscript>
|
|
HTML;
|
|
|
|
return $html;
|
|
}
|
|
|
|
function recaptcha_ban($n, $time, $return_error = 0, $length = 1)
|
|
{
|
|
auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification.");
|
|
if( $return_error == 1 ) {
|
|
return S_GENERICERROR;
|
|
}
|
|
error(S_GENERICERROR);
|
|
}
|
|
|
|
/**
|
|
* Works for both recaptcha and hcaptcha
|
|
*/
|
|
function recaptcha_bad_captcha($return_error = false, $codes = null) {
|
|
$error = S_BADCAPTCHA;
|
|
|
|
if (is_array($codes)) {
|
|
if (in_array('missing-input-response', $codes)) {
|
|
$error = S_NOCAPTCHA;
|
|
}
|
|
|
|
if ($return_error) {
|
|
return $error;
|
|
}
|
|
else {
|
|
error($error);
|
|
}
|
|
}
|
|
else {
|
|
if ($return_error) {
|
|
return $error;
|
|
}
|
|
else {
|
|
error($error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------
|
|
// hCaptcha
|
|
// -----------
|
|
function start_hcaptcha_verify($return_error = false) {
|
|
global $hcaptcha_private_key, $hcaptcha_ch;
|
|
|
|
$response = $_POST["h-captcha-response"];
|
|
|
|
if (!$response) {
|
|
if ($return_error == false) {
|
|
error(S_NOCAPTCHA);
|
|
}
|
|
return S_NOCAPTCHA;
|
|
}
|
|
|
|
$response = urlencode($response);
|
|
|
|
$rlen = strlen($response);
|
|
|
|
if ($rlen > 32768) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
$api_url = 'https://hcaptcha.com/siteverify';
|
|
|
|
$post = array(
|
|
'secret' => $hcaptcha_private_key,
|
|
'response' => $response
|
|
);
|
|
|
|
$hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
|
|
}
|
|
|
|
function end_hcaptcha_verify($return_error = false) {
|
|
global $hcaptcha_ch;
|
|
|
|
if (!$hcaptcha_ch) {
|
|
return;
|
|
}
|
|
|
|
$ret = rpc_finish_request($hcaptcha_ch, $error, $httperror);
|
|
|
|
// BAD
|
|
// 413 Request Too Large is bad; it was caused intentionally by the user.
|
|
if ($httperror == 413) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
// BAD
|
|
if ($ret == null) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
$resp = json_decode($ret, true);
|
|
|
|
// BAD
|
|
// Malformed JSON response from Google
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
// GOOD
|
|
if ($resp['success']) {
|
|
return $resp;
|
|
}
|
|
|
|
// BAD
|
|
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
|
|
}
|
|
|
|
// -----------
|
|
// reCaptcha V2
|
|
// -----------
|
|
// FIXME $challenge_field is no longer used
|
|
function start_recaptcha_verify($return_error = false, $challenge_field = '') {
|
|
global $recaptcha_private_key, $recaptcha_ch;
|
|
|
|
$response = $_POST["g-recaptcha-response"];
|
|
|
|
if (!$response) {
|
|
if ($return_error == false) {
|
|
error(S_NOCAPTCHA);
|
|
}
|
|
return S_NOCAPTCHA;
|
|
}
|
|
|
|
$response = urlencode($response);
|
|
|
|
$rlen = strlen($response);
|
|
|
|
if ($rlen > 4096) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
$api_url = 'https://www.google.com/recaptcha/api/siteverify';
|
|
|
|
$post = array(
|
|
'secret' => $recaptcha_private_key,
|
|
'response' => $response
|
|
);
|
|
|
|
$recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
|
|
}
|
|
|
|
function end_recaptcha_verify($return_error = false) {
|
|
global $recaptcha_ch;
|
|
|
|
if (!$recaptcha_ch) {
|
|
return;
|
|
}
|
|
|
|
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
|
|
|
|
// BAD
|
|
// 413 Request Too Large is bad; it was caused intentionally by the user.
|
|
if ($httperror == 413) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
// BAD
|
|
if ($ret == null) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
$resp = json_decode($ret, true);
|
|
|
|
// BAD
|
|
// Malformed JSON response from Google
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
// GOOD
|
|
if ($resp['success']) {
|
|
return $resp;
|
|
}
|
|
|
|
// BAD
|
|
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
|
|
}
|
|
|
|
// -----------
|
|
// reCaptcha V1
|
|
// -----------
|
|
function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') {
|
|
global $recaptcha_private_key, $recaptcha_ch;
|
|
|
|
$challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field;
|
|
$response = $_POST["recaptcha_response_field"];
|
|
if (!$challenge || !$response) {
|
|
if( $return_error == false ) {
|
|
error(S_NOCAPTCHA);
|
|
}
|
|
return S_NOCAPTCHA;
|
|
}
|
|
|
|
$num_words = 1 + preg_match_all('/\\s/', $response);
|
|
$rlen = strlen($response);
|
|
if ($num_words > 3 || $rlen > 128) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
$post = array(
|
|
"privatekey" => $recaptcha_private_key,
|
|
"challenge" => $challenge,
|
|
"remoteip" => $_SERVER["REMOTE_ADDR"],
|
|
"response" => $response
|
|
);
|
|
|
|
$recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false);
|
|
}
|
|
|
|
function end_recaptcha_verify_alt($return_error = false) {
|
|
global $recaptcha_ch;
|
|
|
|
if (!$recaptcha_ch) return;
|
|
|
|
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
|
|
|
|
if ($httperror == 413) {
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
if ($ret) {
|
|
$lines = explode("\n", $ret);
|
|
if ($lines[0] === "true") {
|
|
// GOOD
|
|
return;
|
|
}
|
|
}
|
|
|
|
// BAD
|
|
return recaptcha_bad_captcha($return_error);
|
|
}
|
|
|
|
?>
|