Chat Federation: using S2S if available.

* if both local and remote instance have external XMPP connections enabled, the user joins the remote room with his local account
* some code refactoring (builtin.ts)

Note: documentation and settings descriptions are to do.

Related to #112
This commit is contained in:
John Livingston 2023-05-04 19:14:23 +02:00
parent 1003378b24
commit 3bc05d88df
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
16 changed files with 483 additions and 285 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## ??? (Not Release Yet)
### New Features
* Chat Federation: if both local and remote instance have external XMPP connections enabled, you can use your local xmpp account to join remote rooms.
## 6.3.0
### New Features

View File

@ -1,4 +1,17 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { InitConverseParams } from './lib/types'
import { inIframe } from './lib/utils'
import { initDom } from './lib/dom'
import {
defaultConverseParams,
localRoomAnonymousParams,
localRoomAuthenticatedParams,
remoteRoomAnonymousParams,
remoteRoomAuthenticatedParams
} from './lib/converse-params'
import { getLocalAuthentInfos } from './lib/auth'
import { randomNick } from './lib/nick'
declare global {
interface Window {
converse: {
initialize: (args: any) => void
@ -6,241 +19,59 @@ interface Window {
add: (name: string, plugin: any) => void
}
}
initConverse: (args: any) => void
}
function inIframe (): boolean {
try {
return window.self !== window.top
} catch (e) {
return true
initConverse: (args: InitConverseParams) => Promise<void>
}
}
interface AuthentInfos {
jid: string
password: string
nickname?: string
}
async function authenticatedMode (authenticationUrl: string): Promise<false | AuthentInfos> {
try {
if (!window.fetch) {
console.error('Your browser has not the fetch api, we cant log you in')
return false
}
if (!window.localStorage) {
// FIXME: is the Peertube token always in localStorage?
console.error('Your browser has no localStorage, we cant log you in')
return false
}
const tokenType = window.localStorage.getItem('token_type') ?? ''
const accessToken = window.localStorage.getItem('access_token') ?? ''
const refreshToken = window.localStorage.getItem('refresh_token') ?? ''
if (tokenType === '' && accessToken === '' && refreshToken === '') {
console.info('User seems not to be logged in.')
return false
}
const response = await window.fetch(authenticationUrl, {
method: 'GET',
headers: new Headers({
Authorization: tokenType + ' ' + accessToken,
'content-type': 'application/json;charset=UTF-8'
})
})
if (!response.ok) {
console.error('Failed fetching user informations')
return false
}
const data = await response.json()
if ((typeof data) !== 'object') {
console.error('Failed reading user informations')
return false
}
if (!data.jid || !data.password) {
console.error('User informations does not contain required fields')
return false
}
return {
jid: data.jid,
password: data.password,
nickname: data.nickname
}
} catch (error) {
console.error(error)
return false
}
}
function randomNick (base: string): string {
// using a 6 digit random number to generate a nickname with low colision risk
const n = 100000 + Math.floor(Math.random() * 900000)
return base + ' ' + n.toString()
}
interface InitConverseParams {
jid: string
remoteAnonymousXMPPServer: boolean
assetsPath: string
room: string
boshServiceUrl: string
websocketServiceUrl: string
authenticationUrl: string
autoViewerMode: boolean
forceReadonly: boolean | 'noscroll'
noScroll: boolean
theme: string
transparent: boolean
}
window.initConverse = async function initConverse ({
jid,
remoteAnonymousXMPPServer,
assetsPath,
room,
boshServiceUrl,
websocketServiceUrl,
authenticationUrl,
autoViewerMode,
forceReadonly,
theme,
transparent
}: InitConverseParams) {
const isInIframe = inIframe()
const converse = window.converse
const body = document.querySelector('body')
if (isInIframe) {
if (body) {
body.classList.add('livechat-iframe')
// prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS)
body.style.overflowX = 'hidden'
}
}
if (forceReadonly) {
body?.classList.add('livechat-readonly')
if (forceReadonly === 'noscroll') {
body?.classList.add('livechat-noscroll')
}
}
if (transparent) {
body?.classList.add('livechat-transparent')
}
if (websocketServiceUrl?.startsWith('/')) {
websocketServiceUrl = new URL(
websocketServiceUrl,
window.initConverse = async function initConverse (initConverseParams: InitConverseParams): Promise<void> {
// First, fixing relative websocket urls.
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
initConverseParams.localWebsocketServiceUrl = new URL(
initConverseParams.localWebsocketServiceUrl,
(window.location.protocol === 'http:' ? 'ws://' : 'wss://') + window.location.host
).toString()
}
const mucShowInfoMessages = forceReadonly
? [
// in readonly mode, show only following info messages:
'301', '307', '321', '322', '332', '333' // disconnected
]
: [
// FIXME: wait for a response here, and rewrite: https://github.com/conversejs/converse.js/issues/3125
'100', '102', '103', '172', '173', '174', // visibility_changes
'110', // self
'104', '201', // non_privacy_changes
'170', '171', // muc_logging_changes
'210', '303', // nickname_changes
'301', '307', '321', '322', '332', '333', // disconnected
'owner', 'admin', 'member', 'exadmin', 'exowner', 'exoutcast', 'exmember', // affiliation_changes
// 'entered', 'exited', // join_leave_events
'op', 'deop', 'voice', 'mute' // role_changes
]
const {
isRemoteChat,
remoteAnonymousXMPPServer,
remoteAuthenticatedXMPPServer,
authenticationUrl,
autoViewerMode,
forceReadonly
} = initConverseParams
const params: any = {
assets_path: assetsPath,
const converse = window.converse
authentication: 'anonymous',
ping_interval: 25, // must be set accordingly to c2s_close_timeout backend websocket settings and nginx timeout
auto_login: true,
auto_join_rooms: [
room
],
keepalive: true,
discover_connection_methods: false, // this parameter seems buggy with converseJS 7.0.4
bosh_service_url: boshServiceUrl === '' ? undefined : boshServiceUrl,
websocket_url: websocketServiceUrl === '' ? undefined : websocketServiceUrl,
jid: jid,
notify_all_room_messages: [
room
],
show_desktop_notifications: false,
show_tab_notifications: false,
singleton: true,
auto_focus: !isInIframe,
hide_muc_participants: isInIframe,
play_sounds: false,
muc_mention_autocomplete_min_chars: 2,
muc_mention_autocomplete_filter: 'contains',
muc_instant_rooms: true,
show_client_info: false,
allow_adhoc_commands: false,
allow_contact_requests: false,
allow_logout: false,
show_controlbox_by_default: false,
view_mode: 'fullscreen',
allow_message_corrections: 'all',
allow_message_retraction: 'all',
visible_toolbar_buttons: {
call: false,
spoiler: false,
emoji: true,
toggle_occupants: true
},
theme: theme || 'peertube',
dark_theme: theme || 'peertube', // dark theme should be the same as theme
persistent_store: 'sessionStorage',
show_images_inline: false, // for security reason, and to avoid bugs when image is larger that iframe
render_media: false, // for security reason, and to avoid bugs when image is larger that iframe
whitelisted_plugins: ['livechatWindowTitlePlugin', 'livechatViewerModePlugin', 'livechatDisconnectOnUnloadPlugin'],
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
muc_show_info_messages: mucShowInfoMessages,
send_chat_state_notifications: false // don't send this for performance reason
}
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
const isInIframe = inIframe()
initDom(initConverseParams, isInIframe)
const params = defaultConverseParams(initConverseParams, isInIframe)
let isAuthenticated: boolean = false
let isRemoteWithNicknameSet: boolean = false
if (authenticationUrl === '') {
throw new Error('Missing authenticationUrl')
}
// The user will never se the «trusted browser» checkbox (that allows to save credentials).
// So we have to disable it
// (and ensure clear_cache_on_logout is true,
// see https://conversejs.org/docs/html/configuration.html#allow-user-trust-override).
params.clear_cache_on_logout = true
params.allow_user_trust_override = false
const auth = await authenticatedMode(authenticationUrl)
const auth = await getLocalAuthentInfos(authenticationUrl)
if (auth) {
if (remoteAnonymousXMPPServer) {
// Spécial case: anonymous connection to remote XMPP server.
if (auth.nickname) {
params.nickname = auth.nickname
isRemoteWithNicknameSet = true
}
} else {
params.authentication = 'login'
params.auto_login = true
params.jid = auth.jid
params.password = auth.password
if (auth.nickname) {
params.nickname = auth.nickname
} else {
params.muc_nickname_from_jid = true
}
// We dont need the keepalive. And I suppose it is related to some bugs when opening a previous chat window.
params.keepalive = false
if (!isRemoteChat) {
localRoomAuthenticatedParams(initConverseParams, auth, params)
isAuthenticated = true
// FIXME: use params.oauth_providers?
} else if (remoteAuthenticatedXMPPServer) {
remoteRoomAuthenticatedParams(initConverseParams, auth, params)
isAuthenticated = true
} else if (remoteAnonymousXMPPServer) {
// remote server does not allow remote authenticated users, falling back to anonymous mode
remoteRoomAnonymousParams(initConverseParams, auth, params)
isRemoteWithNicknameSet = true
} else {
throw new Error('Remote server does not allow remote connection')
}
} else {
if (!isRemoteChat) {
localRoomAnonymousParams(initConverseParams, params)
} else if (remoteAnonymousXMPPServer) {
remoteRoomAnonymousParams(initConverseParams, null, params)
} else {
throw new Error('Remote server does not allow remote connection')
}
}
@ -300,9 +131,9 @@ window.initConverse = async function initConverse ({
function refreshViewerMode (canChat: boolean): void {
console.log('[livechatViewerModePlugin] refreshViewerMode: ' + (canChat ? 'off' : 'on'))
if (canChat) {
body?.setAttribute('livechat-viewer-mode', 'off')
document.querySelector('body')?.setAttribute('livechat-viewer-mode', 'off')
} else {
body?.setAttribute('livechat-viewer-mode', 'on')
document.querySelector('body')?.setAttribute('livechat-viewer-mode', 'on')
}
}

View File

@ -15,12 +15,17 @@
<div id="conversejs-bg" class="theme-peertube"></div>
<script type="text/javascript">
initConverse({
jid: '{{JID}}',
isRemoteChat: '{{IS_REMOTE_CHAT}}' === 'true',
localAnonymousJID: '{{LOCAL_ANONYMOUS_JID}}',
remoteAnonymousJID: '{{REMOTE_ANONYMOUS_JID}}' === '' ? null : '{{REMOTE_ANONYMOUS_JID}}',
remoteAnonymousXMPPServer: '{{REMOTE_ANONYMOUS_XMPP_SERVER}}' === 'true',
remoteAuthenticatedXMPPServer: '{{REMOTE_AUTHENTICATED_XMPP_SERVER}}' === 'true',
assetsPath : '{{BASE_STATIC_URL}}conversejs/',
room: '{{ROOM}}',
boshServiceUrl: '{{BOSH_SERVICE_URL}}',
websocketServiceUrl: '{{WS_SERVICE_URL}}',
localBoshServiceUrl: '{{LOCAL_BOSH_SERVICE_URL}}' === '' ? null : '{{LOCAL_BOSH_SERVICE_URL}}',
localWebsocketServiceUrl: '{{LOCAL_WS_SERVICE_URL}}' === '' ? null : '{{LOCAL_WS_SERVICE_URL}}',
remoteBoshServiceUrl: '{{REMOTE_BOSH_SERVICE_URL}}' === '' ? null : '{{REMOTE_BOSH_SERVICE_URL}}',
remoteWebsocketServiceUrl: '{{REMOTE_WS_SERVICE_URL}}' === '' ? null : '{{REMOTE_WS_SERVICE_URL}}',
authenticationUrl: '{{AUTHENTICATION_URL}}',
autoViewerMode: '{{AUTOVIEWERMODE}}' === 'true',
theme: '{{CONVERSEJS_THEME}}',

65
conversejs/lib/auth.ts Normal file
View File

@ -0,0 +1,65 @@
interface AuthentInfos {
jid: string
password: string
nickname?: string
}
async function getLocalAuthentInfos (authenticationUrl: string): Promise<false | AuthentInfos> {
try {
if (authenticationUrl === '') {
console.error('Missing authenticationUrl')
return false
}
if (!window.fetch) {
console.error('Your browser has not the fetch api, we cant log you in')
return false
}
if (!window.localStorage) {
// FIXME: is the Peertube token always in localStorage?
console.error('Your browser has no localStorage, we cant log you in')
return false
}
const tokenType = window.localStorage.getItem('token_type') ?? ''
const accessToken = window.localStorage.getItem('access_token') ?? ''
const refreshToken = window.localStorage.getItem('refresh_token') ?? ''
if (tokenType === '' && accessToken === '' && refreshToken === '') {
console.info('User seems not to be logged in.')
return false
}
const response = await window.fetch(authenticationUrl, {
method: 'GET',
headers: new Headers({
Authorization: tokenType + ' ' + accessToken,
'content-type': 'application/json;charset=UTF-8'
})
})
if (!response.ok) {
console.error('Failed fetching user informations')
return false
}
const data = await response.json()
if ((typeof data) !== 'object') {
console.error('Failed reading user informations')
return false
}
if (!data.jid || !data.password) {
console.error('User informations does not contain required fields')
return false
}
return {
jid: data.jid,
password: data.password,
nickname: data.nickname
}
} catch (error) {
console.error(error)
return false
}
}
export {
AuthentInfos,
getLocalAuthentInfos
}

View File

@ -0,0 +1,174 @@
import type { InitConverseParams } from './types'
import type { AuthentInfos } from './auth'
/**
* Instanciate defaults params to use for ConverseJS.
* Note: these parameters must be completed with one of the other function present in this module.
* @param param0 global parameters
* @param isInIframe true if we are in iframe mode (inside Peertube, beside video)
* @returns default parameters to provide to ConverseJS.
*/
function defaultConverseParams (
{ forceReadonly, theme, assetsPath, room }: InitConverseParams,
isInIframe: boolean
): any {
const mucShowInfoMessages = forceReadonly
? [
// in readonly mode, show only following info messages:
'301', '307', '321', '322', '332', '333' // disconnected
]
: [
// FIXME: wait for a response here, and rewrite: https://github.com/conversejs/converse.js/issues/3125
'100', '102', '103', '172', '173', '174', // visibility_changes
'110', // self
'104', '201', // non_privacy_changes
'170', '171', // muc_logging_changes
'210', '303', // nickname_changes
'301', '307', '321', '322', '332', '333', // disconnected
'owner', 'admin', 'member', 'exadmin', 'exowner', 'exoutcast', 'exmember', // affiliation_changes
// 'entered', 'exited', // join_leave_events
'op', 'deop', 'voice', 'mute' // role_changes
]
const params: any = {
assets_path: assetsPath,
authentication: 'anonymous',
ping_interval: 25, // must be set accordingly to c2s_close_timeout backend websocket settings and nginx timeout
auto_login: true,
auto_join_rooms: [
room
],
keepalive: true,
discover_connection_methods: false, // this parameter seems buggy with converseJS 7.0.4
notify_all_room_messages: [
room
],
show_desktop_notifications: false,
show_tab_notifications: false,
singleton: true,
auto_focus: !isInIframe,
hide_muc_participants: isInIframe,
play_sounds: false,
muc_mention_autocomplete_min_chars: 2,
muc_mention_autocomplete_filter: 'contains',
muc_instant_rooms: true,
show_client_info: false,
allow_adhoc_commands: false,
allow_contact_requests: false,
allow_logout: false,
show_controlbox_by_default: false,
view_mode: 'fullscreen',
allow_message_corrections: 'all',
allow_message_retraction: 'all',
visible_toolbar_buttons: {
call: false,
spoiler: false,
emoji: true,
toggle_occupants: true
},
theme: theme || 'peertube',
dark_theme: theme || 'peertube', // dark theme should be the same as theme
persistent_store: 'sessionStorage',
show_images_inline: false, // for security reason, and to avoid bugs when image is larger that iframe
render_media: false, // for security reason, and to avoid bugs when image is larger that iframe
whitelisted_plugins: ['livechatWindowTitlePlugin', 'livechatViewerModePlugin', 'livechatDisconnectOnUnloadPlugin'],
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
muc_show_info_messages: mucShowInfoMessages,
send_chat_state_notifications: false // don't send this for performance reason
}
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
// The user will never se the «trusted browser» checkbox (that allows to save credentials).
// So we have to disable it
// (and ensure clear_cache_on_logout is true,
// see https://conversejs.org/docs/html/configuration.html#allow-user-trust-override).
params.clear_cache_on_logout = true
params.allow_user_trust_override = false
return params
}
/**
* The room is local, and we are an authenticated local user
* @param initConverseParams global parameters
* @param auth authent infos.
* @param params ConverseJS parameters to fill
*/
function localRoomAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
_fillAuthenticatedParams(initConverseParams, auth, params)
_fillLocalProtocols(initConverseParams, params)
}
/**
* The room is local, and we are an anonymous local user
* @param initConverseParams global parameters
* @param params ConverseJS parameters to fill
*/
function localRoomAnonymousParams (initConverseParams: InitConverseParams, params: any): void {
params.jid = initConverseParams.localAnonymousJID
_fillLocalProtocols(initConverseParams, params)
}
/**
* The room is remote, and we are an authenticated local user
* @param initConverseParams global parameters
* @param auth authent infos.
* @param params ConverseJS parameters to fill
*/
function remoteRoomAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
_fillAuthenticatedParams(initConverseParams, auth, params)
_fillLocalProtocols(initConverseParams, params)
}
/**
* The room is remote, and we are an anonymous local user
* @param initConverseParams global parameters
* @param auth optionnal authent infos. Used to get the default nickname
* @param params ConverseJS parameters to fill
*/
function remoteRoomAnonymousParams (
initConverseParams: InitConverseParams,
auth: AuthentInfos | null,
params: any
): void {
params.jid = initConverseParams.remoteAnonymousJID
if (auth?.nickname) {
params.nickname = auth.nickname
}
_fillRemoteProtocols(initConverseParams, params)
}
function _fillAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
params.authentication = 'login'
params.auto_login = true
params.jid = auth.jid
params.password = auth.password
if (auth.nickname) {
params.nickname = auth.nickname
} else {
params.muc_nickname_from_jid = true
}
// We dont need the keepalive. And I suppose it is related to some bugs when opening a previous chat window.
params.keepalive = false
// FIXME: use params.oauth_providers?
}
function _fillLocalProtocols (initConverseParams: InitConverseParams, params: any): void {
params.bosh_service_url = initConverseParams.localBoshServiceUrl
params.websocket_url = initConverseParams.localWebsocketServiceUrl
}
function _fillRemoteProtocols (initConverseParams: InitConverseParams, params: any): void {
params.bosh_service_url = initConverseParams.remoteBoshServiceUrl
params.websocket_url = initConverseParams.remoteWebsocketServiceUrl
}
export {
defaultConverseParams,
localRoomAuthenticatedParams,
localRoomAnonymousParams,
remoteRoomAnonymousParams,
remoteRoomAuthenticatedParams
}

25
conversejs/lib/dom.ts Normal file
View File

@ -0,0 +1,25 @@
import type { InitConverseParams } from './types'
function initDom ({ forceReadonly, transparent }: InitConverseParams, isInIframe: boolean): void {
const body = document.querySelector('body')
if (isInIframe) {
if (body) {
body.classList.add('livechat-iframe')
// prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS)
body.style.overflowX = 'hidden'
}
}
if (forceReadonly) {
body?.classList.add('livechat-readonly')
if (forceReadonly === 'noscroll') {
body?.classList.add('livechat-noscroll')
}
}
if (transparent) {
body?.classList.add('livechat-transparent')
}
}
export {
initDom
}

9
conversejs/lib/nick.ts Normal file
View File

@ -0,0 +1,9 @@
function randomNick (base: string): string {
// using a 6 digit random number to generate a nickname with low colision risk
const n = 100000 + Math.floor(Math.random() * 900000)
return base + ' ' + n.toString()
}
export {
randomNick
}

23
conversejs/lib/types.ts Normal file
View File

@ -0,0 +1,23 @@
interface InitConverseParams {
isRemoteChat: boolean
localAnonymousJID: string
remoteAnonymousJID: string | null
remoteAnonymousXMPPServer: boolean
remoteAuthenticatedXMPPServer: boolean
assetsPath: string
room: string
localBoshServiceUrl: string | null
localWebsocketServiceUrl: string | null
remoteBoshServiceUrl: string | null
remoteWebsocketServiceUrl: string | null
authenticationUrl: string
autoViewerMode: boolean
forceReadonly: boolean | 'noscroll'
noScroll: boolean
theme: string
transparent: boolean
}
export {
InitConverseParams
}

11
conversejs/lib/utils.ts Normal file
View File

@ -0,0 +1,11 @@
function inIframe (): boolean {
try {
return window.self !== window.top
} catch (e) {
return true
}
}
export {
inIframe
}

View File

@ -31,6 +31,17 @@ function anonymousConnectionInfos (livechatInfos: LiveChatJSONLDAttribute | fals
return r
}
export {
anonymousConnectionInfos
function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttribute | false): boolean {
if (!livechatInfos) { return false }
if (!livechatInfos.links) { return false }
if (livechatInfos.type !== 'xmpp') { return false }
for (const link of livechatInfos.links) {
if (link.type === 'xmpp-s2s') { return true }
}
return false
}
export {
anonymousConnectionInfos,
remoteAuthenticatedConnectionEnabled
}

View File

@ -31,7 +31,8 @@ async function videoBuildJSONLD (
'disable-websocket',
'prosody-room-type',
'federation-dont-publish-remotely',
'chat-no-anonymous'
'chat-no-anonymous',
'prosody-room-allow-s2s'
])
if (settings['federation-dont-publish-remotely']) {
@ -70,6 +71,11 @@ async function videoBuildJSONLD (
}
const links: LiveChatJSONLDLink[] = []
if (settings['prosody-room-allow-s2s']) {
links.push({
type: 'xmpp-s2s'
})
}
if (!settings['chat-no-anonymous']) {
links.push({
type: 'xmpp-bosh-anonymous',

View File

@ -36,6 +36,11 @@ function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute
url: link.url
})
}
if (link.type === 'xmpp-s2s') {
r.links.push({
type: link.type
})
}
}
return r
}

View File

@ -4,6 +4,10 @@ interface VideoBuildResultContext {
video: MVideoAP
}
interface LiveChatJSONLDS2SLink {
type: 'xmpp-s2s'
}
interface LiveChatJSONLDAnonymousWebsocketLink {
type: 'xmpp-websocket-anonymous'
url: string
@ -16,7 +20,7 @@ interface LiveChatJSONLDAnonymousBOSHLink {
jid: string
}
type LiveChatJSONLDLink = LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
type LiveChatJSONLDLink = LiveChatJSONLDS2SLink | LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
interface LiveChatJSONLDInfos {
type: 'xmpp'

View File

@ -156,6 +156,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
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)
let certificates: ProsodyConfigCertificates = false
const apikey = await getAPIKey(options)
@ -203,7 +204,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.useExternalComponents(componentsPort, components)
}
if (enableRoomS2S) {
if (enableRoomS2S || enableUserS2S) {
certificates = 'generate-self-signed'
if (config.paths.certsDirIsCustom) {
certificates = 'use-from-dir'
@ -223,7 +224,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
if (networkInterface.match(/^[a-f0-9:]+$/)) return
throw new Error('Invalid s2s interfaces')
})
config.useRoomS2S(s2sPort, s2sInterfaces)
config.useS2S(s2sPort, s2sInterfaces, !enableUserS2S)
}
const logExpiration = readLogExpiration(options, logExpirationSetting)

View File

@ -258,13 +258,17 @@ class ProsodyConfigContent {
this.global.set('c2s_ports', [c2sPort])
}
useRoomS2S (s2sPort: string, s2sInterfaces: string[]): void {
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!
}
}
useExternalComponents (componentsPort: string, components: ExternalComponent[]): void {

View File

@ -15,7 +15,7 @@ import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../
import { getBoshUri, getWSUri } from '../uri/webchat'
import { getVideoLiveChatInfos } from '../federation/storage'
import { LiveChatJSONLDAttribute } from '../federation/types'
import { anonymousConnectionInfos } from '../federation/connection-infos'
import { anonymousConnectionInfos, remoteAuthenticatedConnectionEnabled } from '../federation/connection-infos'
import * as path from 'path'
const got = require('got')
@ -50,7 +50,8 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
'prosody-room-type',
'disable-websocket',
'converse-theme', 'converse-autocolors',
'federation-no-remote-chat'
'federation-no-remote-chat',
'prosody-room-allow-s2s'
])
let autoViewerMode: boolean = false
@ -110,26 +111,38 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
const baseStaticUrl = getBaseStaticRoute(options)
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
let connectionInfos: ConnectionInfos | null
const prosodyDomain = await getProsodyDomain(options)
const localAnonymousJID = 'anon.' + prosodyDomain
const localBoshUri = getBoshUri(options)
const localWsUri = settings['disable-websocket']
? ''
: (getWSUri(options) ?? '')
let remoteConnectionInfos: WCRemoteConnectionInfos | undefined
let roomJID: string
if (video?.remote) {
connectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
remoteConnectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
if (!remoteConnectionInfos) {
res.status(404)
res.send('No compatible way to connect to remote chat')
return
}
roomJID = remoteConnectionInfos.roomJID
} else {
connectionInfos = await _localConnectionInfos(
roomJID = await _localRoomJID(
options,
settings,
prosodyDomain,
roomKey,
video,
channelId,
req.query.forcetype === '1'
)
}
if (!connectionInfos) {
res.status(404)
res.send('No compatible way to connect to remote chat')
return
}
page = page.replace(/{{JID}}/g, connectionInfos.userJID)
page = page.replace(/{{IS_REMOTE_CHAT}}/g, video?.remote ? 'true' : 'false')
page = page.replace(/{{LOCAL_ANONYMOUS_JID}}/g, localAnonymousJID)
page = page.replace(/{{REMOTE_ANONYMOUS_JID}}/g, remoteConnectionInfos?.anonymous?.userJID ?? '')
let autocolorsStyles = ''
if (
@ -183,10 +196,20 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
}
// ... then inject it in the page.
page = page.replace(/{{ROOM}}/g, connectionInfos.roomJID)
page = page.replace(/{{BOSH_SERVICE_URL}}/g, connectionInfos.boshUri)
page = page.replace(/{{WS_SERVICE_URL}}/g, connectionInfos.wsUri ?? '')
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, connectionInfos.remoteXMPPServer ? 'true' : 'false')
page = page.replace(/{{ROOM}}/g, roomJID)
page = page.replace(/{{LOCAL_BOSH_SERVICE_URL}}/g, localBoshUri)
page = page.replace(/{{LOCAL_WS_SERVICE_URL}}/g, localWsUri ?? '')
page = page.replace(/{{REMOTE_BOSH_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.boshUri ?? '')
page = page.replace(/{{REMOTE_WS_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.wsUri ?? '')
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, remoteConnectionInfos?.anonymous ? 'true' : 'false')
// Note: to be able to connect to remote XMPP server, with a local account,
// we must enable prosody-room-allow-s2s
// (which is required, so we can use outgoing S2S from the authenticated virtualhost).
// TODO: There should be another settings, rather than prosody-room-allow-s2s
page = page.replace(
/{{REMOTE_AUTHENTICATED_XMPP_SERVER}}/g,
settings['prosody-room-allow-s2s'] && remoteConnectionInfos?.authenticated ? 'true' : 'false'
)
page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
page = page.replace(/{{CONVERSEJS_THEME}}/g, converseJSTheme)
@ -382,44 +405,45 @@ async function enableProxyRoute (
})
}
interface ConnectionInfos {
userJID: string
interface WCRemoteConnectionInfos {
roomJID: string
anonymous?: {
userJID: string
boshUri: string
wsUri?: string
remoteXMPPServer: boolean
}
authenticated?: boolean
}
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<ConnectionInfos | null> {
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<WCRemoteConnectionInfos> {
if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') }
const connectionInfos = anonymousConnectionInfos(remoteChatInfos ?? false)
if (!connectionInfos || !connectionInfos.boshUri) {
return null
if (remoteChatInfos.type !== 'xmpp') { throw new Error('Should have remote xmpp chat infos for remote videos') }
const connectionInfos: WCRemoteConnectionInfos = {
roomJID: remoteChatInfos.jid
}
return {
userJID: connectionInfos.userJID,
roomJID: connectionInfos.roomJID,
boshUri: connectionInfos.boshUri,
wsUri: connectionInfos.wsUri,
remoteXMPPServer: true
if (remoteAuthenticatedConnectionEnabled(remoteChatInfos)) {
connectionInfos.authenticated = true
}
const anonymousCI = anonymousConnectionInfos(remoteChatInfos ?? false)
if (anonymousCI?.boshUri) {
connectionInfos.anonymous = {
userJID: anonymousCI.userJID,
boshUri: anonymousCI.boshUri,
wsUri: anonymousCI.wsUri
}
}
return connectionInfos
}
async function _localConnectionInfos (
async function _localRoomJID (
options: RegisterServerOptions,
settings: SettingEntries,
prosodyDomain: string,
roomKey: string,
video: MVideoThumbnail | undefined,
channelId: number,
forceType: boolean
): Promise<ConnectionInfos> {
const prosodyDomain = await getProsodyDomain(options)
const jid = 'anon.' + prosodyDomain
const boshUri = getBoshUri(options)
const wsUri = settings['disable-websocket']
? ''
: (getWSUri(options) ?? '')
): Promise<string> {
// Computing the room name...
let room: string
if (forceType) {
@ -462,13 +486,7 @@ async function _localConnectionInfos (
room = room.replace(/{{CHANNEL_NAME}}/g, channelName)
}
return {
userJID: jid,
roomJID: room,
boshUri,
wsUri,
remoteXMPPServer: false
}
return room
}
export {