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' {
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;
}
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;
}

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 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"]})`;

View File

@ -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}`);
}

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 { URL } from "url";
import { ACCEPT_HEADER, USER_AGENT } from "./consts";
export async function resolveApEntity(ref: EntityReference): Promise<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`
}
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<BaseEntity> {
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;
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<Data>
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({
links: site_lrs as Link[],
})
res.status(200).json({
links: site_lrs as Link[],
});
}

View File

@ -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<Data>
req: NextApiRequest,
res: NextApiResponse<Data>
) {
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,
});
}

View File

@ -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<Data>
) {
res.status(200).json(collect(req))
res.status(200).json(collect(req));
}