peertube-plugin-livechat/server/lib/prosody/config/content.ts
John Livingston c008e84da7
Fix #295: Prosody: disabling message carbons for anonymous users.
Anonymous users can't use carbons, as they cannot connect with multiple
tabs on the same anonymous account.
So we disable carbons on the anonymous virtualhost, to improve
performances.

See here for some performances tests: https://github.com/JohnXLivingston/livechat-perf-test/tree/main/tests/50-anonymous-carbons
2024-02-01 15:20:52 +01:00

511 lines
18 KiB
TypeScript

import type { ProsodyFilePaths } from './paths'
import type { ExternalComponent } from './components'
import { BotConfiguration } from '../../configuration/bot'
import { userInfo } from 'os'
type ConfigEntryValue = boolean | number | string | ConfigEntryValue[]
type ConfigEntries = Map<string, ConfigEntryValue>
interface ConfigLogExpirationNever {
value: 'never'
type: 'never'
}
interface ConfigLogExpirationSeconds {
value: string
seconds: number
type: 'seconds'
}
interface ConfigLogExpirationPeriod {
value: string
type: 'period'
}
interface ConfigLogExpirationError {
value: string
error: string
type: 'period'
}
type ConfigLogExpiration =
ConfigLogExpirationNever | ConfigLogExpirationPeriod | ConfigLogExpirationSeconds | ConfigLogExpirationError
function writeValue (value: ConfigEntryValue): string {
if (typeof value === 'boolean') {
return value.toString() + ';\n'
}
if (typeof value === 'string') {
return '"' + value.replace(/"/g, '\\"') + '"' + ';\n'
}
if (typeof value === 'number') {
return value.toString() + ';\n'
}
if (Array.isArray(value)) {
let s = '{\n'
for (let i = 0; i < value.length; i++) {
s += ' ' + writeValue(value[i])
}
s += '};\n'
return s
}
throw new Error(`Don't know how to handle this value: ${value as string}`)
}
abstract class ProsodyConfigBlock {
entries: ConfigEntries
prefix: string
constructor (prefix: string) {
this.prefix = prefix
this.entries = new Map()
}
set (name: string, value: ConfigEntryValue): void {
this.entries.set(name, value)
}
add (name: string, value: ConfigEntryValue): void {
if (!this.entries.has(name)) {
this.entries.set(name, [])
}
let entry = this.entries.get(name) as ConfigEntryValue
if (!Array.isArray(entry)) {
entry = [entry]
}
entry.push(value)
this.entries.set(name, entry)
}
remove (name: string, value: ConfigEntryValue): void {
if (!this.entries.has(name)) {
return
}
let entry = this.entries.get(name) as ConfigEntryValue
if (!Array.isArray(entry)) {
entry = [entry]
}
entry = entry.filter(v => v !== value)
this.entries.set(name, entry)
}
write (): string {
let content = ''
// Map keeps order :)
this.entries.forEach((value, key) => {
content += this.prefix + key + ' = ' + writeValue(value)
})
return content
}
}
class ProsodyConfigGlobal extends ProsodyConfigBlock {
constructor () {
super('')
}
}
class ProsodyConfigVirtualHost extends ProsodyConfigBlock {
name: string
constructor (name: string) {
super(' ')
this.name = name
}
write (): string {
return `VirtualHost "${this.name}"\n` + super.write()
}
}
class ProsodyConfigComponent extends ProsodyConfigBlock {
name: string
type?: string
constructor (name: string, type?: string) {
super(' ')
this.type = type
this.name = name
}
write (): string {
if (this.type !== undefined) {
return `Component "${this.name}" "${this.type}"\n` + super.write()
}
return `Component "${this.name}"\n` + super.write()
}
}
type ProsodyLogLevel = 'debug' | 'info' | 'warn' | 'error'
class ProsodyConfigContent {
paths: ProsodyFilePaths
global: ProsodyConfigGlobal
authenticated?: ProsodyConfigVirtualHost
anon?: ProsodyConfigVirtualHost
muc: ProsodyConfigComponent
bot?: ProsodyConfigVirtualHost
externalComponents: ProsodyConfigComponent[] = []
log: string
prosodyDomain: string
constructor (paths: ProsodyFilePaths, prosodyDomain: string) {
this.paths = paths
this.global = new ProsodyConfigGlobal()
this.log = ''
this.prosodyDomain = prosodyDomain
this.muc = new ProsodyConfigComponent('room.' + prosodyDomain, 'muc')
this.global.set('daemonize', false)
this.global.set('allow_registration', false)
this.global.set('admins', [])
this.global.set('prosody_user', userInfo().username)
this.global.set('pidfile', this.paths.pid)
this.global.set('plugin_paths', [this.paths.modules])
this.global.set('data_path', this.paths.data)
// this.global.set('default_storage', 'internal') Not needed as storage is set to a string
this.global.set('storage', 'internal')
this.global.set('modules_enabled', [
'roster', // Allow users to have a roster. Recommended ;)
'saslauth', // Authentication for clients and servers. Recommended if you want to log in.
'carbons', // Keep multiple clients in sync
'version', // Replies to server version requests
'uptime', // Report how long server has been running
'ping', // Replies to XMPP pings with pongs
// 'bosh', // Enable BOSH clients, aka "Jabber over HTTP"
// 'websocket', // Enable Websocket clients
'posix', // POSIX functionality, sends server to background, enables syslog, etc.
// 'pep', // Enables users to publish their avatar, mood, activity, playing music and more
// 'vcard_legacy' // Conversion between legacy vCard and PEP Avatar, vcard
// 'vcard4' // User profiles (stored in PEP)
'disco' // Enable mod_disco (feature discovering)
])
this.global.set('modules_disabled', [
// 'offline' // Store offline messages
// 'c2s' // Handle client connections
's2s' // Handle server-to-server connections
])
// this.global.set('cross_domain_bosh', false) No more needed with Prosody 0.12
this.global.set('consider_bosh_secure', false)
// this.global.set('cross_domain_websocket', false) No more needed with Prosody 0.12
this.global.set('consider_websocket_secure', false)
if (this.paths.certs) {
this.global.set('certificates', this.paths.certs)
}
this.muc.set('admins', [])
this.muc.set('muc_room_locking', false)
this.muc.set('muc_tombstones', false)
this.muc.set('muc_room_default_language', 'en')
this.muc.set('muc_room_default_public', false)
this.muc.set('muc_room_default_persistent', false)
this.muc.set('muc_room_default_members_only', false)
this.muc.set('muc_room_default_moderated', false)
this.muc.set('muc_room_default_public_jids', false)
this.muc.set('muc_room_default_change_subject', false)
this.muc.set('muc_room_default_history_length', 20)
}
useAnonymous (autoBanIP: boolean): void {
this.anon = new ProsodyConfigVirtualHost('anon.' + this.prosodyDomain)
this.anon.set('authentication', 'anonymous')
this.anon.set('modules_enabled', ['ping'])
this.anon.set('modules_disabled', [
'carbons' // carbon make no sense for anonymous users, they can't have multiple windows
])
if (autoBanIP) {
this.anon.add('modules_enabled', 'muc_ban_ip')
}
}
useHttpAuthentication (url: string): void {
this.authenticated = new ProsodyConfigVirtualHost(this.prosodyDomain)
this.authenticated.set('authentication', 'http')
this.authenticated.set('modules_enabled', ['ping'])
this.authenticated.set('http_auth_url', url)
}
/**
* Activate BOSH (and optionnaly Websocket).
* @param prosodyDomain prosody domain
* @param port port to use for BOSH and Websocket interfaces
* @param publicServerUrl public server url
* @param useWS activate Websocket or not
* @param multiplexing activate multiplexing on port. Note: it will only listen on localhost interfaces.
*/
usePeertubeBoshAndWebsocket (
prosodyDomain: string,
port: string,
publicServerUrl: string,
useWS: boolean,
multiplexing: boolean
): void {
// Note: don't activate other http_interface or https_interfaces than localhost.
// Elsewhere it would be a security issue.
this.global.set('c2s_require_encryption', false)
this.global.set('interfaces', ['127.0.0.1', '::1'])
this.global.set('c2s_ports', [])
this.global.set('c2s_interfaces', ['127.0.0.1', '::1'])
this.global.set('s2s_ports', [])
this.global.set('s2s_interfaces', ['127.0.0.1', '::1'])
if (!multiplexing) {
this.global.set('http_ports', [port])
} else {
// Note: don't activate other http_interface or https_interfaces than localhost.
// Elsewhere it would be a security issue.
this.global.add('modules_enabled', 'net_multiplex')
this.global.set('ports', [port])
// FIXME: this generates Prosody error logs saying that BOSH/Websocket won't work... even if it is not true.
this.global.set('http_ports', [])
}
this.global.set('http_interfaces', ['127.0.0.1', '::1'])
this.global.set('https_ports', [])
this.global.set('https_interfaces', ['127.0.0.1', '::1'])
this.global.set('trusted_proxies', ['127.0.0.1', '::1'])
this.global.set('consider_bosh_secure', true)
if (useWS) {
this.global.set('consider_websocket_secure', true)
// c2s_close_timeout must be set accordingly with ConverseJS ping_interval (25s) and nginx timeout (30s)
this.global.set('c2s_close_timeout', 29)
// This line seems to be required by Prosody, otherwise it rejects websocket...
// this.global.set('cross_domain_websocket', [publicServerUrl]) No more needed with Prosody 0.12
}
if (this.anon) {
this.anon.set('allow_anonymous_s2s', false)
this.anon.add('modules_enabled', 'http')
this.anon.add('modules_enabled', 'bosh')
if (useWS) {
this.anon.add('modules_enabled', 'websocket')
}
this.anon.set('http_host', prosodyDomain)
this.anon.set('http_external_url', 'http://' + prosodyDomain)
}
this.muc.set('restrict_room_creation', 'local')
this.muc.set('http_host', prosodyDomain)
this.muc.set('http_external_url', 'http://' + prosodyDomain)
if (this.authenticated) {
this.authenticated.set('allow_anonymous_s2s', false)
this.authenticated.add('modules_enabled', 'http')
this.authenticated.add('modules_enabled', 'bosh')
if (useWS) {
this.authenticated.add('modules_enabled', 'websocket')
}
this.authenticated.set('http_host', prosodyDomain)
this.authenticated.set('http_external_url', 'http://' + prosodyDomain)
}
}
useC2S (c2sPort: string): void {
this.global.set('c2s_ports', [c2sPort])
}
useS2S (
s2sPort: string | null,
s2sInterfaces: string[] | null,
publicServerUrl: string,
serverInfosDir: string
): void {
if (s2sPort !== null) {
this.global.set('s2s_ports', [s2sPort])
} else {
this.global.set('s2s_ports', [])
}
if (s2sInterfaces !== null) {
this.global.set('s2s_interfaces', s2sInterfaces)
} else {
this.global.set('s2s_interfaces', [])
}
this.global.set('s2s_secure_auth', false)
this.global.remove('modules_disabled', 's2s')
this.global.add('modules_enabled', 's2s')
this.global.add('modules_enabled', 'tls') // required for s2s and co
this.global.add('modules_enabled', 's2s_peertubelivechat')
this.global.set('peertubelivechat_server_infos_path', serverInfosDir)
this.global.set('peertubelivechat_instance_url', publicServerUrl)
this.global.add('modules_enabled', 'websocket_s2s_peertubelivechat')
// Nginx closes the websockets connection after a timeout. Seems the default is 60s.
// So we will ping on outgoing websocket s2s connection every 55s.
this.global.set('websocket_s2s_ping_interval', 55)
// FIXME: seems to be necessary to add the module on the muc host, so that dialback can trigger route/remote.
this.muc.add('modules_enabled', 'websocket_s2s_peertubelivechat')
// Using direct S2S for outgoing connection can be an issue, if the local instance dont allow incomming S2S.
// Indeed, the remote instance will not necessarely be able to discover the Websocket Endpoint.
// To be sure the remote instance knows the websocket endpoint, we must use Websocket for the firt outgoing connect.
// So, we will add a parameter for mod_s2s_peertubelivechat, to tell him not to use outgoint s2s connection.
this.global.set('s2s_peertubelivechat_no_outgoing_directs2s_to_peertube', s2sPort === null)
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
this.authenticated?.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
}
useExternalComponents (
componentsPort: string,
componentsInterfaces: string[] | null,
components: ExternalComponent[]
): void {
this.global.set('component_ports', [componentsPort])
if (componentsInterfaces !== null) {
this.global.set('component_interfaces', componentsInterfaces)
} else {
this.global.set('component_interfaces', [])
}
for (const component of components) {
const c = new ProsodyConfigComponent(component.name)
c.set('component_secret', component.secret)
c.set('disco_hidden', true)
this.externalComponents.push(c)
}
}
useMucHttpDefault (url: string): void {
this.muc.add('modules_enabled', 'muc_http_defaults')
this.muc.add('muc_create_api_url', url)
// restrict_room_creation: we can override the 'local' value.
// Indeed, when muc_http_default is used, room creation will be managed by api.
this.muc.set('restrict_room_creation', false)
}
/**
* Calling this method makes Prosody use mod_muc_mam to store rooms history.
* @param logByDefault: if the room content should be archived by default.
* @param logExpiration: how long the server must store messages. See https://prosody.im/doc/modules/mod_muc_mam
*/
useMam (logByDefault: boolean, logExpiration: ConfigLogExpiration): void {
this.muc.add('modules_enabled', 'muc_mam')
this.muc.set('muc_log_by_default', !!logByDefault)
this.muc.set('muc_log_presences', true)
this.muc.set('log_all_rooms', false)
this.muc.set('muc_log_expires_after', logExpiration.value)
const defaultCleanupInterval = 4 * 60 * 60
if (logExpiration.type === 'seconds' && logExpiration.seconds && logExpiration.seconds < defaultCleanupInterval) {
// if the log expiration is less than the default cleanup interval, we have to decrease it.
this.muc.set('muc_log_cleanup_interval', logExpiration.seconds)
} else {
this.muc.set('muc_log_cleanup_interval', defaultCleanupInterval)
}
// We can also use mod_muc_moderation
// NB: Prosody has a partial support of this feature in combination with «internal» storage
// (Requires the `set` function in mod_storage_internal).
// This was fixed in Prosody 0.12.x.
this.muc.add('modules_enabled', 'muc_moderation')
}
/**
* Rooms will be persistent by default (they will not be deleted if no participant).
*/
useDefaultPersistent (): void {
this.muc.set('muc_room_default_persistent', true)
}
useListRoomsApi (apikey: string): void {
this.muc.add('modules_enabled', 'http_peertubelivechat_list_rooms')
this.muc.set('peertubelivechat_list_rooms_apikey', apikey)
}
useTestModule (prosodyApikey: string, apiurl: string): void {
this.muc.add('modules_enabled', 'http_peertubelivechat_test')
this.muc.set('peertubelivechat_test_apikey', prosodyApikey)
this.muc.set('peertubelivechat_test_peertube_api_url', apiurl)
}
usePeertubeVCards (peertubeUrl: string): void {
if (this.authenticated) {
this.authenticated.add('modules_enabled', 'vcard_peertubelivechat')
this.authenticated.set('peertubelivechat_vcard_peertube_url', peertubeUrl)
}
}
useAnonymousRandomVCards (avatarPath: string, avatarFiles: string[]): void {
if (this.anon) {
this.anon.add('modules_enabled', 'random_vcard_peertubelivechat')
this.anon.set('peertubelivechat_random_vcard_avatars_path', avatarPath)
this.anon.set('peertubelivechat_random_vcard_avatars_files', avatarFiles)
}
}
/**
* Enable the bots virtualhost.
*/
useBotsVirtualHost (botAvatarPath: string, botAvatarFiles: string[]): void {
this.bot = new ProsodyConfigVirtualHost('bot.' + this.prosodyDomain)
this.bot.set('modules_enabled', ['ping'])
this.bot.set('authentication', 'peertubelivechat_bot')
// For now, just using random_vcard_peertubelivechat to set bot avatar
this.bot.add('modules_enabled', 'random_vcard_peertubelivechat')
this.bot.set('peertubelivechat_random_vcard_avatars_path', botAvatarPath)
this.bot.set('peertubelivechat_random_vcard_avatars_files', botAvatarFiles)
// Adding the moderation bot as admin to the muc component.
this.muc.add('admins', BotConfiguration.singleton().moderationBotJID())
const configurationPaths = BotConfiguration.singleton().configurationPaths()
if (configurationPaths.moderation?.globalDir) {
this.bot.set('livechat_bot_conf_folder', configurationPaths.moderation.globalDir)
}
}
setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void {
let log = ''
log += 'log = {\n'
if (level !== 'error') {
log += ' ' + level + ' = ' + writeValue(this.paths.log)
}
// always log error level in a separate file.
log += ' error = ' + writeValue(this.paths.error)
if (syslog) {
log += ' { to = "syslog"; levels = ' + writeValue(syslog) + ' };\n'
}
log += '\n};\n'
this.log = log
}
public write (): string {
let content = ''
content += this.global.write()
content += this.log + '\n'
content += '\n\n'
if (this.authenticated) {
content += this.authenticated.write()
content += '\n\n'
}
if (this.anon) {
content += this.anon.write()
content += '\n\n'
}
if (this.bot) {
content += this.bot.write()
content += '\n\n'
}
content += this.muc.write()
content += '\n\n'
for (const externalComponent of this.externalComponents) {
content += '\n\n'
content += externalComponent.write()
content += '\n\n'
}
return content
}
}
export {
ProsodyLogLevel,
ProsodyConfigContent,
ConfigLogExpiration
}