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) ## (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 ### Minor changes and fixes
* Spanish translations (thanks [rnek0](https://github.com/rnek0)). * 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-log-by-default':
case 'prosody-muc-expiration': case 'prosody-muc-expiration':
case 'prosody-c2s': case 'prosody-c2s':
case 'prosody-components':
return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType) return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType)
case 'prosody-c2s-port': case 'prosody-c2s-port':
return !( return !(
options.formValues['chat-type'] === ('builtin-prosody' as ChatType) && options.formValues['chat-type'] === ('builtin-prosody' as ChatType) &&
options.formValues['prosody-c2s'] === true 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-server':
case 'chat-room': case 'chat-room':
case 'chat-bosh-uri': 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. XMPP clients shall use this port to connect.
Change it if this port is already in use on your server. 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 ## Moderation
You can access rooms settings and moderation tools by opening the chat in a new window, 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 fs from 'fs'
import * as path from 'path' import * as path from 'path'
import { getBaseRouterRoute } from '../helpers' import { getBaseRouterRoute } from '../helpers'
@ -5,7 +6,7 @@ import { ProsodyFilePaths } from './config/paths'
import { ConfigLogExpiration, ProsodyConfigContent } from './config/content' import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
import { getProsodyDomain } from './config/domain' import { getProsodyDomain } from './config/domain'
import { getAPIKey } from '../apikey' import { getAPIKey } from '../apikey'
import type { ProsodyLogLevel } from './config/content' import { parseExternalComponents } from './config/components'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> { async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers const peertubeHelpers = options.peertubeHelpers
@ -72,7 +73,7 @@ interface ProsodyConfig {
roomType: 'video' | 'channel' roomType: 'video' | 'channel'
logByDefault: boolean logByDefault: boolean
logExpiration: ConfigLogExpiration logExpiration: ConfigLogExpiration
valuesToHideInDiagnostic: {[key: string]: string} valuesToHideInDiagnostic: Map<string, string>
} }
async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> { async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
@ -83,12 +84,15 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
'prosody-muc-log-by-default', 'prosody-muc-log-by-default',
'prosody-muc-expiration', 'prosody-muc-expiration',
'prosody-c2s', 'prosody-c2s',
'prosody-c2s-port',
'prosody-room-type', 'prosody-room-type',
'prosody-peertube-uri', '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' const port = (settings['prosody-port'] as string) || '52800'
if (!/^\d+$/.test(port)) { if (!/^\d+$/.test(port)) {
throw new Error('Invalid 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 logByDefault = (settings['prosody-muc-log-by-default'] as boolean) ?? true
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2s = (settings['prosody-c2s'] as boolean) || false const enableC2s = (settings['prosody-c2s'] as boolean) || false
const enableComponents = (settings['prosody-c2s'] as boolean) || false
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options) const paths = await getProsodyFilePaths(options)
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video' const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
const apikey = await getAPIKey(options) const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.APIKey = apikey valuesToHideInDiagnostic.set('APIKey', apikey)
let baseApiUrl = settings['prosody-peertube-uri'] as string let baseApiUrl = settings['prosody-peertube-uri'] as string
if (baseApiUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(baseApiUrl)) { if (baseApiUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(baseApiUrl)) {
@ -129,6 +134,18 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
config.useC2S(c2sPort) 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) const logExpiration = readLogExpiration(options, logExpirationSetting)
config.useMam(logByDefault, logExpiration) config.useMam(logByDefault, logExpiration)
// TODO: add a settings to choose? // TODO: add a settings to choose?
@ -238,9 +255,9 @@ function readLogExpiration (options: RegisterServerOptions, logExpiration: strin
function getProsodyConfigContentForDiagnostic (config: ProsodyConfig, content?: string): string { function getProsodyConfigContentForDiagnostic (config: ProsodyConfig, content?: string): string {
let r: string = content ?? config.content 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: // replaceAll not available, using trick:
r = r.split(config.valuesToHideInDiagnostic[key]).join(`***${key}***`) r = r.split(value).join(`***${key}***`)
} }
return r 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 { ProsodyFilePaths } from './paths'
import type { ExternalComponent } from './components'
type ConfigEntryValue = boolean | number | string | ConfigEntryValue[] type ConfigEntryValue = boolean | number | string | ConfigEntryValue[]
@ -102,16 +103,19 @@ class ProsodyConfigVirtualHost extends ProsodyConfigBlock {
class ProsodyConfigComponent extends ProsodyConfigBlock { class ProsodyConfigComponent extends ProsodyConfigBlock {
name: string name: string
type: string type?: string
constructor (type: string, name: string) { constructor (name: string, type?: string) {
super(' ') super(' ')
this.type = type this.type = type
this.name = name this.name = name
} }
write (): string { 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 authenticated?: ProsodyConfigVirtualHost
anon: ProsodyConfigVirtualHost anon: ProsodyConfigVirtualHost
muc: ProsodyConfigComponent muc: ProsodyConfigComponent
externalComponents: ProsodyConfigComponent[] = []
log: string log: string
prosodyDomain: string prosodyDomain: string
@ -132,7 +137,7 @@ class ProsodyConfigContent {
this.log = '' this.log = ''
this.prosodyDomain = prosodyDomain this.prosodyDomain = prosodyDomain
this.anon = new ProsodyConfigVirtualHost('anon.' + 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('daemonize', false)
this.global.set('allow_registration', false) this.global.set('allow_registration', false)
@ -228,6 +233,17 @@ class ProsodyConfigContent {
this.global.set('c2s_ports', [c2sPort]) 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 { useMucHttpDefault (url: string): void {
this.muc.add('modules_enabled', 'muc_http_defaults') this.muc.add('modules_enabled', 'muc_http_defaults')
this.muc.add('muc_create_api_url', url) this.muc.add('muc_create_api_url', url)
@ -309,6 +325,11 @@ class ProsodyConfigContent {
content += '\n\n' content += '\n\n'
content += this.muc.write() content += this.muc.write()
content += '\n\n' content += '\n\n'
for (const externalComponent of this.externalComponents) {
content += '\n\n'
content += externalComponent.write()
content += '\n\n'
}
return content 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> `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> XMPP clients shall use this port to connect.<br>
Change it if this port is already in use on your server.<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 // ********** settings changes management