From 54d84ff2e0cc4fac5b8fef00d37681ce08d2170a Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Mon, 7 Sep 2026 15:24:27 +0800 Subject: [PATCH 01/32] feat: add browser environment builtin --- doc/api/browser-env.md | 87 +++ doc/api/cli.md | 14 + doc/api/index.md | 1 + lib/browser-env.js | 9 + lib/internal/browser_env.js | 798 +++++++++++++++++++++++++ lib/internal/process/pre_execution.js | 8 + node.gyp | 1 + src/node_binding.cc | 1 + src/node_browser_env.cc | 148 +++++ src/node_external_reference.h | 1 + src/node_options.cc | 4 + src/node_options.h | 1 + test/fixtures/browser-env/profile.json | 19 + test/parallel/test-browser-env.js | 133 +++++ 14 files changed, 1225 insertions(+) create mode 100644 doc/api/browser-env.md create mode 100644 lib/browser-env.js create mode 100644 lib/internal/browser_env.js create mode 100644 src/node_browser_env.cc create mode 100644 test/fixtures/browser-env/profile.json create mode 100644 test/parallel/test-browser-env.js diff --git a/doc/api/browser-env.md b/doc/api/browser-env.md new file mode 100644 index 000000000000..1eef339488c3 --- /dev/null +++ b/doc/api/browser-env.md @@ -0,0 +1,87 @@ +# Browser environment + + + + + +> Stability: 1 - Experimental + + + +The `node:browser-env` module installs a browser-compatible global environment +in the current Realm. It provides a lightweight DOM tree, virtual navigation, +profiled `navigator` and `screen` values, in-memory storage, and a native +`document.all` implementation. It does not provide rendering, layout, Canvas, +WebGL, real page navigation, or automatic execution of scripts in HTML. + +```cjs +const { install } = require('node:browser-env'); + +install({ + url: 'https://example.test/page', + html: '
Hello
', + navigator: { platform: 'Win32', languages: ['zh-CN', 'zh'] }, + window: { properties: { customFlag: true } }, + document: { properties: { visibilityState: 'hidden' } }, +}); + +console.log(document.querySelector('#app').textContent); +``` + +```mjs +import { install } from 'node:browser-env'; + +install({ url: 'https://example.test/' }); +``` + +## `browserEnv.install(options)` + +Installs the environment in the current Realm and returns +`{ window, document, navigator, location }`. It must be called before loading +code that reads browser globals. It throws if the Realm already has an +installed browser environment. + +* `options` {Object} + * `url` {string} Required initial URL. `location.href` is also accepted for + JSON profile compatibility. + * `html` {string} Optional initial HTML. Scripts in this HTML are not run. + * `navigator` {Object} Optional overrides for browser identity fields such + as `userAgent`, `platform`, `language`, and `languages`. + * `screen` {Object} Optional screen-dimension overrides. + * `cookies` {string|Object} Optional initial in-memory cookies. + * `localStorage` {Object} Optional initial local storage values. + * `sessionStorage` {Object} Optional initial session storage values. + * `window.properties` {Object} Optional ordinary custom global properties. + * `window.descriptors` {Object} Optional custom property descriptors. + * `document.properties` {Object} Optional ordinary custom document + properties. + * `document.descriptors` {Object} Optional custom document property + descriptors. + +Core browser-environment properties, including `document.all`, DOM methods, +`location`, `navigator`, and the `window` identity aliases cannot be replaced +through `properties` or `descriptors`. + +## `browserEnv.isInstalled()` + +Returns `true` when `install()` or `--browser-env-profile` has installed the +environment in the current Realm. + +## `--browser-env-profile=file` + +The command-line option loads the same `options` shape from a JSON file before +user code executes: + +```json +{ + "url": "https://example.test/", + "html": "
", + "navigator": { "platform": "Win32" } +} +``` + +The CLI profile is applied independently in each Node Realm that starts with +the option. Programmatic installation applies only to the Realm that calls +`install()`. diff --git a/doc/api/cli.md b/doc/api/cli.md index 514af04c02cd..c8242adacd72 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -564,6 +564,18 @@ The following options are currently supported: When using this flag, additional script files provided on the command line will not be executed and instead be interpreted as regular command line arguments. +### `--browser-env-profile=file` + + + +Install the browser-compatible environment described by the JSON profile before +any user code is evaluated. The profile must contain a `url` string and can +provide initial HTML, browser identity values, and custom `window` or +`document` properties. See [`node:browser-env`][] for the supported profile +shape and limitations. + ### `-c`, `--check` |<\/?([A-Za-z][\w:-]*)([^>]*)>|([^<]+)/g; let match; while ((match = tokenPattern.exec(source)) !== null) { + if (match[0].startsWith('
', navigator: { userAgent: 'Mozilla/5.0 ModeTest' }, }); @@ -109,7 +114,17 @@ spawnSyncAndAssert(process.execPath, [ assert(document instanceof Document); assert(document.body instanceof HTMLElement); assert(document.createTextNode('text') instanceof Text); - assert.strictEqual(Object.prototype.toString.call(document.getElementsByTagName('input')), '[object HTMLCollection]'); + const inputs = document.getElementsByTagName('input'); + assert.strictEqual(Object.prototype.toString.call(inputs), '[object HTMLCollection]'); + assert(inputs instanceof HTMLCollection); + assert.strictEqual(inputs.constructor, HTMLCollection); + assert.deepStrictEqual(Object.keys(inputs), ['0']); + assert.strictEqual(Object.prototype.toString.call(document), '[object HTMLDocument]'); + assert.strictEqual(Object.prototype.toString.call(document.head), '[object HTMLHeadElement]'); + assert.strictEqual(Object.prototype.toString.call(document.body), '[object HTMLBodyElement]'); + assert.deepStrictEqual(Object.keys(document), []); + assert.deepStrictEqual(Object.keys(document.body), []); + assert.deepStrictEqual(Object.keys(navigator.mimeTypes), ['0', '1']); assert.deepStrictEqual(document.createExpression(), Object.create(null)); const anchor = document.createElement('a'); @@ -117,6 +132,22 @@ spawnSyncAndAssert(process.execPath, [ assert(anchor instanceof HTMLAnchorElement); assert.strictEqual(anchor.href, 'https://example.test/next?q=1#hash'); assert.strictEqual(anchor.host, 'example.test'); + + const meta = document.querySelector('#challenge'); + assert(meta instanceof HTMLMetaElement); + assert.strictEqual(Object.prototype.toString.call(meta), '[object HTMLMetaElement]'); + assert.strictEqual(meta.content, 'initial-content'); + meta.content = 'updated-content'; + assert.strictEqual(meta.getAttribute('content'), 'updated-content'); + const script = document.querySelector('script'); + assert(script instanceof HTMLScriptElement); + assert.strictEqual(Object.prototype.toString.call(script), '[object HTMLScriptElement]'); + assert.strictEqual(script.innerText, 'initial-script'); + script.innerText = 'updated-script'; + assert.strictEqual(script.textContent, 'updated-script'); + assert.strictEqual(script.src, 'https://example.test/challenge.js'); + assert.strictEqual(document.getElementsByTagName('script').length, 1); + const form = document.querySelector('#form'); assert(form instanceof HTMLFormElement); assert.strictEqual(form.elements.namedItem('token').value, 'initial'); @@ -158,7 +189,7 @@ spawnSyncAndAssert(process.execPath, [ assert.strictEqual(typeof webkitRequestFileSystem, 'function'); assert.strictEqual(document.all[0], document.documentElement); - assert.strictEqual(document.all[3], form); + assert.strictEqual(document.all.form, form); })().catch((error) => { console.error(error.stack); process.exitCode = 1; @@ -166,6 +197,22 @@ spawnSyncAndAssert(process.execPath, [ `, ], { status: 0, stderr: '' }); +spawnSyncAndAssert(process.execPath, [ + '-e', + ` + const assert = require('node:assert'); + const { install } = require('node:browser-env'); + install({ url: 'https://example.test/', hideNodeGlobals: true }); + assert.deepStrictEqual( + new Function('return [typeof global, typeof process, typeof Buffer, typeof require, typeof module, typeof exports, typeof __dirname, typeof __filename, typeof setImmediate, typeof clearImmediate].join(\",\")')(), + 'undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined', + ); + for (const name of ['global', 'process', 'Buffer', 'require', 'module', 'exports', '__dirname', '__filename', 'setImmediate', 'clearImmediate']) { + assert.strictEqual(Object.getOwnPropertyDescriptor(globalThis, name), undefined); + } + `, +], { status: 0, stderr: '' }); + spawnSyncAndAssert(process.execPath, [ '-e', ` From 90db1dbeef1a7c4a9cb77f4e385e6360c07b8024 Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Sat, 12 Sep 2026 22:15:36 +0800 Subject: [PATCH 25/32] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=E6=8C=91?= =?UTF-8?q?=E6=88=98=E8=84=9A=E6=9C=AC=E6=89=80=E9=9C=80=E7=9A=84=20Buffer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish-native-release.yml | 2 +- README.md | 8 ++++---- doc/api/browser-env.md | 4 ++-- lib/internal/browser_env.js | 2 +- rs_mode_server/README.md | 10 ++++++---- rs_mode_server/test/server.test.js | 2 +- test/parallel/test-browser-env.js | 7 ++++--- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-native-release.yml b/.github/workflows/publish-native-release.yml index a61c48958b23..b45af9161cde 100644 --- a/.github/workflows/publish-native-release.yml +++ b/.github/workflows/publish-native-release.yml @@ -144,7 +144,7 @@ jobs: '## 本次更新' \ '' \ '- 补全 Mode 浏览器环境的 DOM 对象标签、构造器和集合描述符,降低普通 JavaScript 对象特征。' \ - '- `rs_mode_server` 的挑战 Worker 现在隔离 `global`、`process`、`Buffer`、`setImmediate` 等 Node 全局别名,并取消向挑战脚本注入 CommonJS `require`。' \ + '- `rs_mode_server` 的挑战 Worker 现在隔离 `global`、`process`、`module`、`require`、`setImmediate` 等 Node 全局别名,并取消向挑战脚本注入 CommonJS `require`;保留 `Buffer` 兼容既有挑战脚本。' \ '- 保持 `document.all`、原始 HTML、动态 UA、Cookie、Storage、location/history 与每请求独立 Worker 的兼容行为。' \ '' \ '## 构建与验证' \ diff --git a/README.md b/README.md index 98725b026660..30b7aa6ccbb9 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Download the archive that matches the host CPU from [GitHub Releases][mode-relea | Platform | Unified release | Archive | | --- | --- | --- | -| macOS Apple Silicon (ARM64) | [mode_20260912_v22.0.6][mode-release] | `mode_mac_arm_20260912_v22.0.6.tar.gz` | -| Windows x64 | [mode_20260912_v22.0.6][mode-release] | `mode_win_x64_20260912_v22.0.6.zip` | -| Linux x64 | [mode_20260912_v22.0.6][mode-release] | `mode_linux_x64_20260912_v22.0.6.tar.gz` | +| macOS Apple Silicon (ARM64) | [mode_20260912_v22.0.7][mode-release] | `mode_mac_arm_20260912_v22.0.7.tar.gz` | +| Windows x64 | [mode_20260912_v22.0.7][mode-release] | `mode_win_x64_20260912_v22.0.7.zip` | +| Linux x64 | [mode_20260912_v22.0.7][mode-release] | `mode_linux_x64_20260912_v22.0.7.tar.gz` | On macOS or Linux, extract the archive and place `mode` on `PATH`: @@ -1090,7 +1090,7 @@ additions comply with the project’s license guidelines. [Code of Conduct]: https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md [Contributing to the project]: CONTRIBUTING.md [browser-env-api]: doc/api/browser-env.md -[mode-release]: https://github.com/wen2go/mode/releases/tag/mode_20260912_v22.0.6 +[mode-release]: https://github.com/wen2go/mode/releases/tag/mode_20260912_v22.0.7 [mode-releases]: https://github.com/wen2go/mode/releases [Node.js website]: https://nodejs.org/ [OpenJS Foundation]: https://openjsf.org/ diff --git a/doc/api/browser-env.md b/doc/api/browser-env.md index eac8d8f4bf7b..e6ff8c4f7ae9 100644 --- a/doc/api/browser-env.md +++ b/doc/api/browser-env.md @@ -54,8 +54,8 @@ installed browser environment. * `localStorage` {Object} Optional initial local storage values. * `sessionStorage` {Object} Optional initial session storage values. * `hideNodeGlobals` {boolean} When `true`, removes the configurable Node-only - global aliases `global`, `process`, `Buffer`, `require`, `module`, - `exports`, `__dirname`, `__filename`, `setImmediate`, and `clearImmediate` + global aliases `global`, `process`, `require`, `module`, `exports`, + `__dirname`, `__filename`, `setImmediate`, and `clearImmediate` from this Realm after installation. Use it when browser challenge code is evaluated through a separate function; it is `false` by default so normal Mode scripts retain their Node entry points. diff --git a/lib/internal/browser_env.js b/lib/internal/browser_env.js index 3feacd794981..5a8855bc6573 100644 --- a/lib/internal/browser_env.js +++ b/lib/internal/browser_env.js @@ -1375,7 +1375,7 @@ function applyCustomDescriptors(target, source, protectedNames, targetName) { function hideNodeGlobals() { for (const name of [ - 'global', 'process', 'Buffer', 'require', 'module', 'exports', + 'global', 'process', 'require', 'module', 'exports', '__dirname', '__filename', 'setImmediate', 'clearImmediate', ]) { const descriptor = ObjectGetOwnPropertyDescriptor(globalThis, name); diff --git a/rs_mode_server/README.md b/rs_mode_server/README.md index fa18759f80c6..b7596e4c7384 100644 --- a/rs_mode_server/README.md +++ b/rs_mode_server/README.md @@ -49,10 +49,12 @@ environment with the supplied URL, original HTML, dynamic user agent, DOM, basic XHR state transitions, base navigator values (including connection, mimeTypes, battery, and beacon APIs), screen dimensions, window dimensions, and hidden document visibility. The Worker also removes the Node-only global aliases -`global`, `process`, `Buffer`, `require`, `module`, `exports`, `__dirname`, -`__filename`, `setImmediate`, and `clearImmediate`; challenge code is evaluated -without a CommonJS `require` parameter. External `r="m"` scripts are cached -under `out_js/`; the cache is ignored by Git. +`global`, `process`, `require`, `module`, `exports`, `__dirname`, +`__filename`, `setImmediate`, and `clearImmediate`. `Buffer` stays available +for compatibility with existing challenge code; it does not cross Worker +boundaries. Challenge code is evaluated without a CommonJS `require` parameter. +External `r="m"` scripts are cached under `out_js/`; the cache is ignored by +Git. ## Strict compatibility boundary diff --git a/rs_mode_server/test/server.test.js b/rs_mode_server/test/server.test.js index 32e3443df34f..8607ef6ad944 100644 --- a/rs_mode_server/test/server.test.js +++ b/rs_mode_server/test/server.test.js @@ -117,7 +117,7 @@ test('challenge workers expose browser globals without Node global aliases', asy ...validPayload(), html: fixtureHtml.replace('var $_ = { fixture: true };', ` var $_ = { fixture: true }; - if ([typeof global, typeof process, typeof Buffer, typeof require, typeof setImmediate].some((type) => type !== 'undefined')) { + if ([typeof global, typeof process, typeof require, typeof setImmediate].some((type) => type !== 'undefined')) { throw new Error('Node global leaked into challenge'); } `), diff --git a/test/parallel/test-browser-env.js b/test/parallel/test-browser-env.js index 1a899717d1ec..94efff6d8472 100644 --- a/test/parallel/test-browser-env.js +++ b/test/parallel/test-browser-env.js @@ -204,10 +204,11 @@ spawnSyncAndAssert(process.execPath, [ const { install } = require('node:browser-env'); install({ url: 'https://example.test/', hideNodeGlobals: true }); assert.deepStrictEqual( - new Function('return [typeof global, typeof process, typeof Buffer, typeof require, typeof module, typeof exports, typeof __dirname, typeof __filename, typeof setImmediate, typeof clearImmediate].join(\",\")')(), - 'undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined', + new Function('return [typeof global, typeof process, typeof require, typeof module, typeof exports, typeof __dirname, typeof __filename, typeof setImmediate, typeof clearImmediate].join(\",\")')(), + 'undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined', ); - for (const name of ['global', 'process', 'Buffer', 'require', 'module', 'exports', '__dirname', '__filename', 'setImmediate', 'clearImmediate']) { + assert.strictEqual(typeof Buffer, 'function'); + for (const name of ['global', 'process', 'require', 'module', 'exports', '__dirname', '__filename', 'setImmediate', 'clearImmediate']) { assert.strictEqual(Object.getOwnPropertyDescriptor(globalThis, name), undefined); } `, From 1a27296a93ac9493b68a12c50f9d495f7b027580 Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Sun, 13 Sep 2026 00:28:48 +0800 Subject: [PATCH 26/32] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E7=8E=AF=E5=A2=83=E6=BA=90=E7=A0=81=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/browser-env-source-runner.js | 177 +++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tools/browser-env-source-runner.js diff --git a/tools/browser-env-source-runner.js b/tools/browser-env-source-runner.js new file mode 100644 index 000000000000..702e97fc0f11 --- /dev/null +++ b/tools/browser-env-source-runner.js @@ -0,0 +1,177 @@ +'use strict'; + +// Executes the browser environment JavaScript from the working tree without +// rebuilding Node. The running Mode binary supplies only the native +// `document.all` binding; `lib/internal/browser_env.js` is loaded from disk. +// +// Run with a previously built Mode binary: +// mode --expose-internals tools/browser-env-source-runner.js \ +// js_reverse_cache/.../first_412.html --deterministic +// Add --trace only when inspecting environment accesses. It instruments DOM +// methods and is therefore deliberately not part of the production-equivalent +// default execution. + +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const { internalBinding, primordials } = require('internal/test/binding'); +const nodeProcess = process; + +const [htmlPath, ...flags] = nodeProcess.argv.slice(2); +if (!htmlPath || flags.some((flag) => !['--deterministic', '--trace'].includes(flag))) { + throw new Error('usage: mode --expose-internals tools/browser-env-source-runner.js [--deterministic] [--trace]'); +} + +const root = path.resolve(__dirname, '..'); +const url = 'https://etax.qingdao.chinatax.gov.cn:8443/'; +const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; +const html = fs.readFileSync(htmlPath, 'utf8'); + +function loadBrowserEnvironmentFromSource() { + const sourcePath = path.join(root, 'lib', 'internal', 'browser_env.js'); + const source = fs.readFileSync(sourcePath, 'utf8'); + const module = { exports: {} }; + const factory = vm.runInThisContext( + `(function(exports, module, require, primordials, internalBinding) {\n${source}\n})`, + { filename: sourcePath }, + ); + factory(module.exports, module, require, primordials, internalBinding); + return module.exports; +} + +function parseAttributes(source) { + const attributes = {}; + const matcher = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g; + let match; + while ((match = matcher.exec(source)) !== null) { + attributes[match[1].toLowerCase()] = match[2] ?? match[3] ?? match[4] ?? ''; + } + return attributes; +} + +function parseChallenge(source) { + const metas = Array.from(source.matchAll(/]*)>/gi), (match) => parseAttributes(match[1])); + const scripts = Array.from(source.matchAll(/]*)>([\s\S]*?)<\/script\s*>/gi), (match) => ({ + attributes: parseAttributes(match[1]), + content: match[2], + })); + const meta = metas.find((tag) => tag.r === 'm'); + const inline = scripts.find((tag) => tag.attributes.r === 'm' && !tag.attributes.src && tag.content.includes('$_')); + const external = scripts.find((tag) => tag.attributes.r === 'm' && tag.attributes.src); + if (!meta || !inline || !external) throw new Error('missing r=m challenge parts'); + return { external: external.attributes.src, inline: inline.content }; +} + +function deterministicPrelude() { + if (!flags.includes('--deterministic')) return ''; + return ` + var __modeSeed = 0x13579bdf; + Math.random = function random() { + __modeSeed = (__modeSeed * 1664525 + 1013904223) >>> 0; + return __modeSeed / 0x100000000; + }; + var __modeRealDate = Date; + var __modeTime = 1700000000000; + Date = function Date() { + if (!new.target) return __modeRealDate(); + return arguments.length ? Reflect.construct(__modeRealDate, arguments) : new __modeRealDate(__modeTime++); + }; + Date.now = function now() { return __modeTime++; }; + Date.parse = __modeRealDate.parse; + Date.UTC = __modeRealDate.UTC; + Date.prototype = __modeRealDate.prototype; + `; +} + +function cookieSummary(cookieHeader) { + return String(cookieHeader).split(';').flatMap((part) => { + const separator = part.indexOf('='); + if (separator <= 0) return []; + const value = part.slice(separator + 1).trim(); + let hash = 2166136261; + for (let index = 0; index < value.length; index++) { + hash = Math.imul(hash ^ value.charCodeAt(index), 16777619); + } + return [{ + name: part.slice(0, separator).trim(), + valueHash: (hash >>> 0).toString(16), + valueLength: value.length, + }]; + }); +} + +const traceEnabled = flags.includes('--trace'); +const hook = traceEnabled ? + fs.readFileSync(path.join(root, 'js_reverse_cache', 'compare_browser_env_hook.js'), 'utf8') : ''; +const challenge = parseChallenge(html); +const fileName = path.basename(new URL(challenge.external, url).pathname); +const outJs = fs.readFileSync(path.join(root, 'rs_mode_server', 'out_js', fileName), 'utf8'); +const { install } = loadBrowserEnvironmentFromSource(); + +install({ + url, + html, + hideNodeGlobals: true, + navigator: { + appName: 'Netscape', + appVersion: ua.replace(/^Mozilla\//, ''), + language: 'zh-CN', + languages: ['zh-CN', 'zh'], + maxTouchPoints: 0, + platform: 'Win32', + userAgent: ua, + vendor: 'Google Inc.', + webdriver: false, + }, + screen: { + availHeight: 1040, + availLeft: 0, + availTop: 0, + availWidth: 1920, + colorDepth: 24, + height: 1080, + pixelDepth: 24, + width: 1920, + }, + window: { + properties: { + innerHeight: 1080, + innerWidth: 1920, + outerHeight: 1080, + outerWidth: 1920, + screenLeft: 0, + screenTop: 0, + screenX: 0, + screenY: 0, + }, + }, + document: { properties: { visibilityState: 'hidden' } }, +}); + +const originalConsole = globalThis.console; +globalThis.console = { debug() {}, error() {}, info() {}, log() {}, warn() {} }; +try { + const result = new Function(`${deterministicPrelude()}\n${hook}\n${challenge.inline}\n${outJs} + const result = { + label: 'source', + source: 'lib/internal/browser_env.js', + traceEnabled: ${traceEnabled}, + nodeGlobals: { + Buffer: typeof Buffer, + clearImmediate: typeof clearImmediate, + exports: typeof exports, + global: typeof global, + module: typeof module, + process: typeof process, + require: typeof require, + setImmediate: typeof setImmediate, + }, + cookies: (${cookieSummary.toString()})(document.cookie), + }; + if (${traceEnabled}) Object.assign(result, __modeEnvTrace.dump('source')); + return result;`)(); + nodeProcess.stdout.write(JSON.stringify(result), () => nodeProcess.exit(0)); +} finally { + globalThis.console = originalConsole; +} From 78a69aa360379e034a0e9ea10c52aeb6b45540cd Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Sun, 13 Sep 2026 22:36:25 +0800 Subject: [PATCH 27/32] =?UTF-8?q?ci:=20=E8=A1=A5=E5=85=85=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E7=8E=AF=E5=A2=83=E8=AF=8A=E6=96=AD=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish-native-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-native-release.yml b/.github/workflows/publish-native-release.yml index b45af9161cde..303c606d352f 100644 --- a/.github/workflows/publish-native-release.yml +++ b/.github/workflows/publish-native-release.yml @@ -144,6 +144,7 @@ jobs: '## 本次更新' \ '' \ '- 补全 Mode 浏览器环境的 DOM 对象标签、构造器和集合描述符,降低普通 JavaScript 对象特征。' \ + '- 新增源码诊断器:可不重新编译而直接以当前工作区的浏览器环境源码执行缓存挑战,用于发布前定位环境差异。' \ '- `rs_mode_server` 的挑战 Worker 现在隔离 `global`、`process`、`module`、`require`、`setImmediate` 等 Node 全局别名,并取消向挑战脚本注入 CommonJS `require`;保留 `Buffer` 兼容既有挑战脚本。' \ '- 保持 `document.all`、原始 HTML、动态 UA、Cookie、Storage、location/history 与每请求独立 Worker 的兼容行为。' \ '' \ From 630229c1d7c7d7e5a29fc0bd0565a3e988dfe783 Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Mon, 14 Sep 2026 10:03:26 +0800 Subject: [PATCH 28/32] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E6=8C=91?= =?UTF-8?q?=E6=88=98=E4=BD=BF=E7=94=A8=E7=9A=84=20DOM=20=E4=B8=8E=20BOM=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish-native-release.yml | 3 +- lib/internal/browser_env.js | 245 ++++++++++++++++--- test/parallel/test-browser-env.js | 24 +- 3 files changed, 233 insertions(+), 39 deletions(-) diff --git a/.github/workflows/publish-native-release.yml b/.github/workflows/publish-native-release.yml index 303c606d352f..41681bd8efb6 100644 --- a/.github/workflows/publish-native-release.yml +++ b/.github/workflows/publish-native-release.yml @@ -143,7 +143,8 @@ jobs: printf '%s\n' \ '## 本次更新' \ '' \ - '- 补全 Mode 浏览器环境的 DOM 对象标签、构造器和集合描述符,降低普通 JavaScript 对象特征。' \ + '- 补齐 `HTMLDivElement`、`HTMLIFrameElement`、空锚点 URL、`NetworkInformation` 与 `BatteryManager`,修正挑战实际创建的 DOM/BOM 对象原型和标签。' \ + '- 浏览器环境方法现在为常见 `fn.toString()` 检测提供原生函数形态,并保留真实调用行为。' \ '- 新增源码诊断器:可不重新编译而直接以当前工作区的浏览器环境源码执行缓存挑战,用于发布前定位环境差异。' \ '- `rs_mode_server` 的挑战 Worker 现在隔离 `global`、`process`、`module`、`require`、`setImmediate` 等 Node 全局别名,并取消向挑战脚本注入 CommonJS `require`;保留 `Buffer` 兼容既有挑战脚本。' \ '- 保持 `document.all`、原始 HTML、动态 UA、Cookie、Storage、location/history 与每请求独立 Worker 的兼容行为。' \ diff --git a/lib/internal/browser_env.js b/lib/internal/browser_env.js index 5a8855bc6573..5c3a31d5e3ef 100644 --- a/lib/internal/browser_env.js +++ b/lib/internal/browser_env.js @@ -10,6 +10,7 @@ const { ObjectCreate, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyNames, ObjectGetPrototypeOf, ObjectKeys, ObjectSetPrototypeOf, @@ -72,8 +73,9 @@ const kProtectedWindowProperties = new Set([ 'MutationObserver', 'HTMLAnchorElement', 'HTMLCanvasElement', 'CanvasRenderingContext2D', 'HTMLFormElement', 'HTMLInputElement', 'HTMLBodyElement', 'HTMLCollection', 'HTMLHeadElement', 'HTMLHtmlElement', - 'HTMLMetaElement', 'HTMLScriptElement', 'HTMLTitleElement', 'MimeType', - 'MimeTypeArray', 'Storage', + 'HTMLDivElement', 'HTMLIFrameElement', 'HTMLMetaElement', 'HTMLScriptElement', + 'HTMLTitleElement', 'MimeType', 'MimeTypeArray', 'Storage', 'BatteryManager', + 'NetworkInformation', 'indexedDB', 'chrome', 'clientInformation', 'msCrypto', 'name', 'open', 'prompt', 'webkitRequestFileSystem', 'TEMPORARY', 'global', 'process', 'Buffer', 'require', 'module', 'exports', @@ -116,6 +118,37 @@ function defineInternal(target, name, value) { }); } +function markAsNativeFunction(value, name = value.name) { + if (typeof value !== 'function') return value; + if (value.name !== name) { + ObjectDefineProperty(value, 'name', { + __proto__: null, + configurable: true, + enumerable: false, + value: name, + writable: false, + }); + } + ObjectDefineProperty(value, 'toString', { + __proto__: null, + configurable: true, + enumerable: false, + value: function toString() { return `function ${name}() { [native code] }`; }, + writable: true, + }); + return value; +} + +function markNativePrototype(prototype) { + for (const name of ObjectGetOwnPropertyNames(prototype)) { + if (name === 'constructor') continue; + const descriptor = ObjectGetOwnPropertyDescriptor(prototype, name); + if (typeof descriptor?.value === 'function') markAsNativeFunction(descriptor.value); + if (typeof descriptor?.get === 'function') markAsNativeFunction(descriptor.get); + if (typeof descriptor?.set === 'function') markAsNativeFunction(descriptor.set); + } +} + function createEventTarget(target) { const listeners = new Map(); ObjectDefineProperty(target, '_listeners', { @@ -129,7 +162,7 @@ function createEventTarget(target) { __proto__: null, configurable: true, enumerable: false, - value: function addEventListener(type, callback) { + value: markAsNativeFunction(function addEventListener(type, callback) { if (typeof callback !== 'function') return; const key = String(type); let callbacks = listeners.get(key); @@ -138,26 +171,26 @@ function createEventTarget(target) { listeners.set(key, callbacks); } if (!ArrayPrototypeIncludes(callbacks, callback)) ArrayPrototypePush(callbacks, callback); - }, + }), writable: true, }); ObjectDefineProperty(target, 'removeEventListener', { __proto__: null, configurable: true, enumerable: false, - value: function removeEventListener(type, callback) { + value: markAsNativeFunction(function removeEventListener(type, callback) { const callbacks = listeners.get(String(type)); if (callbacks === undefined) return; const index = callbacks.indexOf(callback); if (index !== -1) callbacks.splice(index, 1); - }, + }), writable: true, }); ObjectDefineProperty(target, 'dispatchEvent', { __proto__: null, configurable: true, enumerable: false, - value: function dispatchEvent(event) { + value: markAsNativeFunction(function dispatchEvent(event) { if (event === null || typeof event !== 'object' || !event.type) { throw new TypeError('dispatchEvent requires an event with a type'); } @@ -172,7 +205,7 @@ function createEventTarget(target) { const handler = this[`on${event.type}`]; if (typeof handler === 'function') FunctionPrototypeCall(handler, this, event); return !event.defaultPrevented; - }, + }), writable: true, }); return target; @@ -361,6 +394,7 @@ class BrowserElement extends BrowserNode { case 'meta': return 'HTMLMetaElement'; case 'script': return 'HTMLScriptElement'; case 'title': return 'HTMLTitleElement'; + case 'iframe': return 'HTMLIFrameElement'; default: return `HTML${this.localName[0].toUpperCase()}${this.localName.slice(1)}Element`; } } @@ -517,6 +551,26 @@ class HTMLBodyElement extends BrowserElement { } } +class HTMLDivElement extends BrowserElement { + constructor(ownerDocument) { + super(ownerDocument, 'div'); + } +} + +class HTMLIFrameElement extends BrowserElement { + constructor(ownerDocument) { + super(ownerDocument, 'iframe'); + } + + get contentDocument() { + return null; + } + + get contentWindow() { + return null; + } +} + class HTMLMetaElement extends BrowserElement { constructor(ownerDocument) { super(ownerDocument, 'meta'); @@ -545,21 +599,21 @@ class HTMLAnchorElement extends BrowserElement { } get href() { - return this._url.href; + return this.getAttribute('href') === null ? '' : this._url.href; } set href(value) { this.setAttribute('href', value); } - get origin() { return this._url.origin; } - get protocol() { return this._url.protocol; } - get host() { return this._url.host; } - get hostname() { return this._url.hostname; } - get port() { return this._url.port; } - get pathname() { return this._url.pathname; } - get search() { return this._url.search; } - get hash() { return this._url.hash; } + get origin() { return this.getAttribute('href') === null ? '' : this._url.origin; } + get protocol() { return this.getAttribute('href') === null ? '' : this._url.protocol; } + get host() { return this.getAttribute('href') === null ? '' : this._url.host; } + get hostname() { return this.getAttribute('href') === null ? '' : this._url.hostname; } + get port() { return this.getAttribute('href') === null ? '' : this._url.port; } + get pathname() { return this.getAttribute('href') === null ? '' : this._url.pathname; } + get search() { return this.getAttribute('href') === null ? '' : this._url.search; } + get hash() { return this.getAttribute('href') === null ? '' : this._url.hash; } } class CanvasRenderingContext2D { @@ -742,6 +796,8 @@ class BrowserDocument extends BrowserNode { return new HTMLBodyElement(this); case 'canvas': return new HTMLCanvasElement(this); + case 'div': + return new HTMLDivElement(this); case 'form': return new HTMLFormElement(this); case 'head': @@ -750,6 +806,8 @@ class BrowserDocument extends BrowserNode { return new HTMLHtmlElement(this); case 'input': return new HTMLInputElement(this); + case 'iframe': + return new HTMLIFrameElement(this); case 'meta': return new HTMLMetaElement(this); case 'script': @@ -991,10 +1049,10 @@ function createLocation(href) { }, }); } - location.assign = function assign(value) { replace(value); }; - location.replace = function replaceLocation(value) { replace(value); }; - location.reload = function reload() {}; - location.toString = function toString() { return url.href; }; + location.assign = markAsNativeFunction(function assign(value) { replace(value); }); + location.replace = markAsNativeFunction(function replaceLocation(value) { replace(value); }, 'replace'); + location.reload = markAsNativeFunction(function reload() {}); + location.toString = markAsNativeFunction(function toString() { return url.href; }); ObjectDefineProperty(location, 'ancestorOrigins', { __proto__: null, configurable: false, @@ -1051,6 +1109,82 @@ function createHistory(location) { }; } +function createNetworkInformation(values) { + function NetworkInformation() { + throw new TypeError('Illegal constructor'); + } + const connection = ObjectCreate(NetworkInformation.prototype); + defineInternal(connection, '_values', values); + defineInternal(connection, '_onchange', values.onchange ?? null); + for (const name of ['downlink', 'effectiveType', 'rtt', 'saveData']) { + ObjectDefineProperty(NetworkInformation.prototype, name, { + __proto__: null, + configurable: true, + enumerable: true, + get() { return this._values[name]; }, + }); + } + ObjectDefineProperty(NetworkInformation.prototype, 'onchange', { + __proto__: null, + configurable: true, + enumerable: true, + get() { return this._onchange; }, + set(value) { this._onchange = value; }, + }); + ObjectDefineProperty(NetworkInformation.prototype, Symbol.toStringTag, { + __proto__: null, + configurable: true, + value: 'NetworkInformation', + }); + markAsNativeFunction(NetworkInformation); + markNativePrototype(NetworkInformation.prototype); + return { NetworkInformation, connection }; +} + +function createBatteryManagerFactory() { + function BatteryManager() { + throw new TypeError('Illegal constructor'); + } + for (const name of [ + 'charging', 'chargingTime', 'dischargingTime', 'level', + 'onchargingchange', 'onchargingtimechange', 'ondischargingtimechange', + 'onlevelchange', + ]) { + ObjectDefineProperty(BatteryManager.prototype, name, { + __proto__: null, + configurable: true, + enumerable: true, + get() { return this._values[name]; }, + set(value) { this._values[name] = value; }, + }); + } + ObjectDefineProperty(BatteryManager.prototype, Symbol.toStringTag, { + __proto__: null, + configurable: true, + value: 'BatteryManager', + }); + markAsNativeFunction(BatteryManager); + markNativePrototype(BatteryManager.prototype); + return { + BatteryManager, + createBattery() { + const battery = ObjectCreate(BatteryManager.prototype); + defineInternal(battery, '_values', { + charging: true, + chargingTime: Infinity, + dischargingTime: Infinity, + level: 0.8, + onchargingchange: null, + onchargingtimechange: null, + ondischargingtimechange: null, + onlevelchange: null, + }); + createEventTarget(battery); + return battery; + }, + }; +} + function createNavigator(values) { function Navigator() { throw new TypeError('Illegal constructor'); @@ -1060,7 +1194,7 @@ function createNavigator(values) { __proto__: null, configurable: true, enumerable: true, - get() { return value; }, + get: markAsNativeFunction(function getNavigatorValue() { return value; }, `get ${key}`), }); } ObjectDefineProperty(Navigator.prototype, Symbol.toStringTag, { @@ -1069,6 +1203,8 @@ function createNavigator(values) { value: 'Navigator', }); const navigator = ObjectCreate(Navigator.prototype); + markAsNativeFunction(Navigator); + markNativePrototype(Navigator.prototype); return { Navigator, navigator }; } @@ -1326,6 +1462,37 @@ function createChrome() { }; } +function markBrowserInterfaces() { + for (const constructor of [ + BrowserEvent, + BrowserNode, + BrowserText, + BrowserElement, + HTMLHtmlElement, + HTMLHeadElement, + HTMLBodyElement, + HTMLDivElement, + HTMLIFrameElement, + HTMLMetaElement, + HTMLScriptElement, + HTMLTitleElement, + HTMLAnchorElement, + HTMLCanvasElement, + CanvasRenderingContext2D, + HTMLFormElement, + HTMLInputElement, + BrowserDocument, + HTMLCollection, + DOMStringList, + Storage, + MimeType, + MimeTypeArray, + ]) { + markAsNativeFunction(constructor); + markNativePrototype(constructor.prototype); + } +} + function assertCustomPropertyName(name, protectedNames, targetName) { if (protectedNames.has(name)) { throw new TypeError(`${targetName}.${name} is a protected browser environment property`); @@ -1395,6 +1562,7 @@ function install(options) { validateCustomDescriptors(options.window?.descriptors, kProtectedWindowProperties, 'window'); validateCustomProperties(options.document?.properties, kProtectedDocumentProperties, 'document'); validateCustomDescriptors(options.document?.descriptors, kProtectedDocumentProperties, 'document'); + markBrowserInterfaces(); const location = createLocation(getHref(options)); const document = new BrowserDocument(location); @@ -1421,22 +1589,17 @@ function install(options) { navigatorValues.appVersion = String(navigatorValues.userAgent).replace(/^Mozilla\//, ''); } if (ArrayIsArray(navigatorValues.mimeTypes)) navigatorValues.mimeTypes = createMimeTypeArray(navigatorValues.mimeTypes); - if (navigatorValues.sendBeacon === undefined) navigatorValues.sendBeacon = function sendBeacon() { return true; }; + assertObject(navigatorValues.connection, 'navigator.connection'); + const { NetworkInformation, connection } = createNetworkInformation(navigatorValues.connection); + navigatorValues.connection = connection; + const { BatteryManager, createBattery } = createBatteryManagerFactory(); + if (navigatorValues.sendBeacon === undefined) { + navigatorValues.sendBeacon = markAsNativeFunction(function sendBeacon() { return true; }); + } if (navigatorValues.getBattery === undefined) { - navigatorValues.getBattery = function getBattery() { - const battery = { - charging: true, - chargingTime: Infinity, - dischargingTime: Infinity, - level: 0.8, - onchargingchange: null, - onchargingtimechange: null, - ondischargingtimechange: null, - onlevelchange: null, - }; - createEventTarget(battery); - return Promise.resolve(battery); - }; + navigatorValues.getBattery = markAsNativeFunction(function getBattery() { + return Promise.resolve(createBattery()); + }); } const { Navigator, navigator } = createNavigator(navigatorValues); const screen = { @@ -1491,6 +1654,10 @@ function install(options) { function Location() { throw new TypeError('Illegal constructor'); } + for (const constructor of [Window, History, Screen, Location]) { + markAsNativeFunction(constructor); + markNativePrototype(constructor.prototype); + } ObjectSetPrototypeOf(Window.prototype, ObjectGetPrototypeOf(globalThis)); ObjectSetPrototypeOf(globalThis, Window.prototype); ObjectSetPrototypeOf(history, History.prototype); @@ -1536,15 +1703,19 @@ function install(options) { ObjectDefineProperty(globalThis, 'HTMLCanvasElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLCanvasElement, writable: true }); ObjectDefineProperty(globalThis, 'CanvasRenderingContext2D', { __proto__: null, configurable: true, enumerable: false, value: CanvasRenderingContext2D, writable: true }); ObjectDefineProperty(globalThis, 'HTMLCollection', { __proto__: null, configurable: true, enumerable: false, value: HTMLCollection, writable: true }); + ObjectDefineProperty(globalThis, 'HTMLDivElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLDivElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLFormElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLFormElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLHeadElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLHeadElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLHtmlElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLHtmlElement, writable: true }); + ObjectDefineProperty(globalThis, 'HTMLIFrameElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLIFrameElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLInputElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLInputElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLMetaElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLMetaElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLScriptElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLScriptElement, writable: true }); ObjectDefineProperty(globalThis, 'HTMLTitleElement', { __proto__: null, configurable: true, enumerable: false, value: HTMLTitleElement, writable: true }); ObjectDefineProperty(globalThis, 'MimeType', { __proto__: null, configurable: true, enumerable: false, value: MimeType, writable: true }); ObjectDefineProperty(globalThis, 'MimeTypeArray', { __proto__: null, configurable: true, enumerable: false, value: MimeTypeArray, writable: true }); + ObjectDefineProperty(globalThis, 'BatteryManager', { __proto__: null, configurable: true, enumerable: false, value: BatteryManager, writable: true }); + ObjectDefineProperty(globalThis, 'NetworkInformation', { __proto__: null, configurable: true, enumerable: false, value: NetworkInformation, writable: true }); ObjectDefineProperty(globalThis, 'Storage', { __proto__: null, configurable: true, enumerable: false, value: Storage, writable: true }); ObjectDefineProperty(globalThis, 'DOMParser', { __proto__: null, configurable: true, enumerable: false, value: DOMParser, writable: true }); ObjectDefineProperty(globalThis, 'XMLHttpRequest', { __proto__: null, configurable: true, enumerable: false, value: XMLHttpRequest, writable: true }); diff --git a/test/parallel/test-browser-env.js b/test/parallel/test-browser-env.js index 94efff6d8472..fd66fce7ab58 100644 --- a/test/parallel/test-browser-env.js +++ b/test/parallel/test-browser-env.js @@ -106,10 +106,17 @@ spawnSyncAndAssert(process.execPath, [ assert.strictEqual(navigator.appName, 'Netscape'); assert.strictEqual(navigator.appVersion, '5.0 ModeTest'); assert.strictEqual(navigator.connection.effectiveType, '4g'); + assert(navigator.connection instanceof NetworkInformation); + assert.strictEqual(Object.prototype.toString.call(navigator.connection), '[object NetworkInformation]'); + assert.deepStrictEqual(Object.keys(navigator.connection), []); assert.strictEqual(navigator.mimeTypes.length, 2); assert.strictEqual(navigator.mimeTypes.namedItem('application/pdf').suffixes, 'pdf'); assert.strictEqual(navigator.sendBeacon('/beacon', 'body'), true); - assert.strictEqual((await navigator.getBattery()).charging, true); + assert.match(String(navigator.getBattery), /^function getBattery\(\) \{ \[native code\] \}$/); + const battery = await navigator.getBattery(); + assert(battery instanceof BatteryManager); + assert.strictEqual(Object.prototype.toString.call(battery), '[object BatteryManager]'); + assert.strictEqual(battery.charging, true); assert(document instanceof Document); assert(document.body instanceof HTMLElement); @@ -128,11 +135,26 @@ spawnSyncAndAssert(process.execPath, [ assert.deepStrictEqual(document.createExpression(), Object.create(null)); const anchor = document.createElement('a'); + assert.strictEqual(anchor.href, ''); anchor.href = '/next?q=1#hash'; assert(anchor instanceof HTMLAnchorElement); assert.strictEqual(anchor.href, 'https://example.test/next?q=1#hash'); assert.strictEqual(anchor.host, 'example.test'); + const div = document.createElement('div'); + assert(div instanceof HTMLDivElement); + assert.strictEqual(div.constructor, HTMLDivElement); + assert.strictEqual(Object.prototype.toString.call(div), '[object HTMLDivElement]'); + assert.match(String(div.getAttribute), /^function getAttribute\(\) \{ \[native code\] \}$/); + + const iframe = document.createElement('iframe'); + assert(iframe instanceof HTMLIFrameElement); + assert.strictEqual(iframe.constructor, HTMLIFrameElement); + assert.strictEqual(Object.prototype.toString.call(iframe), '[object HTMLIFrameElement]'); + assert.strictEqual(iframe.contentDocument, null); + assert.strictEqual(iframe.contentWindow, null); + assert.match(String(location.assign), /^function assign\(\) \{ \[native code\] \}$/); + const meta = document.querySelector('#challenge'); assert(meta instanceof HTMLMetaElement); assert.strictEqual(Object.prototype.toString.call(meta), '[object HTMLMetaElement]'); From 843a760390cb0e1abaebc2e6e59b399663717662 Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Mon, 14 Sep 2026 13:42:59 +0800 Subject: [PATCH 29/32] =?UTF-8?q?test:=20=E4=BF=AE=E6=AD=A3=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=87=BD=E6=95=B0=E5=BD=A2=E6=80=81=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E8=BD=AC=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/parallel/test-browser-env.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-browser-env.js b/test/parallel/test-browser-env.js index fd66fce7ab58..15842d317b77 100644 --- a/test/parallel/test-browser-env.js +++ b/test/parallel/test-browser-env.js @@ -112,7 +112,7 @@ spawnSyncAndAssert(process.execPath, [ assert.strictEqual(navigator.mimeTypes.length, 2); assert.strictEqual(navigator.mimeTypes.namedItem('application/pdf').suffixes, 'pdf'); assert.strictEqual(navigator.sendBeacon('/beacon', 'body'), true); - assert.match(String(navigator.getBattery), /^function getBattery\(\) \{ \[native code\] \}$/); + assert.strictEqual(String(navigator.getBattery), 'function getBattery() { [native code] }'); const battery = await navigator.getBattery(); assert(battery instanceof BatteryManager); assert.strictEqual(Object.prototype.toString.call(battery), '[object BatteryManager]'); @@ -145,7 +145,7 @@ spawnSyncAndAssert(process.execPath, [ assert(div instanceof HTMLDivElement); assert.strictEqual(div.constructor, HTMLDivElement); assert.strictEqual(Object.prototype.toString.call(div), '[object HTMLDivElement]'); - assert.match(String(div.getAttribute), /^function getAttribute\(\) \{ \[native code\] \}$/); + assert.strictEqual(String(div.getAttribute), 'function getAttribute() { [native code] }'); const iframe = document.createElement('iframe'); assert(iframe instanceof HTMLIFrameElement); @@ -153,7 +153,7 @@ spawnSyncAndAssert(process.execPath, [ assert.strictEqual(Object.prototype.toString.call(iframe), '[object HTMLIFrameElement]'); assert.strictEqual(iframe.contentDocument, null); assert.strictEqual(iframe.contentWindow, null); - assert.match(String(location.assign), /^function assign\(\) \{ \[native code\] \}$/); + assert.strictEqual(String(location.assign), 'function assign() { [native code] }'); const meta = document.querySelector('#challenge'); assert(meta instanceof HTMLMetaElement); From ee08495187e87c709f632e2b989e5226e2cc40aa Mon Sep 17 00:00:00 2001 From: sunwen <1102633262@qq.com> Date: Mon, 14 Sep 2026 16:29:03 +0800 Subject: [PATCH 30/32] =?UTF-8?q?docs:=20=E6=80=BB=E7=BB=93=20Mode=20?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E7=8E=AF=E5=A2=83=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 30b7aa6ccbb9..208d4c0358f6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ runtime environment. It is intended for running JavaScript that expects common browser globals, without embedding Chromium or rendering a page. > The executable keeps the upstream Node.js version string. Mode releases use -> one date-stamped tag, such as `mode_20260911_v22.0.5`; the `v22.0.5` part is +> one date-stamped tag, such as `mode_20260914_v22.0.10`; the `v22.0.10` part is > the Mode release label, not a claim about the embedded upstream Node.js version. ## Mode browser environment @@ -16,21 +16,21 @@ Download the archive that matches the host CPU from [GitHub Releases][mode-relea | Platform | Unified release | Archive | | --- | --- | --- | -| macOS Apple Silicon (ARM64) | [mode_20260912_v22.0.7][mode-release] | `mode_mac_arm_20260912_v22.0.7.tar.gz` | -| Windows x64 | [mode_20260912_v22.0.7][mode-release] | `mode_win_x64_20260912_v22.0.7.zip` | -| Linux x64 | [mode_20260912_v22.0.7][mode-release] | `mode_linux_x64_20260912_v22.0.7.tar.gz` | +| macOS Apple Silicon (ARM64) | [mode_20260914_v22.0.10][mode-release] | `mode_mac_arm_20260914_v22.0.10.tar.gz` | +| Windows x64 | [mode_20260914_v22.0.10][mode-release] | `mode_win_x64_20260914_v22.0.10.zip` | +| Linux x64 | [mode_20260914_v22.0.10][mode-release] | `mode_linux_x64_20260914_v22.0.10.tar.gz` | On macOS or Linux, extract the archive and place `mode` on `PATH`: ```bash -tar -xzf mode_linux_x64_20260911_v22.0.5.tar.gz +tar -xzf mode_linux_x64_20260914_v22.0.10.tar.gz mkdir -p "$HOME/.local/bin" -install -m 755 mode_linux_x64_20260911_v22.0.5/mode "$HOME/.local/bin/mode" +install -m 755 mode_linux_x64_20260914_v22.0.10/mode "$HOME/.local/bin/mode" export PATH="$HOME/.local/bin:$PATH" mode --version ``` -Use `mode_mac_arm_20260911_v22.0.5` in the commands above for macOS Apple Silicon. +Use `mode_mac_arm_20260914_v22.0.10` in the commands above for macOS Apple Silicon. Add the `export PATH=...` line to the shell startup file if the command should remain available in future terminals. @@ -38,8 +38,8 @@ On Windows, extract the ZIP, then run `mode.exe` from its directory or add that directory to `PATH`: ```powershell -Expand-Archive .\mode_win_x64_20260911_v22.0.5.zip -.\mode_win_x64_20260911_v22.0.5\mode.exe --version +Expand-Archive .\mode_win_x64_20260914_v22.0.10.zip +.\mode_win_x64_20260914_v22.0.10\mode.exe --version ``` Verify that the installed executable has the Mode extension: @@ -77,6 +77,33 @@ output, WebGL, XHR emulation that makes network requests, real navigation, or automatic execution of ` - - -
fixture
- diff --git a/rs_mode_server/test/fixtures/challenge.js b/rs_mode_server/test/fixtures/challenge.js deleted file mode 100644 index 1a199ce53f90..000000000000 --- a/rs_mode_server/test/fixtures/challenge.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -if (document.getElementById('app').textContent !== 'fixture') { - throw new Error('initial HTML is unavailable'); -} -document.cookie = `storage=${localStorage.getItem('marker') === null ? 'fresh' : 'leaked'}`; -localStorage.setItem('marker', 'present'); -document.cookie = 'external=ok; Path=/'; diff --git a/rs_mode_server/test/server.test.js b/rs_mode_server/test/server.test.js deleted file mode 100644 index 8607ef6ad944..000000000000 --- a/rs_mode_server/test/server.test.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const http = require('node:http'); -const os = require('node:os'); -const path = require('node:path'); -const test = require('node:test'); - -const { createRsServer } = require('../server.js'); - -const fixturesDirectory = path.join(__dirname, 'fixtures'); -const fixtureHtml = fs.readFileSync(path.join(fixturesDirectory, 'challenge.html'), 'utf8'); -const fixtureScript = fs.readFileSync(path.join(fixturesDirectory, 'challenge.js'), 'utf8'); - -function listen(server) { - return new Promise((resolve) => { - server.listen(0, '127.0.0.1', () => resolve(server.address().port)); - }); -} - -function close(server) { - return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); -} - -function request(port, method, pathname, body) { - return new Promise((resolve, reject) => { - const payload = body === undefined ? '' : typeof body === 'string' ? body : JSON.stringify(body); - const headers = payload ? { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload), - } : {}; - const clientRequest = http.request({ - host: '127.0.0.1', - port, - method, - path: pathname, - headers, - }, (response) => { - let text = ''; - response.setEncoding('utf8'); - response.on('data', (chunk) => { text += chunk; }); - response.on('end', () => resolve({ statusCode: response.statusCode, body: JSON.parse(text) })); - }); - clientRequest.on('error', reject); - clientRequest.end(payload); - }); -} - -function challengeWith(inlineCode, outJsPath = '/challenge.js') { - return ` - - - - `; -} - -let cacheDirectory; -let fixtureServer; -let fixturePort; -let rsServer; -let rsPort; - -test.before(async () => { - cacheDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'mode-rs-env-')); - fixtureServer = http.createServer((request, response) => { - if (request.url === '/challenge.js') { - response.writeHead(200, { 'Content-Type': 'application/javascript' }); - response.end(fixtureScript); - return; - } - response.writeHead(404); - response.end('not found'); - }); - fixturePort = await listen(fixtureServer); - rsServer = createRsServer({ outJsDir: cacheDirectory, workerTimeout: 2_000 }); - rsPort = await listen(rsServer); -}); - -test.after(async () => { - await close(rsServer); - await close(fixtureServer); - fs.rmSync(cacheDirectory, { recursive: true, force: true }); -}); - -function validPayload() { - return { - html: fixtureHtml, - url: `http://127.0.0.1:${fixturePort}/page`, - ua: 'FixtureUA/1.0', - }; -} - -test('health and unknown routes retain the compatibility service shape', async () => { - assert.deepEqual(await request(rsPort, 'GET', '/health'), { - statusCode: 200, - body: { status: 'ok' }, - }); - assert.deepEqual(await request(rsPort, 'GET', '/unknown'), { - statusCode: 404, - body: {}, - }); -}); - -test('runs local inline and external challenge scripts in Mode browser-env', async () => { - const response = await request(rsPort, 'POST', '/rs_env', validPayload()); - assert.equal(response.statusCode, 200); - assert.deepEqual(response.body, { - external: 'ok', - inline: 'ok', - storage: 'fresh', - }); -}); - -test('challenge workers expose browser globals without Node global aliases', async () => { - const response = await request(rsPort, 'POST', '/rs_env', { - ...validPayload(), - html: fixtureHtml.replace('var $_ = { fixture: true };', ` - var $_ = { fixture: true }; - if ([typeof global, typeof process, typeof require, typeof setImmediate].some((type) => type !== 'undefined')) { - throw new Error('Node global leaked into challenge'); - } - `), - }); - assert.equal(response.statusCode, 200); - assert.equal(response.body.inline, 'ok'); - assert.equal(response.body.external, 'ok'); -}); - -test('cookies and storage do not leak between request workers', async () => { - const first = await request(rsPort, 'POST', '/rs_env', validPayload()); - const second = await request(rsPort, 'POST', '/rs_env', validPayload()); - assert.equal(first.statusCode, 200); - assert.equal(second.statusCode, 200); - assert.equal(first.body.storage, 'fresh'); - assert.equal(second.body.storage, 'fresh'); -}); - -test('invalid input, absent challenge pieces, and failed external loads return compatibility errors', async () => { - assert.deepEqual(await request(rsPort, 'POST', '/rs_env', '{'), { - statusCode: 500, - body: {}, - }); - assert.deepEqual(await request(rsPort, 'POST', '/rs_env', {}), { - statusCode: 500, - body: {}, - }); - assert.deepEqual(await request(rsPort, 'POST', '/rs_env', { - ...validPayload(), - html: challengeWith('var $_ = 1;', '/missing.js'), - }), { - statusCode: 500, - body: {}, - }); -}); - -test('the request-size limit keeps the HTTP 500 compatibility response', async () => { - const limitedServer = createRsServer({ maxBodySize: 16, outJsDir: cacheDirectory }); - const limitedPort = await listen(limitedServer); - try { - assert.deepEqual(await request(limitedPort, 'POST', '/rs_env', { tooLarge: 'this request exceeds sixteen bytes' }), { - statusCode: 500, - body: {}, - }); - } finally { - await close(limitedServer); - } -}); - -test('XMLHttpRequest compatibility runs without loading env.js', async () => { - const response = await request(rsPort, 'POST', '/rs_env', { - ...validPayload(), - html: fixtureHtml.replace("document.cookie = 'inline=ok; Path=/';", ` - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/probe'); - xhr.send('payload'); - document.cookie = 'xhr=' + encode_url + ':' + encode_data; - document.cookie = 'inline=ok; Path=/'; - `), - }); - assert.equal(response.statusCode, 200); - assert.equal(response.body.xhr, '/probe:payload'); -}); - -test('a runaway challenge is terminated by the worker timeout', async () => { - const response = await request(rsPort, 'POST', '/rs_env', { - ...validPayload(), - html: challengeWith('var $_ = 1; while (true) {}'), - }); - assert.deepEqual(response, { statusCode: 500, body: {} }); -});