You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

9.0 KiB

会议纪要分享:落地与上线步骤(2026-09-23)

App 里「分享 → 分享链接」发出去的那条链接,之前从来没有打开过:域名解析着但没有落地页, 而且链接里带的是用户的完整登录态。这一轮把两件事一起做掉了。


一、改了什么

1. 链接里不再是登录 JWT

之前 现在
URL 最后一段 storage.read("logintoken"),即 Authorization 头里那个 JWT 服务端签发的分享票据
拿到链接能做什么 调任何业务接口、看该账号全部纪要、消费算力 只能读这一条纪要
失效手段 只能改密码 默认 7 天自动过期

票据是自包含的 HMAC 签名串,不落库:

<payload>.<sig>
payload = base64url(JSON{"i":记录id,"u":owner uid,"e":到期unix秒})
sig     = base64url(HMAC-SHA256(shareKey, payload))
shareKey = HMAC-SHA256(FIELD_ENCRYPT_KEY, "echomeet.share.v1")

签名密钥从 FIELD_ENCRYPT_KEY 派生而不是直接用:HMAC 单向,从派生 key 推不回原 key, 两个用途不共用同一份秘密。选它是因为它已经在每台机的 .env 里,上线不需要新增任何配置项。

⚠️ 代价:票据签发后无法单独撤销,只能等过期或换 FIELD_ENCRYPT_KEY(那会让全部票据失效)。 这是刻意接受的取舍——要撤销能力时补一张吊销表即可,票据格式不用变。

接口 登录 说明
echomeet_createsharelink 要 入参 {id, expiredays},回 {ticket, expiretime}。签发前先校验这条记录是本人的
echomeet_getsharedrecord 不要 入参 {ticket},回落地页要展示的只读字段

第二个必须在 gateway 的 WhiteList 里——打开链接的人本来就没登录。已加进 gateway.yaml.example,⚠️ 各机上手工维护的 confs/gateway.yaml 要同步加这一行,漏了就是 code:18,表现为「分享链接打开一片空白」。

响应刻意不含 uid / filepath / taskid / logid / *_svc_id:这是任何人凭链接就能 打开的接口,多回一个字段就是多泄一份内部信息。

3. 落地页嵌进了 gateway 二进制

