import Router from "@koa/router"; import config from "../config/index.js"; import * as Acct from "../misc/acct.js"; import { links } from "./nodeinfo.js"; import { escapeAttribute, escapeValue } from "../prelude/xml.js"; import { Users } from "../models/index.js"; import { IsNull } from "typeorm"; // Init router const router = new Router(); const XRD = (...x)=>`${x.map(({ element, value, attributes })=>`<${Object.entries(typeof attributes === "object" && attributes || {}).reduce((a, [k, v])=>`${a} ${k}="${escapeAttribute(v)}"`, element)}${typeof value === "string" ? `>${escapeValue(value)}`).reduce((a, c)=>a + c, "")}`; const allPath = "/.well-known/{*splat}"; const webFingerPath = "/.well-known/webfinger"; const jrd = "application/jrd+json"; const xrd = "application/xrd+xml"; router.use(allPath, async (ctx, next)=>{ ctx.set({ "Access-Control-Allow-Headers": "Accept", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Origin": "*", "Access-Control-Expose-Headers": "Vary" }); await next(); }); router.options(allPath, async (ctx)=>{ ctx.status = 204; }); router.get("/.well-known/host-meta", async (ctx)=>{ ctx.set("Content-Type", xrd); ctx.body = XRD({ element: "Link", attributes: { rel: "lrdd", type: xrd, template: `${config.url}${webFingerPath}?resource={uri}` } }); }); router.get("/.well-known/host-meta.json", async (ctx)=>{ ctx.set("Content-Type", jrd); ctx.body = { links: [ { rel: "lrdd", type: jrd, template: `${config.url}${webFingerPath}?resource={uri}` } ] }; }); if (config.twa != null) { router.get("/.well-known/assetlinks.json", async (ctx)=>{ ctx.set("Content-Type", "application/json"); ctx.body = [ { relation: [ "delegate_permission/common.handle_all_urls" ], target: { namespace: config.twa.nameSpace, package_name: config.twa.packageName, sha256_cert_fingerprints: config.twa.sha256CertFingerprints } } ]; }); } router.get("/.well-known/nodeinfo", async (ctx)=>{ ctx.body = { links }; }); /* TODO router.get('/.well-known/change-password', async ctx => { }); */ router.get(webFingerPath, async (ctx)=>{ const fromId = (id)=>({ id, host: IsNull(), isSuspended: false }); const generateQuery = (resource)=>resource.startsWith(`${config.url.toLowerCase()}/users/`) ? fromId(resource.split("/").pop()) : fromAcct(Acct.parse(resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split("/").pop() : resource.startsWith("acct:") ? resource.slice("acct:".length) : resource)); const fromAcct = (acct)=>!acct.host || acct.host === config.host.toLowerCase() || acct.host === config.domain.toLowerCase() ? { usernameLower: acct.username, host: IsNull(), isSuspended: false } : 422; if (typeof ctx.query.resource !== "string") { ctx.status = 400; return; } const query = generateQuery(ctx.query.resource.toLowerCase()); if (typeof query === "number") { ctx.status = query; return; } const user = await Users.findOneBy(query); if (user == null) { ctx.status = 404; return; } const subject = `acct:${user.username}@${config.domain}`; const self = { rel: "self", type: "application/activity+json", href: `${config.url}/users/${user.id}` }; const profilePage = { rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: `${config.url}/@${user.username}` }; const subscribe = { rel: "http://ostatus.org/schema/1.0/subscribe", template: `${config.url}/authorize-follow?acct={uri}` }; if (ctx.accepts(jrd, xrd) === xrd) { ctx.body = XRD({ element: "Subject", value: subject }, { element: "Link", attributes: self }, { element: "Link", attributes: profilePage }, { element: "Link", attributes: subscribe }); ctx.type = xrd; } else { ctx.body = { subject, links: [ self, profilePage, subscribe ] }; ctx.type = jrd; } ctx.vary("Accept"); ctx.set("Cache-Control", "public, max-age=180"); }); // Return 404 for other .well-known router.all(allPath, async (ctx)=>{ ctx.status = 404; }); export default router;