mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-19 00:01:00 +08:00
Merge pull request #384 from specialpointcentral/php8
[RFC] Refine captcha configuration and drivers
This commit is contained in:
16
.env.example
16
.env.example
@@ -96,6 +96,22 @@ CHANNEL_NAME_SETTING=channel_setting
|
||||
CHANNEL_NAME_MODEL_EVENT=channel_model_event
|
||||
FORCE_SCHEME=
|
||||
|
||||
# Captcha settings
|
||||
# Available drivers: image, cloudflare_turnstile, google_recaptcha_v2
|
||||
CAPTCHA_DRIVER=image
|
||||
|
||||
# Cloudflare Turnstile keys (used when CAPTCHA_DRIVER=cloudflare_turnstile)
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
TURNSTILE_THEME=light
|
||||
TURNSTILE_SIZE=flexible
|
||||
|
||||
# Google reCAPTCHA v2 keys (used when CAPTCHA_DRIVER=google_recaptcha_v2)
|
||||
RECAPTCHA_SITE_KEY=
|
||||
RECAPTCHA_SECRET_KEY=
|
||||
RECAPTCHA_THEME=light
|
||||
RECAPTCHA_SIZE=normal
|
||||
|
||||
CROWDIN_ACCESS_TOKEN=
|
||||
CROWDIN_PROJECT_ID=
|
||||
|
||||
|
||||
19
app/Services/Captcha/CaptchaDriverInterface.php
Normal file
19
app/Services/Captcha/CaptchaDriverInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha;
|
||||
|
||||
interface CaptchaDriverInterface
|
||||
{
|
||||
public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Render the captcha markup for HTML forms.
|
||||
*/
|
||||
public function render(array $context = []): string;
|
||||
|
||||
/**
|
||||
* Verify the captcha response.
|
||||
*/
|
||||
public function verify(array $payload, array $context = []): bool;
|
||||
}
|
||||
|
||||
120
app/Services/Captcha/CaptchaManager.php
Normal file
120
app/Services/Captcha/CaptchaManager.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha;
|
||||
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CaptchaManager
|
||||
{
|
||||
/** @var array<string, CaptchaDriverInterface> */
|
||||
protected array $drivers = [];
|
||||
|
||||
protected ?array $config = null;
|
||||
|
||||
public function driver(?string $name = null): CaptchaDriverInterface
|
||||
{
|
||||
$name = $name ?? $this->getDefaultDriver();
|
||||
|
||||
$driver = $this->getDriverInstance($name);
|
||||
|
||||
if ($name !== 'image' && !$driver->isEnabled()) {
|
||||
return $this->driver('image');
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
return $this->driver()->render($context);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
try {
|
||||
return $this->driver()->verify($payload, $context);
|
||||
} catch (CaptchaValidationException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->driver()->isEnabled();
|
||||
}
|
||||
|
||||
protected function getDriverInstance(string $name): CaptchaDriverInterface
|
||||
{
|
||||
if (!isset($this->drivers[$name])) {
|
||||
try {
|
||||
$this->drivers[$name] = $this->resolveDriver($name);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
if ($name !== 'image') {
|
||||
return $this->getDriverInstance('image');
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->drivers[$name];
|
||||
}
|
||||
|
||||
protected function resolveDriver(string $name): CaptchaDriverInterface
|
||||
{
|
||||
$config = $this->getConfigValue("drivers.$name", []);
|
||||
|
||||
if (!is_array($config) || empty($config)) {
|
||||
throw new \InvalidArgumentException("Captcha driver [$name] is not defined.");
|
||||
}
|
||||
|
||||
$driverClass = Arr::get($config, 'class');
|
||||
|
||||
if (!$driverClass || !class_exists($driverClass)) {
|
||||
throw new \InvalidArgumentException("Captcha driver class for [$name] is invalid.");
|
||||
}
|
||||
|
||||
$driver = new $driverClass($config);
|
||||
|
||||
if (!$driver instanceof CaptchaDriverInterface) {
|
||||
throw new \InvalidArgumentException("Captcha driver [$name] must implement " . CaptchaDriverInterface::class);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
protected function getDefaultDriver(): string
|
||||
{
|
||||
return (string) $this->getConfigValue('default', 'image');
|
||||
}
|
||||
|
||||
protected function getConfigValue(string $key, $default = null)
|
||||
{
|
||||
if ($this->config === null) {
|
||||
$config = null;
|
||||
if (function_exists('app')) {
|
||||
try {
|
||||
$repository = app('config');
|
||||
if ($repository) {
|
||||
$config = $repository->get('captcha');
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$config = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($config) && function_exists('nexus_config')) {
|
||||
$config = nexus_config('captcha', []);
|
||||
}
|
||||
|
||||
if (!is_array($config)) {
|
||||
$path = (defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 3) . DIRECTORY_SEPARATOR) . 'config/captcha.php';
|
||||
$config = is_file($path) ? require $path : [];
|
||||
}
|
||||
|
||||
$this->config = is_array($config) ? $config : [];
|
||||
}
|
||||
|
||||
return Arr::get($this->config, $key, $default);
|
||||
}
|
||||
}
|
||||
156
app/Services/Captcha/Drivers/ImageCaptchaDriver.php
Normal file
156
app/Services/Captcha/Drivers/ImageCaptchaDriver.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class ImageCaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
$labels = $context['labels'] ?? [];
|
||||
$imageLabel = $labels['image'] ?? 'Security Image';
|
||||
$codeLabel = $labels['code'] ?? 'Security Code';
|
||||
$secret = $context['secret'] ?? '';
|
||||
|
||||
$imagehash = $this->issue();
|
||||
$imageUrl = htmlspecialchars(sprintf('image.php?action=regimage&imagehash=%s&secret=%s', $imagehash, $secret ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return implode("\n", [
|
||||
sprintf('<tr><td class="rowhead">%s</td><td align="left"><img src="%s" border="0" alt="CAPTCHA" /></td></tr>', htmlspecialchars($imageLabel, ENT_QUOTES, 'UTF-8'), $imageUrl),
|
||||
sprintf('<tr><td class="rowhead">%s</td><td align="left"><input type="text" autocomplete="off" style="width: 180px; border: 1px solid gray" name="imagestring" value="" /><input type="hidden" name="imagehash" value="%s" /></td></tr>', htmlspecialchars($codeLabel, ENT_QUOTES, 'UTF-8'), htmlspecialchars($imagehash, ENT_QUOTES, 'UTF-8')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$imagehash = trim((string) ($payload['imagehash'] ?? ''));
|
||||
$imagestring = trim((string) ($payload['imagestring'] ?? ''));
|
||||
|
||||
if ($imagehash === '' || $imagestring === '') {
|
||||
throw new CaptchaValidationException('Missing captcha parameters.');
|
||||
}
|
||||
|
||||
$query = sprintf(
|
||||
"SELECT dateline FROM regimages WHERE imagehash='%s' AND imagestring='%s'",
|
||||
mysql_real_escape_string($imagehash),
|
||||
mysql_real_escape_string($imagestring)
|
||||
);
|
||||
|
||||
$sql = sql_query($query);
|
||||
$imgcheck = mysql_fetch_array($sql);
|
||||
|
||||
$this->deleteByHash($imagehash);
|
||||
|
||||
if (empty($imgcheck['dateline'])) {
|
||||
throw new CaptchaValidationException('Invalid captcha response.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function issue(): string
|
||||
{
|
||||
$random = random_str();
|
||||
$imagehash = md5($random);
|
||||
$dateline = time();
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO `regimages` (`imagehash`, `imagestring`, `dateline`) VALUES ('%s', '%s', '%s')",
|
||||
mysql_real_escape_string($imagehash),
|
||||
mysql_real_escape_string($random),
|
||||
mysql_real_escape_string((string) $dateline)
|
||||
);
|
||||
|
||||
sql_query($sql);
|
||||
|
||||
return $imagehash;
|
||||
}
|
||||
|
||||
public function outputImage(string $imagehash): void
|
||||
{
|
||||
$query = sprintf(
|
||||
"SELECT imagestring FROM regimages WHERE imagehash=%s",
|
||||
sqlesc($imagehash)
|
||||
);
|
||||
|
||||
$sql = sql_query($query);
|
||||
$regimage = mysql_fetch_array($sql);
|
||||
$imagestring = $regimage['imagestring'] ?? '';
|
||||
|
||||
if ($imagestring === '') {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$characters = implode(' ', str_split($imagestring));
|
||||
|
||||
if (!function_exists('imagecreatefrompng')) {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$fontwidth = imageFontWidth(5);
|
||||
$fontheight = imageFontHeight(5);
|
||||
$textwidth = $fontwidth * strlen($characters);
|
||||
$textheight = $fontheight;
|
||||
|
||||
$randimg = rand(1, 5);
|
||||
$imagePath = ROOT_PATH . "public/pic/regimages/reg{$randimg}.png";
|
||||
|
||||
if (!is_file($imagePath)) {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$im = imagecreatefrompng($imagePath);
|
||||
$imgheight = imagesy($im);
|
||||
$imgwidth = imagesx($im);
|
||||
$textposh = (int) floor(($imgwidth - $textwidth) / 2);
|
||||
$textposv = (int) floor(($imgheight - $textheight) / 2);
|
||||
|
||||
$dots = (int) floor($imgheight * $imgwidth / 35);
|
||||
for ($i = 1; $i <= $dots; $i++) {
|
||||
imagesetpixel($im, rand(0, $imgwidth - 1), rand(0, $imgheight - 1), imagecolorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255)));
|
||||
}
|
||||
|
||||
$textcolor = imagecolorallocate($im, 0, 0, 0);
|
||||
imagestring($im, 5, $textposh, $textposv, $characters, $textcolor);
|
||||
|
||||
header('Content-type: image/png');
|
||||
imagepng($im);
|
||||
imagedestroy($im);
|
||||
}
|
||||
|
||||
protected function deleteByHash(string $imagehash): void
|
||||
{
|
||||
if ($imagehash === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$delete = sprintf(
|
||||
"DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string($imagehash)
|
||||
);
|
||||
|
||||
sql_query($delete);
|
||||
}
|
||||
|
||||
protected function renderFallback(): void
|
||||
{
|
||||
http_response_code(404);
|
||||
}
|
||||
}
|
||||
134
app/Services/Captcha/Drivers/RecaptchaV2CaptchaDriver.php
Normal file
134
app/Services/Captcha/Drivers/RecaptchaV2CaptchaDriver.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class RecaptchaV2CaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return !empty($this->config['site_key']) && !empty($this->config['secret_key']);
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$labels = $context['labels'] ?? [];
|
||||
$label = $labels['image'] ?? $labels['code'] ?? 'Security Check';
|
||||
$theme = $this->config['theme'] ?? 'light';
|
||||
$size = $this->config['size'] ?? 'normal';
|
||||
$validSizes = ['compact', 'normal'];
|
||||
if (!in_array($size, $validSizes, true)) {
|
||||
$size = 'normal';
|
||||
}
|
||||
|
||||
$attributes = sprintf(
|
||||
'class="g-recaptcha" data-sitekey="%s" data-theme="%s" data-size="%s"',
|
||||
htmlspecialchars($this->config['site_key'], ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($theme, ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($size, ENT_QUOTES, 'UTF-8')
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<tr><td class="rowhead">%s</td><td align="left"><div %s></div>%s</td></tr>',
|
||||
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
||||
$attributes,
|
||||
'<script src="https://www.google.com/recaptcha/api.js" async defer></script>'
|
||||
);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$token = trim((string) ($payload['request']['g-recaptcha-response'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
throw new CaptchaValidationException('Captcha verification token is missing.');
|
||||
}
|
||||
|
||||
$secret = $this->config['secret_key'] ?? '';
|
||||
|
||||
if ($secret === '') {
|
||||
throw new CaptchaValidationException('Captcha secret key is not configured.');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
|
||||
$remoteIp = $context['ip'] ?? null;
|
||||
|
||||
if (!empty($remoteIp)) {
|
||||
$data['remoteip'] = $remoteIp;
|
||||
}
|
||||
|
||||
$result = $this->sendVerificationRequest('https://www.google.com/recaptcha/api/siteverify', $data);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
throw new CaptchaValidationException('Captcha verification failed.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sendVerificationRequest(string $url, array $data): array
|
||||
{
|
||||
$payload = http_build_query($data);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new CaptchaValidationException('Captcha verification request failed: ' . $error);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
} else {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 10,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
throw new CaptchaValidationException('Captcha verification request failed.');
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new CaptchaValidationException('Unexpected captcha verification response.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
143
app/Services/Captcha/Drivers/TurnstileCaptchaDriver.php
Normal file
143
app/Services/Captcha/Drivers/TurnstileCaptchaDriver.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class TurnstileCaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected static bool $scriptInjected = false;
|
||||
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return !empty($this->config['site_key']) && !empty($this->config['secret_key']);
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$labels = $context['labels'] ?? [];
|
||||
$label = $labels['image'] ?? $labels['code'] ?? 'Security Check';
|
||||
$theme = $this->config['theme'] ?? 'light';
|
||||
$size = $this->config['size'] ?? 'auto';
|
||||
if (is_string($size)) {
|
||||
$size = strtolower($size);
|
||||
}
|
||||
$validSizes = ['compact', 'normal', 'flexible'];
|
||||
if (!in_array($size, $validSizes, true)) {
|
||||
$size = 'auto';
|
||||
}
|
||||
|
||||
$attributes = sprintf(
|
||||
'class="cf-turnstile" data-sitekey="%s" data-theme="%s"%s',
|
||||
htmlspecialchars($this->config['site_key'], ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($theme, ENT_QUOTES, 'UTF-8'),
|
||||
$size === 'auto' ? '' : sprintf(' data-size="%s"', htmlspecialchars($size, ENT_QUOTES, 'UTF-8'))
|
||||
);
|
||||
|
||||
$markup = sprintf(
|
||||
'<tr><td class="rowhead">%s</td><td align="left"><div %s></div>%s</td></tr>',
|
||||
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
||||
$attributes,
|
||||
self::$scriptInjected ? '' : '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'
|
||||
);
|
||||
|
||||
self::$scriptInjected = true;
|
||||
|
||||
return $markup;
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$token = trim((string) ($payload['request']['cf-turnstile-response'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
throw new CaptchaValidationException('Captcha verification token is missing.');
|
||||
}
|
||||
|
||||
$secret = $this->config['secret_key'] ?? '';
|
||||
|
||||
if ($secret === '') {
|
||||
throw new CaptchaValidationException('Captcha secret key is not configured.');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
|
||||
$remoteIp = $context['ip'] ?? null;
|
||||
|
||||
if (!empty($remoteIp)) {
|
||||
$data['remoteip'] = $remoteIp;
|
||||
}
|
||||
|
||||
$result = $this->sendVerificationRequest('https://challenges.cloudflare.com/turnstile/v0/siteverify', $data);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
throw new CaptchaValidationException('Captcha verification failed.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sendVerificationRequest(string $url, array $data): array
|
||||
{
|
||||
$payload = http_build_query($data);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new CaptchaValidationException('Captcha verification request failed: ' . $error);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
} else {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 10,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
throw new CaptchaValidationException('Captcha verification request failed.');
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new CaptchaValidationException('Unexpected captcha verification response.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CaptchaValidationException extends RuntimeException
|
||||
{
|
||||
}
|
||||
|
||||
27
config/captcha.php
Normal file
27
config/captcha.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => nexus_env('CAPTCHA_DRIVER', 'image'),
|
||||
|
||||
'drivers' => [
|
||||
'image' => [
|
||||
'class' => \App\Services\Captcha\Drivers\ImageCaptchaDriver::class,
|
||||
],
|
||||
|
||||
'cloudflare_turnstile' => [
|
||||
'class' => \App\Services\Captcha\Drivers\TurnstileCaptchaDriver::class,
|
||||
'site_key' => nexus_env('TURNSTILE_SITE_KEY'),
|
||||
'secret_key' => nexus_env('TURNSTILE_SECRET_KEY'),
|
||||
'theme' => nexus_env('TURNSTILE_THEME', 'auto'),
|
||||
'size' => nexus_env('TURNSTILE_SIZE', 'auto'),
|
||||
],
|
||||
|
||||
'google_recaptcha_v2' => [
|
||||
'class' => \App\Services\Captcha\Drivers\RecaptchaV2CaptchaDriver::class,
|
||||
'site_key' => nexus_env('RECAPTCHA_SITE_KEY'),
|
||||
'secret_key' => nexus_env('RECAPTCHA_SECRET_KEY'),
|
||||
'theme' => nexus_env('RECAPTCHA_THEME', 'light'),
|
||||
'size' => nexus_env('RECAPTCHA_SIZE', 'normal'),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -1767,56 +1767,102 @@ function random_str($length="6")
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
function image_code () {
|
||||
$randomstr = random_str();
|
||||
$imagehash = md5($randomstr);
|
||||
$dateline = time();
|
||||
$sql = 'INSERT INTO `regimages` (`imagehash`, `imagestring`, `dateline`) VALUES (\''.$imagehash.'\', \''.$randomstr.'\', \''.$dateline.'\');';
|
||||
sql_query($sql);
|
||||
return $imagehash;
|
||||
function captcha_manager(): \App\Services\Captcha\CaptchaManager
|
||||
{
|
||||
static $manager;
|
||||
|
||||
if (!$manager) {
|
||||
$manager = new \App\Services\Captcha\CaptchaManager();
|
||||
}
|
||||
|
||||
return $manager;
|
||||
}
|
||||
|
||||
function check_code ($imagehash, $imagestring, $where = 'signup.php',$maxattemptlog=false,$head=true) {
|
||||
global $lang_functions;
|
||||
function image_code () {
|
||||
$driver = captcha_manager()->driver('image');
|
||||
|
||||
if (!method_exists($driver, 'issue')) {
|
||||
throw new \RuntimeException('Image captcha driver is unavailable.');
|
||||
}
|
||||
|
||||
return $driver->issue();
|
||||
}
|
||||
|
||||
function check_code ($imagehash, $imagestring, $where = 'signup.php', $maxattemptlog = false, $head = true) {
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
|
||||
if ($iv !== 'yes') {
|
||||
return true;
|
||||
}
|
||||
$query = sprintf("SELECT * FROM regimages WHERE imagehash='%s' AND imagestring='%s'",
|
||||
mysql_real_escape_string((string)$imagehash),
|
||||
mysql_real_escape_string((string)$imagestring)
|
||||
);
|
||||
$sql = sql_query($query);
|
||||
$imgcheck = mysql_fetch_array($sql);
|
||||
if(!$imgcheck['dateline']) {
|
||||
$delete = sprintf("DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string((string)$imagehash)
|
||||
);
|
||||
sql_query($delete);
|
||||
if (!$maxattemptlog)
|
||||
stderr('Error',$lang_functions['std_invalid_image_code']."<a href=\"".htmlspecialchars($where)."\">".$lang_functions['std_here_to_request_new'], false);
|
||||
else
|
||||
failedlogins($lang_functions['std_invalid_image_code']."<a href=\"".htmlspecialchars($where)."\">".$lang_functions['std_here_to_request_new'],true,$head);
|
||||
}else{
|
||||
$delete = sprintf("DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string((string)$imagehash)
|
||||
);
|
||||
sql_query($delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
$manager = captcha_manager();
|
||||
|
||||
if (!$manager->isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'imagehash' => $imagehash,
|
||||
'imagestring' => $imagestring,
|
||||
'request' => array_merge($_POST ?? [], $_GET ?? []),
|
||||
];
|
||||
|
||||
$context = [
|
||||
'where' => $where,
|
||||
'maxattemptlog' => $maxattemptlog,
|
||||
'head' => $head,
|
||||
'ip' => getip(),
|
||||
];
|
||||
|
||||
try {
|
||||
if ($manager->verify($payload, $context)) {
|
||||
return true;
|
||||
}
|
||||
} catch (\App\Services\Captcha\Exceptions\CaptchaValidationException $exception) {
|
||||
$message = $exception->getMessage();
|
||||
|
||||
$defaultMessage = $lang_functions['std_invalid_image_code'] . "<a href=\"" . htmlspecialchars($where) . "\">" . $lang_functions['std_here_to_request_new'];
|
||||
|
||||
if ($message === '' || $message === 'Invalid captcha response.' || $message === 'Missing captcha parameters.') {
|
||||
$message = $defaultMessage;
|
||||
}
|
||||
|
||||
if (!$maxattemptlog) {
|
||||
stderr('Error', $message, false);
|
||||
} else {
|
||||
failedlogins($message, true, $head);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function show_image_code () {
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
if ($iv == "yes") {
|
||||
unset($imagehash);
|
||||
$imagehash = image_code () ;
|
||||
print ("<tr><td class=\"rowhead\">".$lang_functions['row_security_image']."</td>");
|
||||
print ("<td align=\"left\"><img src=\"".htmlspecialchars("image.php?action=regimage&imagehash=".$imagehash."&secret=".($_GET['secret'] ?? ''))."\" border=\"0\" alt=\"CAPTCHA\" /></td></tr>");
|
||||
print ("<tr><td class=\"rowhead\">".$lang_functions['row_security_code']."</td><td align=\"left\">");
|
||||
print("<input type=\"text\" autocomplete=\"off\" style=\"width: 180px; border: 1px solid gray\" name=\"imagestring\" value=\"\" />");
|
||||
print("<input type=\"hidden\" name=\"imagehash\" value=\"$imagehash\" /></td></tr>");
|
||||
}
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
|
||||
if ($iv !== 'yes') {
|
||||
return;
|
||||
}
|
||||
|
||||
$manager = captcha_manager();
|
||||
|
||||
if (!$manager->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$markup = $manager->render([
|
||||
'labels' => [
|
||||
'image' => $lang_functions['row_security_image'],
|
||||
'code' => $lang_functions['row_security_code'],
|
||||
],
|
||||
'secret' => $_GET['secret'] ?? '',
|
||||
]);
|
||||
|
||||
if ($markup !== '') {
|
||||
echo $markup;
|
||||
}
|
||||
}
|
||||
|
||||
function get_ip_location($ip)
|
||||
|
||||
@@ -307,6 +307,7 @@ function nexus_config($key, $default = null)
|
||||
$files = [
|
||||
ROOT_PATH . 'config/nexus.php',
|
||||
ROOT_PATH . 'config/emoji.php',
|
||||
ROOT_PATH . 'config/captcha.php',
|
||||
];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file);
|
||||
|
||||
@@ -18,7 +18,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST'){
|
||||
switch($action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS)){
|
||||
case 'new':
|
||||
cur_user_check();
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'complains.php');
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,'complains.php');
|
||||
\Nexus\Database\NexusLock::lockOrFail("complains:lock:" . getip(), 10);
|
||||
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||
\Nexus\Database\NexusLock::lockOrFail("complains:lock:" . $email, 600);
|
||||
|
||||
@@ -29,7 +29,7 @@ bark($lang_confirm_resend['std_need_admin_verification']);
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],"confirm_resend.php",true);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,"confirm_resend.php",true);
|
||||
$email = unesc(htmlspecialchars(trim($_POST["email"] ?? '')));
|
||||
$wantpassword = unesc(htmlspecialchars(trim($_POST["wantpassword"])));
|
||||
$passagain = unesc(htmlspecialchars(trim($_POST["passagain"])));
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
<?php
|
||||
require_once("../include/bittorrent.php");
|
||||
dbconn();
|
||||
$action = $_GET['action'];
|
||||
$imagehash = $_GET['imagehash'];
|
||||
if($action == "regimage")
|
||||
{
|
||||
$query = "SELECT * FROM regimages WHERE imagehash= ".sqlesc($imagehash);
|
||||
$sql = sql_query($query);
|
||||
$regimage = mysql_fetch_array($sql);
|
||||
$imagestring = $regimage['imagestring'];
|
||||
$space = $newstring = '';
|
||||
for($i=0;$i<strlen($imagestring);$i++)
|
||||
{
|
||||
$newstring .= $space.$imagestring[$i];
|
||||
$space = " ";
|
||||
}
|
||||
$imagestring = $newstring;
|
||||
|
||||
if(function_exists("imagecreatefrompng"))
|
||||
{
|
||||
$fontwidth = imageFontWidth(5);
|
||||
$fontheight = imageFontHeight(5);
|
||||
$textwidth = $fontwidth*strlen($imagestring);
|
||||
$textheight = $fontheight;
|
||||
$action = $_GET['action'] ?? '';
|
||||
$imagehash = $_GET['imagehash'] ?? '';
|
||||
|
||||
$randimg = rand(1, 5);
|
||||
$im = imagecreatefrompng("pic/regimages/reg".$randimg.".png");
|
||||
|
||||
$imgheight = 40;
|
||||
$imgwidth = 150;
|
||||
$textposh = floor(($imgwidth-$textwidth)/2);
|
||||
$textposv = floor(($imgheight-$textheight)/2);
|
||||
|
||||
$dots = $imgheight*$imgwidth/35;
|
||||
$gd = imagecreatetruecolor($imgwidth, $imgheight);
|
||||
for($i=1;$i<=$dots;$i++)
|
||||
{
|
||||
imagesetpixel($im, rand(0, $imgwidth), rand(0, $imgheight), imagecolorallocate($gd, rand(0, 255), rand(0, 255), rand(0, 255)));
|
||||
}
|
||||
|
||||
$textcolor = imagecolorallocate($im, 0, 0, 0);
|
||||
imagestring($im, 5, $textposh, $textposv, $imagestring, $textcolor);
|
||||
|
||||
// output the image
|
||||
header("Content-type: image/png");
|
||||
imagepng($im);
|
||||
imagedestroy($im);
|
||||
exit;
|
||||
}
|
||||
else
|
||||
{
|
||||
header("Location: pic/clear.gif");
|
||||
}
|
||||
if ($action !== 'regimage') {
|
||||
http_response_code(404);
|
||||
exit('Invalid captcha action');
|
||||
}
|
||||
else
|
||||
{
|
||||
die('invalid action');
|
||||
|
||||
$driver = captcha_manager()->driver('image');
|
||||
|
||||
if (!method_exists($driver, 'outputImage')) {
|
||||
http_response_code(404);
|
||||
exit('Captcha driver does not support image rendering');
|
||||
}
|
||||
|
||||
$driver->outputImage($imagehash);
|
||||
|
||||
?>
|
||||
|
||||
@@ -28,7 +28,7 @@ $mailTwoFour = sprintf($lang_recover['mail_two_four'], $siteName);
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],"recover.php",true);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,"recover.php",true);
|
||||
$email = unesc(htmlspecialchars(trim($_POST["email"] ?? '')));
|
||||
$email = safe_email($email);
|
||||
if (!$email)
|
||||
|
||||
@@ -15,7 +15,7 @@ function bark($text = "")
|
||||
stderr($lang_takelogin['std_login_fail'], $text,false);
|
||||
}
|
||||
if ($iv == "yes") {
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'login.php',true);
|
||||
check_code($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null, 'login.php', true);
|
||||
}
|
||||
//同时支持新旧两种登录方式
|
||||
$useChallengeResponse = \App\Models\Setting::getIsUseChallengeResponseAuthentication();
|
||||
|
||||
@@ -21,13 +21,13 @@ if ($type == 'invite'){
|
||||
registration_check();
|
||||
failedloginscheck ("Invite Signup");
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'signup.php?type=invite&invitenumber='.htmlspecialchars($_POST['hash']));
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,'signup.php?type=invite&invitenumber='.htmlspecialchars($_POST['hash']));
|
||||
}
|
||||
else{
|
||||
registration_check("normal");
|
||||
failedloginscheck ("Signup");
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring']);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null);
|
||||
}
|
||||
function isportopen($port)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user