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:
parent
71f38cca62
commit
2701786de8
7 changed files with 107 additions and 130 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -7,7 +7,8 @@
|
||||||
"ngrok": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
|
"ngrok": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node-forge": "^1.3.5"
|
"@types/node-forge": "^1.3.5",
|
||||||
|
"bun-types": "^1.0.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
|
@ -2,21 +2,32 @@ import { ACCOUNT, ACTOR, HOSTNAME, PUBLIC_KEY } from "./env"
|
||||||
import * as db from "./db"
|
import * as db from "./db"
|
||||||
import { reqIsActivityPub, send, verify } from "./request"
|
import { reqIsActivityPub, send, verify } from "./request"
|
||||||
import outbox from "./outbox"
|
import outbox from "./outbox"
|
||||||
|
import inbox from "./inbox"
|
||||||
|
|
||||||
export default (req: Request): Response | Promise<Response> | undefined => {
|
export default (req: Request): Response | Promise<Response> | undefined => {
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
let match
|
let match
|
||||||
|
|
||||||
if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 })
|
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(/^\/([^\/]+)\/?$/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 == "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 == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1])
|
||||||
else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(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(/^\/([^\/]+)\/?$/i))) return getActor(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(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2])
|
// 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\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2])
|
// 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
|
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
|
// ensure that the verified actor matches the actor in the request body
|
||||||
if (ACTOR !== body.actor) return new Response("", { status: 401 })
|
if (ACTOR !== body.actor) return new Response("", { status: 401 })
|
||||||
|
|
||||||
// console.log(body)
|
|
||||||
|
|
||||||
return await outbox(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
|
// ensure that the verified actor matches the actor in the request body
|
||||||
if (from !== body.actor) return new Response("", { status: 401 })
|
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!
|
// TODO: add support for more types! we want replies, likes, boosts, etc!
|
||||||
switch (body.type) {
|
// switch (body.type) {
|
||||||
case "Follow": await follow(body);
|
// case "Follow": await follow(body);
|
||||||
case "Undo": await undo(body);
|
// case "Undo": await undo(body);
|
||||||
case "Accept": await accept(body);
|
// case "Accept": await accept(body);
|
||||||
case "Reject": await reject(body);
|
// case "Reject": await reject(body);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return new Response("", { status: 204 })
|
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOutbox = async (req:Request, account:string):Promise<Response> => {
|
const getOutbox = async (req:Request, account:string):Promise<Response> => {
|
||||||
|
|
|
@ -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 NODE_ENV = Bun.env.NODE_ENV || "development"
|
||||||
export const PORT = Bun.env.PORT || "3000"
|
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
|
// in development, generate a key pair to make it easier to get started
|
||||||
const keypair =
|
const keypair =
|
||||||
|
|
|
@ -25,6 +25,8 @@ export default async function inbox(activity:any) {
|
||||||
case "Reject": reject(activity); break;
|
case "Reject": reject(activity); break;
|
||||||
case "Undo": undo(activity); break;
|
case "Undo": undo(activity); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Response("", { status: 204 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const follow = async (activity:any, id:string) => {
|
const follow = async (activity:any, id:string) => {
|
||||||
|
|
39
src/index.ts
39
src/index.ts
|
@ -5,26 +5,13 @@ import { fetchObject } from "./request";
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
fetch(req): Response | Promise<Response> {
|
fetch(req: Request): Response | Promise<Response> {
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
|
|
||||||
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`)
|
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`)
|
||||||
|
|
||||||
if(req.method === "GET" && url.pathname === "/.well-known/webfinger") {
|
if(req.method === "GET" && url.pathname === "/.well-known/webfinger") {
|
||||||
const resource = url.searchParams.get("resource")
|
return webfinger(req, 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" }})
|
|
||||||
}
|
}
|
||||||
else if(req.method === "GET" && url.pathname === "/fetch") {
|
else if(req.method === "GET" && url.pathname === "/fetch") {
|
||||||
const object_url = url.searchParams.get('url')
|
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 })
|
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} ...`);
|
console.log(`Listening on http://localhost:${server.port} ...`);
|
113
src/request.ts
113
src/request.ts
|
@ -1,60 +1,69 @@
|
||||||
import forge from "node-forge" // import crypto from "node:crypto"
|
import forge from "node-forge" // import crypto from "node:crypto"
|
||||||
import { ACTOR, PRIVATE_KEY } from "./env";
|
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) {
|
export function reqIsActivityPub(req:Request) {
|
||||||
const contentType = req.headers.get("Accept")
|
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; profile="https://www.w3.org/ns/activitystreams"')
|
||||||
|| contentType?.includes('application/ld+json')
|
|| contentType?.includes('application/ld+json')
|
||||||
|
return activityPub
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches and returns an actor at a URL. */
|
// this function adds / modifies the appropriate headers for signing a request, then calls fetch
|
||||||
async function fetchActor(url:string) {
|
export function signedFetch(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>
|
||||||
return (await fetchObject(ACTOR, url)).json()
|
{
|
||||||
}
|
const urlObj = typeof url === 'string' ? new URL(url)
|
||||||
|
: url instanceof Request ? new URL(url.url)
|
||||||
|
: url
|
||||||
|
|
||||||
/** Fetches and returns an object at a URL. */
|
if(!init) init = {}
|
||||||
// export async function fetchObject(url:string) {
|
const headers:any = init.headers || {}
|
||||||
// const res = await fetch(url, {
|
const method = init.method || (url instanceof Request ? url.method : 'GET')
|
||||||
// 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
|
|
||||||
|
|
||||||
|
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 d = new Date();
|
||||||
|
|
||||||
const key = forge.pki.privateKeyFromPem(PRIVATE_KEY)
|
const key = forge.pki.privateKeyFromPem(PRIVATE_KEY)
|
||||||
|
|
||||||
const data = [
|
const dataObj:any = { }
|
||||||
`(request-target): get ${path}`,
|
dataObj['(request-target)'] = `${method.toLowerCase()} ${path}`
|
||||||
`host: ${url.hostname}`,
|
dataObj.host = urlObj.hostname
|
||||||
`date: ${d.toUTCString()}`
|
dataObj.date = d.toUTCString()
|
||||||
].join("\n")
|
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 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, {
|
headers.host = urlObj.hostname
|
||||||
method: "GET",
|
headers.date = d.toUTCString()
|
||||||
headers: {
|
if(digest) headers.digest = `SHA-256=${digest}`
|
||||||
host: url.hostname,
|
headers.signature = Object.entries(signatureObj).map(([key,value]) => `${key}="${value}"`).join(',')
|
||||||
date: d.toUTCString(),
|
headers.accept = 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
|
||||||
"content-type": "application/json",
|
if(body) headers["Content-Type"] = 'application/ld+json; profile="http://www.w3.org/ns/activitystreams"'
|
||||||
signature: `keyId="${sender}#main-key",headers="(request-target) host date",signature="${signature}"`,
|
|
||||||
accept: "application/json",
|
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) {
|
if (res.status < 200 || 299 < res.status) {
|
||||||
throw new Error(res.statusText + ": " + (await res.text()));
|
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) {
|
export async function send(sender:string, recipient:string, message:any) {
|
||||||
console.log(`Sending to ${recipient}`, message)
|
console.log(`Sending to ${recipient}`, message)
|
||||||
const url = new URL(recipient)
|
|
||||||
// TODO: revisit fetch actor to use webfinger to get the inbox maybe?
|
// TODO: revisit fetch actor to use webfinger to get the inbox maybe?
|
||||||
const actor = await fetchActor(recipient)
|
const actor = await fetchActor(recipient)
|
||||||
const path = actor.inbox.replace("https://" + url.hostname, "")
|
|
||||||
|
|
||||||
const body = JSON.stringify(message)
|
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 res = await signedFetch(actor.inbox, {
|
||||||
|
|
||||||
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, {
|
|
||||||
method: "POST",
|
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,
|
body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue