style: format

main
xtexChooser 2023-02-03 07:23:24 +08:00
parent fe109f7fdb
commit bf09166c51
No known key found for this signature in database
GPG Key ID: 978F2E760D9DB0EB
10 changed files with 341 additions and 271 deletions

View File

@ -1,77 +1,98 @@
declare module '@peertube/http-signature' { declare module "@peertube/http-signature" {
import type { IncomingMessage, ClientRequest } from 'node:http'; import type { IncomingMessage, ClientRequest } from "node:http";
interface ISignature { interface ISignature {
keyId: string; keyId: string;
algorithm: string; algorithm: string;
headers: string[]; headers: string[];
signature: string; signature: string;
} }
interface IOptions { interface IOptions {
headers?: string[]; headers?: string[];
algorithm?: string; algorithm?: string;
strict?: boolean; strict?: boolean;
authorizationHeaderName?: string; authorizationHeaderName?: string;
} }
interface IParseRequestOptions extends IOptions { interface IParseRequestOptions extends IOptions {
clockSkew?: number; clockSkew?: number;
} }
interface IParsedSignature { interface IParsedSignature {
scheme: string; scheme: string;
params: ISignature; params: ISignature;
signingString: string; signingString: string;
algorithm: string; algorithm: string;
keyId: string; keyId: string;
} }
type RequestSignerConstructorOptions = type RequestSignerConstructorOptions =
IRequestSignerConstructorOptionsFromProperties | | IRequestSignerConstructorOptionsFromProperties
IRequestSignerConstructorOptionsFromFunction; | IRequestSignerConstructorOptionsFromFunction;
interface IRequestSignerConstructorOptionsFromProperties { interface IRequestSignerConstructorOptionsFromProperties {
keyId: string; keyId: string;
key: string | Buffer; key: string | Buffer;
algorithm?: string; algorithm?: string;
} }
interface IRequestSignerConstructorOptionsFromFunction { interface IRequestSignerConstructorOptionsFromFunction {
sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
} }
class RequestSigner { class RequestSigner {
constructor(options: RequestSignerConstructorOptions); 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 { interface ISignRequestOptions extends IOptions {
keyId: string; keyId: string;
key: string; key: string;
httpVersion?: string; httpVersion?: string;
} }
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; export function parse(
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; request: IncomingMessage,
options?: IParseRequestOptions
): IParsedSignature;
export function parseRequest(
request: IncomingMessage,
options?: IParseRequestOptions
): IParsedSignature;
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; export function sign(
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; request: ClientRequest,
export function createSigner(): RequestSigner; options: ISignRequestOptions
export function isSigner(obj: any): obj is RequestSigner; ): 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 sshKeyToPEM(key: string): string;
export function sshKeyFingerprint(key: string): string; export function sshKeyFingerprint(key: string): string;
export function pemToRsaSSHKey(pem: string, comment: string): string; export function pemToRsaSSHKey(pem: string, comment: string): string;
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; export function verify(
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; parsedSignature: IParsedSignature,
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; pubkey: string | Buffer
} ): boolean;
export function verifySignature(
parsedSignature: IParsedSignature,
pubkey: string | Buffer
): boolean;
export function verifyHMAC(
parsedSignature: IParsedSignature,
secret: string
): boolean;
}

View File

@ -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 ACCEPT_HEADER =
export const USER_AGENT = `xtex-home/1.0@${env['VERCEL_GIT_COMMIT_SHA']} (${env['NEXT_PUBLIC_VERCEL_URL']})` '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"]})`;

View File

@ -1,35 +1,35 @@
import { signRequest } from "@peertube/http-signature"; 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 { ClientRequest } from "http";
import { env } from "process"; 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) { export async function deliveryAPActivity(url: URL, doc: BaseEntity) {
console.log(`deliverying AP document ${url}`) console.log(`deliverying AP document ${url}`);
const request = { const request = {
url: url.toString(), url: url.toString(),
method: 'POST', method: "POST",
headers: { headers: {
'Date': new Date().toUTCString(), Date: new Date().toUTCString(),
'Host': url.hostname, Host: url.hostname,
}, },
} };
const signature = signRequest(request as unknown as ClientRequest, { const signature = signRequest(request as unknown as ClientRequest, {
key: env['XTEX_HOME_AP_PRIV_KEY']!!, key: env["XTEX_HOME_AP_PRIV_KEY"]!!,
keyId: 'XTEX-HOME-AP-INSTANCE-ACTOR', keyId: "XTEX-HOME-AP-INSTANCE-ACTOR",
}) });
console.log(signature) console.log(signature);
const result = await fetch(url, { const result = await fetch(url, {
method: 'POST', method: "POST",
body: JSON.stringify(doc), body: JSON.stringify(doc),
headers: { headers: {
'User-Agent': USER_AGENT, "User-Agent": USER_AGENT,
'Accept': ACCEPT_HEADER, Accept: ACCEPT_HEADER,
'Content-Type': 'application/activity+json', "Content-Type": "application/activity+json",
} },
}) });
console.log(`delivered AP doc ${url}: ${result.status}`) console.log(`delivered AP doc ${url}: ${result.status}`);
} }

View File

@ -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 { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity";
import { URL } from "url"; import { URL } from "url";
import { ACCEPT_HEADER, USER_AGENT } from "./consts"; import { ACCEPT_HEADER, USER_AGENT } from "./consts";
export async function resolveApEntity(ref: EntityReference): Promise<Entity> { export async function resolveApEntity(ref: EntityReference): Promise<Entity> {
if (typeof ref == 'string') { if (typeof ref == "string") {
return await getApDocument(ref) as Entity return (await getApDocument(ref)) as Entity;
} else if (typeof (ref as Link).href == 'string') { } else if (typeof (ref as Link).href == "string") {
return resolveApEntity((ref as Link).href!!) return resolveApEntity((ref as Link).href!!);
} else if (typeof (ref as CoreObject).type == 'string') { } else if (typeof (ref as CoreObject).type == "string") {
return ref as CoreObject return ref as CoreObject;
} else { } else {
throw `${ref} cannot be resolved as a AP entity` throw `${ref} cannot be resolved as a AP entity`;
} }
} }
export async function getApDocument(url: URL): Promise<BaseEntity> { export async function getApDocument(url: URL): Promise<BaseEntity> {
console.log(`resolving AP document ${url}, UA: ${USER_AGENT}`) console.log(`resolving AP document ${url}, UA: ${USER_AGENT}`);
const result = await fetch(url, { const result = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'User-Agent': USER_AGENT, "User-Agent": USER_AGENT,
'Accept': ACCEPT_HEADER, Accept: ACCEPT_HEADER,
} },
}) });
console.log(`resolved AP doc ${url}: ${result.status}`) console.log(`resolved AP doc ${url}: ${result.status}`);
const json = await result.text() const json = await result.text();
try { try {
return JSON.parse(json) as BaseEntity return JSON.parse(json) as BaseEntity;
} catch (e) { } catch (e) {
console.error(json) console.error(json);
throw e throw e;
} }
} }

