From 7bd4eb728d27636321e9ac6dadd764ef5fa95af1 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Fri, 2 Feb 2024 12:14:38 +0000 Subject: [PATCH] refactor: send and accept only spec-compliant AP headers --- cypress/e2e/event.cy.ts | 4 +- src/activitypub.js | 99 +++++++++--------- src/app.ts | 8 +- src/lib/activitypub.ts | 23 +++- src/routes.js | 227 ++++++++++++++++++++-------------------- src/routes/frontend.ts | 13 ++- 6 files changed, 196 insertions(+), 178 deletions(-) diff --git a/cypress/e2e/event.cy.ts b/cypress/e2e/event.cy.ts index 46e35fb..78cc2ca 100644 --- a/cypress/e2e/event.cy.ts +++ b/cypress/e2e/event.cy.ts @@ -113,7 +113,7 @@ describe("Events", () => { cy.request({ url: `/${this.eventID}/featured`, headers: { - Accept: "application/activity+json", + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', }, }).then((response) => { expect(response.body).to.have.property("@context"); @@ -139,7 +139,7 @@ describe("Events", () => { this.eventID }@${Cypress.env("CYPRESS_DOMAIN")}`, headers: { - Accept: "application/activity+json", + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', }, }).then((response) => { expect(response.body).to.have.property("subject"); diff --git a/src/activitypub.js b/src/activitypub.js index 857dce8..a6f4ada 100644 --- a/src/activitypub.js +++ b/src/activitypub.js @@ -10,6 +10,7 @@ const domain = config.general.domain; const siteName = config.general.site_name; const isFederated = config.general.is_federated; import Event from "./models/Event.js"; +import { activityPubContentType, alternateActivityPubContentType } from "./lib/activitypub.js"; // This alphabet (used to generate all event, group, etc. IDs) is missing '-' // because ActivityPub doesn't like it in IDs @@ -35,9 +36,9 @@ export function createActivityPubActor( "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { - "toot": "http://joinmastodon.org/ns#", - "discoverable": "toot:discoverable", - "indexable": "toot:indexable" + "toot": "http://joinmastodon.org/ns#", + "discoverable": "toot:discoverable", + "indexable": "toot:indexable" }, ], indexable: false, @@ -93,9 +94,9 @@ export function createActivityPubEvent( "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { - "toot": "http://joinmastodon.org/ns#", - "discoverable": "toot:discoverable", - "indexable": "toot:indexable" + "toot": "http://joinmastodon.org/ns#", + "discoverable": "toot:discoverable", + "indexable": "toot:indexable" }, ], indexable: false, @@ -219,8 +220,8 @@ export function signAndSend(message, eventID, targetDomain, inbox, callback) { Date: d.toUTCString(), Signature: header, Digest: `SHA-256=${digest}`, - "Content-Type": "application/activity+json", - Accept: "application/activity+json", + "Content-Type": activityPubContentType, + Accept: activityPubContentType, }, method: "POST", json: true, @@ -257,7 +258,7 @@ export function signAndSend(message, eventID, targetDomain, inbox, callback) { "addActivityPubMessage", "success", "ActivityPubMessage added to event " + - eventID, + eventID, ); callback(null, message.id, 200); }) @@ -266,9 +267,9 @@ export function signAndSend(message, eventID, targetDomain, inbox, callback) { "addActivityPubMessage", "error", "Attempt to add ActivityPubMessage to event " + - eventID + - " failed with error: " + - err, + eventID + + " failed with error: " + + err, ); callback(err, null, 500); }); @@ -463,7 +464,7 @@ export function broadcastUpdateMessage(apObject, followers, eventID) { } export function broadcastDeleteMessage(apObject, followers, eventID, callback) { - callback = callback || function () {}; + callback = callback || function () { }; if (!isFederated) { callback([]); return; @@ -560,7 +561,7 @@ export function broadcastDeleteMessage(apObject, followers, eventID, callback) { // this sends a message "to:" an individual fediverse user export function sendDirectMessage(apObject, actorId, eventID, callback) { if (!isFederated) return; - callback = callback || function () {}; + callback = callback || function () { }; const guidCreate = crypto.randomBytes(16).toString("hex"); const guidObject = crypto.randomBytes(16).toString("hex"); let d = new Date(); @@ -618,7 +619,7 @@ export function sendDirectMessage(apObject, actorId, eventID, callback) { export function sendAcceptMessage(thebody, eventID, targetDomain, callback) { if (!isFederated) return; - callback = callback || function () {}; + callback = callback || function () { }; const guid = crypto.randomBytes(16).toString("hex"); const actorId = thebody.actor; let message = { @@ -666,8 +667,8 @@ function _handleFollow(req, res) { { url: req.body.actor, headers: { - Accept: "application/activity+json", - "Content-Type": "application/activity+json", + Accept: activityPubContentType, + "Content-Type": activityPubContentType, }, }, function (error, response, body) { @@ -794,9 +795,9 @@ function _handleFollow(req, res) { "addEventFollower", "error", "Attempt to add follower to event " + - eventID + - " failed with error: " + - err, + eventID + + " failed with error: " + + err, ); return res .status(500) @@ -851,9 +852,9 @@ function _handleUndoFollow(req, res) { "removeEventFollower", "error", "Attempt to remove follower from event " + - eventID + - " failed with error: " + - err, + eventID + + " failed with error: " + + err, ); return res.send( "Database error, please try again :(", @@ -887,8 +888,8 @@ function _handleAcceptEvent(req, res) { { url: actor, headers: { - Accept: "application/activity+json", - "Content-Type": "application/activity+json", + Accept: activityPubContentType, + "Content-Type": activityPubContentType, }, }, function (error, response, body) { @@ -913,7 +914,7 @@ function _handleAcceptEvent(req, res) { "addEventAttendee", "success", "Attendee added to event " + - req.params.eventID, + req.params.eventID, ); // get the new attendee with its hidden id from the full event let fullAttendee = fullEvent.attendees.find( @@ -947,9 +948,9 @@ function _handleAcceptEvent(req, res) { "addEventAttendee", "error", "Attempt to add attendee to event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); return res .status(500) @@ -996,7 +997,7 @@ function _handleUndoAcceptEvent(req, res) { "oneClickUnattend", "success", "Attendee removed via one click unattend " + - req.params.eventID, + req.params.eventID, ); }); } @@ -1039,8 +1040,8 @@ function _handleCreateNote(req, res) { { url: attributedTo, headers: { - Accept: "application/activity+json", - "Content-Type": "application/activity+json", + Accept: activityPubContentType, + "Content-Type": activityPubContentType, }, }, function (error, response, body) { @@ -1069,7 +1070,7 @@ function _handleCreateNote(req, res) { "addEventAttendee", "success", "Attendee added to event " + - req.params.eventID, + req.params.eventID, ); // get the new attendee with its hidden id from the full event let fullAttendee = @@ -1105,9 +1106,9 @@ function _handleCreateNote(req, res) { "addEventAttendee", "error", "Attempt to add attendee to event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); return res .status(500) @@ -1169,7 +1170,7 @@ function _handleDelete(req, res) { return ( comment.activityJson && JSON.parse(comment.activityJson).object.id === - req.body.object.id + req.body.object.id ); }, ); @@ -1189,11 +1190,11 @@ function _handleDelete(req, res) { "deleteComment", "error", "Attempt to delete comment " + - req.body.object.id + - "from event " + - eventWithComment.id + - " failed with error: " + - err, + req.body.object.id + + "from event " + + eventWithComment.id + + " failed with error: " + + err, ); return res.sendStatus(500); }); @@ -1233,8 +1234,8 @@ function _handleCreateNoteComment(req, res) { { url: req.body.actor, headers: { - Accept: "application/activity+json", - "Content-Type": "application/activity+json", + Accept: activityPubContentType, + "Content-Type": activityPubContentType, }, }, function (error, response, actor) { @@ -1296,13 +1297,13 @@ function _handleCreateNoteComment(req, res) { "addEventComment", "error", "Attempt to add comment to event " + - eventID + - " failed with error: " + - err, + eventID + + " failed with error: " + + err, ); res.status(500).send( "Database error, please try again :(" + - err, + err, ); }); }, @@ -1387,7 +1388,7 @@ export function createWebfinger(eventID, domain) { links: [ { rel: "self", - type: "application/activity+json", + type: alternateActivityPubContentType, href: `https://${domain}/${eventID}`, }, ], diff --git a/src/app.ts b/src/app.ts index 3370d27..f3c99c7 100755 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,10 @@ import group from "./routes/group.js"; import staticPages from "./routes/static.js"; import { initEmailService } from "./lib/email.js"; +import { + activityPubContentType, + alternateActivityPubContentType, +} from "./lib/activitypub.js"; const app = express(); @@ -48,8 +52,8 @@ app.set("hbsInstance", hbsInstance); app.use(express.static("public")); // Body parser // -app.use(express.json({ type: "application/activity+json" })); -app.use(express.json({ type: "application/ld+json" })); +app.use(express.json({ type: alternateActivityPubContentType })); +app.use(express.json({ type: activityPubContentType })); app.use(express.json({ type: "application/json" })); app.use(express.urlencoded({ extended: true })); diff --git a/src/lib/activitypub.ts b/src/lib/activitypub.ts index 0a3db7b..11f0770 100644 --- a/src/lib/activitypub.ts +++ b/src/lib/activitypub.ts @@ -1,9 +1,22 @@ -import { Request } from "express"; +import { Request, Response } from "express"; +// From https://www.w3.org/TR/activitypub/#client-to-server-interactions: +// "Servers MAY interpret a Content-Type or Accept header of application/activity+json +// as equivalent to application/ld+json; profile="https://www.w3.org/ns/activitystreams" +// for client-to-server interactions. +// For best compatibility, we always send application/ld+json; profile="https://www.w3.org/ns/activitystreams" +// and accept both application/ld+json; profile="https://www.w3.org/ns/activitystreams" and application/activity+json. +export const activityPubContentType = + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; +export const alternateActivityPubContentType = "application/activity+json"; + +// Cf. https://www.w3.org/TR/activitypub/#retrieving-objects export const acceptsActivityPub = (req: Request) => { - return ( - req.headers.accept && - (req.headers.accept.includes("application/activity+json") || - req.headers.accept.includes("application/ld+json")) + const validAcceptHeaders = [ + activityPubContentType, + alternateActivityPubContentType, + ]; + return validAcceptHeaders.some( + (header) => req.headers.accept?.includes(header), ); }; diff --git a/src/routes.js b/src/routes.js index d59a738..360f387 100755 --- a/src/routes.js +++ b/src/routes.js @@ -22,6 +22,7 @@ import { import Event from "./models/Event.js"; import EventGroup from "./models/EventGroup.js"; import path from "path"; +import { activityPubContentType } from "./lib/activitypub.js"; const config = getConfig(); const domain = config.general.domain; @@ -104,9 +105,9 @@ schedule.scheduleJob("59 23 * * *", function (fireDate) { "deleteOldEvents", "error", "Attempt to delete old event " + - id + - " failed with error: " + - err, + id + + " failed with error: " + + err, ); }); }; @@ -123,9 +124,9 @@ schedule.scheduleJob("59 23 * * *", function (fireDate) { "deleteOldEvents", "error", "Attempt to delete event image for old event " + - event.id + - " failed with error: " + - err, + event.id + + " failed with error: " + + err, ); } // Image removed @@ -172,9 +173,9 @@ schedule.scheduleJob("59 23 * * *", function (fireDate) { "deleteOldEvents", "error", "Attempt to delete old event " + - event.id + - " failed with error: " + - err, + event.id + + " failed with error: " + + err, ); }); @@ -227,9 +228,9 @@ router.post("/deleteimage/:eventID/:editToken", (req, res) => { "deleteEventImage", "error", "Attempt to delete event image for event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); } // Image removed @@ -250,9 +251,9 @@ router.post("/deleteimage/:eventID/:editToken", (req, res) => { "deleteEventImage", "error", "Attempt to delete event image for event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }, @@ -294,9 +295,9 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { "deleteEvent", "error", "Attempt to delete event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); } }, @@ -316,9 +317,9 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { "deleteEvent", "error", "Attempt to delete event image for event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); } // Image removed @@ -326,8 +327,8 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { "deleteEvent", "success", "Event " + - req.params.eventID + - " deleted", + req.params.eventID + + " deleted", ); }, ); @@ -349,7 +350,7 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { if (attendeeEmails.length) { console.log( "Sending emails to: " + - attendeeEmails, + attendeeEmails, ); req.app.get("hbsInstance").renderView( "./views/emails/deleteEvent/deleteEventHtml.handlebars", @@ -408,15 +409,15 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { .catch((err) => { res.send( "Sorry! Something went wrong (error deleting): " + - err, + err, ); addToLog( "deleteEvent", "error", "Attempt to delete event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }, @@ -428,8 +429,8 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { "deleteEvent", "error", "Attempt to delete event " + - req.params.eventID + - " failed with error: token does not match", + req.params.eventID + + " failed with error: token does not match", ); } }) @@ -439,9 +440,9 @@ router.post("/deleteevent/:eventID/:editToken", (req, res) => { "deleteEvent", "error", "Attempt to delete event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }); @@ -474,9 +475,9 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { "deleteEventGroup", "error", "Attempt to delete event group " + - req.params.eventGroupID + - " failed with error: " + - err, + req.params.eventGroupID + + " failed with error: " + + err, ); } }, @@ -496,9 +497,9 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { "deleteEventGroup", "error", "Attempt to delete event image for event group " + - req.params.eventGroupID + - " failed with error: " + - err, + req.params.eventGroupID + + " failed with error: " + + err, ); } }, @@ -515,8 +516,8 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { "deleteEventGroup", "success", "Event group " + - req.params.eventGroupID + - " deleted", + req.params.eventGroupID + + " deleted", ); res.writeHead(302, { Location: "/", @@ -526,30 +527,30 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { .catch((err) => { res.send( "Sorry! Something went wrong (error deleting): " + - err, + err, ); addToLog( "deleteEventGroup", "error", "Attempt to delete event group " + - req.params.eventGroupID + - " failed with error: " + - err, + req.params.eventGroupID + + " failed with error: " + + err, ); }); }) .catch((err) => { res.send( "Sorry! Something went wrong (error deleting): " + - err, + err, ); addToLog( "deleteEventGroup", "error", "Attempt to delete event group " + - req.params.eventGroupID + - " failed with error: " + - err, + req.params.eventGroupID + + " failed with error: " + + err, ); }); } else { @@ -559,8 +560,8 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { "deleteEventGroup", "error", "Attempt to delete event group " + - req.params.eventGroupID + - " failed with error: token does not match", + req.params.eventGroupID + + " failed with error: token does not match", ); } }) @@ -570,9 +571,9 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => { "deleteEventGroup", "error", "Attempt to delete event group " + - req.params.eventGroupID + - " failed with error: " + - err, + req.params.eventGroupID + + " failed with error: " + + err, ); }); }); @@ -590,9 +591,9 @@ router.post("/attendee/provision", async (req, res) => { "provisionEventAttendee", "error", "Attempt to provision attendee in event " + - req.query.eventID + - " failed with error: " + - e, + req.query.eventID + + " failed with error: " + + e, ); return res.sendStatus(500); }); @@ -608,9 +609,9 @@ router.post("/attendee/provision", async (req, res) => { "provisionEventAttendee", "error", "Attempt to provision attendee in event " + - req.query.eventID + - " failed with error: " + - e, + req.query.eventID + + " failed with error: " + + e, ); return res.sendStatus(500); }); @@ -646,9 +647,9 @@ router.post("/attendevent/:eventID", async (req, res) => { "attendEvent", "error", "Attempt to attend event " + - req.params.eventID + - " failed with error: " + - e, + req.params.eventID + + " failed with error: " + + e, ); return res.sendStatus(500); }); @@ -747,9 +748,9 @@ router.post("/attendevent/:eventID", async (req, res) => { "addEventAttendee", "error", "Attempt to add attendee to event " + - req.params.eventID + - " failed with error: " + - error, + req.params.eventID + + " failed with error: " + + error, ); }); }); @@ -825,9 +826,9 @@ router.post("/unattendevent/:eventID", (req, res) => { "removeEventAttendee", "error", "Attempt to remove attendee from event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }); @@ -906,9 +907,9 @@ router.get("/oneclickunattendevent/:eventID/:attendeeID", (req, res) => { "removeEventAttendee", "error", "Attempt to remove attendee by admin from event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }); @@ -979,9 +980,9 @@ router.post("/removeattendee/:eventID/:attendeeID", (req, res) => { "removeEventAttendee", "error", "Attempt to remove attendee by admin from event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }); @@ -1055,11 +1056,11 @@ router.post("/subscribe/:eventGroupID", (req, res) => { "addSubscription", "error", "Attempt to subscribe " + - req.body.emailAddress + - " to event group " + - req.params.eventGroupID + - " failed with error: " + - error, + req.body.emailAddress + + " to event group " + + req.params.eventGroupID + + " failed with error: " + + error, ); return res.sendStatus(500); }); @@ -1087,11 +1088,11 @@ router.get("/unsubscribe/:eventGroupID", (req, res) => { "removeSubscription", "error", "Attempt to unsubscribe " + - req.query.email + - " from event group " + - req.params.eventGroupID + - " failed with error: " + - error, + req.query.email + + " from event group " + + req.params.eventGroupID + + " failed with error: " + + error, ); return res.sendStatus(500); }); @@ -1217,9 +1218,9 @@ router.post("/post/comment/:eventID", (req, res) => { "addEventComment", "error", "Attempt to add comment to event " + - req.params.eventID + - " failed with error: " + - err, + req.params.eventID + + " failed with error: " + + err, ); }); }, @@ -1250,9 +1251,9 @@ router.post("/post/reply/:eventID/:commentID", (req, res) => { "addEventReply", "success", "Reply added to comment " + - commentID + - " in event " + - req.params.eventID, + commentID + + " in event " + + req.params.eventID, ); // broadcast an identical message to all followers, will show in their home timeline const guidObject = crypto.randomBytes(16).toString("hex"); @@ -1348,11 +1349,11 @@ router.post("/post/reply/:eventID/:commentID", (req, res) => { "addEventReply", "error", "Attempt to add reply to comment " + - commentID + - " in event " + - req.params.eventID + - " failed with error: " + - err, + commentID + + " in event " + + req.params.eventID + + " failed with error: " + + err, ); }); }, @@ -1388,17 +1389,17 @@ router.post("/deletecomment/:eventID/:commentID/:editToken", (req, res) => { .catch((err) => { res.send( "Sorry! Something went wrong (error deleting): " + - err, + err, ); addToLog( "deleteComment", "error", "Attempt to delete comment " + - req.params.commentID + - "from event " + - req.params.eventID + - " failed with error: " + - err, + req.params.commentID + + "from event " + + req.params.eventID + + " failed with error: " + + err, ); }); } else { @@ -1408,10 +1409,10 @@ router.post("/deletecomment/:eventID/:commentID/:editToken", (req, res) => { "deleteComment", "error", "Attempt to delete comment " + - req.params.commentID + - "from event " + - req.params.eventID + - " failed with error: token does not match", + req.params.commentID + + "from event " + + req.params.eventID + + " failed with error: token does not match", ); } }) @@ -1421,11 +1422,11 @@ router.post("/deletecomment/:eventID/:commentID/:editToken", (req, res) => { "deleteComment", "error", "Attempt to delete comment " + - req.params.commentID + - "from event " + - req.params.eventID + - " failed with error: " + - err, + req.params.commentID + + "from event " + + req.params.eventID + + " failed with error: " + + err, ); }); }); @@ -1452,8 +1453,8 @@ router.post("/activitypub/inbox", (req, res) => { { url: signature_header.keyId, headers: { - Accept: "application/activity+json", - "Content-Type": "application/activity+json", + Accept: activityPubContentType, + "Content-Type": activityPubContentType, }, }, function (error, response, actor) { diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index c405572..cc97ab8 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -6,6 +6,10 @@ import getConfig, { frontendConfig } from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; import EventGroup, { IEventGroup } from "../models/EventGroup.js"; +import { + acceptsActivityPub, + activityPubContentType, +} from "../lib/activitypub.js"; const config = getConfig(); @@ -174,13 +178,8 @@ router.get("/:eventID", async (req: Request, res: Response) => { : null, url: `https://${config.general.domain}/` + req.params.eventID, }; - if ( - req.headers.accept && - (req.headers.accept.includes("application/activity+json") || - req.headers.accept.includes("application/json") || - req.headers.accept.includes("application/json+ld")) - ) { - res.header("Content-Type", "application/activity+json").send( + if (acceptsActivityPub(req)) { + res.header("Content-Type", activityPubContentType).send( JSON.parse(event.activityPubActor || "{}"), ); } else {