Vydané zdroje
Stáhnout zdroje ZIP
CH-J Server Manager
Procházejte adresáře a soubory konkrétního vydání aplikace.
Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
1
"use strict";3
const test = require("node:test");4
const assert = require("node:assert/strict");5
const crypto = require("node:crypto");6
const { RemoteFileService } = require("../src/main/files/remoteFileService");8
const hash = (data) => crypto.createHash("sha256").update(data).digest("hex");9
function fixture() {10
let content = Buffer.from("original");11
let baseline = { uid: 0, gid: 33, mode: 0o6754, mtimeNs: "1000000000", hash: hash(content), xattrs: { "user.test": "dGVzdA==" } };12
let finalError, uploadError, cleanupError;13
const calls = [], copies = new Map(), journals = new Map();14
const manager = {15
list: () => [{ sessionId: "s", state: "connected", host: "server", port: 22, username: "user", profileId: "p" }],16
openSftp: async () => ({17
writeFile(path, data, options, callback) {18
calls.push({ op: "upload", path, options });19
if (copies.has(path)) return callback(new Error("Exists"));20
if (uploadError) { copies.set(path, data.subarray(0, 2)); return callback(uploadError); }21
copies.set(path, Buffer.from(data)); callback(null);22
}, end() {}23
}),24
async remoteEditor(_session, request, authorization) {25
calls.push({ ...request, authorization });26
const path = "/srv/custom-home/ch-j-sm/." + request.id + ".tmp";27
if (request.operation === "read") return { data: content.toString("base64"), baseline: { ...baseline } };28
if (request.operation === "prepare") {29
journals.set(request.id, request);30
return { temporary: path, recoveryId: request.id, uid: 1001 };31
}32
if (request.operation === "finalize") {33
if (finalError) throw finalError;34
assert.equal(hash(copies.get(path)), request.hash);35
if (request.baseline.hash !== baseline.hash) { const e = new Error("Conflict"); e.code = "FILE_CONFLICT"; throw e; }36
content = copies.get(path); baseline = { ...baseline, hash: request.hash };37
return { status: "saved", hash: request.hash, baseline: { ...baseline } };38
}39
if (request.operation === "cleanup") {40
if (cleanupError) throw cleanupError;41
copies.delete(path); journals.delete(request.id); return { status: "confirmed" };42
}43
if (request.operation === "list") return { items: [...journals.keys()].map((recoveryId) => ({ recoveryId })) };44
if (request.operation === "inspect") return { data: copies.get(path).toString("base64"), hash: hash(copies.get(path)), complete: true, status: "recovery-available", recoveryId: request.id };45
throw new Error("Unexpected operation");46
}47
};48
return { service: new RemoteFileService({ sessionManager: manager }), manager, calls, copies, journals,49
get content() { return content.toString(); }, get baseline() { return baseline; },50
set finalError(value) { finalError = value; }, set uploadError(value) { uploadError = value; }, set cleanupError(value) { cleanupError = value; },51
change() { content = Buffer.from("external change"); baseline = { ...baseline, hash: hash(content) }; } };52
}54
test("editor captures a baseline, stages privately in actual home and finalizes once before cleanup", async () => {55
const f = fixture();56
const opened = await f.service.readText("s", "/etc/test config");57
assert.equal(opened.text, "original");58
assert.equal(opened.metadata.uid, 0);59
const result = await f.service.writeText("s", "/etc/test config", "modified", { editId: opened.editId, sudo: true, sudoPassword: "secret" });60
assert.equal(result.status, "saved");61
assert.equal(f.content, "modified");62
assert.deepEqual(f.calls.map((call) => call.operation || call.op), ["read", "prepare", "upload", "finalize", "cleanup"]);63
const upload = f.calls[2];64
assert.match(upload.path, /^\/srv\/custom-home\/ch-j-sm\/\.[a-f0-9]{32}\.tmp$/);65
assert.deepEqual(upload.options, { flag: "wx", mode: 0o600 });66
assert.equal(f.calls[3].baseline.mode, 0o6754);67
assert.equal(f.calls[3].baseline.gid, 33);68
assert.equal(f.calls[3].authorization.sudoPassword, "secret");69
assert.equal(f.copies.size, 0);70
});72
for (const code of ["FILE_SUDO_FAILED", "FILE_PERMISSION_DENIED", "FILE_CONFLICT", "FILE_REMOTE_IO_FAILED", "FILE_NOT_FOUND", "FILE_METADATA_UNSUPPORTED"]) {73
test(code + " preserves original and complete recovery copy", async () => {74
const f = fixture(); await f.service.readText("s", "/etc/config");75
f.finalError = Object.assign(new Error("failure"), { code });76
const result = await f.service.saveText("s", "/etc/config", "modified");77
assert.equal(result.ok, false); assert.equal(result.error.code, code);78
assert.equal(f.content, "original"); assert.equal([...f.copies.values()][0].toString(), "modified");79
assert.equal(f.calls.some((c) => c.operation === "cleanup"), false);80
assert.equal((await f.service.listRecovery("s")).items.length, 1);81
assert.equal((await f.service.readRecovery("s", result.error.recoveryId)).text, "modified");82
});83
}85
test("upload failure leaves target intact, partial copy discoverable and never finalizes", async () => {86
const f = fixture(); await f.service.readText("s", "/var/www/config");87
f.uploadError = new Error("Disk full");88
const result = await f.service.saveText("s", "/var/www/config", "modified");89
assert.equal(result.error.code, "FILE_UPLOAD_FAILED"); assert.equal(f.content, "original");90
assert.equal(f.calls.some((c) => c.operation === "finalize"), false);91
assert.equal(f.copies.size, 1);92
});94
test("stalled SFTP upload times out without finalization and closes late channels", async (t) => {95
t.mock.timers.enable({ apis: ["setTimeout"] });96
const f = fixture(); await f.service.readText("s", "/etc/config");97
let finishOpen, ended = 0;98
f.manager.openSftp = () => new Promise((resolve) => { finishOpen = resolve; });99
const saving = f.service.saveText("s", "/etc/config", "modified");100
await new Promise((resolve) => setImmediate(resolve));101
t.mock.timers.tick(60000);102
const result = await saving;103
assert.equal(result.error.code, "FILE_UPLOAD_TIMEOUT"); assert.equal(f.content, "original");104
assert.equal(f.calls.some((c) => c.operation === "finalize"), false);105
finishOpen({ end() { ended++; } });106
await new Promise((resolve) => setImmediate(resolve));107
assert.equal(ended, 1);108
});110
test("lost final confirmation retains recovery and prevents automatic repeat, including after reconnect", async () => {111
const f = fixture(); await f.service.readText("s", "/etc/config");112
f.finalError = Object.assign(new Error("disconnected"), { code: "FILE_RESULT_UNKNOWN" });113
const result = await f.service.saveText("s", "/etc/config", "modified");114
assert.equal(result.error.status, "unknown");115
assert.equal((await f.service.saveText("s", "/etc/config", "modified")).error.code, "FILE_RESULT_UNKNOWN");116
assert.equal(f.calls.filter((c) => c.operation === "finalize").length, 1);117
const reconnected = new RemoteFileService({ sessionManager: f.manager });118
assert.equal((await reconnected.listRecovery("s")).items[0].recoveryId, result.error.recoveryId);119
assert.equal((await reconnected.readRecovery("s", result.error.recoveryId)).text, "modified");120
});122
test("unexpected remote failures remain unknown and never enable automatic repetition", async () => {123
const f = fixture(); await f.service.readText("s", "/etc/config");124
f.finalError = Object.assign(new Error("Unexpected exit"), { code: "FILE_UNEXPECTED_EXIT" });125
const result = await f.service.saveText("s", "/etc/config", "modified");126
assert.equal(result.error.code, "FILE_RESULT_UNKNOWN"); assert.equal(result.error.status, "unknown");127
assert.equal(f.copies.size, 1);128
});130
test("simultaneous saves of one document are rejected while the first upload is pending", async () => {131
const f = fixture(); const opened = await f.service.readText("s", "/etc/config");132
let finishUpload;133
const open = f.manager.openSftp;134
f.manager.openSftp = async () => {135
const sftp = await open(); const write = sftp.writeFile;136
sftp.writeFile = (...args) => { finishUpload = () => write(...args); };137
return sftp;138
};139
const first = f.service.writeText("s", "/etc/config", "first", { editId: opened.editId });140
await new Promise((resolve) => setImmediate(resolve));141
const second = await f.service.saveText("s", "/etc/config", "second", { editId: opened.editId });142
assert.equal(second.ok, false); assert.equal(f.calls.filter((c) => c.operation === "prepare").length, 1);143
finishUpload(); await first;144
assert.equal(f.content, "first");145
});147
test("editor preserves a trailing space in a filename", async () => {148
const f = fixture(); await f.service.readText("s", "/etc/config ");149
await f.service.writeText("s", "/etc/config ", "modified");150
assert.equal(f.calls.find((c) => c.operation === "finalize").path, "/etc/config ");151
});153
test("confirmed save stays successful when cleanup cannot be confirmed", async () => {154
const f = fixture(); await f.service.readText("s", "/etc/config"); f.cleanupError = new Error("disconnected");155
const saved = await f.service.writeText("s", "/etc/config", "modified");156
assert.equal(saved.status, "saved"); assert.equal(saved.recoveryAvailable, true); assert.equal(f.copies.size, 1);157
});159
test("independent editor baselines detect concurrent changes and temporary names never collide", async () => {160
const f = fixture(); const one = await f.service.readText("s", "/etc/config"); const two = await f.service.readText("s", "/etc/config");161
await f.service.writeText("s", "/etc/config", "first", { editId: one.editId });162
const result = await f.service.saveText("s", "/etc/config", "second", { editId: two.editId });163
assert.equal(result.error.code, "FILE_CONFLICT"); assert.equal(f.content, "first");164
const paths = f.calls.filter((c) => c.op === "upload").map((c) => c.path);165
assert.equal(new Set(paths).size, 2);166
});168
test("legacy calls cannot silently choose a baseline when multiple documents share a path", async () => {169
const f = fixture(); const one = await f.service.readText("s", "/etc/config"); const two = await f.service.readText("s", "/etc/config");170
const ambiguous = await f.service.saveText("s", "/etc/config", "modified");171
assert.equal(ambiguous.error.code, "FILE_EDIT_ID_REQUIRED"); assert.equal(f.content, "original");172
f.service.closeText(one.editId);173
const result = await f.service.writeText("s", "/etc/config", "modified");174
assert.equal(result.editId, two.editId);175
});177
test("editor rejects traversal and cross-plugin/cross-server baseline reuse", async () => {178
const f = fixture();179
await assert.rejects(() => f.service.readText("s", "/etc/../etc/passwd"), /traversal/);180
const opened = await f.service.readText("s", "/etc/config", {}, "plugin-a");181
await assert.rejects(() => f.service.writeText("s", "/etc/config", "x", { editId: opened.editId }, "plugin-b"), /baseline/);182
f.manager.list = () => [{ sessionId: "s", state: "connected", host: "other", username: "user" }];183
await assert.rejects(() => f.service.writeText("s", "/etc/config", "x", { editId: opened.editId }, "plugin-a"), /baseline/);184
});186
test("ordinary writable saves work with legacy arguments after opening; document handles can be released", async () => {187
const f = fixture(); const opened = await f.service.readText("s", "/home/user/config");188
await f.service.writeText("s", "/home/user/config", "modified");189
assert.deepEqual(f.calls.find((c) => c.operation === "finalize").authorization, {});190
assert.equal(f.service.closeText(opened.editId).closed, true);191
await assert.rejects(() => f.service.writeText("s", "/home/user/config", "x"), /baseline/);192
});SHA-256: b4ca996f0b9f8e892d8e18d6e4189ba6b56534f1a8b9de135d64a894ae389ec9
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0