diff --git a/config/global.php b/config/global.php index 167848f9..fde39f51 100644 --- a/config/global.php +++ b/config/global.php @@ -69,6 +69,11 @@ $config['plugin'] = [ 'load_dir' => 'plugins', ]; +/* 内部默认启用的插件 */ +$config['native_plugin'] = [ + 'onebot12' => true, +]; + /* 静态文件读取器 */ $config['file_server'] = [ 'enable' => true, diff --git a/src/ZM/Command/CheckConfigCommand.php b/src/ZM/Command/CheckConfigCommand.php index 651e4ef0..6b0c6c55 100644 --- a/src/ZM/Command/CheckConfigCommand.php +++ b/src/ZM/Command/CheckConfigCommand.php @@ -15,7 +15,7 @@ class CheckConfigCommand extends Command protected function handle(): int { - $current_cfg = getcwd() . '/config/'; + $current_cfg = SOURCE_ROOT_DIR . '/config/'; $remote_cfg = include FRAMEWORK_ROOT_DIR . '/config/global_old.php'; if (file_exists($current_cfg . 'global.php')) { $this->check($remote_cfg, 'global.php'); @@ -38,11 +38,7 @@ class CheckConfigCommand extends Command return self::SUCCESS; } - /** - * @param mixed $remote - * @param mixed $local - */ - private function check($remote, $local) + private function check(mixed $remote, mixed $local) { $local_file = include WORKING_DIR . '/config/' . $local; if ($local_file === true) { diff --git a/src/ZM/Event/Listener/HttpEventListener.php b/src/ZM/Event/Listener/HttpEventListener.php index 1b2447ad..a5df0c37 100644 --- a/src/ZM/Event/Listener/HttpEventListener.php +++ b/src/ZM/Event/Listener/HttpEventListener.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace ZM\Event\Listener; +use Choir\Http\HttpFactory; +use Choir\Http\Stream; use OneBot\Driver\Event\Http\HttpRequestEvent; -use OneBot\Http\HttpFactory; -use OneBot\Http\Stream; use OneBot\Util\Singleton; use ZM\Annotation\AnnotationHandler; use ZM\Annotation\Framework\BindEvent; @@ -33,9 +33,7 @@ class HttpEventListener // TODO: 这里有个bug,如果是用的Workerman+Fiber协程的话,有个前置协程挂起,这里获取到的Event是被挂起的Event对象,触发两次事件才能归正 // 跑一遍 BindEvent 绑定了 HttpRequestEvent 的注解 $handler = new AnnotationHandler(BindEvent::class); - $handler->setRuleCallback(function (BindEvent $anno) { - return $anno->event_class === HttpRequestEvent::class; - }); + $handler->setRuleCallback(fn (BindEvent $anno) => $anno->event_class === HttpRequestEvent::class); $handler->handleAll($event); // dump($event->getResponse()); $node = null; @@ -51,14 +49,15 @@ class HttpEventListener $div = new Route($node['route']); $div->params = $params; $div->method = $node['method']; - $div->request_method = $node['request_method']; + // TODO:这里有个bug,逻辑上 request_method 应该是个数组,而不是字符串,但是这里 $node['method'] 是字符串,所以这里只能用字符串来判断 + // $div->request_method = $node['request_method']; $div->class = $node['class']; $starttime = microtime(true); $handler->handle($div, null, $params, $event->getRequest(), $event); if (is_string($val = $handler->getReturnVal()) || ($val instanceof \Stringable)) { - $event->withResponse(HttpFactory::getInstance()->createResponse(200, null, [], Stream::create($val))); + $event->withResponse(HttpFactory::createResponse(200, null, [], Stream::create($val))); } elseif ($event->getResponse() === null) { - $event->withResponse(HttpFactory::getInstance()->createResponse(500)); + $event->withResponse(HttpFactory::createResponse(500)); } logger()->warning('Used ' . round((microtime(true) - $starttime) * 1000, 3) . ' ms'); break; @@ -74,7 +73,7 @@ class HttpEventListener * * @throws ConfigException */ - public function onRequest1(HttpRequestEvent $event) + public function onRequest1(HttpRequestEvent $event): void { if ($event->getResponse() === null) { $response = HttpUtil::handleStaticPage($event->getRequest()->getUri()->getPath()); diff --git a/src/ZM/Event/Listener/WSEventListener.php b/src/ZM/Event/Listener/WSEventListener.php index 45d46a82..84d8193b 100644 --- a/src/ZM/Event/Listener/WSEventListener.php +++ b/src/ZM/Event/Listener/WSEventListener.php @@ -4,65 +4,48 @@ declare(strict_types=1); namespace ZM\Event\Listener; +use Choir\Http\HttpFactory; use OneBot\Driver\Event\WebSocket\WebSocketCloseEvent; +use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent; -use OneBot\Driver\Process\ProcessManager; -use OneBot\Http\HttpFactory; use OneBot\Util\Singleton; +use ZM\Annotation\AnnotationHandler; +use ZM\Annotation\Framework\BindEvent; use ZM\Container\ContainerServicesProvider; -use ZM\Process\ProcessStateManager; +use ZM\Utils\ConnectionUtil; class WSEventListener { use Singleton; - private static int $ws_counter = 0; - - private static array $conn_handle = []; - - public function onWebSocketOpen(WebSocketOpenEvent $event) + /** + * @throws \Throwable + */ + public function onWebSocketOpen(WebSocketOpenEvent $event): void { + logger()->info('接入连接: ' . $event->getFd()); // 计数,最多只能接入 1024 个连接,为了适配多进程 - ++self::$ws_counter; - if (self::$ws_counter >= 1024) { - $event->withResponse(HttpFactory::getInstance()->createResponse(503)); + if (!ConnectionUtil::addConnection($event->getFd(), [])) { + $event->withResponse(HttpFactory::createResponse(503)); return; } // 注册容器 resolve(ContainerServicesProvider::class)->registerServices('connection'); - // 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头 - $line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2); - if ($line[0] === '12') { - // 是 OneBot 12 标准的,准许接入,进行鉴权 - $request = $event->getRequest(); - if (($stored_token = $event->getSocketConfig()['access_token'] ?? '') !== '') { - $token = $request->getHeaderLine('Authorization'); - $token = explode('Bearer ', $token); - if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token,鉴权失败 - $event->withResponse(HttpFactory::getInstance()->createResponse(401, 'Unauthorized')); - return; - } - } - // 这里下面为连接准入,允许接入反向 WS,TODO - if (ProcessStateManager::$process_mode['worker'] > 1) { - // 如果开了多 Worker,则需要将连接信息写入文件,以便跨进程读取 - $info = ['impl' => $line[1] ?? 'unknown']; - self::$conn_handle[$event->getFd()] = $info; - file_put_contents(zm_dir(ZM_STATE_DIR . '/.WS' . $event->getFd() . '.' . ProcessManager::getProcessId()), json_encode($info)); - } - } + + // 调用注解 + $handler = new AnnotationHandler(BindEvent::class); + $handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketOpenEvent::class, true)); + $handler->handleAll($event); } - public function onWebSocketClose(WebSocketCloseEvent $event) + public function onWebSocketMessage(WebSocketMessageEvent $event): void { - --self::$ws_counter; - // 删除连接信息 - $fd = $event->getFd(); - $filename = zm_dir(ZM_STATE_DIR . '/.WS' . $fd . '.' . ProcessManager::getProcessId()); - if (file_exists($filename)) { - unlink($filename); - } - unset(self::$conn_handle[$fd]); + } + + public function onWebSocketClose(WebSocketCloseEvent $event): void + { + logger()->info('关闭连接: ' . $event->getFd()); + ConnectionUtil::removeConnection($event->getFd()); resolve(ContainerServicesProvider::class)->cleanup(); } } diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php index f37ff459..6f661def 100644 --- a/src/ZM/Event/Listener/WorkerEventListener.php +++ b/src/ZM/Event/Listener/WorkerEventListener.php @@ -13,6 +13,8 @@ use ZM\Annotation\Framework\Init; use ZM\Container\ContainerServicesProvider; use ZM\Exception\ZMKnownException; use ZM\Framework; +use ZM\Plugin\OneBot12Adapter; +use ZM\Plugin\PluginManager; use ZM\Process\ProcessStateManager; use ZM\Store\Database\DBException; use ZM\Store\Database\DBPool; @@ -134,6 +136,10 @@ class WorkerEventListener } // TODO: 然后加载插件目录下的插件 + PluginManager::addPlugin([ + 'name' => 'onebot12-adapter', + 'plugin' => new OneBot12Adapter(), + ]); // 解析所有注册路径的文件,获取注解 $parser->parseAll(); diff --git a/src/ZM/Exception/InitException.php b/src/ZM/Exception/InitException.php index 49ba9dce..69849cba 100644 --- a/src/ZM/Exception/InitException.php +++ b/src/ZM/Exception/InitException.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace ZM\Exception; +/** + * 初始化命令(./zhamao init)出现的错误 + */ class InitException extends ZMException { } diff --git a/src/ZM/Exception/PluginException.php b/src/ZM/Exception/PluginException.php new file mode 100644 index 00000000..e18f7ce7 --- /dev/null +++ b/src/ZM/Exception/PluginException.php @@ -0,0 +1,12 @@ +addEventListener(WebSocketOpenEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketOpen'], 999); ob_event_provider()->addEventListener(WebSocketCloseEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketClose'], 999); + ob_event_provider()->addEventListener(WebSocketMessageEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketMessage'], 999); // 框架多进程依赖 if (defined('ZM_STATE_DIR') && !is_dir(ZM_STATE_DIR)) { diff --git a/src/ZM/Plugin/PluginManager.php b/src/ZM/Plugin/PluginManager.php new file mode 100644 index 00000000..6a53f156 --- /dev/null +++ b/src/ZM/Plugin/PluginManager.php @@ -0,0 +1,40 @@ += 1024) { + return false; + } + self::$connection_handles[$fd] = $handle; + // 这里下面为连接准入,允许接入反向 WS + if (ProcessStateManager::$process_mode['worker'] > 1) { + // 文件名格式为 .WS{fd}.{pid},文件内容是 impl 名称的 JSON 格式 + file_put_contents(zm_dir(ZM_STATE_DIR . '/.WS' . $fd . '.' . ProcessManager::getProcessId()), json_encode($handle)); + } + return true; + } + + /** + * 更改、覆盖或合并连接元信息 + * @param int $fd WS 连接 ID + * @param array $handle WS 连接元信息 + */ + public static function setConnection(int $fd, array $handle): void + { + self::$connection_handles[$fd] = array_merge(self::$connection_handles[$fd] ?? [], $handle); + // 这里下面为连接准入,允许接入反向 WS + if (ProcessStateManager::$process_mode['worker'] > 1) { + // 文件名格式为 .WS{fd}.{pid},文件内容是 impl 名称的 JSON 格式 + file_put_contents(zm_dir(ZM_STATE_DIR . '/.WS' . $fd . '.' . ProcessManager::getProcessId()), json_encode(self::$connection_handles[$fd])); + } + } + + /** + * 删除连接元信息 + * + * @param int $fd WS 连接 ID + */ + public static function removeConnection(int $fd): void + { + --self::$connection_count; + unset(self::$connection_handles[$fd]); + // 这里下面为连接准入,允许接入反向 WS + if (ProcessStateManager::$process_mode['worker'] > 1) { + // 文件名格式为 .WS{fd}.{pid},文件内容是 impl 名称的 JSON 格式 + @unlink(zm_dir(ZM_STATE_DIR . '/.WS' . $fd . '.' . ProcessManager::getProcessId())); + } + } +} diff --git a/src/ZM/Utils/HttpUtil.php b/src/ZM/Utils/HttpUtil.php index 92e3a072..a124b49f 100644 --- a/src/ZM/Utils/HttpUtil.php +++ b/src/ZM/Utils/HttpUtil.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace ZM\Utils; -use OneBot\Http\HttpFactory; -use OneBot\Http\ServerRequest; -use OneBot\Http\Stream; +use Choir\Http\HttpFactory; +use Choir\Http\Stream; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -32,7 +32,7 @@ class HttpUtil * 第二个参数为路由节点 * 第三个参数为动态路由节点中匹配到的参数列表 */ - public static function parseUri(ServerRequest $request, mixed &$node, mixed &$params): int + public static function parseUri(RequestInterface $request, mixed &$node, mixed &$params): int { // 建立上下文,设置当前请求的方法 $context = new RequestContext(); @@ -90,7 +90,7 @@ class HttpUtil if ($path !== false) { // 安全问题,防止目录穿越,只能囚禁到规定的 Web 根目录下获取文件 $work = realpath($base_dir) . '/'; - if (strpos($path, $work) !== 0) { + if (!str_starts_with($path, $work)) { logger()->info('[403] ' . $uri); return static::handleHttpCodePage(403); } @@ -98,25 +98,25 @@ class HttpUtil if (is_dir($path)) { if (mb_substr($uri, -1, 1) != '/') { logger()->info('[302] ' . $uri); - return HttpFactory::getInstance()->createResponse(302, null, ['Location' => $uri . '/']); + return HttpFactory::createResponse(302, null, ['Location' => $uri . '/']); } // 如果结尾有 /,那么就根据默认搜索的文件名进行搜索文件是否存在,存在则直接返回对应文件 foreach ($base_index as $vp) { if (is_file($path . '/' . $vp)) { logger()->info('[200] ' . $uri); $exp = strtolower(pathinfo($path . $vp)['extension'] ?? 'unknown'); - return HttpFactory::getInstance()->createResponse() + return HttpFactory::createResponse() ->withAddedHeader('Content-Type', config('file_header')[$exp] ?? 'application/octet-stream') - ->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path . '/' . $vp))); + ->withBody(HttpFactory::createStream(file_get_contents($path . '/' . $vp))); } } } elseif (is_file($path)) { // 如果文件存在,则直接返回文件内容 logger()->info('[200] ' . $uri); $exp = strtolower(pathinfo($path)['extension'] ?? 'unknown'); - return HttpFactory::getInstance()->createResponse() + return HttpFactory::createResponse() ->withAddedHeader('Content-Type', config('file_header')[$exp] ?? 'application/octet-stream') - ->withBody(HttpFactory::getInstance()->createStream(file_get_contents($path))); + ->withBody(HttpFactory::createStream(file_get_contents($path))); } } // 否则最终肯定只能返回 404 了 @@ -137,9 +137,9 @@ class HttpUtil $code_page = null; } if ($code_page === null) { - return HttpFactory::getInstance()->createResponse($code); + return HttpFactory::createResponse($code); } - return HttpFactory::getInstance()->createResponse($code, null, [], file_get_contents(config('global.file_server.document_root') . '/' . $code_page)); + return HttpFactory::createResponse($code, null, [], file_get_contents(config('global.file_server.document_root') . '/' . $code_page)); } /** @@ -151,7 +151,7 @@ class HttpUtil */ public static function createJsonResponse(array $data, int $http_code = 200, int $json_flag = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): ResponseInterface { - return HttpFactory::getInstance()->createResponse($http_code) + return HttpFactory::createResponse($http_code) ->withAddedHeader('Content-Type', 'application/json') ->withBody(Stream::create(json_encode($data, $json_flag))); }