Chat Federation: refactoring ActivityPub data:

The data format used by plugin v6.3.0 was not well suited.
Here comes a new data format, with S2S informations.
The plugin can automatically upgrade old format.
It also continues to provide the old format, so than remote instance
that did not update the plugin will still work.
This commit is contained in:
John Livingston 2023-05-24 15:09:56 +02:00
parent 4f9534dc11
commit 6ed69d2c2f
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
10 changed files with 547 additions and 212 deletions

View File

@ -4,28 +4,22 @@ local path = require "util.paths";
local json = require "util.json";
local server_infos_dir = assert(module:get_option_string("peertubelivechat_server_infos_path", nil), "'peertubelivechat_server_infos_path' is a required option");
local instance_url = assert(module:get_option_string("peertubelivechat_instance_url", nil), "'peertubelivechat_instance_url' is a required option");
local current_instance_url = assert(module:get_option_string("peertubelivechat_instance_url", nil), "'peertubelivechat_instance_url' is a required option");
function discover_websocket_s2s(event)
local to_host = event.to_host;
module:log("debug", "Searching websocket s2s for remote host %s", to_host);
-- FIXME: dont to this room. prefix thing. Peertube should create needed files.
local to_host_room = to_host;
if string.sub(to_host_room, 1, 5) ~= 'room.' then
to_host_room = 'room.'..to_host_room;
end
local f_s2s = io.open(path.join(server_infos_dir, to_host_room, 's2s'), "r");
local f_s2s = io.open(path.join(server_infos_dir, to_host, 's2s'), "r");
if f_s2s ~= nil then
io.close(f_s2s);
module.log("debug", "Remote host is a known Peertube %s that has s2s activated, we will let legacy s2s module handle the connection", to_host_room);
module.log("debug", "Remote host is a known Peertube %s that has s2s activated, we will let legacy s2s module handle the connection", to_host);
return;
end
local f_ws_proxy = io.open(path.join(server_infos_dir, to_host_room, 'ws-s2s'), "r");
local f_ws_proxy = io.open(path.join(server_infos_dir, to_host, 'ws-s2s'), "r");
if f_ws_proxy == nil then
module:log("debug", "Remote host %s is not a known remote Peertube, we will let legacy s2s module handle the connection", to_host_room);
module:log("debug", "Remote host %s is not a known remote Peertube, we will let legacy s2s module handle the connection", to_host);
return;
end
local content = f_ws_proxy:read("*all");
@ -44,7 +38,7 @@ function discover_websocket_s2s(event)
module:log("debug", "Found a Websocket endpoint to proxify s2s communications to remote host %s", to_host);
local properties = {};
properties["extra_headers"] = {
["peertube-livechat-ws-s2s-instance-url"] = instance_url;
["peertube-livechat-ws-s2s-instance-url"] = current_instance_url;
};
properties["url"] = remote_ws_proxy_conf["url"];
return properties;

View File

