粥粥
学了个习

把宝塔面板装进手机:329 条接口,为什么不能只做一个调用台

阅读 5 约 8 分钟 暂无评论

App 里那一格「宝塔」现在能干活了:建站、改 PHP 版本、加域名、建库改密、改计划任务、翻文件、配反向代理。做这一整排功能的过程里,我被一条硬约束反复教育——接口不能猜着发。

起点是一句很直白的否决

最早我做出来的东西长这样:把宝塔官方文档(https://docs.bt.cn/api/)里 19 个模块、329 条接口、522 个参数抓成一份 JSON 打进包里,做一个通用调用台:选模块 → 选 action → 按参数表填格子 → 执行 → 把回包摊出来。

功能上它是全覆盖的。用起来它是没法用的。用户的原话:

> 「宝塔页面的功能至少得是可以各种操作的吧,你给我整个调用台这种方案我是不认可的,我想要的是正常的能操作,像面板端一样。」

于是推倒重来,一个模块一个模块做成真正的页面:网站、数据库、FTP、MySQL、计划任务、文件管理器、反向代理,每页都是「列表 → 点进对象 → 页签里各只手」的形状。调用台留着当兜底入口,但界面上写明它不是操作方式。

为什么不能猜:签名失败会锁一小时

面板 API 的鉴权是 request_token = md5(时间戳 + md5(密钥)),application/x-www-form-urlencoded 的 POST。时间戳窗口只有几分钟(手机和面板时间差多了会被判认证失败,这一条 App 单独报,不混成「密钥不对」)。

关键是这条:

> 连续 20 次签名/参数失败,面板把整个 API 锁一小时。

这意味着「先发一发试试参数对不对」这种做法是有代价的,探索的成本必须付在别的地方。我后来的规矩是:

  • 表单在本地先把面板会拒的东西拒掉(名称必填、目标 URL 必须带协议、来源路径以 / 开头、proxy_type 只有 http 和 unix 两档),发出去之前不让下一步
  • 参数名不确定时,用不存在的值去试,而不是拿真对象试
  • 写流程一律先在本地自签 HTTPS 的假面板上端到端跑通
  • 正式面板上只允许碰我自己造的临时对象(aa-test-* 命名,建完改完就删),原有的 9 个站点、4 个数据库、8 条计划任务、Docker 容器、那两个在册的代理,全部只读

读的那一半:同一份数据有四种形状

  • 失败几乎全是 HTTP 200。成不成只看响应体的 status;msg 有时一句话、有时是一串用「;」拼起来的校验错误。认证和白名单类的拒绝回的是 HTML,所以先看 Content-Type 再决定解不解 JSON。
  • 13.1.0 的 GetSystemTotal 是彻底摊平的键(cpuRealUsed、memTotal 单位 MB、time 是一句「41天」),而且它根本不给负载。负载、内存百分比、磁盘、站点计数只在 GetNetWork 里,所以看板必须合着读两个口,GetNetWork 挂了也不能让整页空掉。
  • GetDiskInfo 给的是按下标排的位置数组:size[总, 用, 剩, 率]、byte_size[总, 用, 剩],不是带键名的对象。
  • GetCrontab 是裸数组、GetNetWork 是没有信封的裸对象、getData 裹成 {"data":{"rows":[…],"records":N}},字段名还按版本变(name / sName、cycle / where1)。
  • 列表的 records 常常不在,总数写在旁边那段分页 HTML 的「共 N 条」里,只能抠出来。
  • 文档里 /btdocker/…/get-list 这种路径风格,action 名照抄必失败——面板认的是下划线:get_list。
  • getData 查 databases 时每行都带 password 原文,site?action=GetSSL 回包里有私钥原文。解析层按键名黑名单直接丢,模型里连一个装得下它的字段都不给,界面上也不存在能把私钥显示出来的路径。调用台的回包是先脱敏再上屏:password / private_key / token / secret 这类键名(含 _password、_key、_token 后缀)整块涂成 ***。

写的那一半:面板的脾气全在这儿

计划任务那一页的坑几乎都在写侧,实测之后都写进了 BtCronApi 的类注释:

  • 时刻写进去叫 hour / minute,读出来叫 where_hour / where_minute。建任务时发 where_hour=5/where_minute=6,那一格落回 null,列表里只剩干巴巴一句「每天」;改发 hour=5/minute=6 才存成 05:06。modify_crond 一样,而且两个名字一起发时是 hour 赢
  • modify_crond 是整行覆盖:它只认你发过去的那几格,没发的直接清空。所以改任务要从列表那一行取底稿,只盖用户在表单里摆出来的格子
  • 面板那一行的 name 不会跟着改,改标题要动 rname;sBody 才是脚本正文
  • set_cron_status 是「翻转」不是「设为开」,所以确认框先说现在读到的是什么状态,按完再重读列表核对
  • 批量接口的逐条判决会说谎:set_cron_status_all 回「成功」而状态没动。所以批量一律以重读到的列表为准
  • 「立即执行」要走批量的 exec,面板单独那个 set_execute_script 按下去没反应;而且实测暂停中的任务被 exec 一样会跑,这句话就写在那颗按钮的确认框上
  • set_auto_config、set_rotate_log_status 这两个名字看着像设置项,实际会建出一条新任务——所以那种格子根本不给开关

文件管理器那页八条脾气:

  • 带 search=link 发过去,面板根本不看这一格、照样回整层,却把关键词记进它自己的 search_history(还会出现在网页端)。所以那一格干脆不发,本地筛完写「这一页筛出 N 条」
  • test_path 先探一次:说不存在就不发列表请求,当场把「这个路径不存在」摊出来,当前列表不动
  • 改名和移动是同一个 MvFile{sfile,dfile},dfile 必须是目标全路径,给成目录面板直接回「移动文件或目录失败!」,不替你拼文件名
  • 递归改权限做不到:对目录发 SetFileAccess{all:1} 面板回「设置成功」,实测只改它自己那一层,子文件仍是原来的 755/www——界面上就只说「改这一层」
  • 回收站那一格 path 装的是存盘化名 rname;Re_Recycle_bin 的 rpath 同样是目标全路径,而且目标位置已有东西时面板会先把占位的改名成 原名_时间戳.bak 再放新的进来
  • GetFileHistory 不吃 limit,而且它的 status 是字符串 "true" 不是信封,整包得当数据读

反向代理那一页的难点不在接口数量(15 条),在面板把「装在哪个 web 软件上」写进了 URL(/mod/proxy/com/<方法>/<stype>),而没有任何接口能问这台机器装的是 nginx、apache 还是 openresty——选错了整块回一张空表。所以引擎在页顶三选一,空表那句直接写「按 Apache / OpenResty 再看一眼」,不让人以为这台一条代理都没有。另外 get_global_conf 实际给的是一大堆平铺的 nginx 模板变量(比文档那则示例宽得多,文档没写的摊进「面板还给了」而不是报错),proxy_cache.cache_status 面板给的是布尔 false,不是文档写的 "0" / "1"——只按「有没有这一格」判就会把关着的缓存说成开着。

一条总规矩:按完一律重读

这一整排的写操作发完都再取一次列表,界面上的状态来自重读到的那一行,不是面板那句「修改成功」。上面那些「回成功而没生效」的实测就是这条规矩的来源。

做不到的事,界面上就没有那颗按钮

我特意把「做不到」摊在页底写清楚,因为最坏的情况是给用户一个按下去会静默失败的按钮:

  • 删域名、改伪静态模板:面板收不到有效参数,试遍三种形状都不行 → 只读
  • 改库的字符集:GetDbType 那一口实测报错,库建出来就只能靠 SQL 自己 ALTER
  • 还原 / 导入 SQL:接口只认服务器磁盘上已有的 .sql 路径,手机上没有这条来路
  • 删备份文件:DelBackup 回「已删除」而文件纹丝不动——这种事不能给 App 一个按钮
  • 改一条代理规则的转发目标 / 删一条规则:15 条 action 里根本没有 modify 规则的出口,网页上那一下是「加新的、删旧的」两步拼的
  • 改 FTP 备注和根目录:接口侧六个候选 action 名实测全回「指定参数无效!」,网页点两下就能改的事这里就是做不到
  • 把文件打包成 zip、生成下载直链(create_download_url 实测 400 空 body)、回收站开关(文档只给 db 一个参数,发错的后果是「以后删除直接永久删」)

还有一条实测副作用:删 FTP 账户不会带走根目录——账户没了,/www/wwwroot/ 下那个目录原样留着。这句话就写在确认框里。

危险动作要有门槛

这把密钥等同服务器 root,面板没有只读作用域。所以:

  • 密钥和图床密码、接口密钥一样只存 Keystore 密文(AES-256-GCM),界面只出掩码
  • 停用/启用/切 PHP/改运行目录点一下就成;删站、关 SSL、删重定向、删代理要点确认;删站要手打站点主域名、删库和删任务要手打「删除」,不打或打错按钮按不动
  • 批量启停只弹确认、不打字,但确认框里把要动的是哪几条点名出来
  • 「清空回收站」这颗在正式面板上一次都没按过——那台的回收站里躺着 1545 条别人的记录,按下去就是把它们一起抹掉。界面上只能保证请求形状按文档发对,不保证它在真机上是什么结果
  • send_baota(把服务器上的文件原文发给第三方)这种口子,App 里不给按钮

最后

这一整排做完,我最大的收获不是掌握了宝塔接口,而是学会了一件更通用的事:当探索本身要付锁一小时的代价时,「先试一发」就不再是调试手段,而是需要预算的操作。 于是所有猜测都被推回到两个地方——读源码/读文档,以及在本地起一个假面板。这两个都便宜,而且假面板那个还能把实测到的反直觉形状钉成单测 fixture。

面板本身的地址和密钥当然不会进仓库:单测 fixture 里用的是 192.0.2.1。

粥粥

种一棵树,等它长大

转载请注明出处

不知道看什么?

随机去往一篇

评论 0

添加新评论

还没有评论,来抢沙发