Files
peertube-plugin-livechat/server/lib/routers/api/configuration.ts
John Livingston 3624dd5c3c Reverting usage of RE2 (WIP):
**Breaking changes**

The livechat v13 introduced a new library to handle regular expressions in forbidden words, to avoid
[ReDOS](https://en.wikipedia.org/wiki/ReDoS) attacks.
Unfortunately, this library was not able to install itself properly on some systems, and some admins were not able
to install the livechat plugin.

That's why we have disabled this library in v14, and introduce a new settings to enable regexp in forbidden words.
By default this settings is disabled, and your users won't be able to use regexp in their forbidden words.

The risk by enabling this feature is that a malicious user could cause a denial of service for the chat bot, by using a
special crafted regular expression in their channel options, and sending a special crafter message in one of their
rooms. If you trust your users (those who have rights to livestream), you can enable the settings. Otherwise it is not
recommanded. See the documentation for more informations.

**Minor changes and fixes**

* Channel's forbidden words: new "enable" column.
* New settings to enable regular expressions for channel forbidden words.
* "Channel advanced configuration" settings: removing the "experimental feature" label.
2025-06-19 17:11:13 +02:00

267 lines
10 KiB
TypeScript

// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { Router, Request, Response, NextFunction } from 'express'
import type { ChannelConfiguration, ChannelEmojisConfiguration, ChannelInfos } from '../../../../shared/lib/types'
import { asyncMiddleware } from '../../middlewares/async'
import { getCheckConfigurationChannelMiddleware } from '../../middlewares/configuration/channel'
import { checkConfigurationEnabledMiddleware } from '../../middlewares/configuration/configuration'
import {
getChannelConfigurationOptions,
getDefaultChannelConfigurationOptions,
storeChannelConfigurationOptions
} from '../../configuration/channel/storage'
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
import { getConverseJSParams } from '../../../lib/conversejs/params'
import { Emojis } from '../../../lib/emojis'
import { RoomChannel } from '../../../lib/room-channel'
import { updateProsodyRoom } from '../../../lib/prosody/api/manage-rooms'
async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise<void> {
const logger = options.peertubeHelpers.logger
router.get('/configuration/room/:roomKey', asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
const roomKey = req.params.roomKey
const user = await options.peertubeHelpers.user.getAuthUser(res)
const initConverseJSParam = await getConverseJSParams(
options,
roomKey,
{
forcetype: req.query.forcetype === '1'
},
!!user
)
if (('isError' in initConverseJSParam) && initConverseJSParam.isError) {
res.sendStatus(initConverseJSParam.code)
return
}
res.status(200)
res.json(initConverseJSParam)
}
))
router.get('/configuration/channel/:channelId', asyncMiddleware([
checkConfigurationEnabledMiddleware(options),
getCheckConfigurationChannelMiddleware(options),
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
if (!res.locals.channelInfos) {
logger.error('Missing channelInfos in res.locals, should not happen')
res.sendStatus(500)
return
}
const channelInfos = res.locals.channelInfos as ChannelInfos
const channelOptions =
await getChannelConfigurationOptions(options, channelInfos.id) ??
getDefaultChannelConfigurationOptions(options)
const result: ChannelConfiguration = {
channel: channelInfos,
configuration: channelOptions
}
res.status(200)
res.json(result)
}
]))
router.post('/configuration/channel/:channelId', asyncMiddleware([
checkConfigurationEnabledMiddleware(options),
getCheckConfigurationChannelMiddleware(options),
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
if (!res.locals.channelInfos) {
logger.error('Missing channelInfos in res.locals, should not happen')
res.sendStatus(500)
return
}
const channelInfos = res.locals.channelInfos as ChannelInfos
logger.debug('Trying to save ChannelConfigurationOptions')
let channelOptions
try {
// Note: the front-end should do some input validation.
// If there is any invalid value, we just return a 400 error.
// The frontend should have prevented to post invalid data.
// Note: if !bot.enabled, we wont try to save hidden fields values, to minimize the risk of error
if (req.body.bot?.enabled === false) {
logger.debug('Bot disabled, loading the previous bot conf to not override hidden fields')
const channelOptions =
await getChannelConfigurationOptions(options, channelInfos.id) ??
getDefaultChannelConfigurationOptions(options)
req.body.bot = channelOptions.bot
req.body.bot.enabled = false
}
// TODO: Same for forbidSpecialChars/noDuplicate: if disabled, don't save reason and tolerance
// (disabling for now, because it is not acceptable to load twice the channel configuration.
// Must find better way)
// if (req.body.bot?.enabled === true && req.body.bot.forbidSpecialChars?.enabled === false) {
// logger.debug('Bot disabled, loading the previous bot conf to not override hidden fields')
// const channelOptions =
// await getChannelConfigurationOptions(options, channelInfos.id) ??
// getDefaultChannelConfigurationOptions(options)
// req.body.bot.forbidSpecialChars.reason = channelOptions.bot.forbidSpecialChars.reason
// req.body.bot.forbidSpecialChars.tolerance = channelOptions.bot.forbidSpecialChars.tolerance
// req.body.bot.forbidSpecialChars.applyToModerators = channelOptions.bot.forbidSpecialChars.applyToModerators
// req.body.bot.forbidSpecialChars.enabled = false
// ... NoDuplicate...
// }
channelOptions = await sanitizeChannelConfigurationOptions(options, channelInfos.id, req.body)
} catch (err: any) {
logger.warn(err.message as string)
if (err.validationErrorMessage && (typeof err.validationErrorMessage === 'string')) {
res.status(400)
res.json({
validationErrorMessage: err.validationErrorMessage
})
} else {
res.sendStatus(400)
}
return
}
logger.debug('Data seems ok, storing them.')
const result: ChannelConfiguration = {
channel: channelInfos,
configuration: channelOptions
}
await storeChannelConfigurationOptions(options, channelInfos.id, channelOptions)
res.status(200)
res.json(result)
}
]))
router.get('/configuration/channel/emojis/:channelId', asyncMiddleware([
checkConfigurationEnabledMiddleware(options),
getCheckConfigurationChannelMiddleware(options),
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
try {
if (!res.locals.channelInfos) {
throw new Error('Missing channelInfos in res.locals, should not happen')
}
const emojis = Emojis.singleton()
const channelInfos = res.locals.channelInfos as ChannelInfos
const channelEmojis =
(await emojis.channelCustomEmojisDefinition(channelInfos.id)) ??
emojis.emptyChannelDefinition()
const result: ChannelEmojisConfiguration = {
channel: channelInfos,
emojis: channelEmojis
}
res.status(200)
res.json(result)
} catch (err) {
logger.error(err)
res.sendStatus(500)
}
}
]))
router.post('/configuration/channel/emojis/:channelId', asyncMiddleware([
checkConfigurationEnabledMiddleware(options),
getCheckConfigurationChannelMiddleware(options),
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
try {
if (!res.locals.channelInfos) {
throw new Error('Missing channelInfos in res.locals, should not happen')
}
const emojis = Emojis.singleton()
const channelInfos = res.locals.channelInfos as ChannelInfos
const emojisDefinition = req.body
let emojisDefinitionSanitized, bufferInfos
try {
[emojisDefinitionSanitized, bufferInfos] = await emojis.sanitizeChannelDefinition(
channelInfos.id,
emojisDefinition
)
} catch (err) {
logger.warn(err)
res.sendStatus(400)
return
}
await emojis.saveChannelDefinition(channelInfos.id, emojisDefinitionSanitized, bufferInfos)
// We must update the emoji only regexp on the Prosody server.
const customEmojisRegexp = await emojis.getChannelCustomEmojisRegexp(channelInfos.id)
const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id)
for (const roomJID of roomJIDs) {
// No need to await here
logger.info(`Updating room ${roomJID} emoji only regexp...`)
updateProsodyRoom(options, roomJID, {
livechat_custom_emoji_regexp: customEmojisRegexp
}).then(
() => {},
(err) => logger.error(err)
)
}
// Reloading data, to send them back to front:
const channelEmojis =
(await emojis.channelCustomEmojisDefinition(channelInfos.id)) ??
emojis.emptyChannelDefinition()
const result: ChannelEmojisConfiguration = {
channel: channelInfos,
emojis: channelEmojis
}
res.status(200)
res.json(result)
} catch (err) {
logger.error(err)
res.sendStatus(500)
}
}
]))
router.post('/configuration/channel/emojis/:channelId/enable_emoji_only', asyncMiddleware([
checkConfigurationEnabledMiddleware(options),
getCheckConfigurationChannelMiddleware(options),
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
try {
if (!res.locals.channelInfos) {
throw new Error('Missing channelInfos in res.locals, should not happen')
}
const emojis = Emojis.singleton()
const channelInfos = res.locals.channelInfos as ChannelInfos
logger.info(`Enabling emoji only mode on each channel ${channelInfos.id} rooms ...`)
// We can also update the EmojisRegexp, just in case.
const customEmojisRegexp = await emojis.getChannelCustomEmojisRegexp(channelInfos.id)
const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id)
for (const roomJID of roomJIDs) {
// No need to await here
logger.info(`Enabling emoji only mode on room ${roomJID} ...`)
updateProsodyRoom(options, roomJID, {
livechat_emoji_only: true,
livechat_custom_emoji_regexp: customEmojisRegexp
}).then(
() => {},
(err) => logger.error(err)
)
}
res.status(200)
res.json({ ok: true })
} catch (err) {
logger.error(err)
res.sendStatus(500)
}
}
]))
}
export {
initConfigurationApiRouter
}