@ -1,6 +1,6 @@
import type { RegisterServerOptions, Video, MVideoThumbnail } from '@peertube/peertube-types'
import { getVideoLiveChatInfos } from './federation/storage'
import { anonymousConnectionInfos } from './federation/connection-infos'
import { anonymousConnectionInfos, compatibleRemoteAuthenticatedConnectionEnabled } from './federation/connection-infos'
async function initCustomFields (options: RegisterServerOptions): Promise<void> {
const registerHook = options.registerHook
@ -84,9 +84,27 @@ async function fillVideoRemoteLiveChat (
const infos = await getVideoLiveChatInfos(options, video)
if (!infos) { return }
let ok: boolean = false
// We must check if there is a compatible connection protocol...
// For now, the only that is implemetied is by using a remote anonymous account.
if (!anonymousConnectionInfos(infos)) { return }
if (anonymousConnectionInfos(infos)) {
// Connection ok using a remote anonymous account. That's enought.
ok = true
} else {
const settings = await options.settingsManager.getSettings([
'federation-no-remote-chat',
'prosody-room-allow-s2s'
])
const canWebsocketS2S = !settings['federation-no-remote-chat']
const canDirectS2S = !settings['federation-no-remote-chat'] && !!settings['prosody-room-allow-s2s']
if (compatibleRemoteAuthenticatedConnectionEnabled(infos, canWebsocketS2S, canDirectS2S)) {
// Even better, we can do a proper S2S connection!
ok = true
}
}
if (!ok) {
return
}
const v: LiveChatCustomFieldsVideo = video
if (!v.pluginData) v.pluginData = {}

View File

@ -1,4 +1,4 @@
import type { LiveChatJSONLDAttribute } from './types'
import type { LiveChatJSONLDAttributeV1 } from './types'
interface AnonymousConnectionInfos {
roomJID: string
@ -7,42 +7,59 @@ interface AnonymousConnectionInfos {
userJID: string
}
function anonymousConnectionInfos (livechatInfos: LiveChatJSONLDAttribute | false): AnonymousConnectionInfos | null {
function anonymousConnectionInfos (livechatInfos: LiveChatJSONLDAttributeV1 | false): AnonymousConnectionInfos | null {
if (!livechatInfos) { return null }
if (!livechatInfos.links) { return null }
if (livechatInfos.type !== 'xmpp') { return null }
if (!livechatInfos.xmppserver) { return null }
if (!livechatInfos.xmppserver.anonymous) { return null }
const r: AnonymousConnectionInfos = {
roomJID: livechatInfos.jid,
userJID: ''
userJID: livechatInfos.xmppserver.anonymous.virtualhost
}
for (const link of livechatInfos.links) {
// Note: userJID is on both links. But should have the same value.
if (link.type === 'xmpp-bosh-anonymous') {
r.boshUri = link.url
r.userJID = link.jid
} else if (link.type === 'xmpp-websocket-anonymous') {
r.wsUri = link.url
r.userJID = link.jid
}
if (livechatInfos.xmppserver.anonymous.bosh) {
r.boshUri = livechatInfos.xmppserver.anonymous.bosh
}
if (r.userJID === '') {
if (livechatInfos.xmppserver.anonymous.websocket) {
r.wsUri = livechatInfos.xmppserver.anonymous.websocket
}
if (!r.boshUri && !r.wsUri) {
return null
}
return r
}
function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttribute | false): boolean {
function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttributeV1): 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-peertube-livechat-ws-s2s') { return true }
if (link.type === 'xmpp-s2s') { return true }
}
if (!('xmppserver' in livechatInfos)) { return false }
if (!livechatInfos.xmppserver) { return false }
if (livechatInfos.xmppserver.websockets2s) { return true }
if (livechatInfos.xmppserver.directs2s) { return true }
return false
}
function compatibleRemoteAuthenticatedConnectionEnabled (
livechatInfos: LiveChatJSONLDAttributeV1,
canWebsocketS2S: boolean,
canDirectS2S: boolean
): boolean {
if (!livechatInfos) { return false }
if (livechatInfos.type !== 'xmpp') { return false }
if (!('xmppserver' in livechatInfos)) { return false }
if (!livechatInfos.xmppserver) { return false }
if (canWebsocketS2S && livechatInfos.xmppserver.websockets2s) { return true }
if (canDirectS2S && livechatInfos.xmppserver.directs2s) { return true }
return false
}
export {
anonymousConnectionInfos,
remoteAuthenticatedConnectionEnabled
remoteAuthenticatedConnectionEnabled,
compatibleRemoteAuthenticatedConnectionEnabled
}

View File

@ -16,7 +16,7 @@ async function readIncomingAPVideo (
let peertubeLiveChat = ('peertubeLiveChat' in videoAPObject) ? videoAPObject.peertubeLiveChat : false
// We must sanitize peertubeLiveChat, as it comes for the outer world.
peertubeLiveChat = sanitizePeertubeLiveChatInfos(peertubeLiveChat)
peertubeLiveChat = sanitizePeertubeLiveChatInfos(options, peertubeLiveChat)
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
if (video.remote) {

View File

@ -1,5 +1,11 @@
import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-types'
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink, LiveChatJSONLDAttribute } from './types'
import type { RegisterServerOptions, VideoObject, SettingValue } from '@peertube/peertube-types'
import type {
LiveChatVideoObject,
VideoBuildResultContext,
LiveChatJSONLDLink,
LiveChatJSONLDAttribute,
PeertubeXMPPServerInfos
} from './types'
import { storeVideoLiveChatInfos } from './storage'
import { videoHasWebchat } from '../../../shared/lib/video'
import { getBoshUri, getWSUri, getWSS2SUri } from '../uri/webchat'
@ -63,7 +69,6 @@ async function videoBuildJSONLD (
logger.debug(`Adding LiveChat data on video uuid=${video.uuid}...`)
const prosodyDomain = await getProsodyDomain(options)
const userJID = 'anon.' + prosodyDomain
let roomJID: string
if (settings['prosody-room-type'] === 'channel') {
roomJID = `channel.${video.channelId}@room.${prosodyDomain}`
@ -71,51 +76,38 @@ async function videoBuildJSONLD (
roomJID = `${video.uuid}@room.${prosodyDomain}`
}
const serverInfos = await _serverBuildInfos(options, {
'federation-dont-publish-remotely': settings['federation-dont-publish-remotely'],
'prosody-s2s-port': settings['prosody-s2s-port'],
'prosody-room-allow-s2s': settings['prosody-room-allow-s2s'],
'disable-websocket': settings['disable-websocket'],
'chat-no-anonymous': settings['chat-no-anonymous']
})
// For backward compatibility with remote servers, using plugin <=6.3.0, we must provide links:
const links: LiveChatJSONLDLink[] = []
if (!settings['federation-dont-publish-remotely']) {
const wsS2SUri = getWSS2SUri(options)
if (wsS2SUri) {
if (serverInfos.anonymous) {
if (serverInfos.anonymous.bosh) {
links.push({
type: 'xmpp-peertube-livechat-ws-s2s',
url: canonicalizePluginUri(options, wsS2SUri, {
removePluginVersion: true,
protocol: 'ws'
})
type: 'xmpp-bosh-anonymous',
url: serverInfos.anonymous.bosh,
jid: serverInfos.anonymous.virtualhost
})
}
}
if (settings['prosody-room-allow-s2s']) {
links.push({
type: 'xmpp-s2s',
host: prosodyDomain,
port: (settings['prosody-s2s-port'] as string) ?? ''
})
}
if (!settings['chat-no-anonymous']) {
links.push({
type: 'xmpp-bosh-anonymous',
url: canonicalizePluginUri(options, getBoshUri(options), { removePluginVersion: true }),
jid: userJID
})
if (!settings['disable-websocket']) {
const wsUri = getWSUri(options)
if (wsUri) {
links.push({
type: 'xmpp-websocket-anonymous',
url: canonicalizePluginUri(options, wsUri, {
removePluginVersion: true,
protocol: 'ws'
}),
jid: userJID
})
}
if (serverInfos.anonymous.websocket) {
links.push({
type: 'xmpp-websocket-anonymous',
url: serverInfos.anonymous.websocket,
jid: serverInfos.anonymous.virtualhost
})
}
}
const peertubeLiveChat: LiveChatJSONLDAttribute = {
type: 'xmpp',
jid: roomJID,
links
links,
xmppserver: serverInfos
}
Object.assign(jsonld, {
peertubeLiveChat
@ -125,6 +117,85 @@ async function videoBuildJSONLD (
return jsonld
}
export {
videoBuildJSONLD
async function serverBuildInfos (options: RegisterServerOptions): Promise<PeertubeXMPPServerInfos> {
const settings = await options.settingsManager.getSettings([
'federation-dont-publish-remotely',
'prosody-s2s-port',
'prosody-room-allow-s2s',
'disable-websocket',
'chat-no-anonymous'
])
return _serverBuildInfos(options, {
'federation-dont-publish-remotely': settings['federation-dont-publish-remotely'],
'prosody-s2s-port': settings['prosody-s2s-port'],
'prosody-room-allow-s2s': settings['prosody-room-allow-s2s'],
'disable-websocket': settings['disable-websocket'],
'chat-no-anonymous': settings['chat-no-anonymous']
})
}
async function _serverBuildInfos (
options: RegisterServerOptions,
settings: {
'federation-dont-publish-remotely': SettingValue
'prosody-s2s-port': SettingValue
'prosody-room-allow-s2s': SettingValue
'disable-websocket': SettingValue
'chat-no-anonymous': SettingValue
}
): Promise<PeertubeXMPPServerInfos> {
const prosodyDomain = await getProsodyDomain(options)
const mucDomain = 'room.' + prosodyDomain
const anonDomain = 'anon.' + prosodyDomain
let directs2s
if (settings['prosody-room-allow-s2s'] && settings['prosody-s2s-port']) {
directs2s = {
port: (settings['prosody-s2s-port'] as string) ?? ''
}
}
let websockets2s
if (!settings['federation-dont-publish-remotely']) {
const wsS2SUri = getWSS2SUri(options)
if (wsS2SUri) { // can be undefined for old Peertube version that dont allow WS for plugins
websockets2s = {
url: canonicalizePluginUri(options, wsS2SUri, {
removePluginVersion: true,
protocol: 'ws'
})
}
}
}
let anonymous: PeertubeXMPPServerInfos['anonymous'] | undefined
if (!settings['chat-no-anonymous']) {
anonymous = {
bosh: canonicalizePluginUri(options, getBoshUri(options), { removePluginVersion: true }),
virtualhost: anonDomain
}
if (!settings['disable-websocket']) {
const wsUri = getWSUri(options)
if (wsUri) {
anonymous.websocket = canonicalizePluginUri(options, wsUri, {
removePluginVersion: true,
protocol: 'ws'
})
}
}
}
return {
host: prosodyDomain,
muc: mucDomain,
directs2s,
websockets2s,
anonymous
}
}
export {
videoBuildJSONLD,
serverBuildInfos
}

View File

@ -0,0 +1,83 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { getBaseRouterRoute } from '../helpers'
import { canonicalizePluginUri } from '../uri/canonicalize'
import { URL } from 'url'
const got = require('got')
/**
* This function returns remote server connection informations.
* If these informations are not available (because we receive no ActivityPub
* data from this remote server), we will fetch them on a dedicated url.
*
* This information will also be stored.
*
* For all remote videos that our instance federated, remote server information
* are sent using ActivityPub.
* But there is a case in which we need information about potentially unknown
* servers.
*
* Use case:
* - server A: our server, proposing video V
* - server B: server that follows ours (or used to watch V, without following A)
* - user from B connect to the B XMPP server
* - server B has server A connection informations (got it using ActivityPub)
* - but, when using Websocket S2S, server A needs information from B, that he never receives
*
* Indeed, the XMPP S2S dialback mecanism will try to connect back to
* server A, and transmit a secret key, to ensure that all incomming connection
* are valid.
*
* For more informations about dialback: https://xmpp.org/extensions/xep-0220.html
*
* @param options server options
* @param remoteInstanceUrl remote instance url to check (as readed in the request header)
* @returns true if the remote instance is ok
*/
async function remoteServerInfos (
options: RegisterServerOptions,
remoteInstanceUrl: string
): Promise<boolean> {
const logger = options.peertubeHelpers.logger
logger.debug(`remoteServerInfos: checking if we have remote server infos for host ${remoteInstanceUrl}.`)
let url: string
try {
const u = new URL(remoteInstanceUrl)
// Assuming that the path on the remote instance is the same as on this one
// (but canonicalized to remove the plugin version)
u.pathname = getBaseRouterRoute(options) + 'api/federation_server_infos'
url = canonicalizePluginUri(options, u.toString(), {
protocol: 'http',
removePluginVersion: true
})
} catch (_err) {
logger.info('remoteServerInfos: Invalid remote instance url provided: ' + remoteInstanceUrl)
return false
}
try {
logger.debug('remoteServerInfos: We must check remote server infos using url: ' + url)
const response = await got(url, {
method: 'GET',
headers: {},
responseType: 'json'
}).json()
if (!response) {
logger.info('remoteServerInfos: Invalid remote server options')
return false
}
// FIXME/TODO
return true
} catch (_err) {
logger.info('remoteServerInfos: Can\'t get remote instance informations using url ' + url)
return false
}
}
export {
remoteServerInfos
}

View File

@ -1,73 +1,99 @@
import type { LiveChatJSONLDInfos, LiveChatJSONLDAttribute } from './types'
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { LiveChatJSONLDAttributeV1 } from './types'
import { URL } from 'url'
function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute {
/**
* Use this function for incoming remote informations.
* It will sanitize them, by checking everything is ok.
* It can also migrate from old format to the new one.
*
* This function can be used when informations are incoming,
* or when reading stored information (to automatically migrate them)
*
* @param chatInfos remote chat informations
* @returns a sanitized version of the remote chat informations
*/
function sanitizePeertubeLiveChatInfos (options: RegisterServerOptions, chatInfos: any): LiveChatJSONLDAttributeV1 {
if (chatInfos === false) { return false }
if (typeof chatInfos !== 'object') { return false }
if (chatInfos.type !== 'xmpp') { return false }
if ((typeof chatInfos.jid) !== 'string') { return false }
if (!Array.isArray(chatInfos.links)) { return false }
const r: LiveChatJSONLDInfos = {
if (!('xmppserver' in chatInfos)) {
// V0 format, migrating on the fly to v1.
return _sanitizePeertubeLiveChatInfosV0(options, chatInfos)
}
if (!chatInfos.xmppserver || (typeof chatInfos.xmppserver !== 'object')) {
return false
}
const xmppserver = chatInfos.xmppserver
if ((typeof xmppserver.host) !== 'string') { return false }
const host = _validateHost(xmppserver.host)
if (!host) { return false }
if ((typeof xmppserver.muc) !== 'string') { return false }
const muc = _validateHost(xmppserver.muc)
if (!muc) { return false }
const r: LiveChatJSONLDAttributeV1 = {
type: chatInfos.type,
jid: chatInfos.jid,
links: []
}
for (const link of chatInfos.links) {
if ((typeof link) !== 'object') { continue }
if (['xmpp-bosh-anonymous', 'xmpp-websocket-anonymous'].includes(link.type)) {
if ((typeof link.jid) !== 'string') { continue }
if ((typeof link.url) !== 'string') { continue }
if (
!_validUrl(link.url, {
noSearchParams: true,
protocol: link.type === 'xmpp-websocket-anonymous' ? 'ws.' : 'http.'
})
) {
continue
}
r.links.push({
type: link.type,
jid: link.jid,
url: link.url
})
}
if (link.type === 'xmpp-s2s') {
if (!/^\d+$/.test(link.port)) {
continue
}
const host = _validateHost(link.host)
if (!host) {
continue
}
r.links.push({
type: link.type,
host,
port: link.port
})
}
if (link.type === 'xmpp-peertube-livechat-ws-s2s') {
if ((typeof link.url) !== 'string') { continue }
if (
!_validUrl(link.url, {
noSearchParams: true,
protocol: 'ws.'
})
) {
continue
}
r.links.push({
type: link.type,
url: link.url
})
xmppserver: {
host,
muc
}
}
if (xmppserver.directs2s) {
if ((typeof xmppserver.directs2s) === 'object') {
const port = xmppserver.directs2s.port
if ((typeof port === 'string') && /^\d+$/.test(port)) {
r.xmppserver.directs2s = {
port
}
}
}
}
if (xmppserver.websockets2s) {
if ((typeof xmppserver.websockets2s) === 'object') {
const url = xmppserver.websockets2s.url
if ((typeof url === 'string') && _validUrl(url, {
noSearchParams: true,
protocol: 'ws.'
})) {
r.xmppserver.websockets2s = {
url
}
}
}
}
if (xmppserver.anonymous) {
const virtualhost = _validateHost(xmppserver.anonymous.virtualhost)
if (virtualhost) {
r.xmppserver.anonymous = {
virtualhost
}
const bosh = xmppserver.anonymous.bosh
if ((typeof bosh === 'string') && _validUrl(bosh, {
noSearchParams: true,
protocol: 'http.'
})) {
r.xmppserver.anonymous.bosh = bosh
}
const websocket = xmppserver.anonymous.websocket
if ((typeof websocket === 'string') && _validUrl(websocket, {
noSearchParams: true,
protocol: 'ws.'
})) {
r.xmppserver.anonymous.websocket = websocket
}
}
}
return r
}
@ -107,8 +133,9 @@ function _validUrl (s: string, constraints: URLConstraints): boolean {
return true
}
function _validateHost (s: string): false | string {
function _validateHost (s: any): false | string {
try {
if (typeof s !== 'string') { return false }
if (s.includes('/')) { return false }
const url = new URL('http://' + s)
return url.hostname
@ -117,6 +144,68 @@ function _validateHost (s: string): false | string {
}
}
function _sanitizePeertubeLiveChatInfosV0 (options: RegisterServerOptions, chatInfos: any): LiveChatJSONLDAttributeV1 {
const logger = options.peertubeHelpers.logger
logger.debug('We are have to migrate data from the old JSONLD format')
if (chatInfos === false) { return false }
if (typeof chatInfos !== 'object') { return false }
if (chatInfos.type !== 'xmpp') { return false }
if ((typeof chatInfos.jid) !== 'string') { return false }
// no link? invalid! dropping all.
if (!Array.isArray(chatInfos.links)) { return false }
const muc = _validateHost(chatInfos.jid.split('@')[1])
if (!muc) { return false }
if (!muc.startsWith('room.')) {
logger.error('We expected old format host to begin with "room.". Discarding.')
return false
}
const host = _validateHost(muc.replace(/^room\./, ''))
if (!host) { return false }
const r: LiveChatJSONLDAttributeV1 = {
type: chatInfos.type,
jid: chatInfos.jid,
xmppserver: {
host,
muc
}
}
for (const link of chatInfos.links) {
if ((typeof link) !== 'object') { continue }
if (['xmpp-bosh-anonymous', 'xmpp-websocket-anonymous'].includes(link.type)) {
if ((typeof link.jid) !== 'string') { continue }
if ((typeof link.url) !== 'string') { continue }
if (
!_validUrl(link.url, {
noSearchParams: true,
protocol: link.type === 'xmpp-websocket-anonymous' ? 'ws.' : 'http.'
})
) {
continue
}
if (!r.xmppserver.anonymous) {
r.xmppserver.anonymous = {
virtualhost: link.jid
}
}
if (link.type === 'xmpp-bosh-anonymous') {
r.xmppserver.anonymous.bosh = link.url
} else if (link.type === 'xmpp-websocket-anonymous') {
r.xmppserver.anonymous.websocket = link.url
}
}
}
return r
}
export {
sanitizePeertubeLiveChatInfos
}

View File

@ -1,5 +1,5 @@
import type { RegisterServerOptions, MVideoFullLight, MVideoAP, Video, MVideoThumbnail } from '@peertube/peertube-types'
import type { LiveChatJSONLDAttribute, LiveChatJSONLDS2SLink, LiveChatJSONLDPeertubeWSS2SLink } from './types'
import type { LiveChatJSONLDAttribute, LiveChatJSONLDAttributeV1 } from './types'
import { sanitizePeertubeLiveChatInfos } from './sanitize'
import { URL } from 'url'
import * as fs from 'fs'
@ -17,7 +17,7 @@ If a file exists, it means the video has a chat.
The file itself contains the JSON LiveChatInfos object.
*/
const cache: Map<string, LiveChatJSONLDAttribute> = new Map<string, LiveChatJSONLDAttribute>()
const cache: Map<string, LiveChatJSONLDAttributeV1> = new Map<string, LiveChatJSONLDAttributeV1>()
/**
* This function stores remote LiveChat infos that are contained in ActivityPub objects.
@ -72,7 +72,7 @@ async function storeVideoLiveChatInfos (
async function getVideoLiveChatInfos (
options: RegisterServerOptions,
video: MVideoFullLight | MVideoAP | Video | MVideoThumbnail
): Promise<LiveChatJSONLDAttribute> {
): Promise<LiveChatJSONLDAttributeV1> {
const logger = options.peertubeHelpers.logger
const cached = cache.get(video.url)
@ -92,7 +92,7 @@ async function getVideoLiveChatInfos (
return false
}
// We must sanitize here, in case a previous plugin version did not sanitize enougth.
const r = sanitizePeertubeLiveChatInfos(content)
const r = sanitizePeertubeLiveChatInfos(options, content)
cache.set(video.url, r)
return r
}
@ -103,51 +103,65 @@ async function getVideoLiveChatInfos (
* These information can then be read by Prosody module mod_s2s_peertubelivechat.
*
* We simply store the more recent informations. Indeed, it should be consistent between videos.
*
* Note: XMPP actively uses subdomains to seperate components.
* Peertube chats are on the domain `room.your_instance.tld`. But the server will
* be contacted using `your_instance.tld`.
* We must make sure that the Prosody module mod_s2s_peertubelivechat finds both
* kind of urls.
*
* @param options server optiosn
* @param liveChatInfos livechat stored data
*/
async function storeRemoteServerInfos (
options: RegisterServerOptions,
liveChatInfos: LiveChatJSONLDAttribute
liveChatInfos: LiveChatJSONLDAttributeV1
): Promise<void> {
if (!liveChatInfos) { return }
if (!liveChatInfos.xmppserver) { return }
const logger = options.peertubeHelpers.logger
const roomJID = liveChatInfos.jid
const host = roomJID.split('@')[1]
if (!host) {
logger.error(`Room JID seems not correct, no host: ${roomJID}`)
return
}
if (host.includes('..')) {
logger.error(`Room host seems not correct, contains ..: ${host}`)
return
}
const dir = path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(),
'serverInfos',
host
)
const s2sFilePath = path.resolve(dir, 's2s')
const wsS2SFilePath = path.resolve(dir, 'ws-s2s')
const mainHost = liveChatInfos.xmppserver.host
const hosts = [
liveChatInfos.xmppserver.host,
liveChatInfos.xmppserver.muc
]
const s2sLink = liveChatInfos.links.find(v => v.type === 'xmpp-s2s')
if (s2sLink) {
await _store(options, s2sFilePath, {
host: (s2sLink as LiveChatJSONLDS2SLink).host,
port: (s2sLink as LiveChatJSONLDS2SLink).port
})
} else {
await _del(options, s2sFilePath)
}
const wsS2SLink = liveChatInfos.links.find(v => v.type === 'xmpp-peertube-livechat-ws-s2s')
if (wsS2SLink) {
await _store(options, wsS2SFilePath, {
url: (wsS2SLink as LiveChatJSONLDPeertubeWSS2SLink).url
})
} else {
await _del(options, wsS2SFilePath)
for (const host of hosts) {
if (!host) { continue }
// Some security check, just in case.
if (host.includes('..')) {
logger.error(`Host seems not correct, contains ..: ${host}`)
continue
}
const dir = path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(),
'serverInfos',
host
)
const s2sFilePath = path.resolve(dir, 's2s')
const wsS2SFilePath = path.resolve(dir, 'ws-s2s')
if (liveChatInfos.xmppserver.directs2s?.port) {
await _store(options, s2sFilePath, {
host: mainHost,
port: liveChatInfos.xmppserver.directs2s.port
})
} else {
await _del(options, s2sFilePath)
}
if (liveChatInfos.xmppserver.websockets2s?.url) {
await _store(options, wsS2SFilePath, {
host: mainHost,
url: liveChatInfos.xmppserver.websockets2s.url
})
} else {
await _del(options, wsS2SFilePath)
}
}
}

View File

@ -4,42 +4,64 @@ interface VideoBuildResultContext {
video: MVideoAP
}
interface LiveChatJSONLDPeertubeWSS2SLink {
type: 'xmpp-peertube-livechat-ws-s2s'
url: string
}
interface LiveChatJSONLDS2SLink {
type: 'xmpp-s2s'
host: string
port: string
interface PeertubeXMPPServerInfos {
host: string // main host (should be the peertube url)
muc: string // muc component url
directs2s?: { // if direct S2S is enabled
port: string
}
websockets2s?: { // if Websocket S2S is enabled
url: string
}
anonymous?: { // provide an anonymous component that can be used externally
virtualhost: string
bosh?: string // BOSH endpoint url
websocket?: string // Websocket endpoint url
}
}
// DEPRECATED, but still used for backward compat
interface LiveChatJSONLDAnonymousWebsocketLink {
type: 'xmpp-websocket-anonymous'
url: string
jid: string
}
// DEPRECATED, but still used for backward compat
interface LiveChatJSONLDAnonymousBOSHLink {
type: 'xmpp-bosh-anonymous'
url: string
jid: string
}
type LiveChatJSONLDLink =
LiveChatJSONLDPeertubeWSS2SLink
| LiveChatJSONLDS2SLink
| LiveChatJSONLDAnonymousBOSHLink
| LiveChatJSONLDAnonymousWebsocketLink
// DEPRECATED, but still used for backward compat
type LiveChatJSONLDLink = LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
interface LiveChatJSONLDInfos {
// LiveChatJSONLDInfosV0 is the data format for the plugin v6.3.0. This format is replaced in newer versions.
// DEPRECATED, but still used for backward compat
interface LiveChatJSONLDInfosV0 {
type: 'xmpp'
jid: string
jid: string // room JID
links: LiveChatJSONLDLink[]
}
// LiveChatJSONLDInfosV1 is the data format that comes with plugin v6.4.0.
interface LiveChatJSONLDInfosV1 {
type: 'xmpp'
jid: string // room JID
xmppserver: PeertubeXMPPServerInfos
}
// LiveChatJSONLDInfosV1CompatV0 is a mix of both interface.
// Used for outgoing data, so that older plugin version can still use it.
interface LiveChatJSONLDInfosV1CompatV0 extends LiveChatJSONLDInfosV1 {
links: LiveChatJSONLDLink[]
}
type LiveChatJSONLDInfos = LiveChatJSONLDInfosV0 | LiveChatJSONLDInfosV1 | LiveChatJSONLDInfosV1CompatV0
type LiveChatJSONLDAttribute = LiveChatJSONLDInfos | false
type LiveChatJSONLDAttributeV1 = LiveChatJSONLDInfosV1 | false
interface LiveChatVideoObject extends VideoObject {
peertubeLiveChat: LiveChatJSONLDAttribute
@ -53,10 +75,11 @@ interface RemoteVideoHandlerParams {
export {
VideoBuildResultContext,
LiveChatJSONLDLink,
LiveChatJSONLDS2SLink,
LiveChatJSONLDPeertubeWSS2SLink,
LiveChatJSONLDInfos,
LiveChatJSONLDInfosV1CompatV0,
LiveChatJSONLDAttribute,
LiveChatJSONLDAttributeV1,
LiveChatVideoObject,
RemoteVideoHandlerParams
RemoteVideoHandlerParams,
PeertubeXMPPServerInfos
}

View File

@ -14,8 +14,11 @@ import { getChannelInfosById, getChannelNameById } from '../database/channel'
import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors'
import { getBoshUri, getWSUri } from '../uri/webchat'
import { getVideoLiveChatInfos } from '../federation/storage'
import { LiveChatJSONLDAttribute } from '../federation/types'
import { anonymousConnectionInfos, remoteAuthenticatedConnectionEnabled } from '../federation/connection-infos'
import { LiveChatJSONLDAttributeV1 } from '../federation/types'
import {
anonymousConnectionInfos, compatibleRemoteAuthenticatedConnectionEnabled
} from '../federation/connection-infos'
// import { remoteServerInfos } from '../federation/remote-infos'
import * as path from 'path'
const got = require('got')
@ -79,7 +82,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let video: MVideoThumbnail | undefined
let channelId: number
let remoteChatInfos: LiveChatJSONLDAttribute | undefined
let remoteChatInfos: LiveChatJSONLDAttributeV1 | undefined
const channelMatches = roomKey.match(/^channel\.(\d+)$/)
if (channelMatches?.[1]) {
channelId = parseInt(channelMatches[1])
@ -122,7 +125,9 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let remoteConnectionInfos: WCRemoteConnectionInfos | undefined
let roomJID: string
if (video?.remote) {
remoteConnectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
const canWebsocketS2S = !settings['federation-no-remote-chat']
const canDirectS2S = !settings['federation-no-remote-chat'] && !!settings['prosody-room-allow-s2s']
remoteConnectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false, canWebsocketS2S, canDirectS2S)
if (!remoteConnectionInfos) {
res.status(404)
res.send('No compatible way to connect to remote chat')
@ -258,26 +263,43 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
registerWebSocketRoute({
route: '/xmpp-websocket',
handler: (request, socket, head) => {
if (!currentWebsocketProxy) {
peertubeHelpers.logger.error('There is no current websocket proxy, should not get here.')
// no need to close the socket, Peertube will
// (see https://github.com/Chocobozzz/PeerTube/issues/5752#issuecomment-1510870894)
return
try {
if (!currentWebsocketProxy) {
peertubeHelpers.logger.error('There is no current websocket proxy, should not get here.')
// no need to close the socket, Peertube will
// (see https://github.com/Chocobozzz/PeerTube/issues/5752#issuecomment-1510870894)
return
}
currentWebsocketProxy.ws(request, socket, head)
} catch (err) {
peertubeHelpers.logger.error('Got an error when trying to connect to S2S', err)
}
currentWebsocketProxy.ws(request, socket, head)
}
})
registerWebSocketRoute({
route: '/xmpp-websocket-s2s',
handler: (request, socket, head) => {
if (!currentS2SWebsocketProxy) {
peertubeHelpers.logger.error('There is no current websocket s2s proxy, should not get here.')
// no need to close the socket, Peertube will
// (see https://github.com/Chocobozzz/PeerTube/issues/5752#issuecomment-1510870894)
return
handler: async (request, socket, head) => {
try {
if (!currentS2SWebsocketProxy) {
peertubeHelpers.logger.error('There is no current websocket s2s proxy, should not get here.')
// no need to close the socket, Peertube will
// (see https://github.com/Chocobozzz/PeerTube/issues/5752#issuecomment-1510870894)
return
}
// If the incomming request is from a remote Peertube instance, we must ensure that we know
// how to connect to it using Websocket S2S (for the dialback mecanism).
const remoteInstanceUrl = request.headers['peertube-livechat-ws-s2s-instance-url']
if (remoteInstanceUrl && (typeof remoteInstanceUrl !== 'string')) {
// Note: remoteServerInfos will store the information,
// so that the Prosody mod_s2s_peertubelivechat module can access them.
// TODO
// await remoteServerInfos(options, remoteInstanceUrl)
}
currentS2SWebsocketProxy.ws(request, socket, head)
} catch (err) {
peertubeHelpers.logger.error('Got an error when trying to connect to Websocket S2S', err)
}
currentS2SWebsocketProxy.ws(request, socket, head)
}
})
}
@ -452,13 +474,17 @@ interface WCRemoteConnectionInfos {
authenticated?: boolean
}
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<WCRemoteConnectionInfos> {
async function _remoteConnectionInfos (
remoteChatInfos: LiveChatJSONLDAttributeV1,
canWebsocketS2S: boolean,
canDirectS2S: boolean
): Promise<WCRemoteConnectionInfos> {
if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') }
if (remoteChatInfos.type !== 'xmpp') { throw new Error('Should have remote xmpp chat infos for remote videos') }
const connectionInfos: WCRemoteConnectionInfos = {
roomJID: remoteChatInfos.jid
}
if (remoteAuthenticatedConnectionEnabled(remoteChatInfos)) {
if (compatibleRemoteAuthenticatedConnectionEnabled(remoteChatInfos, canWebsocketS2S, canDirectS2S)) {
connectionInfos.authenticated = true
}
const anonymousCI = anonymousConnectionInfos(remoteChatInfos ?? false)