Some refactoring and removing the need to specify user

This is going to be single user anyway, so why use https://death.id.au/death.au when I can just be https://death.id.au
This commit is contained in:
Gordon Pedersen 2023-09-27 09:49:32 +10:00
parent 71f38cca62
commit 2701786de8
7 changed files with 107 additions and 130 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -7,7 +7,8 @@
"ngrok": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
},
"devDependencies": {
"@types/node-forge": "^1.3.5"
"@types/node-forge": "^1.3.5",
"bun-types": "^1.0.3"
},
"peerDependencies": {
"typescript": "^5.0.0"

View file

@ -2,21 +2,32 @@ import { ACCOUNT, ACTOR, HOSTNAME, PUBLIC_KEY } from "./env"
import * as db from "./db"
import { reqIsActivityPub, send, verify } from "./request"
import outbox from "./outbox"
import inbox from "./inbox"
export default (req: Request): Response | Promise<Response> | undefined => {
const url = new URL(req.url)
let match
if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 })
else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1])
else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/followers\/?$/i))) return getFollowers(req, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(req, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2])
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1])
// else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1])
// else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/followers\/?$/i))) return getFollowers(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(req, match[1])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2])
// else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2])
else if(req.method == "GET" && (match = url.pathname.match(/^\/?$/i))) return getActor(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/?$/i))) return getOutbox(req, ACCOUNT)
else if(req.method == "POST" && (match = url.pathname.match(/^\/inbox\/?$/i))) return postInbox(req, ACCOUNT)
else if(req.method == "POST" && (match = url.pathname.match(/^\/outbox\/?$/i))) return postOutbox(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT)
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1])
else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, ACCOUNT, match[1])
return undefined
}
@ -41,8 +52,6 @@ const postOutbox = async (req:Request, account:string):Promise<Response> => {
// ensure that the verified actor matches the actor in the request body
if (ACTOR !== body.actor) return new Response("", { status: 401 })
// console.log(body)
return await outbox(body)
}
@ -66,46 +75,17 @@ const postInbox = async (req:Request, account:string):Promise<Response> => {
// ensure that the verified actor matches the actor in the request body
if (from !== body.actor) return new Response("", { status: 401 })
// console.log(body)
return await inbox(body)
// TODO: add support for more types! we want replies, likes, boosts, etc!
switch (body.type) {
case "Follow": await follow(body);
case "Undo": await undo(body);
case "Accept": await accept(body);
case "Reject": await reject(body);
}
// switch (body.type) {
// case "Follow": await follow(body);
// case "Undo": await undo(body);
// case "Accept": await accept(body);
// case "Reject": await reject(body);
// }
return new Response("", { status: 204 })
}
const follow = async (body:any) => {
await send(ACTOR, body.actor, {
"@context": "https://www.w3.org/ns/activitystreams",
id: `https://${HOSTNAME}/${crypto.randomUUID()}`, // TODO: de-randomise this?
type: "Accept",
actor: ACTOR,
object: body,
});
await db.createFollower(body.actor, body.id);
}
const undo = async (body:any) => {
switch (body.object.type) {
case "Follow": await db.deleteFollower(body.actor); break
}
}
const accept = async (body:any) => {
switch (body.object.type) {
case "Follow": await db.acceptFollowing(body.actor); break
}
}
const reject = async (body:any) => {
switch (body.object.type) {
case "Follow": await db.deleteFollowing(body.actor); break
}
// return new Response("", { status: 204 })
}
const getOutbox = async (req:Request, account:string):Promise<Response> => {

View file

@ -13,9 +13,9 @@ export const HOSTNAME = /*(Bun.env.PROJECT_DOMAIN && `${Bun.env.PROJECT_DOMAIN}.
export const NODE_ENV = Bun.env.NODE_ENV || "development"
export const PORT = Bun.env.PORT || "3000"
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME + "/"
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME
export const ACTOR = BASE_URL + ACCOUNT
export const ACTOR = BASE_URL //+ '/' + ACCOUNT
// in development, generate a key pair to make it easier to get started
const keypair =

View file

@ -25,6 +25,8 @@ export default async function inbox(activity:any) {
case "Reject": reject(activity); break;
case "Undo": undo(activity); break;
}
return new Response("", { status: 204 })
}
const follow = async (activity:any, id:string) => {

View file

@ -5,26 +5,13 @@ import { fetchObject } from "./request";
const server = Bun.serve({
port: 3000,
fetch(req): Response | Promise<Response> {
fetch(req: Request): Response | Promise<Response> {
const url = new URL(req.url)
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`)
if(req.method === "GET" && url.pathname === "/.well-known/webfinger") {
const resource = url.searchParams.get("resource")
if(resource !== `acct:${ACCOUNT}@${HOSTNAME}`) return new Response("", { status: 404 })
return Response.json({
subject: `acct:${ACCOUNT}@${HOSTNAME}`,
links: [
{
rel: "self",
type: "application/activity+json",
href: ACTOR,
},
],
}, { headers: { "content-type": "application/activity+json" }})
return webfinger(req, url.searchParams.get("resource"))
}
else if(req.method === "GET" && url.pathname === "/fetch") {
const object_url = url.searchParams.get('url')
@ -36,5 +23,27 @@ const server = Bun.serve({
return admin(req) || activitypub(req) || new Response("How did we get here?", { status: 404 })
},
});
const webfinger = async (req: Request, resource: string | null) => {
if(resource !== `acct:${ACCOUNT}@${HOSTNAME}`) return new Response("", { status: 404 })
return Response.json({
subject: `acct:${ACCOUNT}@${HOSTNAME}`,
aliases: [ACTOR],
links: [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": ACTOR
},
{
rel: "self",
type: "application/activity+json",
href: ACTOR,
},
],
}, { headers: { "content-type": "application/activity+json" }})
}
console.log(`Listening on http://localhost:${server.port} ...`);

View file

@ -1,60 +1,69 @@
import forge from "node-forge" // import crypto from "node:crypto"
import { ACTOR, PRIVATE_KEY } from "./env";
export const activityMimeTypes:string[] = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']
export function reqIsActivityPub(req:Request) {
const contentType = req.headers.get("Accept")
return contentType?.includes('application/activity+json')
const activityPub = contentType?.includes('application/activity+json')
|| contentType?.includes('application/ld+json; profile="https://www.w3.org/ns/activitystreams"')
|| contentType?.includes('application/ld+json')
return activityPub
}
/** Fetches and returns an actor at a URL. */
async function fetchActor(url:string) {
return (await fetchObject(ACTOR, url)).json()
}
// this function adds / modifies the appropriate headers for signing a request, then calls fetch
export function signedFetch(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>
{
const urlObj = typeof url === 'string' ? new URL(url)
: url instanceof Request ? new URL(url.url)
: url
/** Fetches and returns an object at a URL. */
// export async function fetchObject(url:string) {
// const res = await fetch(url, {
// headers: { accept: "application/activity+json" },
// });
// if (res.status < 200 || 299 < res.status) {
// throw new Error(`Received ${res.status} fetching object.`);
// }
// return res.json();
// }
/** Fetches and returns an object at a URL. */
export async function fetchObject(sender:string, object_url:string) {
console.log(`fetch ${object_url}`)
const url = new URL(object_url)
const path = url.pathname
if(!init) init = {}
const headers:any = init.headers || {}
const method = init.method || (url instanceof Request ? url.method : 'GET')
const path = urlObj.pathname + urlObj.search + urlObj.hash
const body = init.body ? (typeof init.body === 'string' ? init.body : JSON.stringify(init?.body)) : null
const digest = body ? new Bun.CryptoHasher("sha256").update(body).digest("base64") : null
const d = new Date();
const key = forge.pki.privateKeyFromPem(PRIVATE_KEY)
const data = [
`(request-target): get ${path}`,
`host: ${url.hostname}`,
`date: ${d.toUTCString()}`
].join("\n")
const dataObj:any = { }
dataObj['(request-target)'] = `${method.toLowerCase()} ${path}`
dataObj.host = urlObj.hostname
dataObj.date = d.toUTCString()
if(digest) dataObj.digest = `SHA-256=${digest}`
const data = Object.entries(dataObj).map(([key, value]) => `${key}: ${value}`).join('\n')
const signature = forge.util.encode64(key.sign(forge.md.sha256.create().update(data)))
const signatureObj = {
keyId: `${ACTOR}#main-key`,
headers: Object.keys(dataObj).join(' '),
signature: signature
}
const res = await fetch(object_url, {
method: "GET",
headers: {
host: url.hostname,
date: d.toUTCString(),
"content-type": "application/json",
signature: `keyId="${sender}#main-key",headers="(request-target) host date",signature="${signature}"`,
accept: "application/json",
}
});
headers.host = urlObj.hostname
headers.date = d.toUTCString()
if(digest) headers.digest = `SHA-256=${digest}`
headers.signature = Object.entries(signatureObj).map(([key,value]) => `${key}="${value}"`).join(',')
headers.accept = 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
if(body) headers["Content-Type"] = 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
return fetch(url, {...init, headers })
}
/** Fetches and returns an actor at a URL. */
async function fetchActor(url:string) {
return await (await fetchObject(ACTOR, url)).json()
}
/** Fetches and returns an object at a URL. */
export async function fetchObject(sender:string, object_url:string) {
console.log(`fetch ${object_url}`)
const res = await fetch(object_url);
if (res.status < 200 || 299 < res.status) {
throw new Error(res.statusText + ": " + (await res.text()));
@ -70,37 +79,13 @@ export async function fetchObject(sender:string, object_url:string) {
*/
export async function send(sender:string, recipient:string, message:any) {
console.log(`Sending to ${recipient}`, message)
const url = new URL(recipient)
// TODO: revisit fetch actor to use webfinger to get the inbox maybe?
const actor = await fetchActor(recipient)
const path = actor.inbox.replace("https://" + url.hostname, "")
const body = JSON.stringify(message)
const digest = new Bun.CryptoHasher("sha256").update(body).digest("base64")
const d = new Date();
const key = forge.pki.privateKeyFromPem(PRIVATE_KEY)
const data = [
`(request-target): post ${path}`,
`host: ${url.hostname}`,
`date: ${d.toUTCString()}`,
`digest: SHA-256=${digest}`,
].join("\n")
const signature = forge.util.encode64(key.sign(forge.md.sha256.create().update(data)))
const res = await fetch(actor.inbox, {
const res = await signedFetch(actor.inbox, {
method: "POST",
headers: {
host: url.hostname,
date: d.toUTCString(),
digest: `SHA-256=${digest}`,
"content-type": "application/json",
signature: `keyId="${sender}#main-key",headers="(request-target) host date digest",signature="${signature}"`,
accept: "application/json",
},
body,
});