Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/OneBot/Driver/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public function initDriverProtocols(array $comm)
case 'websocket_reverse':
$has_ws_reverse[] = $v;
break;
default:
// 未知的通信类型直接跳过,并打警告日志
ob_logger()->warning('未知的通信类型: ' . $v['type'] . ',已跳过该配置');
break;
}
}
[$http, $webhook, $ws, $ws_reverse] = $this->initInternalDriverClasses($http_index, $has_http_webhook, $ws_index, $has_ws_reverse);
Expand Down
6 changes: 5 additions & 1 deletion src/OneBot/Driver/Swoole/Socket/WSServerSocket.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public function __construct(?Server $server = null, ?Port $port = null, array $c

public function close($fd): bool
{
return false;
if ($this->server === null || !$this->server->exists($fd)) {
ob_logger()->warning('链接不存在,可能已被关闭或未连接');
return false;
}
return $this->server->close($fd);
}

public function send($data, $fd): bool
Expand Down
1 change: 0 additions & 1 deletion src/OneBot/Driver/Swoole/TopEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ public function onHandshake(array $config, Request $request, Response $response)
$response->end();
return false;
}
echo $request->header['sec-websocket-key'];
$key = base64_encode(
sha1(
$request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
Expand Down
2 changes: 1 addition & 1 deletion src/OneBot/Driver/Swoole/WebSocketClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function connect(): bool
$uri .= '?' . $query;
}
if (($fragment = $this->request->getUri()->getFragment()) !== '') {
$uri .= '?' . $fragment;
$uri .= '#' . $fragment;
}
$r = $this->client->upgrade($uri);
if ($this->client->errCode !== 0) {
Expand Down
20 changes: 12 additions & 8 deletions src/OneBot/Driver/Workerman/ObjectPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class ObjectPool implements PoolInterface
/** @var array 借出去的对象 Hash 表 */
protected $out = [];

/** @var array 用于 Workerman Fiber 对接时保存协程 ID 的 */
private static $coroutine_cid = [];
/** @var array 用于 Workerman Fiber 对接时保存协程 ID 的(每个池实例独立,避免跨池串对象) */
private $coroutine_cid = [];

public function __construct(int $size, string $construct_class, ...$args)
{
Expand All @@ -46,9 +46,9 @@ public function __destruct()
public function get($recursive = 0): object
{
if ($this->getFreeCount() <= 0) { // 当池子见底了,就自动用 Swoole 的 Channel 消费者模型堵起来
if (($cid = Adaptive::getCoroutine()->getCid()) !== -1) {
self::$coroutine_cid[] = $cid;
$result = Adaptive::getCoroutine()->suspend();
if (($co = Adaptive::getCoroutine()) !== null && $co->getCid() !== -1) {
$this->coroutine_cid[] = $co->getCid();
$result = $co->suspend();
} elseif ($recursive <= 10) {
Adaptive::sleep(1);
return $this->get(++$recursive);
Expand All @@ -72,9 +72,13 @@ public function put(object $object): bool
throw new \RuntimeException('Cannot put object that not got from here');
}
unset($this->out[spl_object_hash($object)]);
if (!empty(self::$coroutine_cid)) {
$cid = array_shift(self::$coroutine_cid);
Adaptive::getCoroutine()->resume($cid, $object);
if (!empty($this->coroutine_cid)) {
$cid = array_shift($this->coroutine_cid);
$co = Adaptive::getCoroutine();
if ($co === null || $co->resume($cid, $object) === false) {
// 协程环境已不可用或等待协程已不存在,无法唤醒;把对象放回空闲队列,避免对象丢失
$this->queue->push($object);
}
return true;
}
try {
Expand Down
10 changes: 6 additions & 4 deletions src/OneBot/Driver/Workerman/TopEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,13 @@ public function onHttpRequest(array $config, TcpConnection $connection, Request
}
} catch (\Throwable $e) {
ExceptionHandler::getInstance()->handle($e);
if (isset($response)) {
$response->withStatus(500);
$response->withBody('Internal Server Error');
$connection->send($response);
// 如果异常发生在响应对象创建之前,需要先创建一个 500 响应,否则连接会一直挂起
if (!isset($response)) {
$response = new WorkermanResponse();
}
$response->withStatus(500);
$response->withBody('Internal Server Error');
$connection->send($response);
}
}
}
6 changes: 5 additions & 1 deletion src/OneBot/Driver/Workerman/WebSocketClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ public static function createFromAddress($address, array $header = []): WebSocke
public function withRequest(RequestInterface $request): WebSocketClientInterface
{
// 通过 AsyncTcpConnection 建立连接
$this->connection = new AsyncTcpConnection('ws://' . $request->getUri()->getHost() . ':' . $request->getUri()->getPort());
$port = $request->getUri()->getPort();
if ($port === null) {
$port = 80;
}
$this->connection = new AsyncTcpConnection('ws://' . $request->getUri()->getHost() . ':' . $port);
// 通过 walkor 的隐藏魔法(无语了),设置请求的 Header。因为 PSR 的 Request 对象返回 Headers 是数组形式的,我们不需要重复的 Header 只取一个就行
/* @phpstan-ignore-next-line */
$this->connection->headers = array_map(function ($x) {
Expand Down
8 changes: 7 additions & 1 deletion src/OneBot/ObjectPool/AbstractObjectPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ public function return(object $object): bool
unset($this->actives[$hash]);

// 放回队列里
return $this->queue->push($object);
if ($this->queue instanceof Channel) {
// Swoole Channel 的 push 返回 bool
return $this->queue->push($object);
}
// SplQueue 的 push 返回 void,视为成功
$this->queue->push($object);
return true;
}

abstract protected function makeObject(): object;
Expand Down
44 changes: 41 additions & 3 deletions src/OneBot/V12/Action/ActionHandlerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

abstract class ActionHandlerBase
{
/** @var int 分片上传缓存的最大数量,防止恶意请求耗尽内存 */
private const UPLOAD_FRAGMENT_MAX_COUNT = 100;

/** @var int 分片上传缓存的过期时间(秒),防止长期不结束的传输占用内存 */
private const UPLOAD_FRAGMENT_EXPIRE_TIME = 600;

/** @internal 内部使用的缓存 */
public static $core_cache;

Expand Down Expand Up @@ -115,7 +121,15 @@ public function onUploadFile(Action $action, int $stream_type = ONEBOT_JSON): Ac
case 'path':
Validator::validateParamsByAction($action, ['path' => ONEBOT_TYPE_STRING]);
$from_path = $action->params['path'];
if (!file_exists($from_path = FileUtil::getRealPath($from_path))) {
// 限制只能读取上传目录内的文件,防止任意文件读取漏洞
FileUtil::mkdir($path, 0755, true);
$path_real = realpath($path);
$from_path_real = realpath($from_path);
if ($from_path_real === false || $path_real === false || strpos($from_path_real, $path_real . DIRECTORY_SEPARATOR) !== 0) {
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file not found for path: ' . $from_path);
}
$from_path = $from_path_real;
if (!file_exists($from_path)) {
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file not found for path: ' . $from_path);
}
$file_id = md5_file($from_path);
Expand Down Expand Up @@ -170,14 +184,26 @@ public function onUploadFileFragmented(Action $action, int $stream_type = ONEBOT
if (!is_int($action->params['total_size']) || $action->params['total_size'] <= 0) {
return ActionResponse::create($action)->fail(RetCode::BAD_PARAM);
}
// 文件ID无法通过文件内容算出来,就通过时间戳获取一个文件ID
$file_id = md5(strval(microtime(true)));
// 先清理掉过期的缓存,防止内存被长期占用
foreach (self::$upload_fragment as $k => $v) {
if (time() - $v['time'] > self::UPLOAD_FRAGMENT_EXPIRE_TIME) {
unset(self::$upload_fragment[$k]);
}
}
// 超过数量上限则拒绝继续 prepare,防止内存被耗尽
if (count(self::$upload_fragment) >= self::UPLOAD_FRAGMENT_MAX_COUNT) {
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'too many fragmented uploads prepared, please retry later');
}
// 文件ID无法通过文件内容算出来,就通过时间戳 + 随机数获取一个唯一的文件ID
// (仅用 microtime 的话,快速连续 prepare 时会产生重复 ID,导致分片互相覆盖)
$file_id = md5(strval(microtime(true)) . uniqid('', true));
// 缓存段
self::$upload_fragment[$file_id] = [
'name' => $action->params['name'],
'total_size' => $action->params['total_size'],
'cache' => [],
'stream' => Stream::create(),
'time' => time(),
];
// 返回文件ID
return ActionResponse::create($action)->ok(['file_id' => $file_id]);
Expand All @@ -189,6 +215,11 @@ public function onUploadFileFragmented(Action $action, int $stream_type = ONEBOT
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file id ' . $action->params['file_id'] . ' not found or not prepared yet');
}
$file_id = $action->params['file_id'];
// 检查分片上传是否过期,过期则清理并拒绝继续传输
if (time() - self::$upload_fragment[$file_id]['time'] > self::UPLOAD_FRAGMENT_EXPIRE_TIME) {
unset(self::$upload_fragment[$file_id]);
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file id ' . $file_id . ' expired');
}
$data = $stream_type === ONEBOT_JSON ? base64_decode($action->params['data']) : $action->params['data'];
// 额外的验证,如果数据的长度和传入的size不一致,那么就给个warning
if (isset($action->params['size']) && $action->params['size'] !== strlen($data)) {
Expand All @@ -214,6 +245,8 @@ public function onUploadFileFragmented(Action $action, int $stream_type = ONEBOT
// 传入的 offset 比 stream 长度要长,说明乱序了,要先缓存起来
self::$upload_fragment[$file_id]['cache'][$action->params['offset']] = $data;
}
// 每次成功传输都刷新过期时间,避免慢速大文件传输超过 UPLOAD_FRAGMENT_EXPIRE_TIME 被中途拒绝
self::$upload_fragment[$file_id]['time'] = time();
return ActionResponse::create($action)->ok();
case 'finish': // 结束阶段
Validator::validateParamsByAction($action, ['file_id' => ONEBOT_TYPE_STRING, 'sha256' => ONEBOT_TYPE_STRING]);
Expand All @@ -222,6 +255,11 @@ public function onUploadFileFragmented(Action $action, int $stream_type = ONEBOT
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file id ' . $action->params['file_id'] . ' not found or not prepared yet');
}
$file_id = $action->params['file_id'];
// 检查分片上传是否过期,过期则清理并拒绝结束
if (time() - self::$upload_fragment[$file_id]['time'] > self::UPLOAD_FRAGMENT_EXPIRE_TIME) {
unset(self::$upload_fragment[$file_id]);
return ActionResponse::create($action)->fail(RetCode::FILESYSTEM_ERROR, 'file id ' . $file_id . ' expired');
}
// 首先验证文件是不是全的
$total = self::$upload_fragment[$file_id]['total_size'];
/** @var Stream $stream */
Expand Down
8 changes: 6 additions & 2 deletions src/OneBot/V12/OneBotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ public function build(): OneBot
{
$required_config = ['name', 'platform', 'self_id', 'logger', 'driver', 'communications'];

if (array_keys($this->components) !== $required_config) {
$missing = implode(', ', array_diff($required_config, array_keys($this->components)));
// 不校验组件配置的键顺序,只校验是否都配置齐全
$component_keys = array_keys($this->components);
sort($required_config);
sort($component_keys);
if ($required_config !== $component_keys) {
$missing = implode(', ', array_diff($required_config, $component_keys));
throw new \InvalidArgumentException('Builder must be configured before building, missing: ' . $missing);
}

Expand Down
Loading
Loading