diff --git a/composer.json b/composer.json index 695f98e80..c583d585e 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { "name": "zhamao/framework", - "description": "High performance QQ robot and web server development framework", + "description": "High performance chat robot and web server development framework", "minimum-stability": "stable", "license": "Apache-2.0", - "version": "2.2.8", + "version": "2.2.9", "extra": { "exclude_annotate": [ "src/ZM" @@ -11,12 +11,8 @@ }, "authors": [ { - "name": "whale", - "email": "crazysnowcc@gmail.com" - }, - { - "name": "swift", - "email": "hugo_swift@yahoo.com" + "name": "jerry", + "email": "admin@zhamao.me" } ], "prefer-stable": true, @@ -52,7 +48,6 @@ ] }, "require-dev": { - "swoole/ide-helper": "@dev", - "phpunit/phpunit": "^9.5" + "swoole/ide-helper": "@dev" } } \ No newline at end of file diff --git a/config/global.php b/config/global.php index 0d3409bec..4b744ab3f 100644 --- a/config/global.php +++ b/config/global.php @@ -27,7 +27,7 @@ /** 对应swoole的server->set参数 */ $config['swoole'] = [ 'log_file' => $config['crash_dir'] . 'swoole_error.log', - 'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 + //'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode 'max_coroutine' => 300000, //'task_worker_num' => 4, diff --git a/docs/component/access-token.md b/docs/component/access-token.md new file mode 100644 index 000000000..3e4b4b4e9 --- /dev/null +++ b/docs/component/access-token.md @@ -0,0 +1,59 @@ +# Token 验证 + +为了保障安全,框架支持给接入的 WebSocket 连接验证 Token,如果不设置 Token 同时又将框架的端口暴露在公网将会非常危险。 + +炸毛框架兼容 OneBot 标准的机器人客户端,所以自带一个 Token 验证器。 + +关于 Access Token 方面的标准规范,请参考下面内容: + +- [OneBot - 鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md) +- [go-cqhttp - 配置](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md) + +> 以 go-cqhttp 举例,如果要设置验证,则将 go-cqhttp 配置文件中的 `access_token` 项填入内容即可。 + +## 验证位置 + +框架对 Token 的验证是内置的,在事件 `open`(WebSocket 连接接入时)触发。 + +如果是兼容 OneBot 标准的客户端接入,则一切都是兼容的。 + +如果是自定义的其他 WebSocket 客户端也想接入框架,那么其他 WebSocket 客户端也需要进行相应的设置才能利用此 Token 验证。 + +如果验证成功(Token 符合要求)则分发事件 `@OnOpenEvent`,否则此事件不触发,同时断开 WebSocket 连接。 + +## 标准验证(字符串形式) + +默认的情况下,在框架的全局配置文件 `global.php` 中,对配置项 `access_token` 填入与 OneBot 客户端相同的 `access_token` 即可实现鉴权。下面是一个最基本的和 go-cqhttp 设置鉴权配置: + +go-cqhttp 的配置段: + +```hjson + // 访问密钥, 强烈推荐在公网的服务器设置 + access_token: "emhhbWFvLXJvYm90" +``` + +框架的配置文件配置段: + +```php +/** onebot连接约定的token */ +$config["access_token"] = 'emhhbWFvLXJvYm90'; +``` + +然后重启框架和 go-cqhttp 即可。(其他 OneBot 客户端同理) + +## 自定义验证(Token 验证) + +有些情况下,使用一个单一的字符串可能无法满足你对 Token 验证的安全需求,需要自定义一些判断模式才能满足,所以框架的 `access_token` 配置项支持动态的闭包函数自行编写判断逻辑,例如下面的一个例子,我可以让框架同时允许接入多个不同 token 的 WebSocket 连接: + +```php +/** onebot连接约定的token */ +$config["access_token"] = function($token){ + $allow = ['emhhbWFvLXJvYm90','aXMtdmVyeS1nb29k']; + if (in_array($token, $allow)) return true; + else return false; +}; +``` + +## 自定义验证(open 事件) + +当然,这里设置了自定义方式,其实你也可以在下一层的 `@OnOpenEvent` 注解事件中进行自定义内容和判断,具体见 `@OnOpenEvent` 的相关章节。 \ No newline at end of file diff --git a/docs/component/context.md b/docs/component/context.md index 8d0629dba..c41d8096d 100644 --- a/docs/component/context.md +++ b/docs/component/context.md @@ -51,7 +51,7 @@ public function hello() { * @CQCommand("测试fd") */ public function testfd() { - ctx()->reply("当前机器人连接的fd是:".ctx()->getFd()",机器人QQ是:".ctx()->getRobotId()); + ctx()->reply("当前机器人连接的fd是:".ctx()->getFd().",机器人QQ是:".ctx()->getRobotId()); } ``` @@ -421,4 +421,5 @@ public function argTest1() { ) test abc 334 argtest ( 参数内容:abc, 334, argtest - \ No newline at end of file + + diff --git a/docs/component/data-provider.md b/docs/component/data-provider.md index 5bf8c7c32..390da1324 100644 --- a/docs/component/data-provider.md +++ b/docs/component/data-provider.md @@ -34,4 +34,21 @@ DataProvider 是框架内提供的一个简易的文件管理类。 定义:`loadFromJson($filename)` -文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。 \ No newline at end of file +文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。 + +## 其他文件读取 + +框架比较贴近原生的 PHP,所以推荐直接使用原生的方法来读写文件(`file_get_contents` 和 `file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。 + +```php +// 读取框架工作目录的文件 composer.json 文件 +$r = file_get_contents(working_dir() . "/composer.json"); + +// 写入 Linux 临时目录下的文件 +file_put_contents("/tmp/test.txt", "hello world"); +``` + +!!! warning "注意" + + 在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。 + diff --git a/docs/guide/basic-config.md b/docs/guide/basic-config.md index 59fb8906b..449bd6f2c 100644 --- a/docs/guide/basic-config.md +++ b/docs/guide/basic-config.md @@ -10,29 +10,29 @@ 框架的全局配置文件在 `config/global.php` 文件中。下面是配置文件的各个选项,请根据自己的需要自行配置。 -| 配置名称 | 说明 | 默认值 | -| :--------------------------- | ------------------------------------------------ | ---------------------------- | -| `host` | 框架监听的地址 | 0.0.0.0 | -| `port` | 框架监听的端口 | 20001 | -| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 | -| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` | -| `debug_mode` | 框架是否启动 debug 模式 | false | -| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` | -| `swoole` | 对应 Swoole server 中 set 的参数,参考Swoole文档 | 见子表 `swoole` | -| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` | -| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` | -| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` | -| `redis_config` | Redis 连接信息 | 见子表 `redis_config` | -| `access_token` | OneBot 客户端连接约定的token,留空则无 | 空 | -| `http_header` | HTTP 请求自定义返回的header | 见配置文件 | -| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 | -| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 | -| `info_level` | 终端日志显示等级(0-4) | 2 | -| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` | -| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` | -| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 | -| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 | -| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` | +| 配置名称 | 说明 | 默认值 | +| :--------------------------- | ------------------------------------------------------------ | ---------------------------- | +| `host` | 框架监听的地址 | 0.0.0.0 | +| `port` | 框架监听的端口 | 20001 | +| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 | +| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` | +| `debug_mode` | 框架是否启动 debug 模式 | false | +| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` | +| `swoole` | 对应 Swoole server 中 set 的参数,参考Swoole文档 | 见子表 `swoole` | +| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` | +| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` | +| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` | +| `redis_config` | Redis 连接信息 | 见子表 `redis_config` | +| `access_token` | OneBot 客户端连接约定的token,留空则无,相关设置见 [组件 - Access Token 验证](component/access-token) | 空 | +| `http_header` | HTTP 请求自定义返回的header | 见配置文件 | +| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 | +| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 | +| `info_level` | 终端日志显示等级(0-4) | 2 | +| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` | +| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` | +| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 | +| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 | +| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` | ### 子表 **swoole** diff --git a/docs/update/v2.md b/docs/update/v2.md index 71917b8c0..ee98085cb 100644 --- a/docs/update/v2.md +++ b/docs/update/v2.md @@ -1,5 +1,13 @@ # 更新日志(v2 版本) +## v2.2.9 + +> 更新时间:2021.3.6 + +- 更新:`reply()` 方法传入数组则变为快速相应的 API 操作 +- 修复:在 Worker 进程下调用 `ZMUtil::reload()` 会导致一些奇怪的 bug +- 修复:`reply()` 时会 at 私聊成员的 bug(由 go-cqhttp 导致) + ## v2.2.8 > 更新时间:2021.3.2 diff --git a/mkdocs.yml b/mkdocs.yml index 111e8c41c..9c7613efd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ nav: - 全局方法: component/global-functions.md - HTTP 和 WebSocket 客户端: component/zmrequest.md - Console 终端: component/console.md + - Token 验证: component/access-token.md - 进阶开发: - 进阶开发: advanced/index.md - 框架剖析: advanced/framework-structure.md diff --git a/src/ZM/Context/Context.php b/src/ZM/Context/Context.php index ce7dddcbd..b6feb33c4 100644 --- a/src/ZM/Context/Context.php +++ b/src/ZM/Context/Context.php @@ -104,25 +104,31 @@ public function getCQResponse() { return self::$context[$this->cid]["cq_response * @param $msg * @param bool $yield * @return mixed - * @noinspection PhpMissingBreakStatementInspection */ public function reply($msg, $yield = false) { - switch ($this->getData()["message_type"]) { - case "group": - $operation["at_sender"] = false; - // no break - case "private": - case "discuss": - $this->setCache("has_reply", true); - $data = $this->getData(); - $conn = $this->getConnection(); - $operation["reply"] = $msg; - return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [ - "context" => $data, - "operation" => $operation - ]); + $data = $this->getData(); + $conn = $this->getConnection(); + if (!is_array($msg)) { + switch ($this->getData()["message_type"]) { + case "group": + case "private": + case "discuss": + $this->setCache("has_reply", true); + $operation["reply"] = $msg; + $operation["at_sender"] = false; + return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [ + "context" => $data, + "operation" => $operation + ]); + } + return false; + } else { + $operation = $msg; + return (new ZMRobot($conn))->setCallback(false)->callExtendedAPI(".handle_quick_operation", [ + "context" => $data, + "operation" => $operation + ]); } - return false; } public function finalReply($msg, $yield = false) { diff --git a/src/ZM/Event/ServerEventHandler.php b/src/ZM/Event/ServerEventHandler.php index 5c8e89bde..2ba3db76e 100644 --- a/src/ZM/Event/ServerEventHandler.php +++ b/src/ZM/Event/ServerEventHandler.php @@ -80,11 +80,16 @@ public function onStart() { }); } Process::signal(SIGINT, function () use ($r) { - echo "\r"; - Console::warning("Server interrupted(SIGINT) on Master."); - if ((Framework::$server->inotify ?? null) !== null) - /** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); - ZMUtil::stop(); + if (zm_atomic("_int_is_reload")->get() === 1) { + zm_atomic("_int_is_reload")->set(0); + ZMUtil::reload(); + } else { + echo "\r"; + Console::warning("Server interrupted(SIGINT) on Master."); + if ((Framework::$server->inotify ?? null) !== null) + /** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); + ZMUtil::stop(); + } }); if (Framework::$argv["daemon"]) { $daemon_data = json_encode([ diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 2160c0236..4d4d8ae09 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -74,8 +74,7 @@ public function __construct($args = []) { die($e->getMessage()); } try { - self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port")); - $this->server_set = ZMConfig::get("global", "swoole"); + Console::init( ZMConfig::get("global", "info_level") ?? 2, self::$server, @@ -86,16 +85,14 @@ public function __construct($args = []) { $timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai"; date_default_timezone_set($timezone); + $this->server_set = ZMConfig::get("global", "swoole"); $this->parseCliArgs(self::$argv); - - self::$server->set($this->server_set); - // 打印初始信息 $out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port"); if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)"; else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"]; - $out["env"] = $args["env"] === null ? "default" : $args["env"]; + $out["environment"] = $args["env"] === null ? "default" : $args["env"]; $out["log_level"] = Console::getLevel(); $out["version"] = ZM_VERSION; if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION; @@ -117,6 +114,10 @@ public function __construct($args = []) { $out["working_dir"] = DataProvider::getWorkingDir(); self::printProps($out, $tty_width, $args["log-theme"] === null); + self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port")); + + self::$server->set($this->server_set); + self::printMotd($tty_width); global $asd; @@ -170,6 +171,7 @@ public function __construct($args = []) { } catch (Exception $e) { Console::error("Framework初始化出现错误,请检查!"); Console::error($e->getMessage()); + Console::debug($e); die; } } @@ -350,6 +352,7 @@ public static function printProps($out, $tty_width, $colorful = true) { $line_width = []; $line_data = []; foreach ($out as $k => $v) { + start: if (!isset($line_width[$current_line])) { $line_width[$current_line] = $max_border - 2; } @@ -363,11 +366,7 @@ public static function printProps($out, $tty_width, $colorful = true) { if (strlen($tmp_line) > $line_width[$current_line]) { // 地方不够,另起一行 $line_data[$current_line] = str_replace("| ", "", $line_data[$current_line]); ++$current_line; - $line_data[$current_line] = " " . $k . ": "; - if ($colorful) $line_data[$current_line] .= TermColor::color8(32); - $line_data[$current_line] .= $v; - if ($colorful) $line_data[$current_line] .= TermColor::RESET; - ++$current_line; + goto start; } else { // 地方够,直接写到后面并另起一行 $line_data[$current_line] .= $k . ": "; if ($colorful) $line_data[$current_line] .= TermColor::color8(32); diff --git a/src/ZM/Store/ZMAtomic.php b/src/ZM/Store/ZMAtomic.php index b4e88ff8f..6c5943896 100644 --- a/src/ZM/Store/ZMAtomic.php +++ b/src/ZM/Store/ZMAtomic.php @@ -28,6 +28,7 @@ public static function init() { self::$atomics[$k] = new Atomic($v); } self::$atomics["stop_signal"] = new Atomic(0); + self::$atomics["_int_is_reload"] = new Atomic(0); self::$atomics["wait_msg_id"] = new Atomic(0); self::$atomics["_event_id"] = new Atomic(0); for ($i = 0; $i < 10; ++$i) { diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php index a4a3e949c..c620e71f8 100644 --- a/src/ZM/Utils/ZMUtil.php +++ b/src/ZM/Utils/ZMUtil.php @@ -11,13 +11,16 @@ use ZM\Console\Console; use ZM\Store\LightCache; use ZM\Store\LightCacheInside; +use ZM\Store\Lock\SpinLock; use ZM\Store\ZMAtomic; use ZM\Store\ZMBuf; class ZMUtil { public static function stop() { + if (SpinLock::tryLock("_stop_signal") === false) return; Console::warning(Console::setColor("Stopping server...", "red")); + Console::trace(); LightCache::savePersistence(); if (ZMBuf::$terminal !== null) Event::del(ZMBuf::$terminal); @@ -31,6 +34,12 @@ public static function stop() { } public static function reload($delay = 800) { + if (server()->worker_id !== -1) { + Console::info(server()->worker_id); + zm_atomic("_int_is_reload")->set(1); + system("kill -INT " . intval(server()->master_pid)); + return; + } Console::info(Console::setColor("Reloading server...", "gold")); usleep($delay * 1000); foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) {