Files
peertube-plugin-livechat/server/lib/settings.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

772 lines
22 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 { ConverseJSTheme } from '../../shared/lib/types'
import { ensureProsodyRunning } from './prosody/ctl'
import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl'
import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc'
import { Emojis } from './emojis'
import { LivechatProsodyAuth } from './prosody/auth'
import { loc } from './loc'
import { canEditFirewallConfig } from './firewall/config'
const escapeHTML = require('escape-html')
type AvatarSet = 'sepia' | 'cat' | 'bird' | 'fenec' | 'abstract' | 'legacy' | 'none'
async function initSettings (options: RegisterServerOptions): Promise<void> {
const { peertubeHelpers, settingsManager } = options
const logger = peertubeHelpers.logger
initImportantNotesSettings(options)
initChatSettings(options)
initFederationSettings(options)
initAuth(options)
initExternalAuth(options)
initAdvancedChannelCustomizationSettings(options)
initChatBehaviourSettings(options)
initThemingSettings(options)
await initChatServerAdvancedSettings(options)
await ExternalAuthOIDC.initSingletons(options)
const loadOidcs = (): void => {
const oidcs = ExternalAuthOIDC.allSingletons()
for (const oidc of oidcs) {
try {
const type = oidc.type
oidc.isOk().then(
() => {
logger.info(`Loading External Auth OIDC ${type}...`)
oidc.load().then(
() => {
logger.info(`External Auth OIDC ${type} loaded`)
},
() => {
logger.error(`Loading the External Auth OIDC ${type} failed`)
}
)
},
() => {
logger.info(`No valid External Auth OIDC ${type}, nothing loaded`)
}
)
} catch (err) {
logger.error(err as string)
continue
}
}
}
loadOidcs() // we don't have to wait (can take time, it will do external http requests)
const tmpSettings = await settingsManager.getSettings(['prosody-room-type', 'enable-users-regexp'])
let currentProsodyRoomtype = tmpSettings['prosody-room-type']
let currentUsersRegexp = tmpSettings['enable-users-regexp']
// ********** settings changes management
settingsManager.onSettingsChange(async (settings: any) => {
// In case the Prosody port has changed, we must rewrite the Bot configuration file.
// To avoid race condition, we will just stop and start the bots at every settings saving.
await BotsCtl.destroySingleton()
await BotsCtl.initSingleton(options)
loadOidcs() // we don't have to wait (can take time, it will do external http requests)
await ExternalAuthOIDC.initSingletons(options)
// recreating a Emojis singleton
await Emojis.destroySingleton()
await Emojis.initSingleton(options)
LivechatProsodyAuth.singleton().setUserTokensEnabled(!settings['livechat-token-disabled'])
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
await ensureProsodyRunning(options)
await BotsCtl.singleton().start()
// In case prosody-room-type changed, we must rebuild room-channel links.
// In case enable-users-regexp becomes false, we must rebuild to make sure regexp lines are disabled
if (
settings['prosody-room-type'] !== currentProsodyRoomtype ||
(currentUsersRegexp && !settings['enable-users-regexp'])
) {
peertubeHelpers.logger.info('Settings changed, must rebuild room-channel infos')
// doing it without waiting, could be long!
RoomChannel.singleton().rebuildData().then(
() => peertubeHelpers.logger.info('Room-channel info rebuild ok.'),
(err) => peertubeHelpers.logger.error(err)
)
}
currentProsodyRoomtype = settings['prosody-room-type']
currentUsersRegexp = settings['enable-users-regexp']
})
}
/**
* Registers settings related to the "Important Notes" section.
* @param param0 server options
*/
function initImportantNotesSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('important_note_title')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('important_note_text')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('diagnostic')
})
if (process.arch !== 'x64' && process.arch !== 'x86_64' && process.arch !== 'arm64') {
registerSetting({
name: 'prosody-arch-warning',
type: 'html',
private: true,
// Note: the following text as a variable in it.
// Not translating it: it should be very rare.
descriptionHTML: `<span class="peertube-plugin-livechat-warning">
It seems that your are using a ${process.arch} CPU,
which is not compatible with the plugin.
Please read
<a
href="https://livingston.frama.io/peertube-plugin-livechat/documentation/installation/cpu_compatibility/"
target="_blank"
>
this page
</a> for a workaround.
</span>`
})
}
}
/**
* Register settings related to the "Chat" section.
* @param param0 server options
*/
function initChatSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('chat_title')
})
registerSetting({
name: 'chat-terms',
private: true,
label: loc('chat_terms_label'),
type: 'input-textarea',
default: '',
descriptionHTML: loc('chat_terms_description')
})
registerSetting({
name: 'prosody-list-rooms',
label: loc('list_rooms_label'),
type: 'html',
descriptionHTML: loc('list_rooms_description'),
private: true
})
}
/**
* Registers settings related to the "Federation" section.
* @param param0 server options
*/
function initFederationSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('federation_description')
})
registerSetting({
name: 'federation-no-remote-chat',
label: loc('federation_no_remote_chat_label'),
descriptionHTML: loc('federation_no_remote_chat_description'),
type: 'input-checkbox',
default: false,
private: false
})
registerSetting({
name: 'federation-dont-publish-remotely',
label: loc('federation_dont_publish_remotely_label'),
descriptionHTML: loc('federation_dont_publish_remotely_description'),
type: 'input-checkbox',
default: false,
private: true
})
}
/**
* Initialize settings related to authentication.
* @param options peertube server options
*/
function initAuth (options: RegisterServerOptions): void {
const registerSetting = options.registerSetting
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('auth_description')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('experimental_warning')
})
registerSetting({
name: 'livechat-token-disabled',
label: loc('livechat_token_disabled_label'),
descriptionHTML: loc('livechat_token_disabled_description'),
type: 'input-checkbox',
default: false,
private: false
})
}
/**
* Registers settings related to the "External Authentication" section.
* @param param0 server options
*/
function initExternalAuth (options: RegisterServerOptions): void {
const registerSetting = options.registerSetting
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('external_auth_description')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('external_auth_custom_oidc_title')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('experimental_warning')
})
registerSetting({
name: 'external-auth-custom-oidc',
label: loc('external_auth_custom_oidc_label'),
descriptionHTML: loc('external_auth_custom_oidc_description'),
type: 'input-checkbox',
default: false,
private: true
})
registerSetting({
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris-info',
private: true,
descriptionHTML: loc('external_auth_oidc_redirect_uris_info_description')
})
registerSetting({
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris',
private: true,
descriptionHTML: `<ul><li>${escapeHTML(ExternalAuthOIDC.redirectUrl(options, 'custom')) as string}</li></ul>`
})
registerSetting({
name: 'external-auth-custom-oidc-button-label',
label: loc('external_auth_custom_oidc_button_label_label'),
descriptionHTML: loc('external_auth_custom_oidc_button_label_description'),
type: 'input',
default: '',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-discovery-url',
label: loc('external_auth_custom_oidc_discovery_url_label'),
// descriptionHTML: loc('external_auth_custom_oidc_discovery_url_description'),
type: 'input',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-client-id',
label: loc('external_auth_oidc_client_id_label'),
// descriptionHTML: loc('external_auth_oidc_client_id_description'),
type: 'input',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-client-secret',
label: loc('external_auth_oidc_client_secret_label'),
// descriptionHTML: loc('external_auth_oidc_client_secret_description'),
type: 'input-password',
private: true
})
// FIXME: adding settings for these?
// - scope (default: 'openid profile')
// - user-name property
// - display-name property
// - picture property
// Standard providers....
for (const provider of ['google', 'facebook']) {
let redirectUrl
try {
redirectUrl = ExternalAuthOIDC.redirectUrl(options, provider as ExternalAuthOIDCType)
} catch (err) {
options.peertubeHelpers.logger.error('Cant load redirect url for provider ' + provider)
options.peertubeHelpers.logger.error(err)
continue
}
registerSetting({
name: 'external-auth-' + provider + '-oidc',
label: loc('external_auth_' + provider + '_oidc_label'),
descriptionHTML: loc('external_auth_' + provider + '_oidc_description'),
type: 'input-checkbox',
default: false,
private: true
})
registerSetting({
type: 'html',
name: 'external-auth-' + provider + '-oidc-redirect-uris-info',
private: true,
descriptionHTML: loc('external_auth_oidc_redirect_uris_info_description')
})
registerSetting({
type: 'html',
name: 'external-auth-' + provider + '-oidc-redirect-uris',
private: true,
descriptionHTML: `<ul><li>${escapeHTML(redirectUrl) as string}</li></ul>`
})
registerSetting({
name: 'external-auth-' + provider + '-oidc-client-id',
label: loc('external_auth_oidc_client_id_label'),
// descriptionHTML: loc('external_auth_' + provider + '_oidc_client_id_description'),
type: 'input',
private: true
})
registerSetting({
name: 'external-auth-' + provider + '-oidc-client-secret',
label: loc('external_auth_oidc_client_secret_label'),
// descriptionHTML: loc('external_auth_' + provider + '_oidc_client_secret_description'),
type: 'input-password',
private: true
})
}
}
/**
* Registers settings related to the "Advanced channel customization" section.
* @param param0 server options
*/
function initAdvancedChannelCustomizationSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('configuration_description')
})
registerSetting({
name: 'disable-channel-configuration',
label: loc('disable_channel_configuration_label'),
// descriptionHTML: loc('disable_channel_configuration_description'),
type: 'input-checkbox',
default: false,
private: false
})
registerSetting({
// For now (v14), this settings is used to enable/disable regexp for forbidden words.
// This settings is basically here to say if you trust your users or not concerning regexp
// (because there is a risk of ReDOS on the chatbot).
// This settings could be used for other purpose later on (if we implement regexp anywhere else).
// So we use a pretty standard name, `enable-users-regexp`, that could apply for other uses.
name: 'enable-users-regexp',
label: loc('enable_users_regexp'),
descriptionHTML: loc('enable_users_regexp_description'),
type: 'input-checkbox',
default: false,
private: false
})
}
/**
* Registers settings related to the "Chat behaviour" section.
* @param param0 server options
*/
function initChatBehaviourSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('chat_behaviour_description')
})
registerSetting({
name: 'prosody-room-type',
label: loc('room_type_label'),
type: 'select',
descriptionHTML: loc('room_type_description'),
private: false,
default: 'video',
options: [
{ value: 'video', label: loc('room_type_option_video') },
{ value: 'channel', label: loc('room_type_option_channel') }
]
})
registerSetting({
name: 'chat-auto-display',
label: loc('auto_display_label'),
descriptionHTML: loc('auto_display_description'),
type: 'input-checkbox',
default: true,
private: false
})
registerSetting({
name: 'chat-open-blank',
label: loc('open_blank_label'),
descriptionHTML: loc('open_blank_description'),
private: false,
type: 'input-checkbox',
default: true
})
registerSetting({
name: 'chat-share-url',
label: loc('share_url_label'),
descriptionHTML: loc('share_url_description'),
private: false,
type: 'select',
default: 'owner',
options: [
{ value: 'nobody', label: loc('share_url_option_nobody') },
{ value: 'everyone', label: loc('share_url_option_everyone') },
{ value: 'owner', label: loc('share_url_option_owner') },
{ value: 'owner+moderators', label: loc('share_url_option_owner_moderators') }
]
})
registerSetting({
name: 'chat-per-live-video',
label: loc('per_live_video_label'),
type: 'input-checkbox',
default: true,
descriptionHTML: loc('per_live_video_description'),
private: false
})
registerSetting({
name: 'chat-per-live-video-warning',
type: 'html',
private: true,
descriptionHTML: loc('per_live_video_warning_description')
})
registerSetting({
name: 'chat-all-lives',
label: loc('all_lives_label'),
type: 'input-checkbox',
default: false,
descriptionHTML: loc('all_lives_description'),
private: false
})
registerSetting({
name: 'chat-all-non-lives',
label: loc('all_non_lives_label'),
type: 'input-checkbox',
default: false,
descriptionHTML: loc('all_non_lives_description'),
private: false
})
registerSetting({
name: 'chat-videos-list',
label: loc('videos_list_label'),
type: 'input-textarea',
default: '',
descriptionHTML: loc('videos_list_description'),
private: false
})
registerSetting({
name: 'chat-no-anonymous',
label: loc('no_anonymous_label'),
type: 'input-checkbox',
default: false,
descriptionHTML: loc('no_anonymous_description'),
private: false
})
registerSetting({
name: 'auto-ban-anonymous-ip',
label: loc('auto_ban_anonymous_ip_label'),
type: 'input-checkbox',
default: false,
descriptionHTML: loc('auto_ban_anonymous_ip_description'),
private: true
})
}
/**
* Registers settings related to the "Theming" section.
* @param param0 server options
*/
function initThemingSettings ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
name: 'theming-advanced',
type: 'html',
private: true,
descriptionHTML: loc('theming_advanced_description')
})
registerSetting({
name: 'avatar-set',
label: loc('avatar_set_label'),
descriptionHTML: loc('avatar_set_description'),
type: 'select',
default: 'sepia' as AvatarSet,
private: true,
options: [
{ value: 'sepia', label: loc('avatar_set_option_sepia') },
{ value: 'cat', label: loc('avatar_set_option_cat') },
{ value: 'bird', label: loc('avatar_set_option_bird') },
{ value: 'fenec', label: loc('avatar_set_option_fenec') },
{ value: 'abstract', label: loc('avatar_set_option_abstract') },
{ value: 'legacy', label: loc('avatar_set_option_legacy') },
{ value: 'none', label: loc('avatar_set_option_none') }
] as Array<{
value: AvatarSet
label: string
}>
})
registerSetting({
name: 'converse-theme',
label: loc('converse_theme_label'),
type: 'select',
default: 'peertube' as ConverseJSTheme,
private: false,
options: [
{ value: 'peertube', label: loc('converse_theme_option_peertube') },
{ value: 'default', label: loc('converse_theme_option_default') },
{ value: 'cyberpunk', label: loc('converse_theme_option_cyberpunk') }
] as Array<{ value: ConverseJSTheme, label: string }>,
descriptionHTML: loc('converse_theme_description')
})
registerSetting({
name: 'converse-autocolors',
label: loc('autocolors_label'),
type: 'input-checkbox',
default: true,
private: false,
descriptionHTML: loc('autocolors_description')
})
registerSetting({
name: 'converse-theme-warning',
type: 'html',
private: true,
descriptionHTML: loc('converse_theme_warning_description')
})
registerSetting({
name: 'chat-style',
label: loc('chat_style_label'),
type: 'input-textarea',
default: '',
descriptionHTML: loc('chat_style_description'),
private: false
})
}
/**
* Registers settings related to the "Chat server advanded settings" section.
* @param param0 server options
*/
async function initChatServerAdvancedSettings (options: RegisterServerOptions): Promise<void> {
const { registerSetting } = options
registerSetting({
name: 'prosody-advanced',
type: 'html',
private: true,
descriptionHTML: loc('prosody_advanced_description')
})
registerSetting({
name: 'chat-help-builtin-prosody',
type: 'html',
label: loc('help_builtin_prosody_label'),
descriptionHTML: loc('help_builtin_prosody_description'),
private: true
})
registerSetting({
name: 'use-system-prosody',
type: 'input-checkbox',
label: loc('system_prosody_label'),
descriptionHTML: loc('system_prosody_description'),
private: true,
default: false
})
registerSetting({
name: 'disable-websocket',
type: 'input-checkbox',
label: loc('disable_websocket_label'),
descriptionHTML: loc('disable_websocket_description'),
private: true,
default: false
})
registerSetting({
name: 'prosody-port',
label: loc('prosody_port_label'),
type: 'input',
default: '52800',
private: true,
descriptionHTML: loc('prosody_port_description')
})
registerSetting({
name: 'prosody-peertube-uri',
label: loc('prosody_peertube_uri_label'),
type: 'input',
default: '',
private: true,
descriptionHTML: loc('prosody_peertube_uri_description')
})
registerSetting({
name: 'prosody-muc-log-by-default',
label: loc('prosody_muc_log_by_default_label'),
type: 'input-checkbox',
default: true,
private: true,
descriptionHTML: loc('prosody_muc_log_by_default_description')
})
registerSetting({
name: 'prosody-muc-expiration',
label: loc('prosody_muc_expiration_label'),
type: 'input',
default: '1w',
private: true,
descriptionHTML: loc('prosody_muc_expiration_description')
})
registerSetting({
name: 'prosody-room-allow-s2s',
label: loc('prosody_room_allow_s2s_label'),
type: 'input-checkbox',
default: false,
private: false,
descriptionHTML: loc('prosody_room_allow_s2s_description')
})
registerSetting({
name: 'prosody-s2s-port',
label: loc('prosody_s2s_port_label'),
type: 'input',
default: '5269',
private: true,
descriptionHTML: loc('prosody_s2s_port_description')
})
registerSetting({
name: 'prosody-s2s-interfaces',
label: loc('prosody_s2s_interfaces_label'),
type: 'input',
default: '*, ::',
private: true,
descriptionHTML: loc('prosody_s2s_interfaces_description')
})
registerSetting({
name: 'prosody-certificates-dir',
label: loc('prosody_certificates_dir_label'),
type: 'input',
default: '',
private: true,
descriptionHTML: loc('prosody_certificates_dir_description')
})
registerSetting({
name: 'prosody-c2s',
label: loc('prosody_c2s_label'),
type: 'input-checkbox',
default: false,
private: true,
descriptionHTML: loc('prosody_c2s_description')
})
registerSetting({
name: 'prosody-c2s-port',
label: loc('prosody_c2s_port_label'),
type: 'input',
default: '52822',
private: true,
descriptionHTML: loc('prosody_c2s_port_description')
})
registerSetting({
name: 'prosody-c2s-interfaces',
label: loc('prosody_c2s_interfaces_label'),
type: 'input',
default: '127.0.0.1, ::1',
private: true,
descriptionHTML: loc('prosody_c2s_interfaces_description')
})
registerSetting({
name: 'prosody-components',
label: loc('prosody_components_label'),
type: 'input-checkbox',
default: false,
private: true,
descriptionHTML: loc('prosody_components_description')
})
registerSetting({
name: 'prosody-components-port',
label: loc('prosody_components_port_label'),
type: 'input',
default: '53470',
private: true,
descriptionHTML: loc('prosody_components_port_description')
})
registerSetting({
name: 'prosody-components-interfaces',
label: loc('prosody_components_interfaces_label'),
type: 'input',
default: '127.0.0.1, ::1',
private: true,
descriptionHTML: loc('prosody_components_interfaces_description')
})
registerSetting({
name: 'prosody-components-list',
label: loc('prosody_components_list_label'),
type: 'input-textarea',
default: '',
private: true,
descriptionHTML: loc('prosody_components_list_description')
})
registerSetting({
name: 'prosody-firewall-enabled',
label: loc('prosody_firewall_label'),
type: 'input-checkbox',
default: false,
private: true,
descriptionHTML: loc('prosody_firewall_description')
})
if (await canEditFirewallConfig(options)) {
registerSetting({
type: 'html',
name: 'prosody-firewall-configure-button',
private: true,
descriptionHTML: loc('prosody_firewall_configure_button')
})
}
}
export {
initSettings,
AvatarSet
}