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:
parent
4f9534dc11
commit
6ed69d2c2f
@ -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;
|
||||
|
@ -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 = {}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
83
server/lib/federation/remote-infos.ts
Normal file
83
server/lib/federation/remote-infos.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user