CH-J Server Managerإدارة الخوادم عبر SSH
القائمة
المصدر المنشور

CH-J Server Manager

تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.

تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J / Server Manager 377 ملفات
core-20261019T111616Z-160a00bfcddc تاريخ الإضافة: · تاريخ البناء:
remoteFilePrivileges.test.js تاريخ الإضافة: لم يُسجَّل التاريخ ٧٫٧ KB · 106 أسطر
الشيفرة تنزيل الملف
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const { RemoteFileService, authorization } = require("../src/main/files/remoteFileService");
6const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
8function attrs(type, size = 0) {
9 return { size, mtime: 1, mode: 0o644, isDirectory: () => type === "directory", isSymbolicLink: () => type === "symlink", isFile: () => type === "file" };
11function 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 };
28test("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});
33test("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});
43test("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});
56test("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});
66test("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});
79test("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});
83test("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});
99test("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-25699f69ed792975e2b75d1d9b873139bc2494d4405fbc0d297bf71202df480f71e

CH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.

SHA-256 للأرشيفa9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
الملفات