XMPP external components.

This commit is contained in:
John Livingston 2021-12-11 19:09:01 +01:00
parent 96598f07d1
commit df3f87e903
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 154 additions and 12 deletions

View File

@ -2,6 +2,10 @@
## (unreleased yet)
### Features
* Builtin Prosody: you can now allow «external XMPP components» to connect. This can be used for exemple to connect bots or bridges. For now, only connections from localhost will be allowed.
### Minor changes and fixes
* Spanish translations (thanks [rnek0](https://github.com/rnek0)).

View File

@ -211,12 +211,19 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re
case 'prosody-muc-log-by-default':
case 'prosody-muc-expiration':
case 'prosody-c2s':
case 'prosody-components':
return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType)
case 'prosody-c2s-port':
return !(
options.formValues['chat-type'] === ('builtin-prosody' as ChatType) &&
options.formValues['prosody-c2s'] === true
)
case 'prosody-components-port':
case 'prosody-components-list':
return !(
options.formValues['chat-type'] === ('builtin-prosody' as ChatType) &&
options.formValues['prosody-components'] === true
)
case 'chat-server':
case 'chat-room':
case 'chat-bosh-uri':

View File

@ -100,6 +100,15 @@ The port that will be used by the c2s module of the builtin Prosody server.
XMPP clients shall use this port to connect.
Change it if this port is already in use on your server.
#### Enable external XMPP components
This settings enable XMPP external components to connect to the server.
For now, this option **only allows connections from localhost components**.
This feature could be used to connect bridges or bots.
More informations on Prosody external components [here](https://prosody.im/doc/components).
## Moderation
You can access rooms settings and moderation tools by opening the chat in a new window,

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,17 +103,20 @@ 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 {
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'
@ -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