CH-J Server Managerspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Server Manager

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J Proprietary Software License 1.14

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í.

9,0 KB · 218 řádkůStáhnout soubor
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const crypto = require("node:crypto");
6const fs = require("node:fs");
7const os = require("node:os");
8const path = require("node:path");
9const { HashUrlProvider } = require("../src/main/updates/hashUrlProvider");
11const baseUrl = "https://192.168.10.154/";
13function releasePayload(bytes) {
14 return {
15 update_available: true,
16 current_version: "0.0.1",
17 release: {
18 id: "alpha-002",
19 type: "app",
20 name: "CH-J Server Manager",
21 version: "0.0.2",
22 platform: "mac",
23 arch: "arm64",
24 channel: "alpha",
25 mandatory: false,
26 notes: "Test",
27 sha512: crypto.createHash("sha512").update(bytes).digest("hex"),
28 size: bytes.length,
29 filename: "CH-J.dmg",
30 download_url: `${baseUrl}files/apps/mac/0.0.2-arm64/CH-J.dmg`,
31 signature_url: `${baseUrl}files/apps/mac/0.0.2-arm64/CH-J.dmg.asc`,
32 published_at: "2026-08-07T17:30:00+02:00"
33 }
34 };
37test("provider checks PHP API and independently validates release", async (t) => {
38 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));
39 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
40 const bytes = Buffer.from("release-bytes");
41 const provider = new HashUrlProvider({
42 baseUrls: [baseUrl],
43 downloadRoot: root,
44 fetchImpl: async (url) => {
45 assert.match(url.toString(), /api\/latest\.php/);
46 assert.equal(url.searchParams.get("channel"), "alpha");
47 assert.equal(url.searchParams.get("arch"), "arm64");
48 return new Response(JSON.stringify(releasePayload(bytes)), { status: 200, headers: { "content-type": "application/json" } });
49 }
50 });
51 const result = await provider.checkForUpdates({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "alpha" });
52 assert.equal(result.updateAvailable, true);
53 assert.equal(result.release.version, "0.0.2");
54});
56test("provider lists every compatible build and keeps same-version releases distinct", async () => {
57 const bytes = Buffer.from("release-bytes");
58 const newest = releasePayload(bytes).release;
59 newest.id = "same-version-newer";
60 newest.version = "0.0.1";
61 newest.published_at = "2026-08-07T18:00:00+02:00";
62 const older = { ...newest, id: "same-version-older", published_at: "2026-08-07T17:00:00+02:00" };
63 const provider = new HashUrlProvider({
64 baseUrls: [baseUrl],
65 downloadRoot: os.tmpdir(),
66 fetchImpl: async (url) => {
67 assert.match(url.toString(), /api\/releases\.php/);
68 assert.equal(url.searchParams.get("platform"), "mac");
69 assert.equal(url.searchParams.get("arch"), "arm64");
70 assert.equal(url.searchParams.get("channel"), "alpha");
71 return new Response(JSON.stringify({ releases: [older, newest] }), { status: 200 });
72 }
73 });
74 const result = await provider.listReleases({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "alpha" });
75 assert.deepEqual(result.releases.map((release) => release.id), ["same-version-newer", "same-version-older"]);
76});
78test("all update channels are fetched separately and merged locally", async () => {
79 const bytes = Buffer.from("release-bytes");
80 const requestedChannels = [];
81 const provider = new HashUrlProvider({
82 baseUrls: [baseUrl],
83 downloadRoot: os.tmpdir(),
84 fetchImpl: async (url) => {
85 const channel = url.searchParams.get("channel");
86 requestedChannels.push(channel);
87 const release = releasePayload(bytes).release;
88 release.id = `${channel}-release`;
89 release.channel = channel;
90 return new Response(JSON.stringify({ releases: [release] }), { status: 200 });
91 }
92 });
93 const result = await provider.listReleases({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "all" });
94 assert.deepEqual(requestedChannels, ["alpha", "beta", "stable"]);
95 assert.deepEqual(new Set(result.releases.map((release) => release.channel)), new Set(["alpha", "beta", "stable"]));
96});
98test("provider rejects unsupported macOS x64 target before contacting server", async () => {
99 let contacted = false;
100 const provider = new HashUrlProvider({
101 baseUrls: [baseUrl],
102 downloadRoot: os.tmpdir(),
103 fetchImpl: async () => {
104 contacted = true;
105 return new Response(null, { status: 404 });
106 }
107 });
108 await assert.rejects(
109 () => provider.checkForUpdates({ currentVersion: "0.0.1", platform: "darwin", arch: "x64", channel: "alpha" }),
110 /Unsupported update architecture/
111 );
112 assert.equal(contacted, false);
113});
115test("provider downloads exact bytes and verifies SHA-512", async (t) => {
116 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));
117 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
118 const bytes = Buffer.from("verified release bytes");
119 const payload = releasePayload(bytes);
120 const provider = new HashUrlProvider({
121 baseUrls: [baseUrl],
122 downloadRoot: root,
123 signatureVerifier: {
124 async verifyFile() {
125 return { valid: true, primaryFingerprint: "0".repeat(40), signingFingerprints: ["1".repeat(40)] };
126 }
127 },
128 fetchImpl: async (url) => String(url).endsWith(".asc")
129 ? new Response("test-signature", { status: 200 })
130 : new Response(bytes, { status: 200, headers: { "content-length": String(bytes.length) } })
131 });
132 const release = {
133 ...payload.release,
134 downloadUrl: payload.release.download_url,
135 signatureUrl: payload.release.signature_url
136 };
137 const result = await provider.downloadAndVerify(release);
138 assert.equal(fs.readFileSync(result.path, "utf8"), bytes.toString("utf8"));
139 assert.equal(result.sha512, payload.release.sha512);
140});
142test("provider deletes staging file when SHA-512 does not match", async (t) => {
143 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));
144 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
145 const expected = Buffer.from("expected");
146 const actual = Buffer.from("modified");
147 const payload = releasePayload(expected);
148 payload.release.size = actual.length;
149 const provider = new HashUrlProvider({
150 baseUrls: [baseUrl],
151 downloadRoot: root,
152 signatureVerifier: { async verifyFile() { throw new Error("must not be reached"); } },
153 fetchImpl: async () => new Response(actual, { status: 200 })
154 });
155 await assert.rejects(() => provider.downloadAndVerify({
156 ...payload.release,
157 downloadUrl: payload.release.download_url,
158 signatureUrl: payload.release.signature_url
159 }), /SHA-512/);
160 const remaining = fs.readdirSync(path.dirname(path.join(root, `${payload.release.version}-${payload.release.id}`, payload.release.filename)));
161 assert.equal(remaining.some((name) => name.endsWith(".part")), false);
162});
164test("provider removes an artifact when mandatory OpenPGP verification fails", async (t) => {
165 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));
166 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
167 const bytes = Buffer.from("unsigned release bytes");
168 const payload = releasePayload(bytes);
169 const provider = new HashUrlProvider({
170 baseUrls: [baseUrl],
171 downloadRoot: root,
172 signatureVerifier: { async verifyFile() { throw new Error("invalid signature"); } },
173 fetchImpl: async (url) => String(url).endsWith(".asc")
174 ? new Response("bad-signature", { status: 200 })
175 : new Response(bytes, { status: 200 })
176 });
177 const release = {
178 ...payload.release,
179 downloadUrl: payload.release.download_url,
180 signatureUrl: payload.release.signature_url
181 };
182 await assert.rejects(() => provider.downloadAndVerify(release), /invalid signature/);
183 const destination = path.join(root, `${release.version}-${release.id}`, release.filename);
184 assert.equal(fs.existsSync(destination), false);
185 assert.equal(fs.existsSync(`${destination}.asc`), false);
186});
188test("provider re-hashes the same private-cache artifact immediately before installation", async (t) => {
189 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));
190 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
191 const bytes = Buffer.from("verified release bytes");
192 const payload = releasePayload(bytes);
193 const signatureVerifier = {
194 calls: 0,
195 async verifyFile() {
196 this.calls += 1;
197 return { valid: true, primaryFingerprint: "0".repeat(40), signingFingerprints: ["1".repeat(40)] };
198 }
199 };
200 const provider = new HashUrlProvider({
201 baseUrls: [baseUrl],
202 downloadRoot: root,
203 signatureVerifier,
204 fetchImpl: async (url) => String(url).endsWith(".asc")
205 ? new Response("test-signature", { status: 200 })
206 : new Response(bytes, { status: 200 })
207 });
208 const release = {
209 ...payload.release,
210 downloadUrl: payload.release.download_url,
211 signatureUrl: payload.release.signature_url
212 };
213 const downloaded = await provider.downloadAndVerify(release);
214 fs.chmodSync(downloaded.path, 0o600);
215 fs.writeFileSync(downloaded.path, Buffer.from("tampered release bytes"));
216 await assert.rejects(() => provider.reverifyForInstall(downloaded, release), /changed after verification|size changed/);
217 assert.equal(signatureVerifier.calls, 1, "tampered bytes must be rejected before the second OpenPGP pass");
218});

SHA-256: 354fd94a6f6118d45f397d6847f961bc84997fa2947a07e34836d5c5db6391ed

SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0