Files
YwxAppThink/addon/iot/protocol/Mqtt.php
T
yangwx 18101cf11a 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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

177 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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\protocol;
/**
* MQTT 协议编解码器(Workerman 标准协议类)
*
* 遵循 Workerman 自定义协议规范(https://www.workerman.net/doc/workerman/protocols/how-protocols.html):
* - 必须实现 input() / decode() / encode() 三个静态方法;
* - 无需强制继承 ProtocolInterface,只要类包含这三个静态方法即可;
* - 通过 $worker->protocol = Mqtt::class 绑定,Workerman 在收到数据时自动:
* input() 判定一个完整包的长度(>0 包长 / 0 继续等待 / -1 协议错误断开)
* decode() 解析完整包,结果作为 $data 传入 onMessage
* 业务侧 $connection->send($data) 时自动调用 encode() 打包。
*
* 解码后数据结构(供 Broker 使用):
* ['cmd' => int, 'flags' => int, 'body' => string]
* - cmd :MQTT 控制报文类型(高 4 位固定头)
* - flags:固定头低 4 位(PUBLISH 时为 dup/qos/retain)
* - body :变长头 + 负载(已剔除固定头与剩余长度字段)
*
* 编码时同样接收上述数组,也可直接传原始字符串(透传)。
*/
class Mqtt
{
/* ---------- MQTT 控制报文类型 ---------- */
const CMD_CONNECT = 1;
const CMD_CONNACK = 2;
const CMD_PUBLISH = 3;
const CMD_PUBACK = 4;
const CMD_PUBREC = 5;
const CMD_PUBREL = 6;
const CMD_PUBCOMP = 7;
const CMD_SUBSCRIBE = 8;
const CMD_SUBACK = 9;
const CMD_UNSUBSCRIBE = 10;
const CMD_UNSUBACK = 11;
const CMD_PINGREQ = 12;
const CMD_PINGRESP = 13;
const CMD_DISCONNECT = 14;
const CMD_AUTH = 15;
/**
* 检查包的完整性,返回当前包在 buffer 中的总长度
* @param string $buffer 当前收到的数据缓冲
* @param mixed $connection Workerman 传入的连接对象(此处未使用)
* @return int >0 完整包长度 | 0 数据不足需等待 | -1 协议错误(断开连接)
*/
public static function input(string $buffer, $connection = null): int
{
$len = strlen($buffer);
if ($len < 2) {
return 0; // 至少 1 字节固定头 + 1 字节长度
}
$first = ord($buffer[0]);
$cmd = ($first >> 4) & 0x0F;
if ($cmd < 1 || $cmd > 15) {
return -1; // 非法控制报文类型
}
// 解码剩余长度(变长整数,最多 4 字节)
$value = 0;
$multiplier = 1;
$i = 0;
$pos = 1;
do {
if ($pos + $i >= $len) {
return 0; // 长度字节尚未收全
}
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
if ($i > 4) {
return -1; // 剩余长度字段超过 4 字节,协议错误
}
} while (($byte & 0x80) !== 0);
$headerLen = 1 + $i; // 固定头(1) + 剩余长度字段(i)
if ($len - $headerLen < $value) {
return 0; // 包体尚未收全
}
return $headerLen + $value;
}
/**
* 解包:把完整包解析为结构化数组,作为 onMessage 的 $data
* @param string $buffer 一个完整 MQTT 报文(含固定头)
* @param mixed $connection
* @return array ['cmd'=>int,'flags'=>int,'body'=>string]
*/
public static function decode(string $buffer, $connection = null): array
{
if ($buffer === '') {
return ['cmd' => 0, 'flags' => 0, 'body' => ''];
}
$first = ord($buffer[0]);
$cmd = ($first >> 4) & 0x0F;
$flags = $first & 0x0F;
// 解码剩余长度,定位包体起始
$value = 0;
$multiplier = 1;
$i = 0;
$pos = 1;
do {
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
} while (($byte & 0x80) !== 0);
$body = substr($buffer, $pos + $i, $value);
return ['cmd' => $cmd, 'flags' => $flags, 'body' => $body];
}
/**
* 打包:把结构化数组编码为可发送字节流
* @param array|string $data 编码结构 ['cmd'=>int,'flags'=>int,'body'=>string] 或原始字符串
* @param mixed $connection
* @return string
*/
public static function encode($data, $connection = null): string
{
if (is_string($data)) {
return $data; // 兼容直接发送原始字节
}
$cmd = (int) ($data['cmd'] ?? 0);
$flags = (int) ($data['flags'] ?? 0);
$body = (string) ($data['body'] ?? '');
$fh = chr(($cmd << 4) | ($flags & 0x0F));
return $fh . self::encodeRemainingLength(strlen($body)) . $body;
}
/**
* 编码 MQTT 剩余长度(变长整数,最大 4 字节)
*/
public static function encodeRemainingLength(int $len): string
{
$bytes = '';
do {
$byte = $len % 128;
$len = intdiv($len, 128);
if ($len > 0) {
$byte |= 0x80;
}
$bytes .= chr($byte);
} while ($len > 0);
return $bytes;
}
/**
* 解码 MQTT 剩余长度,返回 [值, 占用字节数]
*/
public static function decodeRemainingLength(string $buffer, int $pos = 0): array
{
$value = 0;
$multiplier = 1;
$i = 0;
do {
$byte = ord($buffer[$pos + $i]);
$value += ($byte & 0x7F) * $multiplier;
$multiplier *= 128;
$i++;
if ($i > 4) {
break;
}
} while (($byte & 0x80) !== 0);
return [$value, $pos + $i];
}
}