Files
Lauri Ojansivu 1bf7a62cab Serve HTTPS from WeKan itself, when a certificate is configured.
#916 (decided 2026-10-14): HTTPS_PORT with HTTPS_CERT_FILE and
HTTPS_KEY_FILE (a path or the PEM itself) makes WeKan serve HTTPS; without
them nothing changes, and a proxy stays the recommendation for
production. Meteor's HTTP server - pages, DDP, websockets - is not
replaced: a TLS listener hands each decrypted connection to it, so
everything works the same over HTTPS. A renewed certificate is loaded
within a minute without a restart (a broken one keeps the old).
HTTPS_REDIRECT=true redirects plain HTTP to ROOT_URL when that is https,
else the same host on the HTTPS port, keeping only the path. The snap
gets https-port, https-cert-file, https-key-file, https-bind-ip and
https-redirect.

tests/httpsServer.test.cjs makes a real certificate with openssl, serves
a page and a websocket upgrade through the bridge, swaps the certificate
without a restart, and refuses incomplete settings, an untrusted
certificate, an injected host and a scheme-relative redirect.

Thanks to 68040 and xet7 !

Fixes #916.
2026-10-14 08:25:25 +03:00

45 lines
1.6 KiB
JavaScript

'use strict';
// The TLS listener of optional HTTPS (#916): encrypted connections in, each
// decrypted socket handed to an existing HTTP server - Meteor's, in WeKan -
// which serves it exactly as a plain connection, websocket upgrades included.
// No Meteor here, so tests/httpsServer.test.cjs runs it with a real
// certificate and a plain http.Server.
const fs = require('fs');
const tls = require('tls');
function startHttpsBridge(httpServer, config, { readConfig = null, log = console } = {}) {
const server = tls.createServer({ cert: config.cert, key: config.key, ALPNProtocols: ['http/1.1'] }, socket => {
httpServer.emit('connection', socket);
});
server.on('tlsClientError', () => { /* a client that cannot speak TLS: its problem */ });
server.on('error', error => log.error(`HTTPS server: ${error.message}`));
// Renewed certificates are picked up without a restart.
const watched = [];
if (readConfig) {
const reload = () => {
try {
const next = readConfig();
if (next) server.setSecureContext({ cert: next.cert, key: next.key });
log.log('HTTPS certificate reloaded.');
} catch (error) {
log.error(`HTTPS certificate not reloaded, keeping the current one: ${error.message}`);
}
};
for (const file of [config.certPath, config.keyPath].filter(Boolean)) {
fs.watchFile(file, { interval: 60000 }, (now, before) => {
if (now.mtimeMs !== before.mtimeMs) reload();
});
watched.push(file);
}
}
const close = () => {
for (const file of watched) fs.unwatchFile(file);
server.close();
};
return { server, close };
}
module.exports = { startHttpsBridge };