Files
duplicati_preview/test/tls.integration.test.js
T

248 lines
8.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { rm } from 'node:fs/promises';
import https from 'node:https';
import os from 'node:os';
import path from 'node:path';
import selfsigned from 'selfsigned';
import { startServer } from '../src/server.js';
async function startTlsRuntime() {
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'duplicati-tls-'));
const runtime = await startServer({
port: 0,
httpsPort: 0,
appDbPath: path.join(tempDirectory, 'app.sqlite'),
uploadDir: path.join(tempDirectory, 'sources'),
tlsDir: path.join(tempDirectory, 'tls'),
previewMaxBytes: 1024
});
const httpAddress = runtime.httpServer.address();
const httpsAddress = runtime.httpsServer.address();
const httpBaseUrl = `http://127.0.0.1:${httpAddress.port}`;
const httpsBaseUrl = `https://127.0.0.1:${httpsAddress.port}`;
return {
runtime,
httpBaseUrl,
httpsBaseUrl,
async close() {
await runtime.close();
await rm(tempDirectory, { recursive: true, force: true });
}
};
}
async function httpsRequestJson(baseUrl, pathname, { method = 'GET', headers = {}, body = null } = {}) {
const target = new URL(pathname, baseUrl);
return new Promise((resolve, reject) => {
const request = https.request(
target,
{
method,
headers,
rejectUnauthorized: false
},
(response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
const payloadText = Buffer.concat(chunks).toString('utf8');
let payload = {};
try {
payload = payloadText ? JSON.parse(payloadText) : {};
} catch (error) {
payload = { raw: payloadText };
}
resolve({
status: response.statusCode ?? 0,
headers: response.headers,
payload
});
});
}
);
request.on('error', reject);
if (body) {
request.write(body);
}
request.end();
});
}
async function setupAuthenticatedClient(baseUrl) {
const response = await fetch(`${baseUrl}/api/auth/setup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'Password123!'
})
});
const payload = await response.json();
const cookie = response.headers.get('set-cookie')?.split(';', 1)?.[0] ?? '';
assert.equal(response.status, 201);
assert.equal(payload.auth.authenticated, true);
assert.ok(cookie);
return {
cookie,
async json(pathname, options = {}) {
const responseValue = await fetch(`${baseUrl}${pathname}`, {
...options,
headers: {
...(options.headers ?? {}),
Cookie: cookie
}
});
const payloadValue = await responseValue.json();
return {
response: responseValue,
payload: payloadValue
};
}
};
}
test('dual-stack startup auto-generates a self-signed certificate and serves health checks over HTTP and HTTPS', async () => {
const runtime = await startTlsRuntime();
try {
const httpHealth = await fetch(`${runtime.httpBaseUrl}/healthz`).then((response) => response.json());
assert.equal(httpHealth.ok, true);
const httpsHealth = await httpsRequestJson(runtime.httpsBaseUrl, '/healthz');
assert.equal(httpsHealth.status, 200);
assert.equal(httpsHealth.payload.ok, true);
const authState = await fetch(`${runtime.httpBaseUrl}/api/auth/state`).then((response) => response.json());
assert.equal(authState.auth.setupRequired, true);
const client = await setupAuthenticatedClient(runtime.httpBaseUrl);
const tlsSummary = await httpsRequestJson(runtime.httpsBaseUrl, '/api/system/tls', {
headers: {
Cookie: client.cookie
}
});
assert.equal(tlsSummary.status, 200);
assert.equal(tlsSummary.payload.tls.mode, 'self-signed');
assert.equal(tlsSummary.payload.tls.activeSource, 'self-signed');
assert.ok(tlsSummary.payload.tls.certificate.fingerprint256);
} finally {
await runtime.close();
}
});
test('saving self-signed TLS settings regenerates certificate metadata and keeps HTTP/HTTPS both available', async () => {
const runtime = await startTlsRuntime();
try {
const client = await setupAuthenticatedClient(runtime.httpBaseUrl);
const before = await client.json('/api/system/tls');
const previousFingerprint = before.payload.tls.certificate.fingerprint256;
const updated = await client.json('/api/system/tls', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
mode: 'self-signed',
primaryDomain: 'preview.example.com',
subjectAltNames: ['preview.internal', '192.168.5.10']
})
});
assert.equal(updated.response.status, 200);
assert.equal(updated.payload.tls.primaryDomain, 'preview.example.com');
assert.equal(updated.payload.tls.activeSource, 'self-signed');
assert.notEqual(updated.payload.tls.certificate.fingerprint256, previousFingerprint);
const httpsState = await httpsRequestJson(runtime.httpsBaseUrl, '/api/system/tls', {
headers: {
Cookie: client.cookie
}
});
assert.equal(httpsState.status, 200);
assert.equal(httpsState.payload.tls.primaryDomain, 'preview.example.com');
assert.deepEqual(httpsState.payload.tls.subjectAltNames, ['preview.internal', '192.168.5.10']);
const httpHealth = await fetch(`${runtime.httpBaseUrl}/healthz`).then((response) => response.json());
assert.equal(httpHealth.ok, true);
const httpsHealth = await httpsRequestJson(runtime.httpsBaseUrl, '/healthz');
assert.equal(httpsHealth.status, 200);
} finally {
await runtime.close();
}
});
test('custom PEM upload activates custom certificate and delete falls back to self-signed', async () => {
const runtime = await startTlsRuntime();
try {
const client = await setupAuthenticatedClient(runtime.httpBaseUrl);
const generated = await selfsigned.generate(
[{ name: 'commonName', value: 'custom.example.com' }],
{
algorithm: 'sha256',
keySize: 2048,
days: 365,
extensions: [
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: 'custom.example.com' },
{ type: 2, value: 'cdn.custom.example.com' }
]
}
]
}
);
const uploadForm = new FormData();
uploadForm.set('primaryDomain', 'custom.example.com');
uploadForm.set('subjectAltNames', 'cdn.custom.example.com');
uploadForm.set('certificate', new File([generated.cert], 'fullchain.pem', { type: 'text/plain' }));
uploadForm.set('privateKey', new File([generated.private], 'privkey.pem', { type: 'text/plain' }));
const uploadResponse = await fetch(`${runtime.httpBaseUrl}/api/system/tls/upload`, {
method: 'POST',
headers: {
Cookie: client.cookie
},
body: uploadForm
});
const uploadPayload = await uploadResponse.json();
assert.equal(uploadResponse.status, 200);
assert.equal(uploadPayload.tls.mode, 'custom-pem');
assert.equal(uploadPayload.tls.activeSource, 'custom-pem');
const customFingerprint = uploadPayload.tls.certificate.fingerprint256;
const httpsState = await httpsRequestJson(runtime.httpsBaseUrl, '/api/system/tls', {
headers: {
Cookie: client.cookie
}
});
assert.equal(httpsState.status, 200);
assert.equal(httpsState.payload.tls.activeSource, 'custom-pem');
const deleteResult = await client.json('/api/system/tls/custom-certificate', {
method: 'DELETE'
});
assert.equal(deleteResult.response.status, 200);
assert.equal(deleteResult.payload.tls.mode, 'self-signed');
assert.equal(deleteResult.payload.tls.activeSource, 'self-signed');
assert.notEqual(deleteResult.payload.tls.certificate.fingerprint256, customFingerprint);
} finally {
await runtime.close();
}
});