Add auth token verification, minor fixes and docker

This commit is contained in:
Gordon Pedersen 2023-10-16 15:39:11 +11:00
parent fd8c2073f0
commit 41c2e27f5e
16 changed files with 197 additions and 116 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
README.md
db.sqlite*
_data
.vscode
node_modules

1
.gitignore vendored
View file

@ -173,4 +173,5 @@ _content/_data/_inbox
_content/_data/_outbox _content/_data/_outbox
_content/posts _content/posts
_site _site
_data
db.sqlite* db.sqlite*

38
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,38 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"command": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000",
"windows":{
"command": "C:\\Users\\death\\AppData\\Local\\Microsoft\\WinGet\\Links\\ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
},
"label": "ngrok tunnel",
"detail": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"type": "shell",
"command": "bun run --watch src/index.ts",
"label": "bun run",
"detail": "bun run --watch src/index.ts",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"type": "shell",
"command": "docker compose up",
"label": "docker compose up",
"detail": "docker compose up",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
}
]
}

1
CNAME
View file

@ -1 +0,0 @@
death.id.au

11
Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM oven/bun:latest
WORKDIR /app
COPY ./package.json /app
COPY ./bun.lockb /app
RUN bun install
COPY . /app
CMD [ "bun", "run", "/app/src/index.ts"]

BIN
bun.lockb

Binary file not shown.

19
docker-compose.yaml Normal file
View file

@ -0,0 +1,19 @@
version: '3'
services:
app:
build: .
container_name: bun-activitypub
command: bun run --watch /app/src/index.ts
environment:
- ACCOUNT=death.au
- REAL_NAME=Gordon Pedersen
- HOSTNAME=death.id.au
- TOKEN_ENDPOINT=https://deathau-cellar-door.glitch.me/token
- WEB_SITE_HOSTNAME=www.death.id.au
- DATA_PATH=/data
ports:
- 3000:3000
volumes:
- .:/app
- ./_data:/data

View file

@ -3,12 +3,11 @@
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "bun run --watch src/index.ts", "start": "bun run --watch src/index.ts"
"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" "bun-types": "^1.0.5"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"

View file

