Update tests and instructions to intercept reports also with v3 endpoint (#388)

In both our instructions and our tests, we use the r0 endpoint to intercept abuse reports. This endpoint is deprecated and not implemented by all clients. This PR updates the instructions and tests to the new endpoint.
This commit is contained in:
David Teller 2022-10-18 15:48:39 +02:00 committed by GitHub
parent 938b9fea8f
commit 7b0edadd17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 214 additions and 206 deletions

View File

@ -48,7 +48,7 @@ This requires two configuration steps:
1. In your Mjölnir configuration file, typically `/etc/mjolnir/config/production.yaml`, copy and paste the `web` section from `default.yaml`, if you don't have it yet (it appears with version 1.20) and set `enabled: true` for both `web` and 1. In your Mjölnir configuration file, typically `/etc/mjolnir/config/production.yaml`, copy and paste the `web` section from `default.yaml`, if you don't have it yet (it appears with version 1.20) and set `enabled: true` for both `web` and
`abuseReporting`. `abuseReporting`.
2. Setup a reverse proxy that will redirect requests from `^/_matrix/client/r0/rooms/([^/]*)/report/(.*)$` to `http://host:port/api/1/report/$1/$2`, where `host` is the host where you run Mjölnir, and `port` is the port you configured in `production.yaml`. For an example nginx configuration, see `test/nginx.conf`. It's the confirmation we use during runtime testing. 2. Setup a reverse proxy that will redirect requests from `^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$` to `http://host:port/api/1/report/$2/$3`, where `host` is the host where you run Mjölnir, and `port` is the port you configured in `production.yaml`. For an example nginx configuration, see `test/nginx.conf`. It's the confirmation we use during runtime testing.
### Security note ### Security note

View File

@ -1,6 +1,5 @@
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import config from "../../src/config";
import { matrixClient } from "./mjolnirSetupUtils"; import { matrixClient } from "./mjolnirSetupUtils";
import { newTestUser } from "./clientHelper"; import { newTestUser } from "./clientHelper";
import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager"; import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager";
@ -20,207 +19,214 @@ const REPORT_NOTICE_REGEXPS = {
describe("Test: Reporting abuse", async () => { describe("Test: Reporting abuse", async () => {
it('Mjölnir intercepts abuse reports', async function() { // Testing with successive versions of the API.
this.timeout(60000); //
// As of this writing, v3 is the standard, while r0 is deprecated. However,
// both versions are still in use in the wild.
// Note that this version change only affects the actual URL at which reports
// are sent.
for (let endpoint of ['v3', 'r0']) {
it(`Mjölnir intercepts abuse reports with endpoint ${endpoint}`, async function() {
this.timeout(90000);
// Listen for any notices that show up. // Listen for any notices that show up.
let notices: any[] = []; let notices: any[] = [];
matrixClient()!.on("room.event", (roomId, event) => { this.mjolnir.client.on("room.event", (roomId, event) => {
if (roomId = this.mjolnir.managementRoomId) { if (roomId = this.mjolnir.managementRoomId) {
notices.push(event); notices.push(event);
}
});
// Create a few users and a room.
let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-good-user" }});
let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-bad-user" }});
let goodUserId = await goodUser.getUserId();
let badUserId = await badUser.getUserId();
let roomId = await goodUser.createRoom({ invite: [await badUser.getUserId()] });
await goodUser.inviteUser(await badUser.getUserId(), roomId);
await badUser.joinRoom(roomId);
console.log("Test: Reporting abuse - send messages");
// Exchange a few messages.
let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported.
let badText = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText2 = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText3 = `<b>BAD</b>: ${Math.random()}`; // Will be reported as abuse.
let badText4 = [...Array(1024)].map(_ => `${Math.random()}`).join(""); // Text is too long.
let badText5 = [...Array(1024)].map(_ => "ABC").join("\n"); // Text has too many lines.
let badEventId = await badUser.sendText(roomId, badText);
let badEventId2 = await badUser.sendText(roomId, badText2);
let badEventId3 = await badUser.sendText(roomId, badText3);
let badEventId4 = await badUser.sendText(roomId, badText4);
let badEventId5 = await badUser.sendText(roomId, badText5);
let badEvent2Comment = `COMMENT: ${Math.random()}`;
console.log("Test: Reporting abuse - send reports");
let reportsToFind: any[] = []
// Time to report, first without a comment, then with one.
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId)}`);
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId,
text: badText,
comment: null,
});
} catch (e) {
console.error("Could not send first report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId2)}`, "", {
reason: badEvent2Comment
});
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId2,
text: badText2,
comment: badEvent2Comment,
});
} catch (e) {
console.error("Could not send second report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId3)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId3,
text: badText3,
comment: null,
});
} catch (e) {
console.error("Could not send third report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId4)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId4,
text: null,
textPrefix: badText4.substring(0, 256),
comment: null,
});
} catch (e) {
console.error("Could not send fourth report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/${endpoint}/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId5)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId5,
text: null,
textPrefix: badText5.substring(0, 256).split("\n").join(" "),
comment: null,
});
} catch (e) {
console.error("Could not send fifth report", e.body || e);
throw e;
}
console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000));
let found: any[] = [];
for (let toFind of reportsToFind) {
for (let event of notices) {
if ("content" in event && "body" in event.content) {
if (!(ABUSE_REPORT_KEY in event.content) || event.content[ABUSE_REPORT_KEY].event_id != toFind.eventId) {
// Not a report or not our report.
continue;
}
let report = event.content[ABUSE_REPORT_KEY];
let body = event.content.body as string;
let matches: Map<string, RegExpMatchArray> | null = new Map();
for (let key of Object.keys(REPORT_NOTICE_REGEXPS)) {
let match = body.match(REPORT_NOTICE_REGEXPS[key]);
if (match) {
console.debug("We have a match", key, REPORT_NOTICE_REGEXPS[key], match.groups);
} else {
console.debug("Not a match", key, REPORT_NOTICE_REGEXPS[key]);
// Not a report, skipping.
matches = null;
break;
}
matches.set(key, match);
}
if (!matches) {
// Not a report, skipping.
continue;
}
assert(body.length < 3000, `The report shouldn't be too long ${body.length}`);
assert(body.split("\n").length < 200, "The report shouldn't have too many newlines.");
assert.equal(matches.get("event")!.groups!.eventId, toFind.eventId, "The report should specify the correct event id");;
assert.equal(matches.get("reporter")!.groups!.reporterId, toFind.reporterId, "The report should specify the correct reporter");
assert.equal(report.reporter_id, toFind.reporterId, "The embedded report should specify the correct reporter");
assert.ok(toFind.reporterId.includes(matches.get("reporter")!.groups!.reporterDisplay), "The report should display the correct reporter");
assert.equal(matches.get("accused")!.groups!.accusedId, toFind.accusedId, "The report should specify the correct accused");
assert.equal(report.accused_id, toFind.accusedId, "The embedded report should specify the correct accused");
assert.ok(toFind.accusedId.includes(matches.get("accused")!.groups!.accusedDisplay), "The report should display the correct reporter");
if (toFind.text) {
assert.equal(matches.get("content")!.groups!.eventContent, toFind.text, "The report should contain the text we inserted in the event");
}
if (toFind.textPrefix) {
assert.ok(matches.get("content")!.groups!.eventContent.startsWith(toFind.textPrefix), `The report should contain a prefix of the long text we inserted in the event: ${toFind.textPrefix} in? ${matches.get("content")!.groups!.eventContent}`);
}
if (toFind.comment) {
assert.equal(matches.get("comments")!.groups!.comments, toFind.comment, "The report should contain the comment we added");
}
assert.equal(matches.get("room")!.groups!.roomAliasOrId, roomId, "The report should specify the correct room");
assert.equal(report.room_id, roomId, "The embedded report should specify the correct room");
found.push(toFind);
break;
}
}
}
assert.deepEqual(found, reportsToFind);
// Since Mjölnir is not a member of the room, the only buttons we should find
// are `help` and `ignore`.
for (let event of notices) {
if (event.content && event.content["m.relates_to"] && event.content["m.relates_to"]["key"]) {
let regexp = /\/([[^]]*)\]/;
let matches = event.content["m.relates_to"]["key"].match(regexp);
if (!matches) {
continue;
}
switch (matches[1]) {
case "bad-report":
case "help":
continue;
default:
throw new Error(`Didn't expect label ${matches[1]}`);
}
}
} }
}); });
}
// Create a few users and a room.
let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-good-user" }});
let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-bad-user" }});
let goodUserId = await goodUser.getUserId();
let badUserId = await badUser.getUserId();
let roomId = await goodUser.createRoom({ invite: [await badUser.getUserId()] });
await goodUser.inviteUser(await badUser.getUserId(), roomId);
await badUser.joinRoom(roomId);
console.log("Test: Reporting abuse - send messages");
// Exchange a few messages.
let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported.
let badText = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText2 = `BAD: ${Math.random()}`; // Will be reported as abuse.
let badText3 = `<b>BAD</b>: ${Math.random()}`; // Will be reported as abuse.
let badText4 = [...Array(1024)].map(_ => `${Math.random()}`).join(""); // Text is too long.
let badText5 = [...Array(1024)].map(_ => "ABC").join("\n"); // Text has too many lines.
let goodEventId = await goodUser.sendText(roomId, goodText);
let badEventId = await badUser.sendText(roomId, badText);
let badEventId2 = await badUser.sendText(roomId, badText2);
let badEventId3 = await badUser.sendText(roomId, badText3);
let badEventId4 = await badUser.sendText(roomId, badText4);
let badEventId5 = await badUser.sendText(roomId, badText5);
let badEvent2Comment = `COMMENT: ${Math.random()}`;
console.log("Test: Reporting abuse - send reports");
let reportsToFind = []
// Time to report, first without a comment, then with one.
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId)}`);
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId,
text: badText,
comment: null,
});
} catch (e) {
console.error("Could not send first report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId2)}`, "", {
reason: badEvent2Comment
});
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId2,
text: badText2,
comment: badEvent2Comment,
});
} catch (e) {
console.error("Could not send second report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId3)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId3,
text: badText3,
comment: null,
});
} catch (e) {
console.error("Could not send third report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId4)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId4,
text: null,
textPrefix: badText4.substring(0, 256),
comment: null,
});
} catch (e) {
console.error("Could not send fourth report", e.body || e);
throw e;
}
try {
await goodUser.doRequest("POST", `/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/report/${encodeURIComponent(badEventId5)}`, "");
reportsToFind.push({
reporterId: goodUserId,
accusedId: badUserId,
eventId: badEventId5,
text: null,
textPrefix: badText5.substring(0, 256).split("\n").join(" "),
comment: null,
});
} catch (e) {
console.error("Could not send fifth report", e.body || e);
throw e;
}
console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000));
let found = [];
for (let toFind of reportsToFind) {
for (let event of notices) {
if ("content" in event && "body" in event.content) {
if (!(ABUSE_REPORT_KEY in event.content) || event.content[ABUSE_REPORT_KEY].event_id != toFind.eventId) {
// Not a report or not our report.
continue;
}
let report = event.content[ABUSE_REPORT_KEY];
let body = event.content.body as string;
let matches = new Map();
for (let key of Object.keys(REPORT_NOTICE_REGEXPS)) {
let match = body.match(REPORT_NOTICE_REGEXPS[key]);
if (match) {
console.debug("We have a match", key, REPORT_NOTICE_REGEXPS[key], match.groups);
} else {
console.debug("Not a match", key, REPORT_NOTICE_REGEXPS[key]);
// Not a report, skipping.
matches = null;
break;
}
matches.set(key, match);
}
if (!matches) {
// Not a report, skipping.
continue;
}
assert(body.length < 3000, `The report shouldn't be too long ${body.length}`);
assert(body.split("\n").length < 200, "The report shouldn't have too many newlines.");
assert.equal(matches.get("event")!.groups.eventId, toFind.eventId, "The report should specify the correct event id");;
assert.equal(matches.get("reporter")!.groups.reporterId, toFind.reporterId, "The report should specify the correct reporter");
assert.equal(report.reporter_id, toFind.reporterId, "The embedded report should specify the correct reporter");
assert.ok(toFind.reporterId.includes(matches.get("reporter")!.groups.reporterDisplay), "The report should display the correct reporter");
assert.equal(matches.get("accused")!.groups.accusedId, toFind.accusedId, "The report should specify the correct accused");
assert.equal(report.accused_id, toFind.accusedId, "The embedded report should specify the correct accused");
assert.ok(toFind.accusedId.includes(matches.get("accused")!.groups.accusedDisplay), "The report should display the correct reporter");
if (toFind.text) {
assert.equal(matches.get("content")!.groups.eventContent, toFind.text, "The report should contain the text we inserted in the event");
}
if (toFind.textPrefix) {
assert.ok(matches.get("content")!.groups.eventContent.startsWith(toFind.textPrefix), `The report should contain a prefix of the long text we inserted in the event: ${toFind.textPrefix} in? ${matches.get("content")!.groups.eventContent}`);
}
if (toFind.comment) {
assert.equal(matches.get("comments")!.groups.comments, toFind.comment, "The report should contain the comment we added");
}
assert.equal(matches.get("room")!.groups.roomAliasOrId, roomId, "The report should specify the correct room");
assert.equal(report.room_id, roomId, "The embedded report should specify the correct room");
found.push(toFind);
break;
}
}
}
assert.deepEqual(found, reportsToFind);
// Since Mjölnir is not a member of the room, the only buttons we should find
// are `help` and `ignore`.
for (let event of notices) {
if (event.content && event.content["m.relates_to"] && event.content["m.relates_to"]["key"]) {
let regexp = /\/([[^]]*)\]/;
let matches = event.content["m.relates_to"]["key"].match(regexp);
if (!matches) {
continue;
}
switch (matches[1]) {
case "bad-report":
case "help":
continue;
default:
throw new Error(`Didn't expect label ${matches[1]}`);
}
}
}
});
it('The redact action works', async function() { it('The redact action works', async function() {
this.timeout(60000); this.timeout(60000);
// Listen for any notices that show up. // Listen for any notices that show up.
let notices = []; let notices: any[] = [];
matrixClient().on("room.event", (roomId, event) => { this.mjolnir.client.on("room.event", (roomId, event) => {
if (roomId = this.mjolnir.managementRoomId) { if (roomId = this.mjolnir.managementRoomId) {
notices.push(event); notices.push(event);
} }
@ -228,7 +234,7 @@ describe("Test: Reporting abuse", async () => {
// Create a moderator. // Create a moderator.
let moderatorUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-moderator-user" }}); let moderatorUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-moderator-user" }});
matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId); this.mjolnir.client.inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
await moderatorUser.joinRoom(this.mjolnir.managementRoomId); await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
// Create a few users and a room. // Create a few users and a room.
@ -244,8 +250,8 @@ describe("Test: Reporting abuse", async () => {
await goodUser.joinRoom(roomId); await goodUser.joinRoom(roomId);
// Setup Mjölnir as moderator for our room. // Setup Mjölnir as moderator for our room.
await moderatorUser.inviteUser(await matrixClient().getUserId(), roomId); await moderatorUser.inviteUser(await this.mjolnir.client.getUserId(), roomId);
await moderatorUser.setUserPowerLevel(await matrixClient().getUserId(), roomId, 100); await moderatorUser.setUserPowerLevel(await this.mjolnir.client.getUserId(), roomId, 100);
console.log("Test: Reporting abuse - send messages"); console.log("Test: Reporting abuse - send messages");
// Exchange a few messages. // Exchange a few messages.
@ -275,7 +281,7 @@ describe("Test: Reporting abuse", async () => {
console.log("Test: Reporting abuse - wait"); console.log("Test: Reporting abuse - wait");
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
let mjolnirRooms = new Set(await matrixClient().getJoinedRooms()); let mjolnirRooms = new Set(await this.mjolnir.client.getJoinedRooms());
assert.ok(mjolnirRooms.has(roomId), "Mjölnir should be a member of the room"); assert.ok(mjolnirRooms.has(roomId), "Mjölnir should be a member of the room");
// Find the notice // Find the notice
@ -293,7 +299,7 @@ describe("Test: Reporting abuse", async () => {
assert.ok(noticeId, "We should have found our notice"); assert.ok(noticeId, "We should have found our notice");
// Find the buttons. // Find the buttons.
let buttons = []; let buttons: any[] = [];
for (let event of notices) { for (let event of notices) {
if (event["type"] != "m.reaction") { if (event["type"] != "m.reaction") {
continue; continue;
@ -347,7 +353,7 @@ describe("Test: Reporting abuse", async () => {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
// This should have redacted the message. // This should have redacted the message.
let newBadEvent = await matrixClient().getEvent(roomId, badEventId); let newBadEvent = await this.mjolnir.client.getEvent(roomId, badEventId);
assert.deepEqual(Object.keys(newBadEvent.content), [], "Redaction should have removed the content of the offending event"); assert.deepEqual(Object.keys(newBadEvent.content), [], "Redaction should have removed the content of the offending event");
}); });
}); });

View File

@ -12,7 +12,7 @@ export const mochaHooks = {
console.error("---- entering test", JSON.stringify(this.currentTest.title)); // Makes MatrixClient error logs a bit easier to parse. console.error("---- entering test", JSON.stringify(this.currentTest.title)); // Makes MatrixClient error logs a bit easier to parse.
console.log("mochaHooks.beforeEach"); console.log("mochaHooks.beforeEach");
// Sometimes it takes a little longer to register users. // Sometimes it takes a little longer to register users.
this.timeout(10000); this.timeout(30000);
const config = this.config = configRead(); const config = this.config = configRead();
this.managementRoomAlias = config.managementRoom; this.managementRoomAlias = config.managementRoom;
this.mjolnir = await makeMjolnir(config); this.mjolnir = await makeMjolnir(config);

View File

@ -6,8 +6,10 @@ http {
server { server {
listen 8081; listen 8081;
location ~ ^/_matrix/client/r0/rooms/([^/]*)/report/(.*)$ { location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ {
# Abuse reports should be sent to Mjölnir. # Abuse reports should be sent to Mjölnir.
# The r0 endpoint is deprecated but still used by many clients.
# As of this writing, the v3 endpoint is the up-to-date version.
# Add CORS, otherwise a browser will refuse this request. # Add CORS, otherwise a browser will refuse this request.
add_header 'Access-Control-Allow-Origin' '*' always; # Note: '*' is for testing purposes. For your own server, you probably want to tighten this. add_header 'Access-Control-Allow-Origin' '*' always; # Note: '*' is for testing purposes. For your own server, you probably want to tighten this.
@ -18,8 +20,8 @@ http {
add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days
# Alias the regexps, to ensure that they're not rewritten. # Alias the regexps, to ensure that they're not rewritten.
set $room_id $1; set $room_id $2;
set $event_id $2; set $event_id $3;
proxy_pass http://127.0.0.1:8082/api/1/report/$room_id/$event_id; proxy_pass http://127.0.0.1:8082/api/1/report/$room_id/$event_id;
} }
location / { location / {