[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
187
electron/plugins/pluginProtocol.test.cjs
Normal file
187
electron/plugins/pluginProtocol.test.cjs
Normal file
@@ -0,0 +1,187 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
|
||||
const {
|
||||
PluginProtocol,
|
||||
decodeRequestPath,
|
||||
readContainedFile,
|
||||
} = require("./pluginProtocol.cjs");
|
||||
|
||||
function createRoots(context) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-plugin-protocol-"));
|
||||
context.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const roots = {
|
||||
packageRoot: path.join(root, "package"),
|
||||
runtimeDirectory: path.join(root, "runtime"),
|
||||
sdkDirectory: path.join(root, "sdk"),
|
||||
contractDirectory: path.join(root, "contract"),
|
||||
};
|
||||
for (const directory of Object.values(roots)) fs.mkdirSync(directory, { recursive: true });
|
||||
fs.writeFileSync(path.join(roots.packageRoot, "index.js"), "export default {};\n");
|
||||
fs.mkdirSync(path.join(roots.packageRoot, ".runtime"));
|
||||
fs.writeFileSync(path.join(roots.packageRoot, ".runtime", "index.js"), "export default {};\n");
|
||||
fs.writeFileSync(path.join(roots.runtimeDirectory, "browserRuntime.mjs"), "export {};\n");
|
||||
fs.writeFileSync(path.join(roots.sdkDirectory, "index.js"), "export {};\n");
|
||||
fs.writeFileSync(path.join(roots.contractDirectory, "index.js"), "export {};\n");
|
||||
return roots;
|
||||
}
|
||||
|
||||
test("plugin URL decoding rejects traversal and platform separators", () => {
|
||||
assert.deepEqual(decodeRequestPath("/package/dist/index.js"), ["package", "dist", "index.js"]);
|
||||
for (const unsafe of ["/package/../escape", "/package/%2e%2e/escape", "/package/a%5cb", "/package//x"]) {
|
||||
assert.throws(() => decodeRequestPath(unsafe));
|
||||
}
|
||||
});
|
||||
|
||||
test("contained resource reads reject symlink escape", async (context) => {
|
||||
if (process.platform === "win32") return;
|
||||
const roots = createRoots(context);
|
||||
const outside = path.join(path.dirname(roots.packageRoot), "outside.js");
|
||||
fs.writeFileSync(outside, "secret");
|
||||
fs.symlinkSync(outside, path.join(roots.packageRoot, "linked.js"));
|
||||
await assert.rejects(readContainedFile(roots.packageRoot, ["linked.js"]), /symlink outside/);
|
||||
});
|
||||
|
||||
test("runtime-scoped protocol serves only registered package and host resources", async (context) => {
|
||||
const roots = createRoots(context);
|
||||
let handler;
|
||||
let unhandled = false;
|
||||
const fakeSession = {
|
||||
protocol: {
|
||||
handle(scheme, callback) {
|
||||
assert.equal(scheme, "netcatty-plugin");
|
||||
handler = callback;
|
||||
},
|
||||
unhandle(scheme) {
|
||||
assert.equal(scheme, "netcatty-plugin");
|
||||
unhandled = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
const protocol = new PluginProtocol(roots);
|
||||
const sessionRegistration = protocol.registerSession(fakeSession);
|
||||
assert.throws(() => protocol.registerSession(fakeSession), /already registered/);
|
||||
const config = { pluginId: "com.example.test" };
|
||||
const registration = protocol.registerRuntime({
|
||||
pluginId: "com.example.test",
|
||||
packageRoot: roots.packageRoot,
|
||||
config,
|
||||
});
|
||||
|
||||
const page = await handler({ method: "GET", url: registration.url });
|
||||
assert.equal(page.status, 200);
|
||||
assert.doesNotMatch(page.headers.get("content-security-policy"), /unsafe-inline/);
|
||||
assert.match(page.headers.get("content-security-policy"), /nonce-/);
|
||||
assert.match(page.headers.get("content-security-policy"), /worker-src 'none'/);
|
||||
assert.equal(page.headers.get("x-dns-prefetch-control"), "off");
|
||||
assert.match(page.headers.get("permissions-policy"), /camera=\(\)/);
|
||||
const packageResponse = await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/package/index.js`,
|
||||
});
|
||||
assert.equal(await packageResponse.text(), "export default {};\n");
|
||||
const dotPackageResponse = await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/package/.runtime/index.js`,
|
||||
});
|
||||
assert.equal(dotPackageResponse.status, 200);
|
||||
assert.equal(await dotPackageResponse.text(), "export default {};\n");
|
||||
const configResponse = await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/__host/runtime/config.json`,
|
||||
});
|
||||
assert.deepEqual(await configResponse.json(), config);
|
||||
const otherRuntime = await handler({ method: "GET", url: "netcatty-plugin://unknown/package/index.js" });
|
||||
assert.equal(otherRuntime.status, 404);
|
||||
registration.dispose();
|
||||
assert.equal((await handler({ method: "GET", url: registration.url })).status, 404);
|
||||
sessionRegistration.dispose();
|
||||
sessionRegistration.dispose();
|
||||
assert.equal(unhandled, true);
|
||||
});
|
||||
|
||||
test("browser runtime import maps accept reviewed host modules without protocol changes", async (context) => {
|
||||
const roots = createRoots(context);
|
||||
const uiDirectory = path.join(path.dirname(roots.packageRoot), "ui");
|
||||
fs.mkdirSync(uiDirectory);
|
||||
fs.writeFileSync(path.join(uiDirectory, "index.js"), "export const ui = true;\n");
|
||||
let handler;
|
||||
const protocol = new PluginProtocol({
|
||||
runtimeDirectory: roots.runtimeDirectory,
|
||||
moduleResources: [
|
||||
{ specifier: "@netcatty/plugin-sdk", directory: roots.sdkDirectory },
|
||||
{ specifier: "@netcatty/plugin-contract", directory: roots.contractDirectory },
|
||||
{ specifier: "@netcatty/plugin-ui", directory: uiDirectory },
|
||||
],
|
||||
});
|
||||
protocol.registerSession({
|
||||
protocol: {
|
||||
handle(_scheme, callback) { handler = callback; },
|
||||
unhandle() {},
|
||||
},
|
||||
});
|
||||
const registration = protocol.registerRuntime({
|
||||
pluginId: "com.example.modules",
|
||||
packageRoot: roots.packageRoot,
|
||||
config: {},
|
||||
});
|
||||
const page = await (await handler({ method: "GET", url: registration.url })).text();
|
||||
assert.match(page, /@netcatty\/plugin-ui/);
|
||||
assert.match(page, /__host\/modules\/m2\/index\.js/);
|
||||
const moduleResponse = await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/__host/modules/m2/index.js`,
|
||||
});
|
||||
assert.equal(await moduleResponse.text(), "export const ui = true;\n");
|
||||
});
|
||||
|
||||
test("custom views load packaged resources only with a network-denying document policy", async (context) => {
|
||||
const roots = createRoots(context);
|
||||
fs.writeFileSync(path.join(roots.packageRoot, "view.html"), "<!doctype html><script src=\"view.js\"></script>\n");
|
||||
fs.writeFileSync(path.join(roots.packageRoot, "view.js"), "globalThis.ready = true;\n");
|
||||
fs.writeFileSync(path.join(roots.packageRoot, "view.svg"), "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>\n");
|
||||
let handler;
|
||||
const protocol = new PluginProtocol(roots);
|
||||
protocol.registerSession({
|
||||
protocol: {
|
||||
handle(_scheme, callback) { handler = callback; },
|
||||
unhandle() {},
|
||||
},
|
||||
});
|
||||
const registration = protocol.registerView({
|
||||
pluginId: "com.example.view",
|
||||
packageRoot: roots.packageRoot,
|
||||
entry: "view.html",
|
||||
});
|
||||
const page = await handler({ method: "GET", url: registration.url });
|
||||
assert.equal(page.status, 200);
|
||||
assert.match(page.headers.get("content-security-policy"), /connect-src 'none'/u);
|
||||
assert.match(page.headers.get("content-security-policy"), /frame-src 'none'/u);
|
||||
assert.match(page.headers.get("permissions-policy"), /clipboard-write=\(\)/u);
|
||||
const svgRegistration = protocol.registerView({
|
||||
pluginId: "com.example.svg-view",
|
||||
packageRoot: roots.packageRoot,
|
||||
entry: "view.svg",
|
||||
});
|
||||
const svgPage = await handler({ method: "GET", url: svgRegistration.url });
|
||||
assert.equal(svgPage.headers.get("content-type"), "image/svg+xml");
|
||||
assert.match(svgPage.headers.get("content-security-policy"), /worker-src 'none'/u);
|
||||
assert.match(svgPage.headers.get("permissions-policy"), /camera=\(\)/u);
|
||||
assert.equal((await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/package/view.js`,
|
||||
})).status, 200);
|
||||
assert.equal((await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/__host/runtime/browserRuntime.mjs`,
|
||||
})).status, 404);
|
||||
assert.equal((await handler({
|
||||
method: "GET",
|
||||
url: `netcatty-plugin://${registration.token}/index.html`,
|
||||
})).status, 404);
|
||||
});
|
||||
Reference in New Issue
Block a user