diff --git a/.eleventy.js b/.eleventy.js index 669ff84..fd0b7e6 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,4 +1,4 @@ -const ACTOR = require("./actor") +const ACTOR = require("./actor").default module.exports = function(eleventyConfig) { // I'm .gitignoring my content for now, so 11ty should not ignore that diff --git a/actor.ts b/actor.ts index b3f8e48..04bfb4e 100644 --- a/actor.ts +++ b/actor.ts @@ -43,7 +43,7 @@ const attachment:{ type:"PropertyValue", name:string, value:string }[] = [ } ] -export default { +const ACTOR = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", @@ -88,4 +88,27 @@ export default { attachment, icon, image -} \ No newline at end of file +} + +export const handle = `${ACTOR.preferredUsername}@${HOSTNAME}` + +export const webfinger = { + subject: `acct:${ACTOR.preferredUsername}@${HOSTNAME}`, + aliases: [ACTOR.id], + links: [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": ACTOR.url + }, + { + rel: "self", + type: "application/activity+json", + href: ACTOR.id, + }, + ], +} + + + +export default ACTOR \ No newline at end of file diff --git a/src/activitypub.ts b/src/activitypub.ts index 6e85e9e..dbb03d9 100644 --- a/src/activitypub.ts +++ b/src/activitypub.ts @@ -1,8 +1,9 @@ import * as db from "./db" -import { reqIsActivityPub, verify } from "./request" +import { verify } from "./request" import outbox from "./outbox" import inbox from "./inbox" import ACTOR from "../actor" +import { activityPubTypes } from "./env" export default (req: Request): Response | Promise | undefined => { const url = new URL(req.url) @@ -22,6 +23,10 @@ export default (req: Request): Response | Promise | undefined => { return undefined } +export function reqIsActivityPub(req:Request) { + const contentType = req.headers.get("Accept") + return activityPubTypes.some(t => contentType?.includes(t)) +} export function idsFromValue(value:any):string[] { if (!value) return [] diff --git a/src/admin.ts b/src/admin.ts index d113eba..28d7ce6 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -1,8 +1,8 @@ import { idsFromValue } from "./activitypub" import * as db from "./db" -import { ADMIN_PASSWORD, ADMIN_USERNAME } from "./env" +import { ADMIN_PASSWORD, ADMIN_USERNAME, activityPubTypes } from "./env" import outbox from "./outbox" -import { activityMimeTypes, fetchObject } from "./request" +import { fetchObject } from "./request" import ACTOR from "../actor" export default (req: Request): Response | Promise | undefined => { @@ -94,15 +94,19 @@ const follow = async (req:Request, handle:string):Promise => { catch { // this is not a valid url. Probably a someone@domain.tld format const [_, host] = handle.split('@') - const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`) + 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 webfinger = await res.json() if(!webfinger.links) return new Response("", { status: 404 }) + const links:any[] = webfinger.links - const actorLink = links.find(l => l.rel === "self" && (activityMimeTypes.includes(l.type))) + const actorLink = links.find(l => l.rel === "self" && (activityPubTypes.includes(l.type))) if(!actorLink) return new Response("", { status: 404 }) + url = actorLink.href } - console.log(`Follow ${url}`) + console.log(`Following ${url}`) // send the follow request to the supplied actor return await outbox({ diff --git a/src/env.ts b/src/env.ts index 1eed62c..faaada0 100644 --- a/src/env.ts +++ b/src/env.ts @@ -45,57 +45,8 @@ export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [ 'default.htm' ] -// export const ACTOR_OBJ = { -// "@context": [ -// "https://www.w3.org/ns/activitystreams", -// "https://w3id.org/security/v1", -// ], -// id: ACTOR, -// type: "Person", -// following: `${ACTOR}/following`, -// followers: `${ACTOR}/followers`, -// inbox: `${ACTOR}/inbox`, -// outbox: `${ACTOR}/outbox`, -// //featured: `${ACTOR}/featured`, -// //featuredTags: `${ACTOR}/tags`, -// preferredUsername: ACCOUNT, -// name: REAL_NAME, -// summary: SUMMARY, -// url: ACTOR, -// manuallyApprovesFollowers: false, -// discoverable: true, -// published: "2023-09-14T00:00:00Z", -// //devices: `${ACTOR}/devices` -// alsoKnownAs: ALSO_KNOWN_AS, -// publicKey: { -// id: `${ACTOR}#main-key`, -// owner: ACTOR, -// publicKeyPem: PUBLIC_KEY, -// }, -// // attachment: [ -// // { -// // "type": "PropertyValue", -// // "name": "Pronouns", -// // "value": "he/him" -// // }, -// // { -// // "type": "PropertyValue", -// // "name": "Website", -// // "value": "https://death.id.au" -// // } -// // ], -// // tag: [], -// // endpoints: { -// // sharedInbox: N/A -// // }, -// icon: { -// type: "Image", -// mediaType: "image/svg+xml", -// url: BASE_URL + "/assets/img/avatar-tt.svg" -// }, -// image: { -// type: "Image", -// mediaType: "image/jpeg", -// url: BASE_URL + "/assets/img/banner-1500x500.jpg" -// } -// } \ No newline at end of file +export const activityPubTypes = [ + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'application/activity+json' +] +export const contentTypeHeader = { 'Content-Type': activityPubTypes[0]} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e3f5deb..53dd718 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import { DEFAULT_DOCUMENTS, HOSTNAME, STATIC_PATH } from "./env" +import { DEFAULT_DOCUMENTS, STATIC_PATH } from "./env" import admin from './admin' import activitypub from "./activitypub" import { fetchObject } from "./request" import path from "path" import { BunFile } from "bun" import { rebuild } from "./db" -import ACTOR from "../actor" +import { handle, webfinger } from "../actor" rebuild() @@ -14,13 +14,30 @@ const server = Bun.serve({ fetch(req: Request): Response | Promise { const url = new URL(req.url) - console.log(`${new Date().toISOString()} ${req.method} ${req.url}`) + // log the incoming request info + console.info(`${new Date().toISOString()} 📥 ${req.method} ${req.url}`) - if(req.method === "GET" && url.pathname === "/.well-known/webfinger") { - return webfinger(req, url.searchParams.get("resource")) + // CORS route (for now, any domain has access) + if(req.method === "OPTIONS") { + const headers:any = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE', + 'Access-Control-Max-Age': '86400', + } + let h + if (h = req.headers.get('Access-Control-Request-Headers')) + headers['Access-Control-Allow-Headers'] = 'Accept, Content-Type, Authorization, Signature, Digest, Date, Host' + return new Response('', { status: 204, headers }) } - else if(req.method === "GET" && url.pathname === "/fetch") { - const object_url = url.searchParams.get('url') + // Webfinger route + else if(req.method === "GET" && url.pathname === "/.well-known/webfinger") { + // make sure the resource matches the current handle. If it doesn't, 404 + if(url.searchParams.get("resource") !== `acct:${handle}`) return new Response("", { status: 404 }) + // return the webfinger + return Response.json(webfinger, { headers: { 'content-type': 'application/jrd+json' }}) + } + else if(req.method === "GET" && url.pathname.startsWith("/fetch/")) { + const object_url = url.pathname.substring(7) if(!object_url) return new Response("No url supplied", { status: 400}) return fetchObject(object_url) @@ -29,29 +46,6 @@ const server = Bun.serve({ return admin(req) || activitypub(req) || staticFile(req) }, }); - -const webfinger = async (req: Request, resource: string | null) => { - - if(resource !== `acct:${ACTOR.preferredUsername}@${HOSTNAME}`) return new Response("", { status: 404 }) - - return Response.json({ - subject: `acct:${ACTOR.preferredUsername}@${HOSTNAME}`, - aliases: [ACTOR.id], - links: [ - { - "rel": "http://webfinger.net/rel/profile-page", - "type": "text/html", - "href": ACTOR.url - }, - { - rel: "self", - type: "application/activity+json", - href: ACTOR.id, - }, - ], - }, { headers: { "content-type": "application/activity+json" }}) -} - const getDefaultDocument = async(base_path: string) => { for(const d of DEFAULT_DOCUMENTS){ const filePath = path.join(base_path, d) diff --git a/src/request.ts b/src/request.ts index 6db4716..2235b95 100644 --- a/src/request.ts +++ b/src/request.ts @@ -2,16 +2,6 @@ import forge from "node-forge" // import crypto from "node:crypto" import { PRIVATE_KEY } from "./env"; import ACTOR from "../actor" -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") - 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 -} - // this function adds / modifies the appropriate headers for signing a request, then calls fetch export function signedFetch(url: string | URL | Request, init?: FetchRequestInit): Promise {