Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for room aliases #107

Merged
merged 9 commits into from
Oct 27, 2022
5 changes: 4 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions server/lib/matrix-utils/ensure-room-joined.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ const StatusError = require('../status-error');
const matrixServerUrl = config.get('matrixServerUrl');
assert(matrixServerUrl);

async function ensureRoomJoined(accessToken, roomId, viaServers = []) {
async function ensureRoomJoined(accessToken, roomIdOrAlias, viaServers = []) {
let qs = new URLSearchParams();
[].concat(viaServers).forEach((viaServer) => {
qs.append('server_name', viaServer);
});

const joinEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/join/${roomId}?${qs.toString()}`
`_matrix/client/r0/join/${encodeURIComponent(roomIdOrAlias)}?${qs.toString()}`
);
try {
await fetchEndpointAsJson(joinEndpoint, {
const joinData = await fetchEndpointAsJson(joinEndpoint, {
method: 'POST',
accessToken,
});
assert(
joinData.room_id,
`Join endpoint (${joinEndpoint}) did not return \`room_id\` as expected. This is probably a problem with that homeserver.`
);
return joinData.room_id;
} catch (err) {
throw new StatusError(403, `Archiver is unable to join room: ${err.message}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ async function fetchEventsFromTimestampBackwards({ accessToken, roomId, ts, limi
// event mark.
const contextEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/context/${eventIdForTimestamp}?limit=0&filter={"lazy_load_members":true}`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(
eventIdForTimestamp
)}?limit=0&filter={"lazy_load_members":true}`
);
const contextResData = await fetchEndpointAsJson(contextEndpoint, {
accessToken,
Expand All @@ -86,7 +88,9 @@ async function fetchEventsFromTimestampBackwards({ accessToken, roomId, ts, limi
// the messages included in the response
const messagesEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/messages?dir=b&from=${contextResData.end}&limit=${limit}&filter={"lazy_load_members":true}`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/messages?dir=b&from=${encodeURIComponent(
contextResData.end
)}&limit=${limit}&filter={"lazy_load_members":true}`
);
const messageResData = await fetchEndpointAsJson(messagesEndpoint, {
accessToken,
Expand Down
22 changes: 18 additions & 4 deletions server/lib/matrix-utils/fetch-room-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,38 @@ async function fetchRoomData(accessToken, roomId) {

const stateNameEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/state/m.room.name`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.name`
);
const canoncialAliasEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.canonical_alias`
);
const stateAvatarEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/state/m.room.avatar`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.avatar`
);
const stateHistoryVisibilityEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/state/m.room.history_visibility`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.history_visibility`
);
const stateJoinRulesEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/r0/rooms/${roomId}/state/m.room.join_rules`
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.join_rules`
);

