Prosody: renew self signed certificates periodically

This commit is contained in:
John Livingston 2023-04-13 17:00:34 +02:00 committed by John Livingston
parent b4dabfeeb9
commit a87a622cba
4 changed files with 135 additions and 32 deletions

View File

@ -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<void> {
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<void> {
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<void> {
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
}

View File

@ -87,6 +87,8 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
} }
} }
type ProsodyConfigCertificates = false | 'generate-self-signed'
interface ProsodyConfig { interface ProsodyConfig {
content: string content: string
paths: ProsodyFilePaths paths: ProsodyFilePaths
@ -97,7 +99,7 @@ interface ProsodyConfig {
logByDefault: boolean logByDefault: boolean
logExpiration: ConfigLogExpiration logExpiration: ConfigLogExpiration
valuesToHideInDiagnostic: Map<string, string> valuesToHideInDiagnostic: Map<string, string>
needCerticates: boolean certificates: ProsodyConfigCertificates
} }
async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<ProsodyConfig> { async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
@ -134,7 +136,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options) const paths = await getProsodyFilePaths(options)
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video' const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
let needCerticates: boolean = false let certificates: ProsodyConfigCertificates = false
const apikey = await getAPIKey(options) const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.set('APIKey', apikey) valuesToHideInDiagnostic.set('APIKey', apikey)
@ -182,7 +184,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
} }
if (enableRoomS2S) { if (enableRoomS2S) {
needCerticates = true certificates = 'generate-self-signed'
const s2sPort = (settings['prosody-s2s-port'] as string) || '5269' const s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
if (!/^\d+$/.test(s2sPort)) { if (!/^\d+$/.test(s2sPort)) {
throw new Error('Invalid s2s port') throw new Error('Invalid s2s port')
@ -238,7 +240,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
logByDefault, logByDefault,
logExpiration, logExpiration,
valuesToHideInDiagnostic, valuesToHideInDiagnostic,
needCerticates certificates
} }
} }

View File

@ -1,6 +1,9 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import { getProsodyConfig, getProsodyFilePaths, writeProsodyConfig, ProsodyConfig } from './config' import { getProsodyConfig, getProsodyFilePaths, writeProsodyConfig } from './config'
import { startProsodyLogRotate, stopProsodyLogRotate } from './logrotate' import { startProsodyLogRotate, stopProsodyLogRotate } from './logrotate'
import {
ensureProsodyCertificates, startProsodyCertificatesRenewCheck, stopProsodyCertificatesRenewCheck
} from './certificates'
import { disableProxyRoute, enableProxyRoute } from '../routers/webchat' import { disableProxyRoute, enableProxyRoute } from '../routers/webchat'
import * as fs from 'fs' import * as fs from 'fs'
import * as child_process from 'child_process' import * as child_process from 'child_process'
@ -347,7 +350,8 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
return return
} }
logger.info('Prosody is running') logger.info('Prosody is running')
await startProsodyLogRotate(options, filePaths, reloadProsody) await startProsodyLogRotate(options, filePaths)
await startProsodyCertificatesRenewCheck(options, config)
} }
async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise<void> { async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise<void> {
@ -356,6 +360,7 @@ async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise
logger.info('Checking if Prosody is running, and shutting it down if so') logger.info('Checking if Prosody is running, and shutting it down if so')
stopProsodyLogRotate(options) stopProsodyLogRotate(options)
stopProsodyCertificatesRenewCheck(options)
// NB: this function is called on plugin unregister, even if prosody is not used // NB: this function is called on plugin unregister, even if prosody is not used
// so we must avoid creating the working dir now // 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}`) logger.info(`ProsodyCtl command returned: ${status.message}`)
} }
async function ensureProsodyCertificates (options: RegisterServerOptions, config: ProsodyConfig): Promise<void> {
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 { export {
getProsodyAbout, getProsodyAbout,
checkProsody, checkProsody,
@ -402,5 +385,7 @@ export {
testProsodyCorrectlyRunning, testProsodyCorrectlyRunning,
prepareProsody, prepareProsody,
ensureProsodyRunning, ensureProsodyRunning,
ensureProsodyNotRunning ensureProsodyNotRunning,
prosodyCtl,
reloadProsody
} }

View File

@ -1,6 +1,7 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ProsodyFilePaths } from './config/paths' import type { ProsodyFilePaths } from './config/paths'
import { isDebugMode } from '../debug' import { isDebugMode } from '../debug'
import { reloadProsody } from './ctl'
type Rotate = (file: string, options: { type Rotate = (file: string, options: {
count?: number count?: number
@ -12,7 +13,6 @@ interface ProsodyLogRotate {
timer: NodeJS.Timeout timer: NodeJS.Timeout
lastRotation: number lastRotation: number
} }
type ReloadProsody = (options: RegisterServerOptions) => Promise<boolean>
let logRotate: ProsodyLogRotate | undefined let logRotate: ProsodyLogRotate | undefined
@ -31,7 +31,7 @@ async function _rotate (options: RegisterServerOptions, path: string): Promise<v
return p return p
} }
function startProsodyLogRotate (options: RegisterServerOptions, paths: ProsodyFilePaths, reload: ReloadProsody): void { function startProsodyLogRotate (options: RegisterServerOptions, paths: ProsodyFilePaths): void {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
const debugMode = isDebugMode(options) const debugMode = isDebugMode(options)
const checkInterval = debugMode ? 60 * 1000 : 60 * 60 * 1000 // check every hour const checkInterval = debugMode ? 60 * 1000 : 60 * 60 * 1000 // check every hour
@ -63,7 +63,7 @@ function startProsodyLogRotate (options: RegisterServerOptions, paths: ProsodyFi
_rotate(options, paths.error) _rotate(options, paths.error)
]) ])
p.then(() => { p.then(() => {
reload(options).then(() => { reloadProsody(options).then(() => {
logger.debug('Prosody reloaded') logger.debug('Prosody reloaded')
}, () => { }, () => {
logger.error('Prosody failed to reload') logger.error('Prosody failed to reload')