mirror of
https://example.com
synced 2024-11-22 16:46:38 +09:00
fix (backend): validate ActivityPub Content-Type
Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
parent
0afd1de14e
commit
a7fc8f22c1
4 changed files with 73 additions and 32 deletions
|
@ -65,7 +65,8 @@ export function createSignedGet(args: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: objectAssignWithLcKey(
|
headers: objectAssignWithLcKey(
|
||||||
{
|
{
|
||||||
Accept: "application/activity+json, application/ld+json",
|
Accept:
|
||||||
|
'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
Date: new Date().toUTCString(),
|
Date: new Date().toUTCString(),
|
||||||
Host: new URL(args.url).hostname,
|
Host: new URL(args.url).hostname,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import { getUserKeypair } from "@/misc/keypair-store.js";
|
import { getUserKeypair } from "@/misc/keypair-store.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User, ILocalUser } from "@/models/entities/user.js";
|
||||||
import { getResponse } from "../../misc/fetch.js";
|
import { getResponse } from "@/misc/fetch.js";
|
||||||
import { createSignedPost, createSignedGet } from "./ap-request.js";
|
import { createSignedPost, createSignedGet } from "./ap-request.js";
|
||||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
import type { Response } from "node-fetch";
|
||||||
|
import type { IObject } from "./type.js";
|
||||||
|
|
||||||
export default async (user: { id: User["id"] }, url: string, object: any) => {
|
export default async (user: { id: User["id"] }, url: string, object: any) => {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
@ -31,30 +32,64 @@ export default async (user: { id: User["id"] }, url: string, object: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get AP object with http-signature
|
* Get ActivityPub object
|
||||||
* @param user http-signature user
|
* @param user http-signature user
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
export async function signedGet(url: string, user: { id: User["id"] }) {
|
export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
|
||||||
apLogger.debug(`Running signedGet on url: ${url}`);
|
let res: Response;
|
||||||
const keypair = await getUserKeypair(user.id);
|
|
||||||
|
|
||||||
const req = createSignedGet({
|
if (user != null) {
|
||||||
key: {
|
const keypair = await getUserKeypair(user.id);
|
||||||
privateKeyPem: keypair.privateKey,
|
const req = createSignedGet({
|
||||||
keyId: `${config.url}/users/${user.id}#main-key`,
|
key: {
|
||||||
},
|
privateKeyPem: keypair.privateKey,
|
||||||
url,
|
keyId: `${config.url}/users/${user.id}#main-key`,
|
||||||
additionalHeaders: {
|
},
|
||||||
"User-Agent": config.userAgent,
|
url,
|
||||||
},
|
additionalHeaders: {
|
||||||
});
|
"User-Agent": config.userAgent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const res = await getResponse({
|
res = await getResponse({
|
||||||
url,
|
url,
|
||||||
method: req.request.method,
|
method: req.request.method,
|
||||||
headers: req.request.headers,
|
headers: req.request.headers,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
res = await getResponse({
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept:
|
||||||
|
'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
"User-Agent": config.userAgent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await res.json();
|
const contentType = res.headers.get("content-type");
|
||||||
|
if (contentType == null || !validateContentType(contentType)) {
|
||||||
|
throw new Error("Invalid Content Type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.body == null) throw new Error("body is null");
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
if (text.length > 65536) throw new Error("too big result");
|
||||||
|
|
||||||
|
return JSON.parse(text) as IObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateContentType(contentType: string): boolean {
|
||||||
|
const parts = contentType.split(/\s*;\s*/);
|
||||||
|
if (parts[0] === "application/activity+json") return true;
|
||||||
|
if (parts[0] !== "application/ld+json") return false;
|
||||||
|
return parts
|
||||||
|
.slice(1)
|
||||||
|
.some(
|
||||||
|
(part) =>
|
||||||
|
part.trim() === 'profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import { getJson } from "@/misc/fetch.js";
|
|
||||||
import type { ILocalUser } from "@/models/entities/user.js";
|
import type { ILocalUser } from "@/models/entities/user.js";
|
||||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||||
import { fetchMeta } from "@/misc/backend-rs.js";
|
import { fetchMeta } from "@/misc/backend-rs.js";
|
||||||
import { extractHost, isSelfHost } from "backend-rs";
|
import { extractHost, isSelfHost } from "backend-rs";
|
||||||
import { signedGet } from "./request.js";
|
import { apGet } from "./request.js";
|
||||||
import type { IObject, ICollection, IOrderedCollection } from "./type.js";
|
import type { IObject, ICollection, IOrderedCollection } from "./type.js";
|
||||||
import { isCollectionOrOrderedCollection, getApId } from "./type.js";
|
import { isCollectionOrOrderedCollection, getApId } from "./type.js";
|
||||||
import { Notes, NoteReactions, Polls, Users } from "@/models/index.js";
|
import { Notes, NoteReactions, Polls, Users } from "@/models/index.js";
|
||||||
|
@ -114,11 +113,7 @@ export default class Resolver {
|
||||||
apLogger.debug("Getting object from remote, authenticated as user:");
|
apLogger.debug("Getting object from remote, authenticated as user:");
|
||||||
apLogger.debug(JSON.stringify(this.user, null, 2));
|
apLogger.debug(JSON.stringify(this.user, null, 2));
|
||||||
|
|
||||||
const object = (
|
const object = await apGet(value, this.user);
|
||||||
this.user
|
|
||||||
? await signedGet(value, this.user)
|
|
||||||
: await getJson(value, "application/activity+json, application/ld+json")
|
|
||||||
) as IObject;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
object == null ||
|
object == null ||
|
||||||
|
|
|
@ -161,7 +161,17 @@ export default async function (ctx: Koa.Context) {
|
||||||
|
|
||||||
// When doing a conditional request, we MUST return a "Cache-Control" header
|
// When doing a conditional request, we MUST return a "Cache-Control" header
|
||||||
// if a normal 200 response would have included.
|
// if a normal 200 response would have included.
|
||||||
ctx.set("Cache-Control", "max-age=31536000, immutable");
|
if (contentType === "application/octet-stream") {
|
||||||
|
ctx.vary("Accept");
|
||||||
|
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
|
||||||
|
|
||||||
|
if (ctx.header.accept?.match(/activity\+json|ld\+json/)) {
|
||||||
|
ctx.status = 400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.set("Cache-Control", "max-age=2592000, s-maxage=172800, immutable");
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.fresh) {
|
if (ctx.fresh) {
|
||||||
ctx.status = 304;
|
ctx.status = 304;
|
||||||
|
|
Loading…
Reference in a new issue