Merge branch 'captcha-workaround' into 'config'

Captcha workaround

See merge request leftypol/leftypol!9
This commit is contained in:
Zankaria Auxa 2024-10-28 12:36:37 +00:00
commit aa8525aa86
4 changed files with 121 additions and 82 deletions

View file

@ -301,9 +301,8 @@
'lock', 'lock',
'raw', 'raw',
'embed', 'embed',
'g-recaptcha-response', 'captcha-response',
'h-captcha-response', 'captcha-form-id',
'cf-turnstile-response',
'spoiler', 'spoiler',
'page', 'page',
'file_url', 'file_url',
@ -330,33 +329,40 @@
'answer' => '4' 'answer' => '4'
); );
*/ */
/** // Enable a captcha system to make spam even harder. Rarely necessary.
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set $config['captcha'] = [
* to 1. /**
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses. * Select the captcha backend, false to disable.
*/ * Can be false, "recaptcha", "hcaptcha" or "turnstile".
$config['dynamic_captcha'] = false; */
'mode' => false,
// Enable reCaptcha to make spam even harder. Rarely necessary. /**
$config['recaptcha'] = false; * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
* to 1.
// Public and private key pair from https://www.google.com/recaptcha/admin/create * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; */
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; 'dynamic' => false,
// Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur.
// Enable hCaptcha. 'passthrough_timeout' => 0,
$config['hcaptcha'] = false; // Configure Google reCAPTCHA.
'recaptcha' => [
// Public and private key pair for using hCaptcha. // Public and private key pair from https://www.google.com/recaptcha/admin/create
$config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0'; 'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f',
$config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17'; 'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_',
],
// Enable Cloudflare's Turnstile captcha. // Configure hCaptcha.
$config['turnstile'] = false; 'hcaptcha' => [
// Public and private key pair for using hCaptcha.
// Public and private key pair for turnstile. 'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0',
$config['turnstile_public'] = ''; 'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17',
$config['turnstile_private'] = ''; ],
// Configure Cloudflare Turnstile.
'turnstile' => [
// Public and private key pair for turnstile.
'public' => '',
'private' => '',
]
];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board // Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false; $config['board_locked'] = false;

120
post.php
View file

@ -167,6 +167,68 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action)
return $json_ret['success'] === true && $json_ret['action'] === $expected_action; return $json_ret['success'] === true && $json_ret['action'] === $expected_action;
} }
/**
* A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban.
*/
function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) {
$dynamic = $captcha_config['dynamic'];
if ($dynamic !== false && $remote_ip !== $dynamic) {
return true;
}
switch ($captcha_config['mode']) {
case 'recaptcha':
case 'hcaptcha':
case 'turnstile':
$mode = $captcha_config['mode'];
break;
case false:
return true;
default:
\error_log("Unknown captcha mode '{$captcha_config['mode']}'");
throw new \RuntimeException('Captcha configuration error');
}
$passthrough_timeout = $captcha_config['passthrough_timeout'];
if ($passthrough_timeout != 0) {
$pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}");
if ($pass !== false) {
$let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response;
if ($let_through) {
return true;
}
}
}
$remote_ip_send = $dynamic !== false ? null : $remote_ip;
$private_key = $captcha_config[$mode]['private'];
$public_key = $captcha_config[$mode]['public'];
switch ($mode) {
case 'recaptcha':
$ret = check_recaptcha($private_key, $response, $remote_ip_send);
break;
case 'hcaptcha':
$ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key);
break;
case 'turnstile':
$ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action);
break;
}
if ($ret && $passthrough_timeout != 0) {
$pass = [
'expires' => time() + $passthrough_timeout,
'board_uri' => $board_uri,
'captcha_response' => $response
];
Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2);
}
return $ret;
}
/** /**
* Deletes the (single) captcha associated with the ip and code. * Deletes the (single) captcha associated with the ip and code.
* *
@ -765,56 +827,16 @@ function handle_post()
if (!$dropped_post) { if (!$dropped_post) {
if ($config['dynamic_captcha'] !== false) { // Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) { if ($config['captcha']['mode'] !== false) {
if ($config['recaptcha']) { if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
if (!isset($_POST['g-recaptcha-response'])) { error($config['error']['bot']);
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
}
} }
} else {
// Check for CAPTCHA right after opening the board so the "return" link is in there. $expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if ($config['recaptcha']) { $ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!isset($_POST['g-recaptcha-response'])) { if (!$ret) {
error($config['error']['bot']); error($config['error']['captcha']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
} }
} }

View file

@ -273,8 +273,9 @@ var captcha_renderer = null;
function onCaptchaLoadHcaptcha() { function onCaptchaLoadHcaptcha() {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = { let renderer = {
renderOn: (container) => hcaptcha.render(container, { applyOn: (container, params) => hcaptcha.render(container, {
sitekey: "{{ config.hcaptcha_public }}", sitekey: "{{ config.hcaptcha_public }}",
callback: params['on-success'],
}), }),
remove: (widgetId) => { /* Not supported */ }, remove: (widgetId) => { /* Not supported */ },
reset: (widgetId) => hcaptcha.reset(widgetId) reset: (widgetId) => hcaptcha.reset(widgetId)
@ -300,10 +301,11 @@ window.onCaptchaLoadTurnstile_post_thread = function() {
function onCaptchaLoadTurnstile(action) { function onCaptchaLoadTurnstile(action) {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = { let renderer = {
renderOn: function(container) { applyOn: function(container, params) {
let widgetId = turnstile.render('#' + container, { let widgetId = turnstile.render('#' + container, {
sitekey: "{{ config.turnstile_public }}", sitekey: "{{ config.turnstile_public }}",
action: action, action: action,
callback: params['on-success'],
}); });
if (widgetId === undefined) { if (widgetId === undefined) {
return null; return null;
@ -320,9 +322,16 @@ function onCaptchaLoadTurnstile(action) {
{% endif %} // End if turnstile {% endif %} // End if turnstile
function onCaptchaLoad(renderer) { function onCaptchaLoad(renderer) {
// Initialize the form identifier with a random password.
document.getElementById('captcha-form-id').value = generatePassword();
captcha_renderer = renderer; captcha_renderer = renderer;
let widgetId = renderer.renderOn('captcha-container'); let widgetId = renderer.applyOn('captcha-container', {
'on-success': function(token) {
document.getElementById('captcha-response').value = token;
}
});
if (widgetId === null) { if (widgetId === null) {
console.error('Could not render captcha!'); console.error('Could not render captcha!');
} }

View file

@ -118,6 +118,8 @@
</th> </th>
<td> <td>
<div id="captcha-container"></div> <div id="captcha-container"></div>
<textarea id="captcha-response" name="captcha-response" style="display:none;"></textarea>
<textarea id="captcha-form-id" name="captcha-form-id" style="display:none;"></textarea>
{{ antibot.html() }} {{ antibot.html() }}
</td> </td>
</tr> </tr>