View File

@ -6,71 +6,95 @@ const nextConfig = {
async redirects() { async redirects() {
return [ return [
{ {
source: '/blog/:path*', source: "/blog/:path*",
destination: 'https://blog.xtexx.ml/:path*', destination: "https://blog.xtexx.ml/:path*",
permanent: true, permanent: true,
}, },
] ];
}, },
async headers() {//application/ld+json; profile="https://www.w3.org/ns/activitystreams" async headers() {
const nodeinfo = { const nodeinfo = {
key: 'Content-Type', key: "Content-Type",
value: 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', 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 [ return [
{ {
source: '/.well-known/nodeinfo', source: "/.well-known/nodeinfo",
headers: [nodeinfo], headers: [nodeinfo],
}, },
{ {
source: '/nodeinfo.json', source: "/nodeinfo.json",
headers: [nodeinfo], headers: [nodeinfo],
}, },
{ {
source: '/.well-known/host-meta', source: "/.well-known/host-meta",
headers: [ headers: [
{ {
key: 'Content-Type', key: "Content-Type",
value: 'application/xrd+xml', value: "application/xrd+xml",
}, },
], ],
}, },
{ {
source: '/.well-known/matrix/:path*', source: "/.well-known/matrix/:path*",
headers: [ headers: [
{ {
key: 'Content-Type', key: "Content-Type",
value: 'application/json; charset=utf-8', value: "application/json; charset=utf-8",
}, },
], ],
}, },
{ {
source: '/ap/:path*', source: "/ap/:path*",
headers: [ headers: [
{ {
key: 'Content-Type', key: "Content-Type",
value: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8', value:
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8',
}, },
], ],
}, },
] ];
}, },
async rewrites() { async rewrites() {
return [ return [
{ {
source: '/.well-known/host-meta.json', source: "/.well-known/host-meta.json",
destination: '/api/host-meta.json', destination: "/api/host-meta.json",
}, },
{ {
source: '/.well-known/webfinger', source: "/.well-known/webfinger",
destination: '/api/webfinger', destination: "/api/webfinger",
}, },
{ {
source: '/ap/api/:path*', source: "/ap/api/:path*",
destination: '/api/ap/:path*', destination: "/api/ap/:path*",
}, },
] ];
}, },
} };
module.exports = nextConfig module.exports = nextConfig;

View File

