Merge branch 'cedar2025:master' into master

This commit is contained in:
socksprox 2025-01-25 19:57:55 +01:00 committed by GitHub
commit 898613170e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 144 additions and 89 deletions

View File

@ -3,6 +3,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use App\Helpers\ApiResponse; use App\Helpers\ApiResponse;
use App\Services\Plugin\InterceptResponseException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\View\ViewException; use Illuminate\View\ViewException;
@ -68,6 +69,19 @@ class Handler extends ExceptionHandler
return parent::render($request, $exception); return parent::render($request, $exception);
} }
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
$this->renderable(function (InterceptResponseException $e) {
return $e->getResponse();
});
}
protected function convertExceptionToArray(Throwable $e) protected function convertExceptionToArray(Throwable $e)
{ {

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Protocols\General; use App\Protocols\General;
use App\Services\Plugin\HookManager;
use App\Services\ServerService; use App\Services\ServerService;
use App\Services\UserService; use App\Services\UserService;
use App\Utils\Helper; use App\Utils\Helper;
@ -47,62 +48,10 @@ class ClientController extends Controller
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2']; private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
/**
* 处理浏览器访问订阅的情况
*/
private function handleBrowserSubscribe($user, UserService $userService)
{
$useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('Unlimited');
$resetDay = $userService->getResetDay($user);
// 获取通用订阅地址
$subscriptionUrl = Helper::getSubscribeUrl($user->token);
// 生成二维码
$writer = new \BaconQrCode\Writer(
new \BaconQrCode\Renderer\ImageRenderer(
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
)
);
$qrCode = base64_encode($writer->writeString($subscriptionUrl));
$data = [
'username' => $user->email,
'status' => $userService->isAvailable($user) ? 'active' : 'inactive',
'data_limit' => $totalTraffic ? Helper::trafficConvert($totalTraffic) : '∞',
'data_used' => Helper::trafficConvert($useTraffic),
'expired_date' => $expiredDate,
'reset_day' => $resetDay,
'subscription_url' => $subscriptionUrl,
'qr_code' => $qrCode
];
// 只有当 device_limit 不为 null 时才添加到返回数据中
if ($user->device_limit !== null) {
$data['device_limit'] = $user->device_limit;
}
return response()->view('client.subscribe', $data);
}
/**
* 检查是否是浏览器访问
*/
private function isBrowserAccess(Request $request): bool
{
$userAgent = strtolower($request->input('flag', $request->header('User-Agent', '')));
return str_contains($userAgent, 'mozilla')
|| str_contains($userAgent, 'chrome')
|| str_contains($userAgent, 'safari')
|| str_contains($userAgent, 'edge');
}
public function subscribe(Request $request) public function subscribe(Request $request)
{ {
HookManager::call('client.subscribe.before');
$request->validate([ $request->validate([
'types' => ['nullable', 'string'], 'types' => ['nullable', 'string'],
'filter' => ['nullable', 'string'], 'filter' => ['nullable', 'string'],
@ -116,10 +65,6 @@ class ClientController extends Controller
return response()->json(['message' => 'Account unavailable'], 403); return response()->json(['message' => 'Account unavailable'], 403);
} }
// 检测是否是浏览器访问
if ($this->isBrowserAccess($request)) {
return $this->handleBrowserSubscribe($user, $userService);
}
$clientInfo = $this->getClientInfo($request); $clientInfo = $this->getClientInfo($request);
$types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']); $types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']);
$filterArr = $this->getFilterArray($request->input('filter')); $filterArr = $this->getFilterArray($request->input('filter'));
@ -194,13 +139,17 @@ class ClientController extends Controller
private function checkHy2Support(string $flag, string $version): bool private function checkHy2Support(string $flag, string $version): bool
{ {
$result = false; $clientFound = false;
foreach (self::CLIENT_VERSIONS as $client => $minVersion) { foreach (self::CLIENT_VERSIONS as $client => $minVersion) {
if (stripos($flag, $client) !== false) { if (stripos($flag, $client) !== false) {
$result = $result || version_compare($version, $minVersion, '>='); $clientFound = true;
if (version_compare($version, $minVersion, '>=')) {
return true;
} }
} }
return $result || !count(self::CLIENT_VERSIONS); }
// 如果客户端不在列表中,返回 true
return !$clientFound;
} }
private function filterServers(array $servers, array $types, ?array $filters): array private function filterServers(array $servers, array $types, ?array $filters): array

View File

@ -2,24 +2,22 @@
namespace App\Services\Plugin; namespace App\Services\Plugin;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\File;
use Symfony\Component\HttpFoundation\Response;
abstract class AbstractPlugin abstract class AbstractPlugin
{ {
protected array $config = []; protected array $config = [];
protected string $basePath;
protected string $pluginCode;
/** public function __construct($pluginCode)
* 插件启动时调用
*/
public function boot(): void
{ {
// 子类实现具体逻辑 $this->pluginCode = $pluginCode;
} $reflection = new \ReflectionClass($this);
$this->basePath = dirname($reflection->getFileName());
/**
* 插件禁用时调用
*/
public function cleanup(): void
{
// 子类实现具体逻辑
} }
/** /**
@ -39,11 +37,19 @@ abstract class AbstractPlugin
} }
/** /**
* 注册事件监听器 * 注册动作钩子监听器
*/ */
protected function listen(string $hook, callable $callback): void protected function listen(string $hook, callable $callback, int $priority = 20): void
{ {
HookManager::register($hook, $callback); HookManager::register($hook, $callback, $priority);
}
/**
* 注册过滤器钩子
*/
protected function filter(string $hook, callable $callback, int $priority = 20): void
{
HookManager::registerFilter($hook, $callback, $priority);
} }
/** /**
@ -53,4 +59,15 @@ abstract class AbstractPlugin
{ {
HookManager::remove($hook); HookManager::remove($hook);
} }
/**
* 中断当前请求并返回新的响应
*
* @param Response|string|array $response
* @return never
*/
protected function intercept(Response|string|array $response): never
{
HookManager::intercept($response);
}
} }

View File

@ -2,32 +2,78 @@
namespace App\Services\Plugin; namespace App\Services\Plugin;
use Illuminate\Support\Facades\Event; use TorMorten\Eventy\Facades\Events as Eventy;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
class HookManager class HookManager
{ {
/** /**
* 触发钩子 * 拦截响应
* *
* @param string $hook 钩子名称 * @param SymfonyResponse|string|array $response 新的响应内容
* @param mixed $payload 传递给钩子的数据 * @return never
* @return mixed * @throws \Exception
*/ */
public static function call(string $hook, mixed $payload = null): mixed public static function intercept(SymfonyResponse|string|array $response): never
{ {
return Event::dispatch($hook, [$payload]); if (is_string($response)) {
$response = response($response);
} elseif (is_array($response)) {
$response = response()->json($response);
}
throw new InterceptResponseException($response);
} }
/** /**
* 注册钩子监听器 * 触发动作钩子
*
* @param string $hook 钩子名称
* @param mixed $payload 传递给钩子的数据
* @return void
*/
public static function call(string $hook, mixed $payload = null): void
{
Eventy::action($hook, $payload);
}
/**
* 触发过滤器钩子
*
* @param string $hook 钩子名称
* @param mixed $value 要过滤的值
* @param mixed ...$args 其他参数
* @return mixed
*/
public static function filter(string $hook, mixed $value, mixed ...$args): mixed
{
return Eventy::filter($hook, $value, ...$args);
}
/**
* 注册动作钩子监听器
* *
* @param string $hook 钩子名称 * @param string $hook 钩子名称
* @param callable $callback 回调函数 * @param callable $callback 回调函数
* @param int $priority 优先级
* @return void * @return void
*/ */
public static function register(string $hook, callable $callback): void public static function register(string $hook, callable $callback, int $priority = 20): void
{ {
Event::listen($hook, $callback); Eventy::addAction($hook, $callback, $priority);
}
/**
* 注册过滤器钩子
*
* @param string $hook 钩子名称
* @param callable $callback 回调函数
* @param int $priority 优先级
* @return void
*/
public static function registerFilter(string $hook, callable $callback, int $priority = 20): void
{
Eventy::addFilter($hook, $callback, $priority);
} }
/** /**
@ -38,6 +84,7 @@ class HookManager
*/ */
public static function remove(string $hook): void public static function remove(string $hook): void
{ {
Event::forget($hook); Eventy::removeAction($hook);
Eventy::removeFilter($hook);
} }
} }

View File

@ -0,0 +1,22 @@
<?php
namespace App\Services\Plugin;
use Exception;
use Symfony\Component\HttpFoundation\Response;
class InterceptResponseException extends Exception
{
protected Response $response;
public function __construct(Response $response)
{
parent::__construct('Response intercepted');
$this->response = $response;
}
public function getResponse(): Response
{
return $this->response;
}
}

View File

@ -5,6 +5,7 @@ namespace App\Services\Plugin;
use App\Models\Plugin; use App\Models\Plugin;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View;
class PluginManager class PluginManager
{ {
@ -89,6 +90,11 @@ class PluginManager
if (File::exists($routesFile)) { if (File::exists($routesFile)) {
require $routesFile; require $routesFile;
} }
// 注册视图
$viewsPath = $this->pluginPath . '/' . $pluginCode . '/resources/views';
if (File::exists($viewsPath)) {
View::addNamespace($pluginCode, $viewsPath);
}
// 初始化插件 // 初始化插件
if (method_exists($plugin, 'boot')) { if (method_exists($plugin, 'boot')) {
@ -150,7 +156,7 @@ class PluginManager
require_once $pluginFile; require_once $pluginFile;
$className = "Plugin\\{$pluginCode}\\Plugin"; $className = "Plugin\\{$pluginCode}\\Plugin";
return new $className(); return new $className($pluginCode);
} }
/** /**