diff --git a/README.md b/README.md index 5869bb9..db91388 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ Sub-Store 后端的 Cloudflare Workers 移植版 - **完整功能**:复用原始后端全部业务逻辑(订阅管理、格式转换、下载、预览等) - **预编译解析器**:peggy 文法在构建时编译,避免运行时 eval() +## 相较原仓库的新增功能 + +- **脚本过滤 / 操作支持**:通过 QuickJS WASM 沙箱在 Workers 运行时执行用户脚本,支持 Script Filter 和 Script Operator(含快捷脚本与函数式脚本),弥补了原仓库因 Workers 禁止 `eval` / `new Function` 而无法使用脚本操作的限制 +- **脚本引擎可切换**:`wrangler.toml` 中通过 `[vars] SCRIPT_ENGINE` 控制脚本引擎,设为 `"quickjs"` 启用,未设置则禁用并返回引导提示 +- **OpenAPI 适配层补全**:修复 Workers 版 `OpenAPI` 缺失 `warn` 方法导致解析订阅时 `I.warn is not a function` 报错的问题 + ## 目录 - [部署](#部署)(核心流程,建议从这里开始) @@ -586,6 +592,7 @@ https://raw.githubusercontent.com/Yu9191/sub-store-workers/main/surge/SubStorePa ## 致谢 基于 [Sub-Store](https://github.com/sub-store-org/Sub-Store) 项目,感谢原作者及所有贡献者。 +反馈请在 [LINUX DO](https://linux.do) 站内反馈 ## 许可证 diff --git a/esbuild.js b/esbuild.js index e7c53b4..78eb6fd 100644 --- a/esbuild.js +++ b/esbuild.js @@ -114,7 +114,12 @@ const evalRewritePlugin = { contents = contents.replace( /function createDynamicFunction\(name, script, \$arguments, \$options\) \{[\s\S]*?\n\}/, `function createDynamicFunction(name, script, $arguments, $options) { - throw new Error('Script Operator is not supported in Cloudflare Workers because dynamic code execution through eval/new Function is disabled. Use built-in filters/operators, mihomo YAML patch, or an external trusted execution service.'); + const engine = globalThis.__workerEnv?.SCRIPT_ENGINE; + if (engine === 'quickjs') { + const { createScriptFunction } = require('@/vendor/quickjs-executor'); + return createScriptFunction(script, name, $arguments, $options); + } + throw new Error('Script Operator is not supported. Set SCRIPT_ENGINE="quickjs" in wrangler.toml [vars] to enable. Alternative: use built-in filters/operators, mihomo YAML patch, or an external trusted execution service.'); }`, ); } @@ -231,6 +236,7 @@ const nodeStubPlugin = { 'https', 'os', 'crypto', + 'dgram', ]; for (const mod of stubs) { @@ -274,6 +280,10 @@ const nodeStubPlugin = { target: 'es2022', outfile: path.join(__dirname, 'dist', 'worker.js'), plugins: [aliasPlugin, peggyPrecompilePlugin, evalRewritePlugin, nodeStubPlugin], + loader: { + '.wasm': 'copy', + }, + assetNames: '[name]', define: { 'process.env.NODE_ENV': '"production"', }, diff --git a/package-lock.json b/package-lock.json index 8d54faf..66367e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "sub-store-workers", "version": "1.1.0", "dependencies": { + "@jitl/quickjs-wasmfile-release-sync": "^0.32.0", "age-encryption": "^0.3.0", "buffer": "^6.0.3", "ip-address": "^9.0.5", @@ -18,8 +19,10 @@ "ms": "^2.1.3", "nanoid": "^3.3.3", "peggy": "^2.0.1", + "quickjs-emscripten": "^0.32.0", "semver": "^7.6.3", - "static-js-yaml": "^1.0.0" + "static-js-yaml": "^1.0.0", + "yaml": "^2.9.0" }, "devDependencies": { "esbuild": "^0.19.8", @@ -1093,6 +1096,48 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jitl/quickjs-ffi-types": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz", + "integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==", + "license": "MIT" + }, + "node_modules/@jitl/quickjs-wasmfile-debug-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz", + "integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-debug-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz", + "integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz", + "integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz", + "integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1829,6 +1874,31 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/quickjs-emscripten": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz", + "integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", + "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-release-sync": "0.32.0", + "quickjs-emscripten-core": "0.32.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quickjs-emscripten-core": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz", + "integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, "node_modules/quote-stream": { "version": "0.0.0", "resolved": "https://registry.npmmirror.com/quote-stream/-/quote-stream-0.0.0.tgz", @@ -2132,6 +2202,7 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -2149,6 +2220,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -2662,6 +2734,21 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index 02a03d8..45402f6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "prepush": "node -e \"let f=require('fs'),t=f.readFileSync('wrangler.toml','utf8');if(!t.includes('你的KV命名空间ID')){console.error('\\x1b[31m请先清除 wrangler.toml 中的 KV ID!\\x1b[0m');process.exit(1)}\"" }, "dependencies": { + "@jitl/quickjs-wasmfile-release-sync": "^0.32.0", "age-encryption": "^0.3.0", "buffer": "^6.0.3", "ip-address": "^9.0.5", @@ -23,8 +24,10 @@ "ms": "^2.1.3", "nanoid": "^3.3.3", "peggy": "^2.0.1", + "quickjs-emscripten": "^0.32.0", "semver": "^7.6.3", - "static-js-yaml": "^1.0.0" + "static-js-yaml": "^1.0.0", + "yaml": "^2.9.0" }, "devDependencies": { "esbuild": "^0.19.8", diff --git a/src/restful/miscs.js b/src/restful/miscs.js index 8d0ac15..ed8af59 100644 --- a/src/restful/miscs.js +++ b/src/restful/miscs.js @@ -179,8 +179,11 @@ function getWorkerStatus(req, res) { }, capabilities: { scriptOperator: { - supported: false, - reason: 'Cloudflare Workers does not allow dynamic code execution through eval/new Function.', + supported: workerEnv.SCRIPT_ENGINE === 'quickjs', + engine: workerEnv.SCRIPT_ENGINE || 'none', + reason: workerEnv.SCRIPT_ENGINE === 'quickjs' + ? 'Enabled via QuickJS WASM sandbox. Set SCRIPT_ENGINE="quickjs" in wrangler.toml [vars] to keep enabled.' + : 'Disabled. Set SCRIPT_ENGINE="quickjs" in wrangler.toml [vars] to enable.', alternatives: [ 'Use built-in filters/operators', 'Use mihomo YAML patch scripts', diff --git a/src/vendor/open-api.js b/src/vendor/open-api.js index 1002afe..6e6d780 100644 --- a/src/vendor/open-api.js +++ b/src/vendor/open-api.js @@ -201,6 +201,10 @@ export class OpenAPI { console.log(`[${this.name}] INFO: ${msg}`); } + warn(msg) { + console.log(`[${this.name}] WARN: ${msg}`); + } + error(msg) { console.log(`[${this.name}] ERROR: ${msg}`); } diff --git a/src/vendor/quickjs-executor.js b/src/vendor/quickjs-executor.js new file mode 100644 index 0000000..1e5c166 --- /dev/null +++ b/src/vendor/quickjs-executor.js @@ -0,0 +1,563 @@ +/** + * QuickJS WASM 脚本执行引擎 + * + * 在 Cloudflare Workers 中以沙箱方式执行用户 JavaScript 脚本, + * 替代 Node.js 环境下使用 new Function() / eval() 的方案。 + * + * 依赖: + * - quickjs-emscripten (RELEASE_SYNC 变体) + * - Wrangler 原生 WASM module import + * + * Cloudflare Workers 禁止运行时 WebAssembly.compile(bytes), + * 因此必须让 Wrangler 把 .wasm 作为 WebAssembly.Module 上传。 + * + * 上游执行模式(来自 Sub-Store processors/index.js): + * 1. func 模式:createDynamicFunction 返回函数 → fn(proxies, targetPlatform, context) + * 用户脚本定义 function filter(proxies, ...) 返回 boolean[] + * 2. nodeFunc 模式(回退):将用户脚本包装为快捷脚本遍历 + * const fn = async ($server) => {