@ -1,39 +1,60 @@
import * as httpSignature from '@peertube/http-signature' import * as httpSignature from "@peertube/http-signature";
import { Activity, ActivityTypes, Actor, Reject } from 'activitypub-core-types/lib/activitypub' import {
import type { NextApiRequest, NextApiResponse } from 'next' Activity,
import { deliveryAPActivity } from '../../../ap/delivery' ActivityTypes,
import { resolveApEntity } from '../../../ap/resolver' 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( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
const signature = req.headers['signature'] const signature = req.headers["signature"];
const activity = req.body as Activity const activity = req.body as Activity;
if (signature == null) { if (signature == null) {
return res.status(400).send('no signature') 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) { await deliveryAPActivity(
return res.status(400).send('actor is more than one AS entity ref') actor.inbox as unknown as URL,
} {
const actor = (await resolveApEntity(activity.actor) as Actor) type: ActivityTypes.REJECT,
if (!httpSignature.verifySignature(httpSignature.parseRequest(req), actor.publicKey!!.publicKeyPem)) { id: new URL(
console.error(`signature check failed, actor: ${actor}, provided: ${signature}`) `https://xtexx.ml/ap/reject_follows/${encodeURI(
return res.status(400).send(`signature check failed, expected: ${actor.publicKey}`) actor.id!!.toString()
} )}`
if (activity.type == ActivityTypes.FOLLOW) { ),
// follow request actor: new URL("https://xtexx.ml/ap/actor.json"),
console.log(`sending follow Reject to ${actor.inbox}`) object: activity.id,
if (actor.inbox! instanceof URL) { target: actor.id,
return res.status(400).send('inbox is not a standalone doc') } as Reject
} );
await deliveryAPActivity(actor.inbox as unknown as URL, { }
type: ActivityTypes.REJECT, res.status(200).end();
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()
} }

View File

@ -1,10 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
export default function handler( export default function handler(req: NextApiRequest, res: NextApiResponse) {
req: NextApiRequest, console.log(req.body);
res: NextApiResponse console.log(JSON.stringify(req.body));
) { res.status(200).end();
console.log(req.body)
console.log(JSON.stringify(req.body))
res.status(200).end()
} }

View File

@ -1,16 +1,16 @@
import { Link } from 'activitypub-core-types/lib/activitypub' import { Link } from "activitypub-core-types/lib/activitypub";
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import site_lrs from '../../data/site_lrs.json' import site_lrs from "../../data/site_lrs.json";
type Data = { type Data = {
links: Link[], links: Link[];
} };
export default function handler( export default function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>
) { ) {
res.status(200).json({ res.status(200).json({
links: site_lrs as Link[], links: site_lrs as Link[],
}) });
} }

View File

@ -1,74 +1,75 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import site_lrs from '../../data/site_lrs.json' import site_lrs from "../../data/site_lrs.json";
import webfingerData from '../../data/webfinger.json' import webfingerData from "../../data/webfinger.json";
import { collect as collectEcho } from './whoami' import { collect as collectEcho } from "./whoami";
type Data = { type Data = {
subject: string, subject: string;
aliases: string[] | undefined, aliases: string[] | undefined;
links: any[], links: any[];
} };
export function lookupData(username: string): { export function lookupData(username: string):
aliases: string[], | {
links: any[] aliases: string[];
} | undefined { links: any[];
let data = (webfingerData as any[string])[username] }
if (data != undefined && data.aliasTo != undefined) | undefined {
return lookupData(data.aliasTo) let data = (webfingerData as any[string])[username];
return data if (data != undefined && data.aliasTo != undefined)
return lookupData(data.aliasTo);
return data;
} }
export default function handler( export default function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>
) { ) {
let uri: string = req.query['resource'] as string let uri: string = req.query["resource"] as string;
if (uri == undefined) { if (uri == undefined) {
res.status(400).end('"resource" query param is not provided') res.status(400).end('"resource" query param is not provided');
return 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:')) { case "echo": {
res.status(404).end('Only acct urls are allowed') links = [
return {
rel: "contents",
href: JSON.stringify(collectEcho(req)),
},
];
break;
} }
if (uri.indexOf('@') == -1) { default: {
res.status(404).end('Username is not provided') let result = lookupData(username);
return 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('//')) res.status(200).json({
username = username.substring(2) subject: uri,
let aliases: string[] = [] aliases: aliases != undefined && aliases.length == 0 ? undefined : aliases,
let links: any[] = [] links,
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,
})
} }

View File

@ -1,15 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from "next";
import type { IncomingHttpHeaders } from 'node:http' import type { IncomingHttpHeaders } from "node:http";
type Data = { type Data = {
httpVersion: string, httpVersion: string;
headers: IncomingHttpHeaders, headers: IncomingHttpHeaders;
address: string, address: string;
port: number, port: number;
ipv6: boolean, ipv6: boolean;
method: string, method: string;
userAgent: string | undefined, userAgent: string | undefined;
} };
export function collect(req: NextApiRequest): Data { export function collect(req: NextApiRequest): Data {
return { return {
@ -17,15 +17,15 @@ export function collect(req: NextApiRequest): Data {
headers: req.headers, headers: req.headers,
address: req.socket.remoteAddress!, address: req.socket.remoteAddress!,
port: req.socket.remotePort!, port: req.socket.remotePort!,
ipv6: req.socket.remoteFamily == 'IPv6', ipv6: req.socket.remoteFamily == "IPv6",
method: req.method!, method: req.method!,
userAgent: req.headers["user-agent"], userAgent: req.headers["user-agent"],
} };
} }
export default function handler( export default function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>
) { ) {
res.status(200).json(collect(req)) res.status(200).json(collect(req));
} }