From a87a622cba7dd89248ad9b593d5f6d6f435000d2 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Thu, 13 Apr 2023 17:00:34 +0200 Subject: [PATCH] Prosody: renew self signed certificates periodically --- server/lib/prosody/certificates.ts | 116 +++++++++++++++++++++++++++++ server/lib/prosody/config.ts | 10 ++- server/lib/prosody/ctl.ts | 35 +++------ server/lib/prosody/logrotate.ts | 6 +- 4 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 server/lib/prosody/certificates.ts diff --git a/server/lib/prosody/certificates.ts b/server/lib/prosody/certificates.ts new file mode 100644 index 00000000..1855afcc --- /dev/null +++ b/server/lib/prosody/certificates.ts @@ -0,0 +1,116 @@ +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { ProsodyConfig } from './config' +import { isDebugMode } from '../debug' +import { prosodyCtl, reloadProsody } from './ctl' +import * as path from 'path' +import * as fs from 'fs' + +interface Renew { + timer: NodeJS.Timer +} +let renew: Renew | undefined + +function _filePathToTest (options: RegisterServerOptions, config: ProsodyConfig): string { + return path.resolve(config.paths.certs, config.host + '.crt') +} + +async function ensureProsodyCertificates (options: RegisterServerOptions, config: ProsodyConfig): Promise { + if (config.certificates !== 'generate-self-signed') { return } + const logger = options.peertubeHelpers.logger + logger.info('Prosody needs certicicates, checking if certificates are okay...') + + const prosodyDomain = config.host + const filepath = _filePathToTest(options, config) + if (fs.existsSync(filepath)) { + logger.info(`The certificate ${filepath} exists, no need to generate it`) + return + } + + // Using: prososyctl --config /.../prosody.cfg.lua cert generate prosodyDomain.tld + await prosodyCtl(options, 'cert', { + additionalArgs: ['generate', prosodyDomain], + yesMode: true, + stdErrFilter: (data) => { + // For an unknow reason, `prosodyctl cert generate` outputs openssl data on stderr... + // So we filter these logs. + if (data.match(/Generating \w+ private key/)) { return false } + if (data.match(/^[.+o*\n]*$/m)) { return false } + if (data.match(/e is \d+/)) { return false } + return true + } + }) +} + +async function renewCheckSelfSigned (options: RegisterServerOptions, config: ProsodyConfig): Promise { + const logger = options.peertubeHelpers.logger + // We have to check if the self signed certificate is still valid. + // Prosodyctl generated certificates are valid 365 days. + // We will renew it every 10 months (and every X minutes in debug mode) + + const renewEvery = isDebugMode(options) ? 5 * 60000 : 3600000 * 24 * 30 * 10 + // getting the file date... + const filepath = _filePathToTest(options, config) + if (!fs.existsSync(filepath)) { + logger.error('Missing certificate file: ' + filepath) + return + } + const stat = fs.statSync(filepath) + const age = (new Date()).getTime() - stat.mtimeMs + if (age <= renewEvery) { + logger.debug(`The age of the certificate ${filepath} is ${age}ms, which is less than the period ${renewEvery}ms`) + return + } + logger.info(`The age of the certificate ${filepath} is ${age}ms, which is more than the period ${renewEvery}ms`) + await ensureProsodyCertificates(options, config) + await reloadProsody(options) +} + +async function renewCheck (options: RegisterServerOptions, config: ProsodyConfig): Promise { + if (config.certificates === 'generate-self-signed') { + return renewCheckSelfSigned(options, config) + } + throw new Error('Unknown value for config.certificates') +} + +function startProsodyCertificatesRenewCheck (options: RegisterServerOptions, config: ProsodyConfig): void { + const logger = options.peertubeHelpers.logger + + if (!config.certificates) { + return + } + + const debugMode = isDebugMode(options) + // check every day (or every minutes in debug mode) + const checkInterval = debugMode ? 60000 : 3600000 * 24 + + if (renew) { + stopProsodyCertificatesRenewCheck(options) + } + + logger.info('Starting Prosody Certificates Renew Check') + renewCheck(options, config).then(() => {}, () => {}) + + const timer = setInterval(() => { + logger.debug('Checking if Prosody certificates need to be renewed') + renewCheck(options, config).then(() => {}, () => {}) + }, checkInterval) + + renew = { + timer: timer + } +} + +function stopProsodyCertificatesRenewCheck (options: RegisterServerOptions): void { + const logger = options.peertubeHelpers.logger + if (renew === undefined) { + return + } + logger.info('Stoping Prosody Certificates Renew Check') + clearInterval(renew.timer) +} + +export { + ensureProsodyCertificates, + startProsodyCertificatesRenewCheck, + stopProsodyCertificatesRenewCheck +} diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 584994ac..b9efa12e 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -87,6 +87,8 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise - needCerticates: boolean + certificates: ProsodyConfigCertificates } async function getProsodyConfig (options: RegisterServerOptionsV5): Promise { const logger = options.peertubeHelpers.logger @@ -134,7 +136,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise { @@ -356,6 +360,7 @@ async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise logger.info('Checking if Prosody is running, and shutting it down if so') stopProsodyLogRotate(options) + stopProsodyCertificatesRenewCheck(options) // NB: this function is called on plugin unregister, even if prosody is not used // so we must avoid creating the working dir now @@ -373,28 +378,6 @@ async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise logger.info(`ProsodyCtl command returned: ${status.message}`) } -async function ensureProsodyCertificates (options: RegisterServerOptions, config: ProsodyConfig): Promise { - if (!config.needCerticates) { return } - options.peertubeHelpers.logger.info('Prosody needs certicicates, checking if certificates are okay...') - - // FIXME: don't generate certicicated everytime, just if it is missing or expired. - - const prosodyDomain = config.host - // Using: prososyctl --config /.../prosody.cfg.lua cert generate prosodyDomain.tld - await prosodyCtl(options, 'cert', { - additionalArgs: ['generate', prosodyDomain], - yesMode: true, - stdErrFilter: (data) => { - // For an unknow reason, `prosodyctl cert generate` outputs openssl data on stderr... - // So we filter these logs. - if (data.match(/Generating \w+ private key/)) { return false } - if (data.match(/^[.+o*\n]*$/m)) { return false } - if (data.match(/e is \d+/)) { return false } - return true - } - }) -} - export { getProsodyAbout, checkProsody, @@ -402,5 +385,7 @@ export { testProsodyCorrectlyRunning, prepareProsody, ensureProsodyRunning, - ensureProsodyNotRunning + ensureProsodyNotRunning, + prosodyCtl, + reloadProsody } diff --git a/server/lib/prosody/logrotate.ts b/server/lib/prosody/logrotate.ts index e5e6f464..881e704c 100644 --- a/server/lib/prosody/logrotate.ts +++ b/server/lib/prosody/logrotate.ts @@ -1,6 +1,7 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' import type { ProsodyFilePaths } from './config/paths' import { isDebugMode } from '../debug' +import { reloadProsody } from './ctl' type Rotate = (file: string, options: { count?: number @@ -12,7 +13,6 @@ interface ProsodyLogRotate { timer: NodeJS.Timeout lastRotation: number } -type ReloadProsody = (options: RegisterServerOptions) => Promise let logRotate: ProsodyLogRotate | undefined @@ -31,7 +31,7 @@ async function _rotate (options: RegisterServerOptions, path: string): Promise { - reload(options).then(() => { + reloadProsody(options).then(() => { logger.debug('Prosody reloaded') }, () => { logger.error('Prosody failed to reload')