XMPP external components.
This commit is contained in:
parent
96598f07d1
commit
df3f87e903
@ -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)).
|
||||||
|
@ -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':
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
38
server/lib/prosody/config/components.ts
Normal file
38
server/lib/prosody/config/components.ts
Normal 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
|
||||||
|
}
|
@ -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,17 +103,20 @@ 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 {
|
||||||
|
if (this.type !== undefined) {
|
||||||
return `Component "${this.name}" "${this.type}"\n` + super.write()
|
return `Component "${this.name}" "${this.type}"\n` + super.write()
|
||||||
}
|
}
|
||||||
|
return `Component "${this.name}"\n` + super.write()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProsodyLogLevel = 'debug' | 'info' | 'warn' | 'error'
|
type ProsodyLogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user