Chat Federation (and a lot more) WIP:

Note: websocket s2s is not working yet, still WIP.

New Features

* Chat Federation:
  * You can now connect to a remote chat with your local account.
  * This remote connection is done using a custom implementation of [XEP-0468: WebSocket S2S](https://xmpp.org/extensions/xep-0468.html), using some specific discovering method (so that it will work without any DNS configuration).

Minor changes and fixes

* Possibility to debug Prosody in development environments.
* Using process.spawn instead of process.exec to launch Prosody (safer, and more optimal).
* Prosody AppImage: fix path mapping: we only map necessary /etc/ subdir, so that the AppImage can access to /etc/resolv.conf, /etc/hosts, ...
* Prosody AppImage: hidden debug mode to disable lua-unbound, that seems broken in some docker dev environments.
This commit is contained in:
John Livingston
2023-05-19 12:52:52 +02:00
parent 1174f661be
commit 9a2da60b7d
27 changed files with 1592 additions and 106 deletions

View File

@ -8,6 +8,7 @@ import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
import { getProsodyDomain } from './config/domain'
import { getAPIKey } from '../apikey'
import { parseExternalComponents } from './config/components'
import { getRemoteServerInfosDir } from '../federation/storage'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers
@ -64,26 +65,24 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
let certsDir: string | undefined = path.resolve(dir, 'certs')
let certsDirIsCustom = false
if (settings['prosody-room-allow-s2s']) {
if ((settings['prosody-certificates-dir'] as string ?? '') !== '') {
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
// We can throw an exception here...
// Because if the user input a wrong directory, the plugin will not register,
// and he will never be able to fix the conf
logger.error('Certificate directory does not exist or is not a directory')
certsDir = undefined
} else {
certsDir = settings['prosody-certificates-dir'] as string
}
certsDirIsCustom = true
if (settings['prosody-room-allow-s2s'] && (settings['prosody-certificates-dir'] as string ?? '') !== '') {
if (!fs.statSync(settings['prosody-certificates-dir'] as string).isDirectory()) {
// We can throw an exception here...
// Because if the user input a wrong directory, the plugin will not register,
// and he will never be able to fix the conf
logger.error('Certificate directory does not exist or is not a directory')
certsDir = undefined
} else {
// In this case we are generating and using self signed certificates
// Note: when using prosodyctl to generate self-signed certificates,
// there are wrongly generated in the data dir.
// So we will use this dir as the certs dir.
certsDir = path.resolve(dir, 'data')
certsDir = settings['prosody-certificates-dir'] as string
}
certsDirIsCustom = true
} else {
// In this case we are generating and using self signed certificates
// Note: when using prosodyctl to generate self-signed certificates,
// there are wrongly generated in the data dir.
// So we will use this dir as the certs dir.
certsDir = path.resolve(dir, 'data')
}
return {
@ -139,7 +138,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
'prosody-components',
'prosody-components-port',
'prosody-components-list',
'chat-no-anonymous'
'chat-no-anonymous',
'federation-dont-publish-remotely'
])
const valuesToHideInDiagnostic = new Map<string, string>()
@ -151,23 +151,27 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
const disableAnon = (settings['chat-no-anonymous'] as boolean) || false
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2S = (settings['prosody-c2s'] as boolean) || false
// enableRoomS2S: room can be joined from remote XMPP servers (Peertube or not)
const enableRoomS2S = (settings['prosody-room-allow-s2s'] as boolean) || false
const enableComponents = (settings['prosody-components'] as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
const enableUserS2S = enableRoomS2S && !(settings['federation-no-remote-chat'] as boolean)
// enableRemoteChatConnections: local users can communicate with external rooms
const enableRemoteChatConnections = !(settings['federation-dont-publish-remotely'] as boolean)
let certificates: ProsodyConfigCertificates = false
const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.set('APIKey', apikey)
const publicServerUrl = options.peertubeHelpers.config.getWebserverUrl()
let basePeertubeUrl = settings['prosody-peertube-uri'] as string
if (basePeertubeUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(basePeertubeUrl)) {
throw new Error('Invalid prosody-peertube-uri')
}
if (!basePeertubeUrl) {
basePeertubeUrl = options.peertubeHelpers.config.getWebserverUrl()
basePeertubeUrl = publicServerUrl
}
const baseApiUrl = basePeertubeUrl + getBaseRouterRoute(options) + 'api/'
@ -181,7 +185,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
}
config.useHttpAuthentication(authApiUrl)
const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, options.peertubeHelpers.config.getWebserverUrl(), useWS)
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS)
config.useMucHttpDefault(roomApiUrl)
if (enableC2S) {
@ -204,27 +208,33 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.useExternalComponents(componentsPort, components)
}
if (enableRoomS2S || enableUserS2S) {
if (enableRoomS2S || enableRemoteChatConnections) {
certificates = 'generate-self-signed'
if (config.paths.certsDirIsCustom) {
certificates = 'use-from-dir'
}
const s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
if (!/^\d+$/.test(s2sPort)) {
throw new Error('Invalid s2s port')
let s2sPort, s2sInterfaces
if (enableRoomS2S) {
s2sPort = (settings['prosody-s2s-port'] as string) || '5269'
if (!/^\d+$/.test(s2sPort)) {
throw new Error('Invalid s2s port')
}
s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
.split(',')
.map(s => s.trim())
// Check that there is no invalid values (to avoid injections):
s2sInterfaces.forEach(networkInterface => {
if (networkInterface === '*') return
if (networkInterface === '::') return
if (networkInterface.match(/^\d+\.\d+\.\d+\.\d+$/)) return
if (networkInterface.match(/^[a-f0-9:]+$/)) return
throw new Error('Invalid s2s interfaces')
})
} else {
s2sPort = null
s2sInterfaces = null
}
const s2sInterfaces = ((settings['prosody-s2s-interfaces'] as string) || '')
.split(',')
.map(s => s.trim())
// Check that there is no invalid values (to avoid injections):
s2sInterfaces.forEach(networkInterface => {
if (networkInterface === '*') return
if (networkInterface === '::') return
if (networkInterface.match(/^\d+\.\d+\.\d+\.\d+$/)) return
if (networkInterface.match(/^[a-f0-9:]+$/)) return
throw new Error('Invalid s2s interfaces')
})
config.useS2S(s2sPort, s2sInterfaces, !enableUserS2S)
config.useS2S(s2sPort, s2sInterfaces, publicServerUrl, getRemoteServerInfosDir(options))
}
const logExpiration = readLogExpiration(options, logExpirationSetting)

View File

@ -72,6 +72,18 @@ abstract class ProsodyConfigBlock {
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 :)
@ -258,17 +270,35 @@ class ProsodyConfigContent {
this.global.set('c2s_ports', [c2sPort])
}
useS2S (s2sPort: string, s2sInterfaces: string[], mucOnly: boolean): void {
this.global.set('s2s_ports', [s2sPort])
this.global.set('s2s_interfaces', s2sInterfaces)
this.global.set('s2s_secure_auth', false)
this.global.add('modules_enabled', 'tls') // required for s2s and co
this.muc.add('modules_enabled', 's2s')
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
if (!mucOnly && this.authenticated) {
this.authenticated.add('modules_enabled', 's2s')
this.authenticated.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
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')
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, components: ExternalComponent[]): void {

View File

@ -8,6 +8,7 @@ import { disableProxyRoute, enableProxyRoute } from '../routers/webchat'
import { fixRoomSubject } from './fix-room-subject'
import * as fs from 'fs'
import * as child_process from 'child_process'
import { disableLuaUnboundIfNeeded, prosodyDebuggerOptions } from '../../lib/debug'
async function _ensureWorkingDir (
options: RegisterServerOptions,
@ -98,6 +99,7 @@ async function prepareProsody (options: RegisterServerOptions): Promise<void> {
})
spawned.on('error', reject)
spawned.on('close', (_code) => { // 'close' and not 'exit', to be sure it is finished.
disableLuaUnboundIfNeeded(options, filePaths.appImageExtractPath)
resolve()
})
})
@ -275,20 +277,28 @@ async function testProsodyCorrectlyRunning (options: RegisterServerOptions): Pro
return result
}
async function ensureProsodyRunning (options: RegisterServerOptions): Promise<void> {
async function ensureProsodyRunning (
options: RegisterServerOptions,
forceRestart?: boolean,
restartProsodyInDebugMode?: boolean
): Promise<void> {
const { peertubeHelpers } = options
const logger = peertubeHelpers.logger
logger.debug('Calling ensureProsodyRunning')
const r = await testProsodyCorrectlyRunning(options)
if (r.ok) {
r.messages.forEach(m => logger.debug(m))
logger.info('Prosody is already running correctly')
// Stop here. Nothing to change.
return
if (forceRestart) {
logger.info('We want to force Prosody restart, skip checking the current state')
} else {
const r = await testProsodyCorrectlyRunning(options)
if (r.ok) {
r.messages.forEach(m => logger.debug(m))
logger.info('Prosody is already running correctly')
// Stop here. Nothing to change.
return
}
logger.info('Prosody is not running correctly: ')
r.messages.forEach(m => logger.info(m))
}
logger.info('Prosody is not running correctly: ')
r.messages.forEach(m => logger.info(m))
// Shutting down...
logger.debug('Shutting down prosody')
@ -309,9 +319,30 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
await ensureProsodyCertificates(options, config)
// launch prosody
const execCmd = filePaths.exec + (filePaths.execArgs.length ? ' ' + filePaths.execArgs.join(' ') : '')
logger.info('Going to launch prosody (' + execCmd + ')')
const prosody = child_process.exec(execCmd, {
let execArgs: string[] = filePaths.execArgs
if (restartProsodyInDebugMode) {
if (!filePaths.exec.includes('squashfs-root')) {
logger.error('Trying to enable the Prosody Debugger, but not using the AppImage. Cant work.')
} else {
const debuggerOptions = prosodyDebuggerOptions(options)
if (debuggerOptions) {
execArgs = [
'debug',
debuggerOptions.mobdebugPath,
debuggerOptions.mobdebugHost,
debuggerOptions.mobdebugPort,
...execArgs
]
}
}
}
logger.info(
'Going to launch prosody (' +
filePaths.exec +
(execArgs.length ? ' ' + execArgs.join(' ') : '') +
')'
)
const prosody = child_process.spawn(filePaths.exec, execArgs, {
cwd: filePaths.dir,
env: {
...process.env,