modules/gateway/share_page.html 经 go:embed 打进二进制,路由 GET /share/*any。

为什么不做成静态文件下发:页面必须跟 echomeet_getsharedrecord 的协议同步, 版本对不上就是白页。嵌进来意味着「部署了新镜像 = 页面也是新的」,服务器上不用维护任何文件。

⚠️ 路由必须是 catch-all。真实链接形如 /share/ym.ymaikj.com/ymaikj.com/42/<ticket>, 路径里的段是数据不是文件路径,用 gin.Static 会去磁盘找同名文件,一律 404。


二、上线状态(2026-09-23 实测)

步骤 状态
镜像 starpivot-app:0.2.5 构建并推送 ✅
正式机 ym-a11 部署(0.2.4 → 0.2.5) ✅ 容器 healthy,日志无 panic
confs/gateway.yaml 加 echomeet_getsharedrecord 白名单 ✅ 已改并重启(备份 gateway.yaml.bak-20260923)
confs/home.yaml 的 AppStoreBundleID 改正 ✅ 顺带修掉(备份 home.yaml.bak-20260923)
NPM 加 share.ymaikj.com 反代 + 证书 ❌ 待办,见下

容器内实测:

/share/... → HTTP 200, 15042 bytes, <title>会议纪要</title>
echomeet_getsharedrecord + 假票据 → {"code":20,"msg":"分享链接无效或已过期"}   ← 白名单生效
echomeet_createsharelink 未登录     → {"code":18,"msg":"NoLogin"}              ← 路由已注册

只剩一步:NPM 反代

share.ymaikj.com 的 DNS 已指向 47.116.104.181,但 NPM 上还没有它的 proxy host (现有四条:console / deepglass / mcp / ym)。这一步要登录 NPM 后台操作:

  1. Proxy Hosts → Add Proxy Host
    • Domain Names: share.ymaikj.com
    • Scheme http,Forward Hostname ym-a11,Forward Port 7100
    • 打开 Block Common Exploits
  2. SSL 标签页 → Request a new SSL Certificate
    • ⚠️ 现有证书都是单域名的(npm-1…npm-mcp,没有通配符),所以必须给 share.ymaikj.com 单独签一张
    • 勾 Force SSL、HTTP/2
    • 不要开 "Use a DNS Challenge",保持默认的 HTTP-01 即可

⚠️ 这台机上签证书用的是 HTTP-01(webroot),不是 DNS challenge。 2026-09-23 实测:5 张现存证书的 /etc/letsencrypt/renewal/npm-*.conf 全部是 authenticator = webroot,容器里也没装 certbot-dns-aliyun (certbot plugins 里搜不到 dns- 开头的任何东西)。最近那张 mcp.ymaikj.com 就是这么签成的。所以不需要阿里云 AK/SK、不需要填 Propagation Seconds、 不需要先装插件。运维-容器断网与免费证书排查.md 那篇写的 DNS challenge 是给灵谱那台机的,别照搬到这里。

HTTP-01 的唯一前置是 80 端口从公网能打到这台机,且该域名的 DNS 已生效—— 两条都已满足(NPM 监听 0.0.0.0:80,share.ymaikj.com 解析到 47.116.104.181)。 ⚠️ 顺序不能反:必须先保存 proxy host、再去签证书。证书验证时 Let's Encrypt 会来访 http://share.ymaikj.com/.well-known/acme-challenge/...,没有这条 proxy host, nginx 上就没有匹配该 server_name 的 80 端口配置,验证必然失败。

配完验收:

curl -sI https://share.ymaikj.com/share/ym.ymaikj.com/ymaikj.com/1/x | head -1   # 期望 200

然后 App 里打开一条纪要 → 分享 → 分享链接 → 发给自己 → 点开。


三、原始上线步骤(留档)

1. 部署新镜像

cd deploy/app
DEPLOY_KEY=$HOME/Documents/keys/loginscre.pem ./prod-build.sh <版本号>
DEPLOY_KEY=$HOME/Documents/keys/loginscre.pem ./prod-deploy.sh ym along <版本号>

2. 补 gateway 白名单(容易漏,漏了整条链路不通)

正式机 47.116.104.181 上 /home/work/ym-a11/confs/gateway.yaml,在 WhiteList 下加:

      - echomeet_getsharedrecord

改完 docker restart ym-a11 即可——confs/ 是以卷挂进容器的,进程重启就会重读 yaml。 ⚠️ 「必须重建容器」那条规矩只适用于改 .env(环境变量在容器创建时注入,restart 不会重读)。

3. NPM 加一条反代

share.ymaikj.com → ym-a11:7100,申请证书。

证书走阿里云 DNS challenge,踩坑记录见 运维-容器断网与免费证书排查.md: Propagation Seconds 必须填整数(如 120),插件要先手动 docker exec <NPM> pip install -i https://mirrors.aliyun.com/pypi/simple certbot-dns-aliyun。

4. 验收

# ① 落地页出得来(应返回 200 + HTML)
curl -sI https://share.ymaikj.com/share/ym.ymaikj.com/ymaikj.com/1/x | head -3

# ② 免登录接口通了(应返回 code:20 链接无效,而**不是** code:18 NoLogin)
curl -sX POST https://ym.ymaikj.com/api/home/echomeet_getsharedrecord \
  -H 'Content-Type: application/json' -d '{"ticket":"bad"}'

拿到 code:18 = 第 2 步的白名单没生效。拿到 code:20 = 通了(票据确实是假的)。

③ 真机:App 里打开一条纪要 → 分享 → 分享链接 → 发给自己 → 点开。

5. 客户端要发版

链接格式变了(最后一段 token → ticket),旧包分享出来的链接落地页打不开 (它会拿旧 JWT 当票据送上去,签名校验必然失败,显示「链接已失效」)。

这不构成兼容问题:分享功能此前从来没有真正可用过,外面不存在能打开的老链接。


四、落地页里两处不能动的地方

后端域名白名单

var ALLOWED_HOSTS = ['ym.ymaikj.com', 'ym-dev.ymaikj.com'];

serverHost 是从 URL 路径里读出来的。不校验就等于:任何人构造一条 /share/<攻击者域名>/.../<票据> 的链接,就能让打开它的人的浏览器向攻击者的服务器发请求, 顺带把票据送过去。既是钓鱼跳板也是票据泄漏。新增环境要在这里加一行。

先转义再套 Markdown 正则

总结是 Markdown(与客户端 _exportMarkdownPdf 同源),页面里有个极简渲染器。 它先 esc() 再套正则——内容来自用户录音与模型输出,顺序反了就是 XSS。

页面刻意不引第三方 Markdown 库:这页要在国内手机、可能很差的网络下秒开, 为渲染几段标题列表去拉 CDN 不值得,而且 CDN 被墙时整页就废了。


五、还没做

  • 票据吊销:见上文取舍。
  • 国际化:落地页是中文的。分享出去的对象大概率与创建者同语言,先不做。
  • web.ymaikj.com:App 里「EAIMAR Web」入口指向它,apps/web 仍是空壳,点开打不开。 与本次分享链路无关,但它俩是同一类「域名解析了、东西没做」的欠账。