feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace addon\iot\service;
|
||||
|
||||
use Workerman\Mqtt\Client;
|
||||
|
||||
/**
|
||||
* 外部 Broker 转发器(规则引擎的投递通道)
|
||||
*
|
||||
* 将本地匹配到的消息转发到外部 MQTT Broker(如 EMQX),实现"桥接到 EMQX"。
|
||||
* 每个目标 Broker 维护一个异步常驻连接(基于 Workerman\Mqtt\Client,复用 Broker 的事件循环),
|
||||
* 连接未就绪时消息进入待发缓冲(上限 200,避免无限堆积),连接成功后冲刷。
|
||||
*
|
||||
* 依赖 workerman/mqtt(composer require workerman/mqtt)。未安装时 isAvailable()=false,
|
||||
* 规则引擎自动跳过转发,不影响 Broker 其它功能。
|
||||
*/
|
||||
class Forwarder
|
||||
{
|
||||
/** @var array key => ['client'=>Client|null,'connected'=>bool,'pending'=>callable[]] */
|
||||
protected $clients = [];
|
||||
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return class_exists(Client::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按规则转发一条消息到外部 Broker
|
||||
*/
|
||||
public function forward(array $rule, string $topic, string $msg, int $qos): void
|
||||
{
|
||||
if (!$this->isAvailable()) {
|
||||
return;
|
||||
}
|
||||
$key = md5(($rule['target_broker'] ?? '') . '|' . ($rule['target_clientid'] ?? '') . '|' . ($rule['target_username'] ?? ''));
|
||||
if (!isset($this->clients[$key]) || $this->clients[$key]['client'] === null) {
|
||||
$this->connect($key, $rule);
|
||||
}
|
||||
$entry = &$this->clients[$key];
|
||||
$target = $this->buildTargetTopic((string) ($rule['target_topic'] ?? ''), $topic);
|
||||
$pubQos = min($qos, (int) ($rule['target_qos'] ?? 0));
|
||||
|
||||
$send = function () use ($entry, $target, $msg, $pubQos) {
|
||||
try {
|
||||
$entry['client']->publish($target, $msg, ['qos' => $pubQos]);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
};
|
||||
|
||||
if ($entry['connected']) {
|
||||
$send();
|
||||
} elseif (count($entry['pending']) < 200) {
|
||||
$entry['pending'][] = $send;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立到目标 Broker 的异步连接(懒连接)
|
||||
*/
|
||||
protected function connect(string $key, array $rule): void
|
||||
{
|
||||
$this->clients[$key] = ['client' => null, 'connected' => false, 'pending' => []];
|
||||
try {
|
||||
$uri = $this->buildUri((string) ($rule['target_broker'] ?? ''));
|
||||
$opts = [];
|
||||
if (!empty($rule['target_clientid'])) {
|
||||
$opts['clientId'] = $rule['target_clientid'];
|
||||
}
|
||||
if (!empty($rule['target_username'])) {
|
||||
$opts['username'] = $rule['target_username'];
|
||||
$opts['password'] = $rule['target_password'] ?? '';
|
||||
}
|
||||
$client = new Client($uri, $opts);
|
||||
$self = $this;
|
||||
$client->onConnect = function () use ($key, $self) {
|
||||
$self->clients[$key]['connected'] = true;
|
||||
foreach ($self->clients[$key]['pending'] as $cb) {
|
||||
try { $cb(); } catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
$self->clients[$key]['pending'] = [];
|
||||
};
|
||||
$client->onError = function () use ($key, $self) {
|
||||
$self->clients[$key]['connected'] = false;
|
||||
};
|
||||
$client->onClose = function () use ($key, $self) {
|
||||
$self->clients[$key]['connected'] = false;
|
||||
};
|
||||
$client->connect();
|
||||
$this->clients[$key]['client'] = $client;
|
||||
} catch (\Throwable $e) {
|
||||
$this->clients[$key] = ['client' => null, 'connected' => false, 'pending' => []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function buildUri(string $broker): string
|
||||
{
|
||||
$broker = trim($broker);
|
||||
if ($broker === '' || strpos($broker, '://') !== false) {
|
||||
return $broker === '' ? 'mqtt://127.0.0.1:1883' : $broker;
|
||||
}
|
||||
return 'mqtt://' . $broker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标主题模板:空或 ${topic} 表示沿用原主题,否则替换变量
|
||||
*/
|
||||
protected function buildTargetTopic(string $tpl, string $srcTopic): string
|
||||
{
|
||||
if ($tpl === '' || $tpl === '${topic}') {
|
||||
return $srcTopic;
|
||||
}
|
||||
return str_replace('${topic}', $srcTopic, $tpl);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user