diff --git a/CHANGELOG.md b/CHANGELOG.md index 559c4a0b..22608d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * ConverseJS v10.1.6 (instead of v10.0.0). * New polish translation (Thanks [ewm](https://weblate.framasoft.org/user/ewm/)). * Links to documentation are now using the front-end language to point to the translated documentation page (except for some links generated from the backend, in the diagnostic tool for example). +* Some code refactoring. ## 7.2.2 diff --git a/server/lib/prosody/auth.ts b/server/lib/prosody/auth.ts index cac22ea7..f33cc13f 100644 --- a/server/lib/prosody/auth.ts +++ b/server/lib/prosody/auth.ts @@ -1,15 +1,5 @@ /* This module provides user credential for the builtin prosody module. - -A user can get a password thanks to a call to prosodyRegisterUser (see api user/auth). - -Then, we can test that the user exists with prosodyUserRegistered, and test password with prosodyCheckUserPassword. - -Passwords are randomly generated. - -These password are stored internally in a global variable, and are valid for 24h. -Each call to registerUser extends the validity by 24h. - */ interface Password { @@ -30,6 +20,20 @@ function _getAndClean (user: string): Password | undefined { return undefined } +/** + * A user can get a password thanks to a call to prosodyRegisterUser (see api user/auth). + * + * Then, we can test that the user exists with prosodyUserRegistered, and test password with prosodyCheckUserPassword. + * + * Passwords are randomly generated. + * + * These password are stored internally in a global variable, and are valid for 24h. + * Each call to registerUser extends the validity by 24h. + * + * Prosody will use an API call to api/user/check_password to check the password transmitted by the frontend. + * @param user username + * @returns the password to use to connect to Prosody + */ async function prosodyRegisterUser (user: string): Promise { const entry = _getAndClean(user) const validity = Date.now() + (24 * 60 * 60 * 1000) // 24h diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts index d667f487..6395ac34 100644 --- a/server/lib/routers/api.ts +++ b/server/lib/routers/api.ts @@ -1,44 +1,24 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' import type { Router, Request, Response, NextFunction } from 'express' -import { videoHasWebchat } from '../../../shared/lib/video' import { asyncMiddleware } from '../middlewares/async' import { getCheckAPIKeyMiddleware } from '../middlewares/apikey' -import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered } from '../prosody/auth' -import { getUserNickname } from '../helpers' -import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../prosody/config/affiliations' -import { getProsodyDomain } from '../prosody/config/domain' -import { fillVideoCustomFields } from '../custom-fields' -import { getChannelInfosById } from '../database/channel' import { ensureProsodyRunning } from '../prosody/ctl' -import { serverBuildInfos } from '../federation/outgoing' import { isDebugMode } from '../debug' +import { initRoomApiRouter } from './api/room' +import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth' +import { initFederationServerInfosApiRouter } from './api/federation-server-infos' -// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html -interface RoomDefaults { - config: { - name: string - description: string - language?: string - persistent?: boolean - public?: boolean - members_only?: boolean - allow_member_invites?: boolean - public_jids?: boolean - // subject_from: string - // subject: string - changesubject?: boolean - // historylength: number - moderated?: boolean - archiving?: boolean - } - affiliations?: Affiliations -} - +/** + * Initiate API routes + * @param options server register options + * @returns the router + */ async function initApiRouter (options: RegisterServerOptions): Promise { const { peertubeHelpers, getRouter } = options const router = getRouter() const logger = peertubeHelpers.logger + // /test endpoint: used by the prosody module http_peertubelivechat_test to test Peertube API. router.get('/test', asyncMiddleware([ getCheckAPIKeyMiddleware(options), async (req: Request, res: Response, _next: NextFunction) => { @@ -47,191 +27,12 @@ async function initApiRouter (options: RegisterServerOptions): Promise { } ])) - router.get('/room', asyncMiddleware([ - getCheckAPIKeyMiddleware(options), - async (req: Request, res: Response, _next: NextFunction) => { - const jid: string = req.query.jid as string || '' - logger.info(`Requesting room information for room '${jid}'.`) + await initRoomApiRouter(options, router) - const settings = await options.settingsManager.getSettings([ - 'prosody-room-type' - ]) - // Now, we have two different room type: per video or per channel. - if (settings['prosody-room-type'] === 'channel') { - const matches = jid.match(/^channel\.(\d+)$/) - if (!matches || !matches[1]) { - logger.warn(`Invalid channel room jid '${jid}'.`) - res.sendStatus(403) - return - } - const channelId = parseInt(matches[1]) - const channelInfos = await getChannelInfosById(options, channelId) - if (!channelInfos) { - logger.warn(`Channel ${channelId} not found`) - res.sendStatus(403) - return - } + await initAuthApiRouter(options, router) + await initUserAuthApiRouter(options, router) - let affiliations: Affiliations - try { - affiliations = await getChannelAffiliations(options, channelId) - } catch (error) { - logger.error(`Failed to get channel affiliations for ${channelId}:`, error) - // affiliations: should at least be {}, so that the first user will not be moderator/admin - affiliations = {} - } - - const roomDefaults: RoomDefaults = { - config: { - name: channelInfos.displayName, - description: '' - // subject: channelInfos.displayName - }, - affiliations: affiliations - } - res.json(roomDefaults) - } else { - // FIXME: @peertube/peertype-types@4.2.2: wrongly considere video as MVideoThumbnail. - const video = await peertubeHelpers.videos.loadByIdOrUUID(jid) - if (!video) { - logger.warn(`Video ${jid} not found`) - res.sendStatus(403) - return - } - - // Adding the custom fields and data: - await fillVideoCustomFields(options, video) - - // check settings (chat enabled for this video?) - const settings = await options.settingsManager.getSettings([ - 'chat-per-live-video', - 'chat-all-lives', - 'chat-all-non-lives', - 'chat-videos-list' - ]) - if (!videoHasWebchat({ - 'chat-per-live-video': !!settings['chat-per-live-video'], - 'chat-all-lives': !!settings['chat-all-lives'], - 'chat-all-non-lives': !!settings['chat-all-non-lives'], - 'chat-videos-list': settings['chat-videos-list'] as string - }, video)) { - logger.warn(`Video ${jid} has not chat activated`) - res.sendStatus(403) - return - } - - let affiliations: Affiliations - try { - affiliations = await getVideoAffiliations(options, video) - } catch (error) { - logger.error(`Failed to get video affiliations for ${video.uuid}:`, error) - // affiliations: should at least be {}, so that the first user will not be moderator/admin - affiliations = {} - } - - const roomDefaults: RoomDefaults = { - config: { - name: video.name, - description: '', - language: video.language - // subject: video.name - }, - affiliations: affiliations - } - res.json(roomDefaults) - } - } - ])) - - router.get('/auth', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - const user = await peertubeHelpers.user.getAuthUser(res) - if (!user) { - res.sendStatus(403) - return - } - if (user.blocked) { - res.sendStatus(403) - return - } - // NB 2021-08-05: Peertube usernames should be lowercase. But it seems that - // in some old installation, there can be uppercase letters in usernames. - // When Peertube checks username unicity, it does a lowercase search. - // So it feels safe to normalize usernames like so: - const normalizedUsername = user.username.toLowerCase() - const prosodyDomain = await getProsodyDomain(options) - const password: string = await prosodyRegisterUser(normalizedUsername) - const nickname: string | undefined = await getUserNickname(options, user) - res.status(200).json({ - jid: normalizedUsername + '@' + prosodyDomain, - password: password, - nickname: nickname - }) - } - )) - - router.post('/user/register', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - res.sendStatus(501) - } - )) - - router.get('/user/check_password', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - const prosodyDomain = await getProsodyDomain(options) - const user = req.query.user - const server = req.query.server - const pass = req.query.pass - if (server !== prosodyDomain) { - logger.warn(`Cannot call check_password on user on server ${server as string}.`) - res.status(200).send('false') - return - } - if (user && pass && await prosodyCheckUserPassword(user as string, pass as string)) { - res.status(200).send('true') - return - } - res.status(200).send('false') - } - )) - - router.get('/user/user_exists', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - const prosodyDomain = await getProsodyDomain(options) - const user = req.query.user - const server = req.query.server - if (server !== prosodyDomain) { - logger.warn(`Cannot call user_exists on user on server ${server as string}.`) - res.status(200).send('false') - return - } - if (user && await prosodyUserRegistered(user as string)) { - res.status(200).send('true') - return - } - res.status(200).send('false') - } - )) - - router.post('/user/set_password', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - res.sendStatus(501) - } - )) - - router.post('/user/remove_user', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - res.sendStatus(501) - } - )) - - router.get('/federation_server_infos', asyncMiddleware( - async (req: Request, res: Response, _next: NextFunction) => { - logger.info('federation_server_infos api call') - const result = await serverBuildInfos(options) - res.json(result) - } - )) + await initFederationServerInfosApiRouter(options, router) if (isDebugMode(options)) { // Only add this route if the debug mode is enabled at time of the server launch. diff --git a/server/lib/routers/api/auth.ts b/server/lib/routers/api/auth.ts new file mode 100644 index 00000000..3e17d179 --- /dev/null +++ b/server/lib/routers/api/auth.ts @@ -0,0 +1,110 @@ +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Router, Request, Response, NextFunction } from 'express' +import { asyncMiddleware } from '../../middlewares/async' +import { getProsodyDomain } from '../../prosody/config/domain' +import { prosodyRegisterUser, prosodyCheckUserPassword, prosodyUserRegistered } from '../../prosody/auth' +import { getUserNickname } from '../../helpers' + +/** + * Instanciate the authentication API. + * This API is used by the frontend to get current user's XMPP credentials. + * @param options server register options + */ +async function initAuthApiRouter (options: RegisterServerOptions, router: Router): Promise { + router.get('/auth', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + const user = await options.peertubeHelpers.user.getAuthUser(res) + if (!user) { + res.sendStatus(403) + return + } + if (user.blocked) { + res.sendStatus(403) + return + } + // NB 2021-08-05: Peertube usernames should be lowercase. But it seems that + // in some old installation, there can be uppercase letters in usernames. + // When Peertube checks username unicity, it does a lowercase search. + // So it feels safe to normalize usernames like so: + const normalizedUsername = user.username.toLowerCase() + const prosodyDomain = await getProsodyDomain(options) + const password: string = await prosodyRegisterUser(normalizedUsername) + const nickname: string | undefined = await getUserNickname(options, user) + res.status(200).json({ + jid: normalizedUsername + '@' + prosodyDomain, + password: password, + nickname: nickname + }) + } + )) +} + +/** + * Instanciates API used by the Prosody module http_auth. + * This is used to check user's credentials. + * @param options server register options + * @returns a router + */ +async function initUserAuthApiRouter (options: RegisterServerOptions, router: Router): Promise { + const logger = options.peertubeHelpers.logger + + router.post('/user/register', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + res.sendStatus(501) + } + )) + + router.get('/user/check_password', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + const prosodyDomain = await getProsodyDomain(options) + const user = req.query.user + const server = req.query.server + const pass = req.query.pass + if (server !== prosodyDomain) { + logger.warn(`Cannot call check_password on user on server ${server as string}.`) + res.status(200).send('false') + return + } + if (user && pass && await prosodyCheckUserPassword(user as string, pass as string)) { + res.status(200).send('true') + return + } + res.status(200).send('false') + } + )) + + router.get('/user/user_exists', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + const prosodyDomain = await getProsodyDomain(options) + const user = req.query.user + const server = req.query.server + if (server !== prosodyDomain) { + logger.warn(`Cannot call user_exists on user on server ${server as string}.`) + res.status(200).send('false') + return + } + if (user && await prosodyUserRegistered(user as string)) { + res.status(200).send('true') + return + } + res.status(200).send('false') + } + )) + + router.post('/user/set_password', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + res.sendStatus(501) + } + )) + + router.post('/user/remove_user', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + res.sendStatus(501) + } + )) +} + +export { + initAuthApiRouter, + initUserAuthApiRouter +} diff --git a/server/lib/routers/api/federation-server-infos.ts b/server/lib/routers/api/federation-server-infos.ts new file mode 100644 index 00000000..0e78ea48 --- /dev/null +++ b/server/lib/routers/api/federation-server-infos.ts @@ -0,0 +1,25 @@ +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Router, Request, Response, NextFunction } from 'express' +import { asyncMiddleware } from '../../middlewares/async' +import { serverBuildInfos } from '../../federation/outgoing' + +/** + * Instanciate the authentication API. + * This API is used by the frontend to get current user's XMPP credentials. + * @param options server register options + */ +async function initFederationServerInfosApiRouter (options: RegisterServerOptions, router: Router): Promise { + const logger = options.peertubeHelpers.logger + + router.get('/federation_server_infos', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + logger.info('federation_server_infos api call') + const result = await serverBuildInfos(options) + res.json(result) + } + )) +} + +export { + initFederationServerInfosApiRouter +} diff --git a/server/lib/routers/api/room.ts b/server/lib/routers/api/room.ts new file mode 100644 index 00000000..6208d7f5 --- /dev/null +++ b/server/lib/routers/api/room.ts @@ -0,0 +1,138 @@ +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Router, Request, Response, NextFunction } from 'express' +import { videoHasWebchat } from '../../../../shared/lib/video' +import { asyncMiddleware } from '../../middlewares/async' +import { getCheckAPIKeyMiddleware } from '../../middlewares/apikey' +import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../../prosody/config/affiliations' +import { fillVideoCustomFields } from '../../custom-fields' +import { getChannelInfosById } from '../../database/channel' + +// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html +interface RoomDefaults { + config: { + name: string + description: string + language?: string + persistent?: boolean + public?: boolean + members_only?: boolean + allow_member_invites?: boolean + public_jids?: boolean + // subject_from: string + // subject: string + changesubject?: boolean + // historylength: number + moderated?: boolean + archiving?: boolean + } + affiliations?: Affiliations +} + +/** + * Instanciate the route for room APIs. + * These APIs are used by Prosody to get room defaults from the Peertube server. + * @param options server register options + */ +async function initRoomApiRouter (options: RegisterServerOptions, router: Router): Promise { + const logger = options.peertubeHelpers.logger + + router.get('/room', asyncMiddleware([ + getCheckAPIKeyMiddleware(options), + async (req: Request, res: Response, _next: NextFunction) => { + const jid: string = req.query.jid as string || '' + logger.info(`Requesting room information for room '${jid}'.`) + + const settings = await options.settingsManager.getSettings([ + 'prosody-room-type' + ]) + // Now, we have two different room type: per video or per channel. + if (settings['prosody-room-type'] === 'channel') { + const matches = jid.match(/^channel\.(\d+)$/) + if (!matches || !matches[1]) { + logger.warn(`Invalid channel room jid '${jid}'.`) + res.sendStatus(403) + return + } + const channelId = parseInt(matches[1]) + const channelInfos = await getChannelInfosById(options, channelId) + if (!channelInfos) { + logger.warn(`Channel ${channelId} not found`) + res.sendStatus(403) + return + } + + let affiliations: Affiliations + try { + affiliations = await getChannelAffiliations(options, channelId) + } catch (error) { + logger.error(`Failed to get channel affiliations for ${channelId}:`, error) + // affiliations: should at least be {}, so that the first user will not be moderator/admin + affiliations = {} + } + + const roomDefaults: RoomDefaults = { + config: { + name: channelInfos.displayName, + description: '' + // subject: channelInfos.displayName + }, + affiliations: affiliations + } + res.json(roomDefaults) + } else { + // FIXME: @peertube/peertype-types@4.2.2: wrongly considere video as MVideoThumbnail. + const video = await options.peertubeHelpers.videos.loadByIdOrUUID(jid) + if (!video) { + logger.warn(`Video ${jid} not found`) + res.sendStatus(403) + return + } + + // Adding the custom fields and data: + await fillVideoCustomFields(options, video) + + // check settings (chat enabled for this video?) + const settings = await options.settingsManager.getSettings([ + 'chat-per-live-video', + 'chat-all-lives', + 'chat-all-non-lives', + 'chat-videos-list' + ]) + if (!videoHasWebchat({ + 'chat-per-live-video': !!settings['chat-per-live-video'], + 'chat-all-lives': !!settings['chat-all-lives'], + 'chat-all-non-lives': !!settings['chat-all-non-lives'], + 'chat-videos-list': settings['chat-videos-list'] as string + }, video)) { + logger.warn(`Video ${jid} has not chat activated`) + res.sendStatus(403) + return + } + + let affiliations: Affiliations + try { + affiliations = await getVideoAffiliations(options, video) + } catch (error) { + logger.error(`Failed to get video affiliations for ${video.uuid}:`, error) + // affiliations: should at least be {}, so that the first user will not be moderator/admin + affiliations = {} + } + + const roomDefaults: RoomDefaults = { + config: { + name: video.name, + description: '', + language: video.language + // subject: video.name + }, + affiliations: affiliations + } + res.json(roomDefaults) + } + } + ])) +} + +export { + initRoomApiRouter +}