XMPP external components.

This commit is contained in:
John Livingston
2021-12-11 19:09:01 +01:00
parent 96598f07d1
commit df3f87e903
7 changed files with 154 additions and 12 deletions

View File

@ -1,3 +1,4 @@
import type { ProsodyLogLevel } from './config/content'
import * as fs from 'fs'
import * as path from 'path'
import { getBaseRouterRoute } from '../helpers'
@ -5,7 +6,7 @@ import { ProsodyFilePaths } from './config/paths'
import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
import { getProsodyDomain } from './config/domain'
import { getAPIKey } from '../apikey'
import type { ProsodyLogLevel } from './config/content'
import { parseExternalComponents } from './config/components'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers
@ -72,7 +73,7 @@ interface ProsodyConfig {
roomType: 'video' | 'channel'
logByDefault: boolean
logExpiration: ConfigLogExpiration
valuesToHideInDiagnostic: {[key: string]: string}
valuesToHideInDiagnostic: Map<string, string>
}
async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger
@ -83,12 +84,15 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
'prosody-muc-log-by-default',
'prosody-muc-expiration',
'prosody-c2s',
'prosody-c2s-port',
'prosody-room-type',
'prosody-peertube-uri',
'prosody-c2s-port'
'prosody-components',
'prosody-components-port',
'prosody-components-list'
])
const valuesToHideInDiagnostic: {[key: string]: string} = {}
const valuesToHideInDiagnostic = new Map<string, string>()
const port = (settings['prosody-port'] as string) || '52800'
if (!/^\d+$/.test(port)) {
throw new Error('Invalid port')
@ -96,12 +100,13 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
const logByDefault = (settings['prosody-muc-log-by-default'] as boolean) ?? true
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2s = (settings['prosody-c2s'] as boolean) || false
const enableComponents = (settings['prosody-c2s'] as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.APIKey = apikey
valuesToHideInDiagnostic.set('APIKey', apikey)
let baseApiUrl = settings['prosody-peertube-uri'] as string
if (baseApiUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(baseApiUrl)) {
@ -129,6 +134,18 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
config.useC2S(c2sPort)
}
if (enableComponents) {
const componentsPort = (settings['prosody-components-port'] as string) || '53470'
if (!/^\d+$/.test(componentsPort)) {
throw new Error('Invalid external components port')
}
const components = parseExternalComponents((settings['prosody-components-list'] as string) || '', prosodyDomain)
for (const component of components) {
valuesToHideInDiagnostic.set('Component ' + component.name + ' secret', component.secret)
}
config.useExternalComponents(componentsPort, components)
}
const logExpiration = readLogExpiration(options, logExpirationSetting)
config.useMam(logByDefault, logExpiration)
// TODO: add a settings to choose?
@ -238,9 +255,9 @@ function readLogExpiration (options: RegisterServerOptions, logExpiration: strin
function getProsodyConfigContentForDiagnostic (config: ProsodyConfig, content?: string): string {
let r: string = content ?? config.content
for (const key in config.valuesToHideInDiagnostic) {
for (const [key, value] of config.valuesToHideInDiagnostic.entries()) {
// replaceAll not available, using trick:
r = r.split(config.valuesToHideInDiagnostic[key]).join(`***${key}***`)
r = r.split(value).join(`***${key}***`)
}
return r
}

View File

@ -0,0 +1,38 @@
interface ExternalComponent {
name: string
secret: string
}
function parseExternalComponents (s: string, prosodyDomain: string): ExternalComponent[] {
if (!s) {
return []
}
let lines = s.split('\n')
lines = lines.map(line => {
return line.replace(/#.*$/, '')
.replace(/^\s+/, '')
.replace(/\s+$/, '')
})
lines = lines.filter(line => line !== '')
const r: ExternalComponent[] = []
for (const line of lines) {
const matches = line.match(/^([\w.]+)\s*:\s*(\w+)$/)
if (matches) {
let name = matches[1]
if (!name.includes('.')) {
name = name + '.' + prosodyDomain
}
r.push({
name,
secret: matches[2]
})
}
}
return r
}
export {
ExternalComponent,
parseExternalComponents
}

View File

@ -1,4 +1,5 @@
import type { ProsodyFilePaths } from './paths'
import type { ExternalComponent } from './components'
type ConfigEntryValue = boolean | number | string | ConfigEntryValue[]
@ -102,16 +103,19 @@ class ProsodyConfigVirtualHost extends ProsodyConfigBlock {
class ProsodyConfigComponent extends ProsodyConfigBlock {
name: string
type: string
type?: string
constructor (type: string, name: string) {
constructor (name: string, type?: string) {
super(' ')
this.type = type
this.name = name
}
write (): string {
return `Component "${this.name}" "${this.type}"\n` + super.write()
if (this.type !== undefined) {
return `Component "${this.name}" "${this.type}"\n` + super.write()
}
return `Component "${this.name}"\n` + super.write()
}
}
@ -123,6 +127,7 @@ class ProsodyConfigContent {
authenticated?: ProsodyConfigVirtualHost
anon: ProsodyConfigVirtualHost
muc: ProsodyConfigComponent
externalComponents: ProsodyConfigComponent[] = []
log: string
prosodyDomain: string
@ -132,7 +137,7 @@ class ProsodyConfigContent {
this.log = ''
this.prosodyDomain = prosodyDomain
this.anon = new ProsodyConfigVirtualHost('anon.' + prosodyDomain)
this.muc = new ProsodyConfigComponent('muc', 'room.' + prosodyDomain)
this.muc = new ProsodyConfigComponent('room.' + prosodyDomain, 'muc')
this.global.set('daemonize', false)
this.global.set('allow_registration', false)
@ -228,6 +233,17 @@ class ProsodyConfigContent {
this.global.set('c2s_ports', [c2sPort])
}
useExternalComponents (componentsPort: string, components: ExternalComponent[]): void {
this.global.set('component_ports', [componentsPort])
this.global.set('component_interfaces', ['127.0.0.1', '::1'])
for (const component of components) {
const c = new ProsodyConfigComponent(component.name)
c.set('component_secret', component.secret)
this.externalComponents.push(c)
}
}
useMucHttpDefault (url: string): void {
this.muc.add('modules_enabled', 'muc_http_defaults')
this.muc.add('muc_create_api_url', url)
@ -309,6 +325,11 @@ class ProsodyConfigContent {
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
}
}

View File

@ -380,7 +380,53 @@ This option alone only allows connections from localhost clients.`
`The port that will be used by the c2s module of the builtin Prosody server.<br>
XMPP clients shall use this port to connect.<br>
Change it if this port is already in use on your server.<br>
Keep it close this port on your firewall for now, it will not be accessed from the outer world.`
You can keep this port closed on your firewall for now, it will not be accessed from the outer world.`
})
registerSetting({
name: 'prosody-components',
label: 'Enable custom Prosody external components',
type: 'input-checkbox',
default: false,
private: true,
descriptionHTML:
`Enable the use of external XMPP components.<br>
This option alone only allows connections from localhost.<br>
This feature can for example be used to connect some bots to the chatting rooms.`
})
registerSetting({
name: 'prosody-components-port',
label: 'Prosody external components port',
type: 'input',
default: '53470',
private: true,
descriptionHTML:
`The port that will be used by XMPP components to connect to the Prosody server.<br>
Change it if this port is already in use on your server.<br>
You can keep this port closed on your firewall for now, it will not be accessed from the outer world.`
})
registerSetting({
name: 'prosody-components-list',
label: 'External components',
type: 'input-textarea',
default: '',
private: true,
descriptionHTML:
`The external components to create:
<ul>
<li>One per line.</li>
<li>Use the format «component_name:component_secret» (spaces will be trimmed)</li>
<li>You can add comments: everything after the # character will be stripped off, and empty lines ignored</li>
<li>The name can only contain alphanumeric characters and dots</li>
<li>
If the name contains only alphanumeric characters, it will be suffixed with the XMPP domain.
For exemple «bridge» will become «bridge.your_domain.tld».
You can also specify a full domain name, but you have to make sure to configure your DNS correctly.
</li>
<li>Only use alphanumeric characters in the secret passphrase (use at least 15 characters).</li>
</ul>`
})
// ********** settings changes management