qq-bot-roost 0.8.11 → 0.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="零依赖">
11
11
  <img src="https://img.shields.io/badge/tests-600%2B%20passed-brightgreen" alt="测试 600+ 项">
12
12
  <img src="https://img.shields.io/badge/plugins-JS%20%7C%20Python%20%7C%20Go%20%7C%20any-blue" alt="多语言插件">
13
- <img src="https://img.shields.io/badge/version-0.8.11-blueviolet" alt="版本 0.8.11">
13
+ <img src="https://img.shields.io/badge/version-0.8.12-blueviolet" alt="版本 0.8.12">
14
14
  <img src="https://img.shields.io/badge/license-Non--Commercial-red" alt="禁止商用">
15
15
  <a href="https://github.com/tdcq9594/qq-bot-roost/actions"><img src="https://img.shields.io/github/actions/workflow/status/tdcq9594/qq-bot-roost/ci.yml?branch=master" alt="CI"></a>
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qq-bot-roost",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "type": "module",
5
5
  "description": "栖 Roost:零依赖的 QQ 官方机器人框架,填 AppID/AppSecret 即连,插件可写任意语言",
6
6
  "license": "SEE LICENSE IN LICENSE",
package/src/web/server.js CHANGED
@@ -111,6 +111,57 @@ const CSP_PAGE = [
111
111
  "connect-src http: https:",
112
112
  ].join('; ');
113
113
 
114
+ /**
115
+ * 插件页的「自动带令牌」补丁 —— 注入到插件页 HTML 最前面。
116
+ *
117
+ * **为什么需要(0.8.12 修的回归)**:iframe 地址上带的凭证以前是面板密钥
118
+ * `?key=`,插件页面自己把它转发给接口。0.8.9 起换成了一次性页面令牌
119
+ * `?pt=`,但**已有插件的页面还在转发 `key`** —— 于是它们发出的请求一个
120
+ * 凭证都没带;iframe 又是 opaque origin(`Origin: null`),`isSameOrigin()`
121
+ * 判跨站 → 403,页面永远停在「加载中…」,控制台只看到 `Failed to load
122
+ * resource: 403`。dice 的战绩页就是这么挂的。
123
+ *
124
+ * 不能指望每个插件作者都回来改这一行(市场里的插件都在外部仓库),
125
+ * 所以框架替页面把 `pt` 补上。
126
+ *
127
+ * **安全边界没变**:
128
+ * - 只补发给**面板自己**(目标 origin 必须等于文档基准的 origin),
129
+ * 第三方站点拿不到令牌;
130
+ * - 能碰哪些路径仍由 `pageTokenAllows()` 决定,拿着令牌也调不了
131
+ * /api/config、/api/logs、/api/bots。
132
+ *
133
+ * @param {string} pt 本次请求的页面令牌
134
+ * @returns {string} 一段 `<script>`,无令牌时返回空串
135
+ */
136
+ export const pageTokenPatch = (pt) => {
137
+ const token = String(pt ?? '').replace(/[^A-Za-z0-9]/g, '');
138
+ if (!token) return '';
139
+ return (
140
+ '<script>(function(){var PT=' +
141
+ JSON.stringify(token) +
142
+ ',BASE=document.baseURI||location.href,SELF="";' +
143
+ 'try{SELF=new URL(BASE).origin}catch(e){}' +
144
+ // 只给面板自己的地址补令牌:第三方 origin 原样发出,绝不外泄
145
+ 'function add(u){try{var n=new URL(u,BASE);' +
146
+ 'if(SELF&&n.origin!==SELF)return u;' +
147
+ 'if(n.searchParams.get("pt"))return n.href;' +
148
+ 'n.searchParams.set("pt",PT);return n.href}catch(e){return u}}' +
149
+ 'var f=window.fetch;if(f)window.fetch=function(i,o){try{' +
150
+ 'if(typeof i==="string")i=add(i);' +
151
+ 'else if(i instanceof URL)i=new URL(add(i.href));' +
152
+ 'else if(typeof Request!=="undefined"&&i instanceof Request){var h=add(i.url);' +
153
+ 'if(h!==i.url)i=new Request(h,i)}}catch(e){}return f.call(window,i,o)};' +
154
+ 'var X=window.XMLHttpRequest;' +
155
+ 'if(X&&X.prototype&&X.prototype.open){var O=X.prototype.open;' +
156
+ 'X.prototype.open=function(m,u){if(typeof u==="string"){' +
157
+ 'try{arguments[1]=add(u)}catch(e){}}return O.apply(this,arguments)}};' +
158
+ 'var E=window.EventSource;' +
159
+ 'if(E){var W=function(u,c){return new E(add(String(u)),c)};W.prototype=E.prototype;' +
160
+ 'try{W.CONNECTING=E.CONNECTING;W.OPEN=E.OPEN;W.CLOSED=E.CLOSED}catch(e){}' +
161
+ 'window.EventSource=W}})();<\/script>'
162
+ );
163
+ };
164
+
114
165
  const MIME = {
115
166
  '.html': 'text/html; charset=utf-8',
116
167
  '.css': 'text/css; charset=utf-8',
@@ -1637,9 +1688,15 @@ export async function startWebServer({
1637
1688
  * 所以有 doctype 就插在它后面。
1638
1689
  */
1639
1690
  const style = PAGE_THEME[theme];
1691
+ /**
1692
+ * 自动带令牌的补丁必须比页面自己的脚本**先**执行,所以跟主题样式
1693
+ * 一起插在最前面(doctype 之后 / 没有 doctype 就文档开头)。
1694
+ */
1695
+ const patch = pageTokenPatch(url.searchParams.get('pt') || ptPage?.token || '');
1696
+ const prefix = patch + style;
1640
1697
  const html = /^<!doctype/i.test(p.html.trimStart())
1641
- ? p.html.replace(/^(\s*<!doctype[^>]*>)/i, `$1${style}`)
1642
- : style + p.html;
1698
+ ? p.html.replace(/^(\s*<!doctype[^>]*>)/i, `$1${prefix}`)
1699
+ : prefix + p.html;
1643
1700
  return res.end(html);
1644
1701
  }
1645
1702