@ -1,4 +1,4 @@
import { verify } from "./request" import { authorized, verify } from "./request"
import outbox from "./outbox" import outbox from "./outbox"
import inbox from "./inbox" import inbox from "./inbox"
import ACTOR from "../actor" import ACTOR from "../actor"
@ -31,20 +31,12 @@ export function reqIsActivityPub(req:Request) {
return activityPubTypes.some(t => contentType?.includes(t)) return activityPubTypes.some(t => contentType?.includes(t))
} }
export function idsFromValue(value:any):string[] {
if (!value) return []
else if (typeof value === 'string') return [value]
else if (value.id) return [value.id]
else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[]
return []
}
const postOutbox = async (req:Request):Promise<Response> => { const postOutbox = async (req:Request):Promise<Response> => {
console.log("PostOutbox") console.log("PostOutbox")
const bodyText = await req.text() if(!authorized(req)) return new Response('', { status: 401 })
// TODO: verify calls to the outbox, whether that be by basic authentication, bearer, or otherwise. const bodyText = await req.text()
const body = JSON.parse(bodyText) const body = JSON.parse(bodyText)
// ensure that the verified actor matches the actor in the request body // ensure that the verified actor matches the actor in the request body

View file

@ -1,20 +1,19 @@
import { idsFromValue } from "./activitypub" import { activityPubTypes } from "./env"
import { ADMIN_PASSWORD, ADMIN_USERNAME, activityPubTypes } from "./env"
import outbox from "./outbox" import outbox from "./outbox"
import { fetchObject } from "./request" import { authorized, fetchObject, idsFromValue } from "./request"
import ACTOR from "../actor" import ACTOR from "../actor"
import ActivityPubDB from "./db" import ActivityPubDB, { Activity } from "./db"
let db:ActivityPubDB let db:ActivityPubDB
export default (req: Request, database: ActivityPubDB): Response | Promise<Response> | undefined => { export default async (req: Request, database: ActivityPubDB): Promise<Response | Promise<Response> | undefined> => {
db = database db = database
const url = new URL(req.url) const url = new URL(req.url)
if(!url.pathname.startsWith('/admin')) return undefined if(!url.pathname.startsWith('/admin')) return undefined
url.pathname = url.pathname.substring(6) url.pathname = url.pathname.substring(6)
if(ADMIN_USERNAME && ADMIN_PASSWORD && !checkAuth(req.headers)) return new Response("", { status: 401 }) if(!(await authorized(req))) return new Response("", { status: 401 })
let match let match
if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 }) if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 })
@ -31,30 +30,14 @@ export default (req: Request, database: ActivityPubDB): Response | Promise<Respo
else if(req.method == "POST" && (match = url.pathname.match(/^\/reply\/(.+)\/?$/i))) return create(req, match[1]) else if(req.method == "POST" && (match = url.pathname.match(/^\/reply\/(.+)\/?$/i))) return create(req, match[1])
else if(req.method == "DELETE" && (match = url.pathname.match(/^\/delete\/(.+)\/?$/i))) return deletePost(req, match[1]) else if(req.method == "DELETE" && (match = url.pathname.match(/^\/delete\/(.+)\/?$/i))) return deletePost(req, match[1])
console.log(`Couldn't match admin path ${req.method} "${url.pathname}"`) console.info(`Couldn't match admin path ${req.method} "${url.pathname}"`)
return undefined return undefined
} }
const checkAuth = (headers: Headers): Boolean => {
// check the basic auth header
const auth = headers.get("Authorization")
const split = auth?.split("")
if(!split || split.length != 2 || split[0] !== "Basic") return false
const decoded = atob(split[1])
const [username, password] = decoded.split(":")
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD
}
// // rebuild the 11ty static pages
// export const rebuild = async(req:Request):Promise<Response> => {
// db.rebuild()
// return new Response("", { status: 201 })
// }
// create an activity // create an activity
const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => { const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => {
const body = await req.json() const body = await req.json() as any
if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo
@ -100,7 +83,7 @@ const idFromHandle = async(handle:string) => {
if(!host) return new Response('account not url or name@domain.tld', { status: 400 }) if(!host) return new Response('account not url or name@domain.tld', { status: 400 })
const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`, { headers: { 'accept': 'application/jrd+json'}}) const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`, { headers: { 'accept': 'application/jrd+json'}})
const webfinger = await res.json() const webfinger = await res.json() as any
if(!webfinger.links) return new Response("", { status: 404 }) if(!webfinger.links) return new Response("", { status: 404 })
const links:any[] = webfinger.links const links:any[] = webfinger.links
@ -114,7 +97,7 @@ const idFromHandle = async(handle:string) => {
const follow = async (req:Request, handle:string):Promise<Response> => { const follow = async (req:Request, handle:string):Promise<Response> => {
const url = await idFromHandle(handle) const url = await idFromHandle(handle)
console.log(`Following ${url}`) console.info(`Following ${url}`)
// send the follow request to the supplied actor // send the follow request to the supplied actor
return await outbox({ return await outbox({
@ -143,7 +126,7 @@ const unfollow = async (req:Request, handle:string):Promise<Response> => {
} }
const like = async (req:Request, object_url:string):Promise<Response> => { const like = async (req:Request, object_url:string):Promise<Response> => {
const object = await (await fetchObject(object_url)).json() const object = await (await fetchObject(object_url)).json() as any
return await outbox({ return await outbox({
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
@ -160,7 +143,7 @@ const unlike = async (req:Request, activity_id:string):Promise<Response> => {
const liked = db.listLiked() const liked = db.listLiked()
let existing = liked?.find(o => o?.activity_id === activity_id) let existing = liked?.find(o => o?.activity_id === activity_id)
if (!existing){ if (!existing){
const object = await (await fetchObject(activity_id)).json() const object = await (await fetchObject(activity_id)).json() as any
idsFromValue(object).forEach(id => { idsFromValue(object).forEach(id => {
const e = liked?.find(o => o.activity_id === id) const e = liked?.find(o => o.activity_id === id)
if(e) existing = e if(e) existing = e
@ -172,7 +155,7 @@ const unlike = async (req:Request, activity_id:string):Promise<Response> => {
} }
const dislike = async (req:Request, object_url:string):Promise<Response> => { const dislike = async (req:Request, object_url:string):Promise<Response> => {
const object = await (await fetchObject(object_url)).json() const object = await (await fetchObject(object_url)).json() as any
return await outbox({ return await outbox({
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
@ -201,7 +184,7 @@ const undislike = async (req:Request, object_id:string):Promise<Response> => {
} }
const share = async (req:Request, object_url:string):Promise<Response> => { const share = async (req:Request, object_url:string):Promise<Response> => {
const object = await (await fetchObject(object_url)).json() const object = await (await fetchObject(object_url)).json() as any
return await outbox({ return await outbox({
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",

View file

@ -1,12 +1,15 @@
import { Database, Statement } from "bun:sqlite" import { Database, Statement } from "bun:sqlite"
import { existsSync, mkdirSync } from "fs"
import { dirname } from "path"
import { DB_PATH } from "./env"
export interface ActivityObject { export type ActivityObject = {
activity_id: string activity_id: string
id: string id: string
published: string published: string
} }
export interface Activity extends ActivityObject { export type Activity = ActivityObject & {
type: string type: string
actor: string actor: string
published: string published: string
@ -15,11 +18,11 @@ export interface Activity extends ActivityObject {
object?: any object?: any
} }
export interface Following extends ActivityObject { export type Following = ActivityObject & {
accepted?: boolean accepted?: boolean
} }
export interface Post extends ActivityObject { export type Post = ActivityObject & {
attributedTo: string attributedTo: string
type: string type: string
content?: string content?: string
@ -32,7 +35,9 @@ export default class ActivityPubDB {
// Cached Statements // Cached Statements
constructor() { constructor() {
this.db = new Database('./db.sqlite') const dir = dirname(DB_PATH)
if(!existsSync(dir)) mkdirSync(dir, { recursive: true })
this.db = new Database(DB_PATH, {create:true, readwrite:true})
this.migrate() this.migrate()
this.cacheQueries() this.cacheQueries()
} }
@ -43,13 +48,11 @@ export default class ActivityPubDB {
migrate() { migrate() {
let version = (this.db.query("PRAGMA user_version;").get() as {user_version:number})?.user_version let version = (this.db.query("PRAGMA user_version;").get() as {user_version:number})?.user_version
console.log(`Hi from migrate! User version: ${version}`)
const statements:Statement[] = [] const statements:Statement[] = []
switch(version) { switch(version) {
case 0: case 0:
console.log("migrating db version 1")
this.db.exec("PRAGMA journal_mode = WAL;") this.db.exec("PRAGMA journal_mode = WAL;")
// Create the inbox table // Create the inbox table
@ -59,7 +62,7 @@ export default class ActivityPubDB {
[type] TEXT NOT NULL, [type] TEXT NOT NULL,
[actor] TEXT NOT NULL, [actor] TEXT NOT NULL,
[published] TEXT NOT NULL, [published] TEXT NOT NULL,
[to] TEXT NOT NULL, [to] TEXT,
[cc] TEXT, [cc] TEXT,
[activity] TEXT NOT NULL [activity] TEXT NOT NULL
)`)) )`))
@ -71,7 +74,7 @@ export default class ActivityPubDB {
[type] TEXT NOT NULL, [type] TEXT NOT NULL,
[actor] TEXT NOT NULL, [actor] TEXT NOT NULL,
[published] TEXT NOT NULL, [published] TEXT NOT NULL,
[to] TEXT NOT NULL, [to] TEXT,
[cc] TEXT, [cc] TEXT,
[activity] TEXT NOT NULL [activity] TEXT NOT NULL
)`)) )`))
@ -130,8 +133,7 @@ export default class ActivityPubDB {
} }
const migration = this.db.transaction((statements:Statement[]) => { const migration = this.db.transaction((statements:Statement[]) => {
statements.forEach(s => { statements.forEach(s => {
console.log(s.toString());
s.run(); s.run();
s.finalize() s.finalize()
}) })
@ -271,7 +273,7 @@ export default class ActivityPubDB {
$id: activity.id, $id: activity.id,
$type: activity.type, $type: activity.type,
$actor: activity.actor, $actor: activity.actor,
$published: activity.published, $published: activity.published || new Date().toISOString(),
$to: JSON.stringify(activity.to), $to: JSON.stringify(activity.to),
$cc: JSON.stringify(activity.cc), $cc: JSON.stringify(activity.cc),
$activity: JSON.stringify(activity) $activity: JSON.stringify(activity)
@ -325,7 +327,6 @@ export default class ActivityPubDB {
} }
deleteFollower(id:string) { deleteFollower(id:string) {
console.log("! DELETE Follower ", id)
return this.queries.deleteFollower?.run(id) return this.queries.deleteFollower?.run(id)
} }

View file

@ -1,53 +1,37 @@
import forge from "node-forge" // import crypto from "node:crypto" import forge from "node-forge" // Bun does not implement the required node:crypto functions
import path from "path" import path from "path"
// set up username and password for admin actions // the endpoint for auth token validation
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || ""; export const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ""; export const WEB_SITE_HOSTNAME = process.env.WEB_SITE_HOSTNAME
// get the hostname (`PROJECT_DOMAIN` is set via glitch, but since we're using Bun now, this won't matter) export const HOSTNAME = process.env.HOSTNAME || "localhost"
export const HOSTNAME = /*(process.env.PROJECT_DOMAIN && `${process.env.PROJECT_DOMAIN}.glitch.me`) ||*/ process.env.HOSTNAME || "localhost"
export const NODE_ENV = process.env.NODE_ENV || "development"
export const PORT = process.env.PORT || "3000" export const PORT = process.env.PORT || "3000"
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME
export const NODE_ENV = process.env.NODE_ENV || "production"
// 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 =
NODE_ENV === "development" NODE_ENV === "development"
? forge.pki.rsa.generateKeyPair({bits: 4096}) //crypto.generateKeyPairSync("rsa", { modulusLength: 4096 }) ? forge.pki.rsa.generateKeyPair({bits: 4096})
: undefined : undefined
export const PUBLIC_KEY = export const PUBLIC_KEY =
process.env.PUBLIC_KEY || process.env.PUBLIC_KEY ||
(keypair && forge.pki.publicKeyToPem(keypair.publicKey)) || //keypair?.publicKey.export({ type: "spki", format: "pem" }) || (keypair && forge.pki.publicKeyToPem(keypair.publicKey)) ||
"" ""
export const PRIVATE_KEY = export const PRIVATE_KEY =
process.env.PRIVATE_KEY || process.env.PRIVATE_KEY ||
(keypair && forge.pki.privateKeyToPem(keypair.privateKey)) || //keypair?.privateKey.export({ type: "pkcs8", format: "pem" }) || (keypair && forge.pki.privateKeyToPem(keypair.privateKey)) ||
"" ""
export const STATIC_PATH = path.join('.', '_site') export const DATA_PATH = process.env.DATA_PATH || "/data"
export const CONTENT_PATH = path.join('.', '_content') export const DB_PATH = process.env.DB_PATH || path.join(DATA_PATH, "db.sqlite")
export const POSTS_PATH = path.join(CONTENT_PATH, "posts")
export const DATA_PATH = path.join(CONTENT_PATH, "_data")
export const DB_PATH = path.join(DATA_PATH, "db.sqlite")
export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox")
export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox")
export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [
'index.html',
'index.shtml',
'index.htm',
'Index.html',
'Index.shtml',
'Index.htm',
'default.html',
'default.htm'
]
export const activityPubTypes = [ export const activityPubTypes = [
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'application/ld+json; profile="http://www.w3.org/ns/activitystreams"',
'application/activity+json' 'application/activity+json'
] ]
export const contentTypeHeader = { 'Content-Type': activityPubTypes[0] } export const contentTypeHeader = { 'Content-Type': activityPubTypes[0] }

View file

@ -1,6 +1,5 @@
import { idsFromValue } from "./activitypub"
import outbox from "./outbox" import outbox from "./outbox"
import { send } from "./request" import { idsFromValue, send } from "./request"
import ACTOR from "../actor" import ACTOR from "../actor"
import ActivityPubDB from "./db" import ActivityPubDB from "./db"
@ -18,7 +17,7 @@ export default async function inbox(activity:any, database:ActivityPubDB) {
// save this activity to my inbox // save this activity to my inbox
const activity_id = `${date.getTime().toString(32)}` const activity_id = `${date.getTime().toString(32)}`
console.log(`New inbox activity ${activity_id} (${activity.type})`, activity) console.info(`New inbox activity ${activity_id} (${activity.type})`, activity)
db.createInboxActivity(activity_id, activity) db.createInboxActivity(activity_id, activity)
// TODO: process the activity and update local data // TODO: process the activity and update local data

View file

@ -3,12 +3,14 @@ import activitypub, { reqIsActivityPub } from "./activitypub"
import { fetchObject } from "./request" import { fetchObject } from "./request"
import { handle, webfinger } from "../actor" import { handle, webfinger } from "../actor"
import ActivityPubDB from './db' import ActivityPubDB from './db'
import { HOSTNAME, PORT, WEB_SITE_HOSTNAME } from './env'
const db = new ActivityPubDB() const db = new ActivityPubDB()
const server = Bun.serve({ const server = Bun.serve({
port: 3000, port: PORT,
fetch(req: Request): Response | Promise<Response> { // hostname: HOSTNAME,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url) const url = new URL(req.url)
// log the incoming request info // log the incoming request info
@ -41,9 +43,30 @@ const server = Bun.serve({
return fetchObject(object_url) return fetchObject(object_url)
} }
// admin and activitypub routes // admin and activitypub routes, plus a fallback
return admin(req, db) || activitypub(req, db) || new Response("", { status: 404 }) return await admin(req, db) || activitypub(req, db) || fallback(req)
}, },
}) })
console.log(`Listening on http://localhost:${server.port} ...`) const fallback = async (req: Request) => {
// if we haven't a fallback hostname defined, just return 404
if(!WEB_SITE_HOSTNAME) return new Response('', { status: 404 })
// swap the hostname in the current request
const url = new URL(req.url)
url.hostname = WEB_SITE_HOSTNAME
console.info(`➡️ Forwarding ${req.method} request to ${url}`)
// override the host header, because this can cause problems if wrong
if(req.headers.get("host")) req.headers.set('host', WEB_SITE_HOSTNAME)
// const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await req.blob()
// // just use *all the things* from the request object (body is handled separately)
// TODO: possibly not forwarding the body correctly.
// My current website is static, but probably want to test and/or fix this in the future.
// @ts-ignore typescript doesn't think Request is compatible with RequestInit
const newRequest = new Request(url, req)
return await fetch(newRequest)
}
console.info(`📡 Listening on http://${server.hostname}:${server.port} ...`)

View file

@ -1,7 +1,6 @@
import { idsFromValue } from "./activitypub" import { fetchObject, idsFromValue, send } from "./request"
import { fetchObject, send } from "./request"
import ACTOR from "../actor" import ACTOR from "../actor"
import ActivityPubDB from "./db" import ActivityPubDB, { Activity } from "./db"
let db:ActivityPubDB let db:ActivityPubDB
@ -9,7 +8,7 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
db = database db = database
const date = new Date() const date = new Date()
const activity_id = `${date.getTime().toString(32)}` const activity_id = `${date.getTime().toString(32)}`
console.log('outbox', activity_id, activity) console.info('outbox', activity_id, activity)
// https://www.w3.org/TR/activitypub/#object-without-create // https://www.w3.org/TR/activitypub/#object-without-create
if(!activity.actor && !(activity.object || activity.target || activity.result || activity.origin || activity.instrument)) { if(!activity.actor && !(activity.object || activity.target || activity.result || activity.origin || activity.instrument)) {
@ -46,17 +45,11 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
delete activity.bcc delete activity.bcc
// now that has been taken care of, it's time to update our local data, depending on the contents of the activity // now that has been taken care of, it's time to update our local data, depending on the contents of the activity
switch(activity.type) { // aka "side effects"
case "Accept": await accept(activity, activity_id); break; // Note: I do this *before* adding the activity to the database so I can cache specific objects
case "Follow": await follow(activity, activity_id); break; // for example, for likes / dislikes / shares, the object probably only exists as the url,
case "Like": await like(activity, activity_id); break; // but I want to fetch it and store it so I can use that data later.
case "Dislike": await dislike(activity, activity_id); break; await outboxSideEffect(activity_id, activity)
case "Annouce": await announce(activity, activity_id); break;
case "Create": await create(activity, activity_id); break;
case "Undo": await undo(activity); break;
case "Delete": await deletePost(activity); break;
// TODO: case "Anncounce": return await share(activity)
}
// save the activity data for the outbox // save the activity data for the outbox
db.createOutboxActivity(activity_id, activity) db.createOutboxActivity(activity_id, activity)
@ -70,6 +63,20 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
return new Response("", { status: 201, headers: { location: activity.id } }) return new Response("", { status: 201, headers: { location: activity.id } })
} }
async function outboxSideEffect(activity_id:string, activity:Activity) {
switch(activity.type) {
case "Accept": await accept(activity, activity_id); break;
case "Follow": await follow(activity, activity_id); break;
case "Like": await like(activity, activity_id); break;
case "Dislike": await dislike(activity, activity_id); break;
case "Annouce": await announce(activity, activity_id); break;
case "Create": await create(activity, activity_id); break;
case "Undo": await undo(activity); break;
case "Delete": await deletePost(activity); break;
// TODO: case "Anncounce": return await share(activity)
}
}
async function create(activity:any, activity_id:string) { async function create(activity:any, activity_id:string) {
activity.object.id = activity.object.url = `${ACTOR.url}/posts/${activity_id}` activity.object.id = activity.object.url = `${ACTOR.url}/posts/${activity_id}`
db.createPost(activity_id, activity.object) db.createPost(activity_id, activity.object)
@ -143,7 +150,7 @@ async function undo(activity:any) {
if (!id) return true if (!id) return true
const match = id.match(/\/([0-9a-z]+)\/?$/) const match = id.match(/\/([0-9a-z]+)\/?$/)
const activity_id = match ? match[1] : id const activity_id = match ? match[1] : id
console.log('undo', activity_id) console.info('undo', activity_id)
try{ try{
const existing = db.getOutboxActivity(activity_id) const existing = db.getOutboxActivity(activity_id)

View file

@ -1,7 +1,15 @@
import forge from "node-forge" // import crypto from "node:crypto" import forge from "node-forge" // import crypto from "node:crypto"
import { PRIVATE_KEY } from "./env"; import { PRIVATE_KEY, TOKEN_ENDPOINT } from "./env";
import ACTOR from "../actor" import ACTOR from "../actor"
export function idsFromValue(value:any):string[] {
if (!value) return []
else if (typeof value === 'string') return [value]
else if (value.id) return [value.id]
else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[]
return []
}
// this function adds / modifies the appropriate headers for signing a request, then calls fetch // 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> export function signedFetch(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>
{ {
@ -52,7 +60,6 @@ async function fetchActor(url:string) {
/** Fetches and returns an object at a URL. */ /** Fetches and returns an object at a URL. */
export async function fetchObject(object_url:string) { export async function fetchObject(object_url:string) {
console.log(`fetch ${object_url}`)
const res = await signedFetch(object_url); const res = await signedFetch(object_url);
@ -69,9 +76,9 @@ export async function fetchObject(object_url:string) {
* @param message the body of the request to send. * @param message the body of the request to send.
*/ */
export async function send(recipient:string, message:any, from:string=ACTOR.id) { export async function send(recipient:string, message:any, from:string=ACTOR.id) {
console.log(`Sending to ${recipient}`, message) console.info(`Sending to ${recipient}`, message)
// 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) as any
const body = JSON.stringify(message) const body = JSON.stringify(message)
@ -122,7 +129,7 @@ export async function verify(req:Request, body:string) {
} }
// get the actor's public key // get the actor's public key
const actor = await fetchActor(keyId); const actor = await fetchActor(keyId) as any
if (!actor.publicKey) throw new Error("No public key found.") if (!actor.publicKey) throw new Error("No public key found.")
const key = forge.pki.publicKeyFromPem(actor.publicKey.publicKeyPem) const key = forge.pki.publicKeyFromPem(actor.publicKey.publicKeyPem)
@ -149,4 +156,17 @@ export async function verify(req:Request, body:string) {
throw new Error("Request date too old."); throw new Error("Request date too old.");
return actor.id; return actor.id;
}
/* verifies that the request is authorized
* (i.e. has a valid auth token) */
export async function authorized(req:Request) {
if(!TOKEN_ENDPOINT) return false
const response = await fetch(TOKEN_ENDPOINT, { headers: {"authorization": req.headers.get("authorization") || ''} })
if(!response.ok) return false
const json = await response.json() as any
if(new URL(json.me).hostname !== new URL(ACTOR.id).hostname) return false
return true
} }