diff --git a/@types/http-signature.d.ts b/@types/http-signature.d.ts index 8ad40b6..fd9b2a9 100644 --- a/@types/http-signature.d.ts +++ b/@types/http-signature.d.ts @@ -1,77 +1,98 @@ -declare module '@peertube/http-signature' { - import type { IncomingMessage, ClientRequest } from 'node:http'; +declare module "@peertube/http-signature" { + import type { IncomingMessage, ClientRequest } from "node:http"; - interface ISignature { - keyId: string; - algorithm: string; - headers: string[]; - signature: string; - } + interface ISignature { + keyId: string; + algorithm: string; + headers: string[]; + signature: string; + } - interface IOptions { - headers?: string[]; - algorithm?: string; - strict?: boolean; - authorizationHeaderName?: string; - } + interface IOptions { + headers?: string[]; + algorithm?: string; + strict?: boolean; + authorizationHeaderName?: string; + } - interface IParseRequestOptions extends IOptions { - clockSkew?: number; - } + interface IParseRequestOptions extends IOptions { + clockSkew?: number; + } - interface IParsedSignature { - scheme: string; - params: ISignature; - signingString: string; - algorithm: string; - keyId: string; - } + interface IParsedSignature { + scheme: string; + params: ISignature; + signingString: string; + algorithm: string; + keyId: string; + } - type RequestSignerConstructorOptions = - IRequestSignerConstructorOptionsFromProperties | - IRequestSignerConstructorOptionsFromFunction; + type RequestSignerConstructorOptions = + | IRequestSignerConstructorOptionsFromProperties + | IRequestSignerConstructorOptionsFromFunction; - interface IRequestSignerConstructorOptionsFromProperties { - keyId: string; - key: string | Buffer; - algorithm?: string; - } + interface IRequestSignerConstructorOptionsFromProperties { + keyId: string; + key: string | Buffer; + algorithm?: string; + } - interface IRequestSignerConstructorOptionsFromFunction { - sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; - } + interface IRequestSignerConstructorOptionsFromFunction { + sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; + } - class RequestSigner { - constructor(options: RequestSignerConstructorOptions); + class RequestSigner { + constructor(options: RequestSignerConstructorOptions); - public writeHeader(header: string, value: string): string; + public writeHeader(header: string, value: string): string; - public writeDateHeader(): string; + public writeDateHeader(): string; - public writeTarget(method: string, path: string): void; + public writeTarget(method: string, path: string): void; - public sign(cb: (err: any, authz: string) => void): void; - } + public sign(cb: (err: any, authz: string) => void): void; + } - interface ISignRequestOptions extends IOptions { - keyId: string; - key: string; - httpVersion?: string; - } + interface ISignRequestOptions extends IOptions { + keyId: string; + key: string; + httpVersion?: string; + } - export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; - export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; + export function parse( + request: IncomingMessage, + options?: IParseRequestOptions + ): IParsedSignature; + export function parseRequest( + request: IncomingMessage, + options?: IParseRequestOptions + ): IParsedSignature; - export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; - export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; - export function createSigner(): RequestSigner; - export function isSigner(obj: any): obj is RequestSigner; + export function sign( + request: ClientRequest, + options: ISignRequestOptions + ): boolean; + export function signRequest( + request: ClientRequest, + options: ISignRequestOptions + ): boolean; + export function createSigner(): RequestSigner; + export function isSigner(obj: any): obj is RequestSigner; - export function sshKeyToPEM(key: string): string; - export function sshKeyFingerprint(key: string): string; - export function pemToRsaSSHKey(pem: string, comment: string): string; + export function sshKeyToPEM(key: string): string; + export function sshKeyFingerprint(key: string): string; + export function pemToRsaSSHKey(pem: string, comment: string): string; - export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; -} \ No newline at end of file + export function verify( + parsedSignature: IParsedSignature, + pubkey: string | Buffer + ): boolean; + export function verifySignature( + parsedSignature: IParsedSignature, + pubkey: string | Buffer + ): boolean; + export function verifyHMAC( + parsedSignature: IParsedSignature, + secret: string + ): boolean; +} diff --git a/ap/consts.ts b/ap/consts.ts index 7606267..03cf6cc 100644 --- a/ap/consts.ts +++ b/ap/consts.ts @@ -1,4 +1,5 @@ -import { env } from "process" +import { env } from "process"; -export const ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' -export const USER_AGENT = `xtex-home/1.0@${env['VERCEL_GIT_COMMIT_SHA']} (${env['NEXT_PUBLIC_VERCEL_URL']})` +export const ACCEPT_HEADER = + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; +export const USER_AGENT = `xtex-home/1.0@${env["VERCEL_GIT_COMMIT_SHA"]} (${env["NEXT_PUBLIC_VERCEL_URL"]})`; diff --git a/ap/delivery.ts b/ap/delivery.ts index b2736b8..e196f31 100644 --- a/ap/delivery.ts +++ b/ap/delivery.ts @@ -1,35 +1,35 @@ import { signRequest } from "@peertube/http-signature"; -import { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity" +import { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity"; import { ClientRequest } from "http"; import { env } from "process"; -import { ACCEPT_HEADER, USER_AGENT } from "./consts" +import { ACCEPT_HEADER, USER_AGENT } from "./consts"; export async function deliveryAPActivity(url: URL, doc: BaseEntity) { - console.log(`deliverying AP document ${url}`) + console.log(`deliverying AP document ${url}`); - const request = { - url: url.toString(), - method: 'POST', - headers: { - 'Date': new Date().toUTCString(), - 'Host': url.hostname, - }, - } - const signature = signRequest(request as unknown as ClientRequest, { - key: env['XTEX_HOME_AP_PRIV_KEY']!!, - keyId: 'XTEX-HOME-AP-INSTANCE-ACTOR', - }) - console.log(signature) + const request = { + url: url.toString(), + method: "POST", + headers: { + Date: new Date().toUTCString(), + Host: url.hostname, + }, + }; + const signature = signRequest(request as unknown as ClientRequest, { + key: env["XTEX_HOME_AP_PRIV_KEY"]!!, + keyId: "XTEX-HOME-AP-INSTANCE-ACTOR", + }); + console.log(signature); - const result = await fetch(url, { - method: 'POST', - body: JSON.stringify(doc), - headers: { - 'User-Agent': USER_AGENT, - 'Accept': ACCEPT_HEADER, - 'Content-Type': 'application/activity+json', - } - }) + const result = await fetch(url, { + method: "POST", + body: JSON.stringify(doc), + headers: { + "User-Agent": USER_AGENT, + Accept: ACCEPT_HEADER, + "Content-Type": "application/activity+json", + }, + }); - console.log(`delivered AP doc ${url}: ${result.status}`) + console.log(`delivered AP doc ${url}: ${result.status}`); } diff --git a/ap/resolver.ts b/ap/resolver.ts index 5c3be51..bb2d706 100644 --- a/ap/resolver.ts +++ b/ap/resolver.ts @@ -1,35 +1,40 @@ -import { CoreObject, Entity, EntityReference, Link } from "activitypub-core-types/lib/activitypub"; +import { + CoreObject, + Entity, + EntityReference, + Link, +} from "activitypub-core-types/lib/activitypub"; import { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity"; import { URL } from "url"; import { ACCEPT_HEADER, USER_AGENT } from "./consts"; export async function resolveApEntity(ref: EntityReference): Promise { - if (typeof ref == 'string') { - return await getApDocument(ref) as Entity - } else if (typeof (ref as Link).href == 'string') { - return resolveApEntity((ref as Link).href!!) - } else if (typeof (ref as CoreObject).type == 'string') { - return ref as CoreObject - } else { - throw `${ref} cannot be resolved as a AP entity` - } + if (typeof ref == "string") { + return (await getApDocument(ref)) as Entity; + } else if (typeof (ref as Link).href == "string") { + return resolveApEntity((ref as Link).href!!); + } else if (typeof (ref as CoreObject).type == "string") { + return ref as CoreObject; + } else { + throw `${ref} cannot be resolved as a AP entity`; + } } export async function getApDocument(url: URL): Promise { - console.log(`resolving AP document ${url}, UA: ${USER_AGENT}`) - const result = await fetch(url, { - method: 'GET', - headers: { - 'User-Agent': USER_AGENT, - 'Accept': ACCEPT_HEADER, - } - }) - console.log(`resolved AP doc ${url}: ${result.status}`) - const json = await result.text() - try { - return JSON.parse(json) as BaseEntity - } catch (e) { - console.error(json) - throw e - } + console.log(`resolving AP document ${url}, UA: ${USER_AGENT}`); + const result = await fetch(url, { + method: "GET", + headers: { + "User-Agent": USER_AGENT, + Accept: ACCEPT_HEADER, + }, + }); + console.log(`resolved AP doc ${url}: ${result.status}`); + const json = await result.text(); + try { + return JSON.parse(json) as BaseEntity; + } catch (e) { + console.error(json); + throw e; + } } diff --git a/next.config.js b/next.config.js index 1abd62e..9abed1f 100644 --- a/next.config.js +++ b/next.config.js @@ -6,71 +6,95 @@ const nextConfig = { async redirects() { return [ { - source: '/blog/:path*', - destination: 'https://blog.xtexx.ml/:path*', + source: "/blog/:path*", + destination: "https://blog.xtexx.ml/:path*", permanent: true, }, - ] + ]; }, - async headers() {//application/ld+json; profile="https://www.w3.org/ns/activitystreams" + async headers() { const nodeinfo = { - key: 'Content-Type', - value: 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', - } + key: "Content-Type", + value: + 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', + }; + const cors = [ + { + key: "Access-Control-Allow-Origin", + value: "*", + }, + { + key: "Access-Control-Expose-Headers", + value: "*", + }, + { + key: "Access-Control-Max-Age", + value: "86400", + }, + { + key: "Access-Control-Allow-Methods", + value: "*", + }, + { + key: "Access-Control-Allow-Headers", + value: "*", + }, + ]; return [ { - source: '/.well-known/nodeinfo', + source: "/.well-known/nodeinfo", headers: [nodeinfo], }, { - source: '/nodeinfo.json', + source: "/nodeinfo.json", headers: [nodeinfo], }, { - source: '/.well-known/host-meta', + source: "/.well-known/host-meta", headers: [ { - key: 'Content-Type', - value: 'application/xrd+xml', + key: "Content-Type", + value: "application/xrd+xml", }, ], }, { - source: '/.well-known/matrix/:path*', + source: "/.well-known/matrix/:path*", headers: [ { - key: 'Content-Type', - value: 'application/json; charset=utf-8', + key: "Content-Type", + value: "application/json; charset=utf-8", }, ], }, { - source: '/ap/:path*', + source: "/ap/:path*", headers: [ { - key: 'Content-Type', - value: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8', + key: "Content-Type", + value: + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8', }, ], }, - ] + ]; }, async rewrites() { return [ { - source: '/.well-known/host-meta.json', - destination: '/api/host-meta.json', + source: "/.well-known/host-meta.json", + destination: "/api/host-meta.json", }, { - source: '/.well-known/webfinger', - destination: '/api/webfinger', + source: "/.well-known/webfinger", + destination: "/api/webfinger", }, { - source: '/ap/api/:path*', - destination: '/api/ap/:path*', + source: "/ap/api/:path*", + destination: "/api/ap/:path*", }, - ] + ]; }, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/pages/api/ap/inbox.ts b/pages/api/ap/inbox.ts index 6dfb82f..3e4df07 100644 --- a/pages/api/ap/inbox.ts +++ b/pages/api/ap/inbox.ts @@ -1,39 +1,60 @@ -import * as httpSignature from '@peertube/http-signature' -import { Activity, ActivityTypes, Actor, Reject } from 'activitypub-core-types/lib/activitypub' -import type { NextApiRequest, NextApiResponse } from 'next' -import { deliveryAPActivity } from '../../../ap/delivery' -import { resolveApEntity } from '../../../ap/resolver' +import * as httpSignature from "@peertube/http-signature"; +import { + Activity, + ActivityTypes, + Actor, + Reject, +} from "activitypub-core-types/lib/activitypub"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { deliveryAPActivity } from "../../../ap/delivery"; +import { resolveApEntity } from "../../../ap/resolver"; export default async function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - const signature = req.headers['signature'] - const activity = req.body as Activity - if (signature == null) { - return res.status(400).send('no signature') + const signature = req.headers["signature"]; + const activity = req.body as Activity; + if (signature == null) { + return res.status(400).send("no signature"); + } + if (activity.actor instanceof Array || !activity.actor) { + return res.status(400).send("actor is more than one AS entity ref"); + } + const actor = (await resolveApEntity(activity.actor)) as Actor; + if ( + !httpSignature.verifySignature( + httpSignature.parseRequest(req), + actor.publicKey!!.publicKeyPem + ) + ) { + console.error( + `signature check failed, actor: ${actor}, provided: ${signature}` + ); + return res + .status(400) + .send(`signature check failed, expected: ${actor.publicKey}`); + } + if (activity.type == ActivityTypes.FOLLOW) { + // follow request + console.log(`sending follow Reject to ${actor.inbox}`); + if (actor.inbox! instanceof URL) { + return res.status(400).send("inbox is not a standalone doc"); } - if (activity.actor instanceof Array || !activity.actor) { - return res.status(400).send('actor is more than one AS entity ref') - } - const actor = (await resolveApEntity(activity.actor) as Actor) - if (!httpSignature.verifySignature(httpSignature.parseRequest(req), actor.publicKey!!.publicKeyPem)) { - console.error(`signature check failed, actor: ${actor}, provided: ${signature}`) - return res.status(400).send(`signature check failed, expected: ${actor.publicKey}`) - } - if (activity.type == ActivityTypes.FOLLOW) { - // follow request - console.log(`sending follow Reject to ${actor.inbox}`) - if (actor.inbox! instanceof URL) { - return res.status(400).send('inbox is not a standalone doc') - } - await deliveryAPActivity(actor.inbox as unknown as URL, { - type: ActivityTypes.REJECT, - id: new URL(`https://xtexx.ml/ap/reject_follows/${encodeURI(actor.id!!.toString())}`), - actor: new URL('https://xtexx.ml/ap/actor.json'), - object: activity.id, - target: actor.id, - } as Reject) - } - res.status(200).end() + await deliveryAPActivity( + actor.inbox as unknown as URL, + { + type: ActivityTypes.REJECT, + id: new URL( + `https://xtexx.ml/ap/reject_follows/${encodeURI( + actor.id!!.toString() + )}` + ), + actor: new URL("https://xtexx.ml/ap/actor.json"), + object: activity.id, + target: actor.id, + } as Reject + ); + } + res.status(200).end(); } diff --git a/pages/api/ap/shared_inbox.ts b/pages/api/ap/shared_inbox.ts index 7d49184..7022c92 100644 --- a/pages/api/ap/shared_inbox.ts +++ b/pages/api/ap/shared_inbox.ts @@ -1,10 +1,7 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import type { NextApiRequest, NextApiResponse } from "next"; -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - console.log(req.body) - console.log(JSON.stringify(req.body)) - res.status(200).end() +export default function handler(req: NextApiRequest, res: NextApiResponse) { + console.log(req.body); + console.log(JSON.stringify(req.body)); + res.status(200).end(); } diff --git a/pages/api/host-meta.json.ts b/pages/api/host-meta.json.ts index e5893fa..eb59ad7 100644 --- a/pages/api/host-meta.json.ts +++ b/pages/api/host-meta.json.ts @@ -1,16 +1,16 @@ -import { Link } from 'activitypub-core-types/lib/activitypub' -import type { NextApiRequest, NextApiResponse } from 'next' -import site_lrs from '../../data/site_lrs.json' +import { Link } from "activitypub-core-types/lib/activitypub"; +import type { NextApiRequest, NextApiResponse } from "next"; +import site_lrs from "../../data/site_lrs.json"; type Data = { - links: Link[], -} + links: Link[]; +}; export default function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - res.status(200).json({ - links: site_lrs as Link[], - }) + res.status(200).json({ + links: site_lrs as Link[], + }); } diff --git a/pages/api/webfinger.ts b/pages/api/webfinger.ts index 1835086..97df134 100644 --- a/pages/api/webfinger.ts +++ b/pages/api/webfinger.ts @@ -1,74 +1,75 @@ -import type { NextApiRequest, NextApiResponse } from 'next' -import site_lrs from '../../data/site_lrs.json' -import webfingerData from '../../data/webfinger.json' -import { collect as collectEcho } from './whoami' +import type { NextApiRequest, NextApiResponse } from "next"; +import site_lrs from "../../data/site_lrs.json"; +import webfingerData from "../../data/webfinger.json"; +import { collect as collectEcho } from "./whoami"; type Data = { - subject: string, - aliases: string[] | undefined, - links: any[], -} + subject: string; + aliases: string[] | undefined; + links: any[]; +}; -export function lookupData(username: string): { - aliases: string[], - links: any[] -} | undefined { - let data = (webfingerData as any[string])[username] - if (data != undefined && data.aliasTo != undefined) - return lookupData(data.aliasTo) - return data +export function lookupData(username: string): + | { + aliases: string[]; + links: any[]; + } + | undefined { + let data = (webfingerData as any[string])[username]; + if (data != undefined && data.aliasTo != undefined) + return lookupData(data.aliasTo); + return data; } export default function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - let uri: string = req.query['resource'] as string - if (uri == undefined) { - res.status(400).end('"resource" query param is not provided') - return + let uri: string = req.query["resource"] as string; + if (uri == undefined) { + res.status(400).end('"resource" query param is not provided'); + return; + } + if (!uri.startsWith("acct:")) { + res.status(404).end("Only acct urls are allowed"); + return; + } + if (uri.indexOf("@") == -1) { + res.status(404).end("Username is not provided"); + return; + } + let username = uri.substring(5, uri.indexOf("@")).toLowerCase(); + if (username.startsWith("//")) username = username.substring(2); + let aliases: string[] = []; + let links: any[] = []; + switch (username) { + case "this": { + links = site_lrs; + break; } - if (!uri.startsWith('acct:')) { - res.status(404).end('Only acct urls are allowed') - return + case "echo": { + links = [ + { + rel: "contents", + href: JSON.stringify(collectEcho(req)), + }, + ]; + break; } - if (uri.indexOf('@') == -1) { - res.status(404).end('Username is not provided') - return + default: { + let result = lookupData(username); + if (result == undefined) { + res.status(404).end(`User "${username}" not found`); + return; + } else { + aliases = result.aliases; + links = result.links; + } } - let username = uri.substring(5, uri.indexOf('@')).toLowerCase() - if (username.startsWith('//')) - username = username.substring(2) - let aliases: string[] = [] - let links: any[] = [] - switch (username) { - case 'this': { - links = site_lrs - break - } - case 'echo': { - links = [ - { - rel: 'contents', - href: JSON.stringify(collectEcho(req)), - } - ] - break - } - default: { - let result = lookupData(username) - if (result == undefined) { - res.status(404).end(`User "${username}" not found`) - return - } else { - aliases = result.aliases - links = result.links - } - } - } - res.status(200).json({ - subject: uri, - aliases: (aliases != undefined && aliases.length == 0) ? undefined : aliases, - links, - }) + } + res.status(200).json({ + subject: uri, + aliases: aliases != undefined && aliases.length == 0 ? undefined : aliases, + links, + }); } diff --git a/pages/api/whoami.ts b/pages/api/whoami.ts index 28aa98f..f3ecc01 100644 --- a/pages/api/whoami.ts +++ b/pages/api/whoami.ts @@ -1,15 +1,15 @@ -import type { NextApiRequest, NextApiResponse } from 'next' -import type { IncomingHttpHeaders } from 'node:http' +import type { NextApiRequest, NextApiResponse } from "next"; +import type { IncomingHttpHeaders } from "node:http"; type Data = { - httpVersion: string, - headers: IncomingHttpHeaders, - address: string, - port: number, - ipv6: boolean, - method: string, - userAgent: string | undefined, -} + httpVersion: string; + headers: IncomingHttpHeaders; + address: string; + port: number; + ipv6: boolean; + method: string; + userAgent: string | undefined; +}; export function collect(req: NextApiRequest): Data { return { @@ -17,15 +17,15 @@ export function collect(req: NextApiRequest): Data { headers: req.headers, address: req.socket.remoteAddress!, port: req.socket.remotePort!, - ipv6: req.socket.remoteFamily == 'IPv6', + ipv6: req.socket.remoteFamily == "IPv6", method: req.method!, userAgent: req.headers["user-agent"], - } + }; } export default function handler( req: NextApiRequest, res: NextApiResponse ) { - res.status(200).json(collect(req)) + res.status(200).json(collect(req)); }