mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
#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.
74 lines
3.0 KiB
JavaScript
74 lines
3.0 KiB
JavaScript
'use strict';
|
|
|
|
// Optional HTTPS in WeKan's own server (#916, maintainer decision 2026-10-14).
|
|
// Pure: which settings turn it on and what they mean, tested without a server
|
|
// (tests/httpsServer.test.cjs). server/httpsServer.js does the listening.
|
|
//
|
|
// HTTPS_PORT the port to serve HTTPS on; HTTPS is off without it
|
|
// HTTPS_CERT_FILE the certificate (chain) - a path, or the PEM itself
|
|
// HTTPS_KEY_FILE its private key - a path, or the PEM itself
|
|
// HTTPS_BIND_IP the address to listen on (default: BIND_IP, else all)
|
|
// HTTPS_REDIRECT true: answer plain-HTTP requests with a redirect to HTTPS
|
|
//
|
|
// Without HTTPS_PORT nothing changes: WeKan serves HTTP on PORT, as always,
|
|
// and a proxy (Caddy, nginx) stays the recommendation for production.
|
|
|
|
function pemOrPath(value, readFile) {
|
|
const v = String(value || '').trim();
|
|
if (!v) return null;
|
|
if (v.includes('-----BEGIN ')) return v;
|
|
return readFile(v);
|
|
}
|
|
|
|
/**
|
|
* @return {null | {port: number, host: string|undefined, cert: string|Buffer,
|
|
* key: string|Buffer, redirect: boolean, certPath: string|null, keyPath: string|null}}
|
|
*/
|
|
function httpsConfig(env = process.env, readFile = path => require('fs').readFileSync(path)) {
|
|
const portText = String(env.HTTPS_PORT || '').trim();
|
|
if (!portText) return null;
|
|
const port = Number(portText);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
throw new Error(`HTTPS_PORT must be a port number (got "${portText}")`);
|
|
}
|
|
if (!env.HTTPS_CERT_FILE || !env.HTTPS_KEY_FILE) {
|
|
throw new Error('HTTPS_PORT is set, so HTTPS_CERT_FILE and HTTPS_KEY_FILE are needed too');
|
|
}
|
|
const cert = pemOrPath(env.HTTPS_CERT_FILE, readFile);
|
|
const key = pemOrPath(env.HTTPS_KEY_FILE, readFile);
|
|
const isPath = v => !String(v).includes('-----BEGIN ');
|
|
return {
|
|
port,
|
|
host: (env.HTTPS_BIND_IP || env.BIND_IP || '').trim() || undefined,
|
|
cert,
|
|
key,
|
|
redirect: String(env.HTTPS_REDIRECT || '').trim() === 'true',
|
|
certPath: isPath(env.HTTPS_CERT_FILE) ? String(env.HTTPS_CERT_FILE).trim() : null,
|
|
keyPath: isPath(env.HTTPS_KEY_FILE) ? String(env.HTTPS_KEY_FILE).trim() : null,
|
|
};
|
|
}
|
|
|
|
const HTTPS_SCHEME = 'https:';
|
|
|
|
/**
|
|
* Where a plain-HTTP request is redirected. ROOT_URL's host when ROOT_URL is
|
|
* an https:// address (it names the server); otherwise the host the browser
|
|
* asked for, on the HTTPS port. Only the path of the request is kept.
|
|
*/
|
|
function redirectLocation(hostHeader, url, httpsPort, rootUrl = '') {
|
|
let root = null;
|
|
try {
|
|
root = rootUrl ? new URL(rootUrl) : null;
|
|
} catch (e) {
|
|
root = null;
|
|
}
|
|
const path = String(url || '/').startsWith('/') && !String(url).startsWith('//') ? String(url || '/') : '/';
|
|
if (root && root.protocol === HTTPS_SCHEME) return `${root.origin}${path}`;
|
|
const host = String(hostHeader || '').replace(/:\d+$/, '');
|
|
if (!/^[A-Za-z0-9.-]+$|^\[[0-9A-Fa-f:.]+\]$/.test(host)) return null;
|
|
const port = Number(httpsPort) === 443 ? '' : `:${httpsPort}`;
|
|
return `${HTTPS_SCHEME}//${host}${port}${path}`;
|
|
}
|
|
|
|
module.exports = { httpsConfig, redirectLocation };
|