const [
stateNameResDataOutcome,
stateCanonicalAliasResDataOutcome,
stateAvatarResDataOutcome,
stateHistoryVisibilityResDataOutcome,
stateJoinRulesResDataOutcome,
] = await Promise.allSettled([
fetchEndpointAsJson(stateNameEndpoint, {
accessToken,
}),
fetchEndpointAsJson(canoncialAliasEndpoint, {
accessToken,
}),
fetchEndpointAsJson(stateAvatarEndpoint, {
accessToken,
}),
Expand All @@ -56,6 +64,11 @@ async function fetchRoomData(accessToken, roomId) {
name = stateNameResDataOutcome.value.name;
}

let canonicalAlias;
if (stateCanonicalAliasResDataOutcome.reason === undefined) {
canonicalAlias = stateCanonicalAliasResDataOutcome.value.alias;
}

let avatarUrl;
if (stateAvatarResDataOutcome.reason === undefined) {
avatarUrl = stateAvatarResDataOutcome.value.url;
Expand All @@ -74,6 +87,7 @@ async function fetchRoomData(accessToken, roomId) {
return {
id: roomId,
name,
canonicalAlias,
avatarUrl,
historyVisibility,
joinRule,
Expand Down
4 changes: 3 additions & 1 deletion server/lib/matrix-utils/timestamp-to-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ async function timestampToEvent({ accessToken, roomId, ts, direction }) {

const timestampToEventEndpoint = urlJoin(
matrixServerUrl,
`_matrix/client/unstable/org.matrix.msc3030/rooms/${roomId}/timestamp_to_event?ts=${ts}&dir=${direction}`
`_matrix/client/unstable/org.matrix.msc3030/rooms/${encodeURIComponent(
roomId
)}/timestamp_to_event?ts=${encodeURIComponent(ts)}&dir=${encodeURIComponent(direction)}`
);
const timestampToEventResData = await fetchEndpointAsJson(timestampToEventEndpoint, {
accessToken,
Expand Down
8 changes: 7 additions & 1 deletion server/routes/install-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { handleTracingMiddleware } = require('../tracing/tracing-middleware');
const getVersionTags = require('../lib/get-version-tags');
const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware');
const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware');
const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-archive-url-if-bad-sigil-middleware');

function installRoutes(app) {
app.use(handleTracingMiddleware);
Expand Down Expand Up @@ -58,7 +59,12 @@ function installRoutes(app) {

app.use('/', require('./room-directory-routes'));

app.use('/:roomIdOrAlias', require('./room-routes'));
// For room aliases (/r) or room ID's (/roomid)
app.use('/:entityDescriptor(r|roomid)/:roomIdOrAliasDirty', require('./room-routes'));

// Correct any honest mistakes: If someone accidentally put the sigil in the URL, then
// redirect them to the correct URL without the sigil to the correct path above.
app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil);
}

module.exports = installRoutes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const assert = require('assert');
const escapeStringRegexp = require('escape-string-regexp');

const config = require('../lib/config');
const basePath = config.get('basePath');
assert(basePath);

const VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP = {
'#': 'r',
'!': 'roomid',
};

// Create a regex string that will match a normal string or the URI encoded string or
// any combination of some characters being URI encoded. Only worries about characters
// that would appear in a valid room ID or alias.
function uriEncodedMatrixCompatibleRegex(inputString) {
return inputString.replaceAll(/[!#:]/g, (match) => {
return `(?:${match}|${encodeURIComponent(match)})`;
});
}

// Correct any honest mistakes: If someone accidentally put the sigil in the URL, then
// redirect them to the correct URL without the sigil.
//
// Redirect examples:
// - `/roomid/!xxx:my.synapse.server` -> `/roomid/xxx:my.synapse.server`
// - `/roomid/!xxx:my.synapse.server/date/2022/09/20?via=my.synapse.server` -> `/roomid/xxx:my.synapse.server/date/2022/09/20?via=my.synapse.server`
// - `/!xxx:my.synapse.server` -> `/roomid/xxx:my.synapse.server`
// - `/%23xxx%3Amy.synapse.server` -> `/r/xxx:my.synapse.server`
// - `/roomid/%23xxx%3Amy.synapse.server/date/2022/09/20?via=my.synapse.server` -> `/r/xxx:my.synapse.server/date/2022/09/20`
function redirectToCorrectArchiveUrlIfBadSigilMiddleware(req, res, next) {
// This could be with or with our without the sigil. Although the correct thing here
// is to have no sigil. We will try to correct it for them in any case.
const roomIdOrAliasDirty = req.params.roomIdOrAliasDirty;
const roomIdOrAliasWithoutSigil = roomIdOrAliasDirty.replace(/^(#|!)/, '');

if (
roomIdOrAliasDirty.startsWith('!') ||
// It isn't possible to put the room alias `#` in the URI unless it's URI encoded
// since everything after a `#` is only visible client-side but just put the logic
// here for clarity. We handle this redirect on the client.
roomIdOrAliasDirty.startsWith('#')
) {
const sigil = roomIdOrAliasDirty[0];
const entityDescriptor = VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP[sigil];
if (!entityDescriptor) {
throw new Error(
`Unknown sigil=${sigil} has no entityDescriptor. This is an error with the Matrix Public Archive itself (please open an issue).`
);
}

const urlObj = new URL(req.originalUrl, basePath);
const dirtyPathRegex = new RegExp(
`(/(?:r|roomid))?/${uriEncodedMatrixCompatibleRegex(
escapeStringRegexp(roomIdOrAliasDirty)
)}(/?.*)`
);
urlObj.pathname = urlObj.pathname.replace(dirtyPathRegex, (_match, _beforePath, afterPath) => {
return `/${entityDescriptor}/${roomIdOrAliasWithoutSigil}${afterPath}`;
});

// 301 permanent redirect any mistakes to the correct place
res.redirect(301, urlObj.toString());
return;
}

next();
}

module.exports = redirectToCorrectArchiveUrlIfBadSigilMiddleware;
Loading