peertube-plugin-livechat/server/lib/federation/sanitize.ts
John Livingston 6ed69d2c2f
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.
2023-05-31 16:52:50 +02:00

212 lines
5.7 KiB
TypeScript

import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { LiveChatJSONLDAttributeV1 } from './types'
import { URL } from 'url'
/**
* 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 (!('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,
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
}
interface URLConstraints {
protocol: 'http.' | 'ws.'
noSearchParams: boolean
}
function _validUrl (s: string, constraints: URLConstraints): boolean {
if ((typeof s) !== 'string') { return false }
if (s === '') { return false }
let url: URL
try {
url = new URL(s)
} catch (_err) {
return false
}
if (constraints.protocol) {
if (constraints.protocol === 'http.') {
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
return false
}
} else if (constraints.protocol === 'ws.') {
if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
return false
}
}
}
if (constraints.noSearchParams) {
if (url.search !== '') {
return false
}
}
return true
}
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
} catch (_err) {
return false
}
}
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
}