diff --git a/inc/config.php b/inc/config.php index eff1c596..87959d62 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1210,6 +1210,8 @@ $config['error']['captcha'] = _('You seem to have mistyped the verification.'); $config['error']['flag_undefined'] = _('The flag %s is undefined, your PHP version is too old!'); $config['error']['flag_wrongtype'] = _('defined_flags_accumulate(): The flag %s is of the wrong type!'); + $config['error']['remote_io_error'] = _('IO error while interacting with a remote service.'); + $config['error']['local_io_error'] = _('IO error while interacting with a local resource or service.'); // Moderator errors diff --git a/inc/service/captcha-queries.php b/inc/service/captcha-queries.php new file mode 100644 index 00000000..8f9342dc --- /dev/null +++ b/inc/service/captcha-queries.php @@ -0,0 +1,102 @@ +http = $http; + $this->secret = $secret; + $this->endpoint = $endpoint; + } + + /** + * Checks if the user at the remote ip passed the captcha. + * + * @param string $response User provided response. + * @param string $remote_ip User ip. + * @return bool Returns true if the user passed the captcha. + * @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer. + */ + public function verify(string $response, string $remote_ip): bool { + $data = array( + 'secret' => $this->secret, + 'response' => $response, + 'remoteip' => $remote_ip + ); + + $ret = $this->http->requestGet($this->endpoint, $data); + $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); + + return isset($resp['success']) && $resp['success']; + } +} + +class NativeCaptchaQuery { + private HttpDriver $http; + private string $domain; + private string $provider_check; + + + /** + * @param HttpDriver $http The http client. + * @param string $domain The server's domain. + * @param string $provider_check Path to the endpoint. + */ + function __construct(HttpDriver $http, string $domain, string $provider_check) { + $this->http = $http; + $this->domain = $domain; + $this->provider_check = $provider_check; + } + + /** + * Checks if the user at the remote ip passed the native vichan captcha. + * + * @param string $extra Extra http parameters. + * @param string $user_text Remote user's text input. + * @param string $user_cookie Remote user cookie. + * @return bool Returns true if the user passed the check. + * @throws RuntimeException Throws on IO errors. + */ + public function verify(string $extra, string $user_text, string $user_cookie): bool { + $data = array( + 'mode' => 'check', + 'text' => $user_text, + 'extra' => $extra, + 'cookie' => $user_cookie + ); + + $ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data); + return $ret === '1'; + } +} diff --git a/post.php b/post.php index dc6e444f..ae1e9001 100644 --- a/post.php +++ b/post.php @@ -7,6 +7,7 @@ require_once 'inc/bootstrap.php'; use Vichan\AppContext; use Vichan\Driver\HttpDriver; +use Vichan\Service\{RemoteCaptchaQuery, NativeCaptchaQuery}; /** * Utility functions @@ -144,6 +145,7 @@ function ocr_image(array $config, string $img_path): string { return trim($ret); } + /** * Method handling functions */ @@ -447,22 +449,32 @@ if (isset($_POST['delete'])) { if (count($report) > $config['report_limit']) error($config['error']['toomanyreports']); - if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) { - error($config['error']['bot']); - } if ($config['report_captcha']) { - $ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([ - 'mode' => 'check', - 'text' => $_POST['captcha_text'], - 'extra' => $config['captcha']['extra'], - 'cookie' => $_POST['captcha_cookie'] - ])); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $resp = curl_exec($ch); + if (!isset($_POST['captcha_text'], $_POST['captcha_cookie'])) { + error($config['error']['bot']); + } - if ($resp !== '1') { - error($config['error']['captcha']); + try { + $query = new NativeCaptchaQuery( + $context->getHttpDriver(), + $config['domain'], + $config['captcha']['provider_check'] + ); + $success = $query->verify( + $config['captcha']['extra'], + $_POST['captcha_text'], + $_POST['captcha_cookie'] + ); + + if (!$success) { + error($config['error']['captcha']); + } + } catch (RuntimeException $e) { + if ($config['syslog']) { + _syslog(LOG_ERR, "Native captcha IO exception: {$e->getMessage()}"); + } + error($config['error']['local_io_error']); } } @@ -552,62 +564,60 @@ if (isset($_POST['delete'])) { // Check if banned checkBan($board['uri']); - // Check for CAPTCHA right after opening the board so the "return" link is in there - if ($config['recaptcha']) { - if (!isset($_POST['g-recaptcha-response'])) - error($config['error']['bot']); + // Check for CAPTCHA right after opening the board so the "return" link is in there. + try { + // With our custom captcha provider + if ($config['captcha']['enabled'] || ($post['op'] && $config['new_thread_capt'])) { + $query = new NativeCaptchaQuery($context->getHttpDriver(), $config['domain'], $config['captcha']['provider_check']); + $success = $query->verify($config['captcha']['extra'], $_POST['captcha_text'], $_POST['captcha_cookie']); - // Check what reCAPTCHA has to say... - $resp = json_decode(file_get_contents(sprintf('https://www.recaptcha.net/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s', - $config['recaptcha_private'], - urlencode($_POST['g-recaptcha-response']), - $_SERVER['REMOTE_ADDR'])), true); - - if (!$resp['success']) { - error($config['error']['captcha']); + if (!$success) { + error( + $config['error']['captcha'] + . '' + ); + } } - } - // hCaptcha - if ($config['hcaptcha']) { - if (!isset($_POST['h-captcha-response'])) { - error($config['error']['bot']); + // Remote 3rd party captchas. + else { + // recaptcha + if ($config['recaptcha']) { + if (!isset($_POST['g-recaptcha-response'])) { + error($config['error']['bot']); + } + $response = $_POST['g-recaptcha-response']; + $query = RemoteCaptchaQuery::with_recaptcha($context->getHttpDriver(), $config['recaptcha_private']); + } + // hCaptcha + elseif ($config['hcaptcha']) { + if (!isset($_POST['h-captcha-response'])) { + error($config['error']['bot']); + } + $response = $_POST['g-recaptcha-response']; + $query = RemoteCaptchaQuery::with_hcaptcha($context->getHttpDriver(), $config['hcaptcha_private']); + } + + $success = $query->verify($response, $_SERVER['REMOTE_ADDR']); + if (!$success) { + error($config['error']['captcha']); + } } - - $data = array( - 'secret' => $config['hcaptcha_private'], - 'response' => $_POST['h-captcha-response'], - 'remoteip' => $_SERVER['REMOTE_ADDR'] - ); - - $hcaptchaverify = curl_init(); - curl_setopt($hcaptchaverify, CURLOPT_URL, "https://hcaptcha.com/siteverify"); - curl_setopt($hcaptchaverify, CURLOPT_POST, true); - curl_setopt($hcaptchaverify, CURLOPT_POSTFIELDS, http_build_query($data)); - curl_setopt($hcaptchaverify, CURLOPT_RETURNTRANSFER, true); - $hcaptcharesponse = curl_exec($hcaptchaverify); - - $resp = json_decode($hcaptcharesponse, true); // Decoding $hcaptcharesponse instead of $response - - if (!$resp['success']) { - error($config['error']['captcha']); + } catch (RuntimeException $e) { + if ($config['syslog']) { + _syslog(LOG_ERR, "Captcha IO exception: {$e->getMessage()}"); } + error($config['error']['remote_io_error']); + } catch (JsonException $e) { + if ($config['syslog']) { + _syslog(LOG_ERR, "Bad JSON reply to captcha: {$e->getMessage()}"); + } + error($config['error']['remote_io_error']); } - // Same, but now with our custom captcha provider - if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) { - $ch = curl_init($config['domain'].'/'.$config['captcha']['provider_check'] . "?" . http_build_query([ - 'mode' => 'check', - 'text' => $_POST['captcha_text'], - 'extra' => $config['captcha']['extra'], - 'cookie' => $_POST['captcha_cookie'] - ])); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $resp = curl_exec($ch); - if ($resp !== '1') { - error($config['error']['captcha'] . - ''); - } - } if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || (!$post['op'] && $_POST['post'] == $config['button_reply'])))