Offentliggjort kilde
Download kilde ZIP
CH-J Server Manager
Gennemse mapper og filer til en specifik applikationsudgivelse.
CH-J / Server Manager
377 filer
core-20261019T111616Z-160a00bfcddc
Tilføjet: · Bygget:
1
"use strict";3
const test = require("node:test");4
const assert = require("node:assert/strict");5
const { RemoteFileService, authorization } = require("../src/main/files/remoteFileService");6
const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");8
function attrs(type, size = 0) {9
return { size, mtime: 1, mode: 0o644, isDirectory: () => type === "directory", isSymbolicLink: () => type === "symlink", isFile: () => type === "file" };10
}11
function fixture({ sftpError = null, entries = [], stats = {} } = {}) {12
const helper = []; const sftpCalls = [];13
const sessionManager = {14
list: () => [{ sessionId: "s", state: "connected", host: "h", port: 22, username: "user", profileId: "p" }],15
remoteEditor: async (_session, request, auth) => { helper.push({ request, auth }); return request.operation === "listdir" ? { entries: [{ name: "shadow", type: "file", size: 9, mtime: 1, mode: 0o100640 }] } : request.operation === "remove" ? { type: "file", entries: 1 } : { ok: true }; },16
openSftp: async () => new Proxy({}, { get: (_target, method) => method === "then" ? undefined : method === "end" ? () => {} : (...args) => {17
const cb = args.pop(); sftpCalls.push([method, ...args]);18
if (method === "readdir") return cb(null, entries);19
if (method === "stat" || method === "lstat") return stats[args[0]] ? cb(null, stats[args[0]]) : cb(Object.assign(new Error("No such file"), { code: 2 }));20
if (sftpError) return cb(sftpError);21
cb(null);22
} })23
};24
const privilegedExecutor = { probe: async () => ({ mode: "sudo-password", username: "user" }), hasCachedPassword: () => true };25
return { service: new RemoteFileService({ sessionManager, privilegedExecutor }), helper, sftpCalls };26
}28
test("only explicit authorization fields reach the privileged helper", () => {29
assert.deepEqual(authorization({ sudo: true, sudoPassword: "pw", rememberSudo: true, timeoutMs: 1, editId: "x", useCachedSudo: false }), { sudo: true, sudoPassword: "pw", rememberSudo: true });30
assert.deepEqual(authorization({ sudo: "yes", sudoPassword: "" }), {});31
});33
test("SFTP permission failures become FILE_PERMISSION_DENIED so the UI can offer sudo", async () => {34
const denied = Object.assign(new Error("Permission denied"), { code: 3 });35
const f = fixture({ sftpError: denied });36
await assert.rejects(() => f.service.createFile("s", "/etc/new.conf"), { code: "FILE_PERMISSION_DENIED" });37
await assert.rejects(() => f.service.mkdir("s", "/etc/new.d"), { code: "FILE_PERMISSION_DENIED" });38
await assert.rejects(() => f.service.rename("s", "/etc/a", "/etc/b"), { code: "FILE_PERMISSION_DENIED" });39
const exists = fixture({ sftpError: Object.assign(new Error("Failure"), { code: 4 }), stats: { "/srv/x": attrs("file") } });40
await assert.rejects(() => exists.service.createFile("s", "/srv/x"), { code: "FILE_EXISTS" });41
});43
test("privileged create, mkdir, rename and remove use the shared helper with sudo", async () => {44
const f = fixture();45
await f.service.createFile("s", "/etc/new.conf", { sudo: true, sudoPassword: "pw" });46
await f.service.mkdir("s", "/etc/new.d", { sudo: true });47
await f.service.rename("s", "/etc/a.conf", "/etc/b.conf", { sudo: true });48
await f.service.removeMany("s", ["/etc/nginx/old.conf"], true, { sudo: true });49
assert.deepEqual(f.helper.map((call) => call.request.operation), ["create", "mkdir", "rename", "remove"]);50
assert.ok(f.helper.every((call) => call.auth.sudo === true));51
assert.equal(f.helper[0].auth.sudoPassword, "pw");52
assert.equal(f.helper[3].request.recursive, true);53
assert.equal(f.sftpCalls.length, 0, "no unprivileged SFTP attempt when sudo was chosen");54
});56
test("system directories are never removed or renamed, with or without sudo", async () => {57
const f = fixture();58
for (const target of ["/etc", "/usr", "/var", "/home"]) {59
await assert.rejects(() => f.service.removeMany("s", [target], true, { sudo: true }), { code: "FILE_PROTECTED_PATH" });60
await assert.rejects(() => f.service.remove("s", target), { code: "FILE_PROTECTED_PATH" });61
await assert.rejects(() => f.service.rename("s", target, `${target}.old`, { sudo: true }), { code: "FILE_PROTECTED_PATH" });62
}63
assert.equal(f.helper.length, 0);64
});66
test("listing resolves link targets and privileged listing goes through sudo", async () => {67
const f = fixture({ entries: [{ filename: "sites-enabled", attrs: attrs("directory") }, { filename: "default", attrs: attrs("symlink") }, { filename: "dir-link", attrs: attrs("symlink") }, { filename: "broken", attrs: attrs("symlink") }],68
stats: { "/etc/nginx/default": attrs("file", 321), "/etc/nginx/dir-link": attrs("directory") } });69
const entries = await f.service.list("s", "/etc/nginx");70
const byName = Object.fromEntries(entries.map((entry) => [entry.name, entry]));71
assert.equal(byName.default.targetType, "file"); assert.equal(byName.default.size, 321);72
assert.equal(byName["dir-link"].targetType, "directory");73
assert.equal(byName.broken.targetType, "missing");74
const privileged = await f.service.list("s", "/etc/ssl/private", { sudo: true, sudoPassword: "pw" });75
assert.equal(privileged[0].name, "shadow"); assert.equal(privileged[0].privileged, true);76
assert.equal(f.helper.at(-1).request.operation, "listdir"); assert.equal(f.helper.at(-1).auth.sudo, true);77
});79
test("privileges report the detected mode and whether a password is cached in memory", async () => {80
assert.deepEqual(await fixture().service.privileges("s"), { mode: "sudo-password", username: "user", sudoCached: true });81
});83
test("plugin IPC exposes transfers only with files.transfer and scopes them to the plugin", async () => {84
const handlers = new Map(); const calls = [];85
const transferManager = { on() {}, list: (owner) => { calls.push(["list", owner]); return { jobs: [] }; },86
pause: (owner, ids) => { calls.push(["pause", owner, ids]); return { paused: 0 }; }, start: (owner, planId, options) => { calls.push(["start", owner, planId, options]); return {}; } };87
const runtime = new PluginRuntime({ BrowserWindow: class {}, registry: {}, remoteFileService: {}, transferManager, isVaultUnlocked: () => true });88
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });89
const window = { isDestroyed: () => false };90
runtime.contexts.set(1, { manifest: { id: "chj.file-manager", permissions: ["files.transfer"] }, window });91
runtime.contexts.set(2, { manifest: { id: "other", permissions: ["files.read"] }, window });92
await handlers.get("plugin:files:transfers:list")({ sender: { id: 1 } }, {});93
await handlers.get("plugin:files:transfers:pause")({ sender: { id: 1 } }, { ids: "all" });94
await handlers.get("plugin:files:transfers:start")({ sender: { id: 1 } }, { planId: "p1", options: { conflict: "skip" } });95
assert.deepEqual(calls, [["list", "chj.file-manager"], ["pause", "chj.file-manager", "all"], ["start", "chj.file-manager", "p1", { conflict: "skip" }]]);96
await assert.rejects(() => handlers.get("plugin:files:transfers:list")({ sender: { id: 2 } }, {}), { code: "PLUGIN_PERMISSION_DENIED" });97
});99
test("plugin file errors keep their code in the message across Electron IPC", async () => {100
const handlers = new Map();101
const remoteFileService = { createFile: async () => { throw Object.assign(new Error("Permission denied"), { code: "FILE_PERMISSION_DENIED" }); } };102
const runtime = new PluginRuntime({ BrowserWindow: class {}, registry: {}, remoteFileService, isVaultUnlocked: () => true });103
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });104
runtime.contexts.set(1, { manifest: { id: "fm", permissions: ["files.write"] }, window: { isDestroyed: () => false } });105
await assert.rejects(() => handlers.get("plugin:files:createFile")({ sender: { id: 1 } }, { sessionId: "s", path: "/etc/x" }), /^Error: FILE_PERMISSION_DENIED: /);106
});SHA-256
99f69ed792975e2b75d1d9b873139bc2494d4405fbc0d297bf71202df480f71eCH-J Proprietary Software License 1.14 · Kilden leveres under CH-J Proprietary Software License 1.14. Dens tilgængelighed ændrer ikke licensbetingelserne eller giver yderligere rettigheder.
Arkiv SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a