diff --git a/config/config.default.json b/config/config.default.json index 3af9d99..1f4e57f 100644 --- a/config/config.default.json +++ b/config/config.default.json @@ -21,4 +21,9 @@ // Secrets //"matrixAccessToken": "xxx" + + // Restrict to rooms listed below + //"enableAllowList": true, + // use room ids starting with a `!`, not aliases + //"roomAllowList": ["!ypQyNeReyEPUCKYjPU:matrix.org"], // e.g. room about Matrix Reputation } diff --git a/server/lib/matrix-utils/check-room-allowed.js b/server/lib/matrix-utils/check-room-allowed.js new file mode 100644 index 0000000..dc2b1a5 --- /dev/null +++ b/server/lib/matrix-utils/check-room-allowed.js @@ -0,0 +1,27 @@ +'use strict'; + +const assert = require('assert'); + +const fetchRoomId = require('./fetch-room-id'); + +const config = require('../config'); +const basePath = config.get('basePath'); +assert(basePath); +const matrixServerUrl = config.get('matrixServerUrl'); +assert(matrixServerUrl); + +config.defaults({ + "enableAllowList": false, + "roomAllowList": [], +}); + +async function checkIfAllowed(roomIdOrAlias) { + if (!config.get("enableAllowList")) { + return true; + } + const roomId = await fetchRoomId(roomIdOrAlias); + const result = config.get("roomAllowList").includes(roomId) + return result; +} + +module.exports = checkIfAllowed; diff --git a/server/lib/matrix-utils/ensure-room-joined.js b/server/lib/matrix-utils/ensure-room-joined.js index ee3b964..7f8c243 100644 --- a/server/lib/matrix-utils/ensure-room-joined.js +++ b/server/lib/matrix-utils/ensure-room-joined.js @@ -5,6 +5,7 @@ const urlJoin = require('url-join'); const StatusError = require('../errors/status-error'); const { fetchEndpointAsJson } = require('../fetch-endpoint'); +const checkIfAllowed = require('./check-room-allowed'); const getServerNameFromMatrixRoomIdOrAlias = require('./get-server-name-from-matrix-room-id-or-alias'); const MatrixViewerURLCreator = require('matrix-viewer-shared/lib/url-creator'); @@ -21,6 +22,11 @@ async function ensureRoomJoined( roomIdOrAlias, { viaServers = new Set(), abortSignal } = {} ) { + const result = await checkIfAllowed(roomIdOrAlias); + if (!result) { + throw new StatusError(403, `Bot is not allowed to view room, room is not listed in explicit allow list.`); + } + // We use a `Set` to ensure that we don't have duplicate servers in the list assert(viaServers instanceof Set); diff --git a/server/lib/matrix-utils/fetch-room-id.js b/server/lib/matrix-utils/fetch-room-id.js new file mode 100644 index 0000000..b25a733 --- /dev/null +++ b/server/lib/matrix-utils/fetch-room-id.js @@ -0,0 +1,45 @@ +'use strict'; + +const assert = require('assert'); +const urlJoin = require('url-join'); + +const StatusError = require('../errors/status-error'); +const { fetchEndpointAsJson } = require('../fetch-endpoint'); + +const config = require('../config'); +const basePath = config.get('basePath'); +assert(basePath); +const matrixServerUrl = config.get('matrixServerUrl'); +assert(matrixServerUrl); + +config.defaults({ + "enableAllowlist": false, + "roomAllowlist": [], +}); + +async function fetchRoomId( + roomIdOrAlias, +) { + if (roomIdOrAlias.startsWith("!")) { + return roomIdOrAlias; + } + + const resolveEndpoint = urlJoin( + matrixServerUrl, + `_matrix/client/v3/directory/room/${encodeURIComponent(roomIdOrAlias)}` + ); + try { + const { data: roomData } = await fetchEndpointAsJson(resolveEndpoint, { + method: 'GET', + }); + assert( + roomData.room_id, + `Alias resolve endpoint (${resolveEndpoint}) did not return \`room_id\` as expected. This is probably a problem with that homeserver.` + ); + return roomData.room_id; + } catch (err) { + throw new StatusError(403, `Bot is unable to resolve alias of room: ${err.message}`); + } +} + +module.exports = fetchRoomId; diff --git a/server/routes/room-directory-routes.js b/server/routes/room-directory-routes.js index 84f15a5..bf1f126 100644 --- a/server/routes/room-directory-routes.js +++ b/server/routes/room-directory-routes.js @@ -10,6 +10,7 @@ const { DIRECTION } = require('matrix-viewer-shared/lib/reference-values'); const RouteTimeoutAbortError = require('../lib/errors/route-timeout-abort-error'); const UserClosedConnectionAbortError = require('../lib/errors/user-closed-connection-abort-error'); const identifyRoute = require('../middleware/identify-route-middleware'); +const checkIfAllowed = require('../lib/matrix-utils/check-room-allowed'); const fetchAccessibleRooms = require('../lib/matrix-utils/fetch-accessible-rooms'); const renderHydrogenVmRenderScriptToPageHtml = require('../hydrogen-render/render-hydrogen-vm-render-script-to-page-html'); const setHeadersToPreloadAssets = require('../lib/set-headers-to-preload-assets'); @@ -81,6 +82,13 @@ router.get( } } + // limit to allow list + // Because filtering happens after checking the directory this may lead into a bad UX. + if (config.get("enableAllowList")) { + const checkResults = await Promise.all(rooms.map((r) => r.room_id).map(checkIfAllowed)); + rooms = rooms.filter((_v, index) => checkResults[index]); + } + // We index the room directory unless the config says we shouldn't index anything const stopSearchEngineIndexingFromConfig = config.get('stopSearchEngineIndexing'); const shouldIndex = !stopSearchEngineIndexingFromConfig;