restructure

This commit is contained in:
Moon Man 2023-12-26 08:06:38 -05:00
parent 468878bdaf
commit c46f5ee1eb
5 changed files with 159 additions and 93 deletions

View File

@ -1,79 +1,52 @@
import { Sha256Signer } from "activitypub-http-signatures";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { Article } from "./article.js";
import { User } from "./user.js";
import { readFileSync } from "fs";
import { User, getByActor } from "./user.js";
import { fillRoute } from "./router.js";
import { streamToString, hashDigest } from "./util.js";
import { signedFetch } from "./net.js";
export const CONTEXT = "https://www.w3.org/ns/activitystreams";
export const PUBLIC = CONTEXT + "#Public";
export const TYPE = "application/activity+json";
const hasher = createHash("sha256");
export const handleInboxPost = async (req: Request) => {
try {
if (!req.body) {
console.warn("no body");
return;
}
const hashDigest =(payload: string | Buffer) =>
"sha-256="
+ hasher
.update((Buffer.isBuffer(payload) ? payload : Buffer.from(payload)).toString("hex"))
.digest("base64");
const body = await streamToString(req.body);
let activity: Record<string, any>;
try {
activity = JSON.parse(body);
}
catch (e) {
console.warn("body json parse failed");
return;
}
const flattenHeaders = (headers: Record<string, string>) =>
Object.entries(headers).map(([key, value]) => `${key}: ${value}`);
if (activity.type === "Follow") {
const actor = await getByActor(activity.object);
interface SignedInit {
keyId: string;
privateKey: string;
digest?: string;
};
export const signedFetch = async (url: string, init: RequestInit, signedInit: SignedInit) => {
const signedHeaders: HeadersInit = [
["Date", new Date().toUTCString()],
["Host", new URL(url).host],
["Content-Type", `application/ld+json; profile="${CONTEXT}"`]
];
if (signedInit.digest) {
signedHeaders.push(["Digest", signedInit.digest]);
}
else if (init.body) {
if (Buffer.isBuffer(init.body) || typeof init.body === "string") {
signedHeaders.push(["Digest", hashDigest(init.body)]);
}
else throw "unsupported body type";
}
const signer = new Sha256Signer({
privateKey: signedInit.privateKey,
publicKeyId: signedInit.keyId
});
const signature = signer.sign({
url,
method: init.method as string,
headers: signedHeaders
});
signedHeaders.push(["Signature", signature]);
const newHeaders = new Headers();
if (Array.isArray(init.headers)) {
for (const header of init.headers) {
if (Array.isArray(header))
newHeaders.set(header[0], header[1]);
else throw "unsupported headers type"; // Lazy.
}
for (const [key, value] of signedHeaders) {
newHeaders.set(key, value);
}
init.headers = newHeaders;
}
else throw "unsupported headers type"; // Lazy.
return fetch(url, init);
if (actor) {
const follower = activity.actor;
}
else {
console.warn("follow attempt on unknown user");
}
}
else if (activity.type === "Undo") {
}
else {
console.warn("unsupported activity type");
}
}
finally { }
};
/*
export const sendActivity = async (publicKeyId: string, privateKey: string, activity: string, hash: string, inbox: string) => {
const headers: Record<string, string> = {
date: new Date().toUTCString(),
@ -97,6 +70,7 @@ export const sendActivity = async (publicKeyId: string, privateKey: string, acti
body: activity
});
};
*/
export const deleteArticleActivity = (article: Article, user: User) => {
const actor = fillRoute("actor", user.nickname);
@ -160,7 +134,7 @@ export const sendAll = async (keyId: string, privateKey: string, activity: Recor
activity = typeof activity === "string"
? activity
: JSON.stringify(activity, null, 4);
const hash = hashDigest(activity);
const init: RequestInit = { method: "POST", body: activity };
@ -181,4 +155,26 @@ export const orderedCollection = (id: string, orderedItems: (string | Record<str
context: CONTEXT,
id,
orderedItems
});
});
export const userToPerson = (user: User) => {
const id = fillRoute("actor", user.nickname);
return {
id,
type: "Person",
"@context": CONTEXT,
discoverable: true,
preferredUsername: user.nickname,
name: user.name,
summary: user.bio,
url: id,
inbox: fillRoute("inbox", user.nickname),
outbox: fillRoute("outbox", user.nickname),
publicKey: {
id: + "#main-key",
owner: id,
publicKeyPem: user.public_key
}
};
};

View File

@ -1,10 +1,10 @@
import { readFileSync } from "fs";
import express from "express";
import ActivitypubExpress from "activitypub-express";
import { get as getOutbox } from "./outbox.js";
import { get as getUserByNickname, getId as getUserId } from "./user.js";
import { Routes, fillRoute } from "./router.js";
import { getBySlug } from "./article.js";
import { Routes } from "./router.js";
import { getBySlug as getArticleBySlug } from "./article.js";
import { userToPerson, TYPE as ACTIVITYPUB_TYPE } from "./activity.js";
const port = parseInt(process.env.port || "8080");
const app = express();
@ -42,7 +42,7 @@ app.get(Routes.outbox, async (req, res) => {
const userId = await getUserId(req.params.actor);
if (userId) {
const body = await getOutbox(userId).then((out) => JSON.stringify(out, null, 4));
res.append("Content-Type", "application/activity+json");
res.append("Content-Type", ACTIVITYPUB_TYPE);
res.send(body);
}
else {
@ -56,28 +56,11 @@ app.get(Routes.actor, async (req, res) => {
if (actor) {
const accept = req.headers["accept"] || "";
if (accept.startsWith("application/activity+json")) {
const id = fillRoute("actor", nickname);
const obj = {
id,
type: "Person",
"@context": "https://www.w3.org/ns/activitystreams",
discoverable: true,
preferredUsername: actor.nickname,
name: actor.name,
summary: actor.bio,
url: id,
inbox: fillRoute("inbox", actor.nickname),
outbox: fillRoute("outbox", actor.nickname),
publicKey: {
id: id + "#main-key",
owner: id,
publicKeyPem: actor.public_key
}
};
if (accept.includes(ACTIVITYPUB_TYPE)) {
const obj = userToPerson(actor);
const body = JSON.stringify(obj, null, 4);
res.append("Content-Type", "application/activity+json");
res.set("Content-Type", ACTIVITYPUB_TYPE);
res.send(body);
}
else {
@ -91,7 +74,7 @@ app.get(Routes.actor, async (req, res) => {
});
app.get("/:slug.html", async (req, res) => {
const article = await getBySlug(req.params.slug);
const article = await getArticleBySlug(req.params.slug);
if (!article || article.deleted) {
res.status(404).send("not found");
@ -103,7 +86,7 @@ app.get("/:slug.html", async (req, res) => {
});
app.get("/:slug.md", async (req, res) => {
const article = await getBySlug(req.params.slug);
const article = await getArticleBySlug(req.params.slug);
if (!article || article.deleted) {
res.status(404).send("not found");

61
src/net.ts Normal file
View File

@ -0,0 +1,61 @@
import { CONTEXT } from "./activity.js";
import { hashDigest } from "./util.js";
import { Sha256Signer } from "activitypub-http-signatures";
export const flattenHeaders = (headers: Record<string, string>) =>
Object.entries(headers).map(([key, value]) => `${key}: ${value}`);
export interface SignedInit {
keyId: string;
privateKey: string;
digest?: string;
};
export const signedFetch = async (url: string, init: RequestInit, signedInit: SignedInit) => {
const signedHeaders: HeadersInit = [
["Date", new Date().toUTCString()],
["Host", new URL(url).host],
["Content-Type", `application/ld+json; profile="${CONTEXT}"`]
];
if (signedInit.digest) {
signedHeaders.push(["Digest", signedInit.digest]);
}
else if (init.body) {
if (Buffer.isBuffer(init.body) || typeof init.body === "string") {
signedHeaders.push(["Digest", hashDigest(init.body)]);
}
else throw "unsupported body type";
}
const signer = new Sha256Signer({
privateKey: signedInit.privateKey,
publicKeyId: signedInit.keyId
});
const signature = signer.sign({
url,
method: init.method as string,
headers: signedHeaders
});
signedHeaders.push(["Signature", signature]);
const newHeaders = new Headers();
if (Array.isArray(init.headers)) {
for (const header of init.headers) {
if (Array.isArray(header))
newHeaders.set(header[0], header[1]);
else throw "unsupported headers type"; // Lazy.
}
for (const [key, value] of signedHeaders) {
newHeaders.set(key, value);
}
init.headers = newHeaders;
}
else throw "unsupported headers type"; // Lazy.
return fetch(url, init);
};

View File

@ -2,6 +2,7 @@ import { generateKeyPair } from "node:crypto";
import { z } from "zod";
import db from "./db.js";
import { getFollowers } from "./collection.js";
import { fillRoute } from "./router.js";
export const nicknameRegex = /^[a-zA-Z0-9_]+$/;
@ -25,6 +26,17 @@ export const get = async (nickname: string) =>
.first()
;
const EXTRACT_NICKNAME = new RegExp("/" + fillRoute("actor", "(.+)").replaceAll("/", "\\/") + "/");
export const getByActor = async (actor: string) => {
const matchArray = actor.match(EXTRACT_NICKNAME);
if (matchArray) {
const nickname = matchArray[1];
return get(nickname);
}
else return null;
};
export const getNickname = async (userId: number): Promise<string | null> =>
db("users")
.select("nickname")

14
src/util.ts Normal file
View File

@ -0,0 +1,14 @@
import { createHash } from "node:crypto";
const hasher = createHash("sha256");
export const hashDigest = (payload: string | Buffer) =>
"sha-256="
+ hasher
.update((Buffer.isBuffer(payload) ? payload : Buffer.from(payload)).toString("hex"))
.digest("base64");
export const streamToString = async (stream: ReadableStream<Uint8Array>) => {
const arr = new Uint8Array(await new Response(stream).arrayBuffer());
return new TextDecoder("utf-8").decode(arr);
};