diff --git a/src/app/http/controllers/pub/Webfinger.controller.ts b/src/app/http/controllers/pub/Webfinger.controller.ts new file mode 100644 index 0000000..907ef35 --- /dev/null +++ b/src/app/http/controllers/pub/Webfinger.controller.ts @@ -0,0 +1,41 @@ +import {Controller, ErrorWithContext, http, HTTPStatus, Injectable, json} from '@extollo/lib' +import {User} from '../../../models/User.model' + +@Injectable() +export class Webfinger extends Controller { + + async getUser() { + const username = this.request.safe('username').string() + + // @ts-ignore + const user = await User.query() + .where('username', '=', username) + .first() + + if ( !user ) { + return http(HTTPStatus.NOT_FOUND) + } + + return json(await user.toPub()) + } + + async getWebfinger() { + const resource = this.request.safe('resource').string() + if ( !resource.startsWith('acct:') || !resource.endsWith('@garrettmills.dev') ) { // fixme + throw new ErrorWithContext('Invalid webfinger resource query', { resource }) + } + + const username = resource.slice('acct:'.length, -('@garrettmills.dev'.length)) // fixme + // @ts-ignore + const user = await User.query() + .where('username', '=', username) + .first() + + if ( !user ) { + return http(HTTPStatus.NOT_FOUND) + } + + return json(await user.toWebfinger()) + } + +} diff --git a/src/app/http/routes/pub.routes.ts b/src/app/http/routes/pub.routes.ts new file mode 100644 index 0000000..92ed671 --- /dev/null +++ b/src/app/http/routes/pub.routes.ts @@ -0,0 +1,10 @@ +import {Route} from '@extollo/lib' +import {Webfinger} from '../controllers/pub/Webfinger.controller' + +Route.get('/.well-known/webfinger') + .calls(Webfinger, w => w.getWebfinger) + +Route.group('/pub', () => { + Route.get('/:username') + .calls(Webfinger, w => w.getUser) +}) diff --git a/src/app/migrations/2023-11-07T04:03:41.732Z_CreatePubCertificatesTableMigration.migration.ts b/src/app/migrations/2023-11-07T04:03:41.732Z_CreatePubCertificatesTableMigration.migration.ts new file mode 100644 index 0000000..6058265 --- /dev/null +++ b/src/app/migrations/2023-11-07T04:03:41.732Z_CreatePubCertificatesTableMigration.migration.ts @@ -0,0 +1,52 @@ +import {Injectable, Migration, Inject, DatabaseService, FieldType} from '@extollo/lib' + +/** + * CreatePubCertificatesTableMigration + * ---------------------------------- + * Put some description here. + */ +@Injectable() +export default class CreatePubCertificatesTableMigration extends Migration { + @Inject() + protected readonly db!: DatabaseService + + /** + * Apply the migration. + */ + async up(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('pub_certificates') + + table.primaryKey('certificate_id') + + table.column('reltype') + .type(FieldType.varchar) + + table.column('relid') + .type(FieldType.bigint) + + table.column('pubkey') + .type(FieldType.text) + + table.column('privkey') + .type(FieldType.text) + + table.index('pub_cert_rel') + .field('reltype') + .field('relid') + + await schema.commit(table) + } + + /** + * Undo the migration. + */ + async down(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('pub_certificates') + + table.dropIfExists() + + await schema.commit(table) + } +} diff --git a/src/app/models/User.model.ts b/src/app/models/User.model.ts index 3d05a77..2bb6533 100644 --- a/src/app/models/User.model.ts +++ b/src/app/models/User.model.ts @@ -1,5 +1,77 @@ -import {ORMUser} from '@extollo/lib' +import {Maybe, ModelBuilder, ORMUser, Related} from '@extollo/lib' +import {Pub} from '../../pub/types' +import {Certificate} from './pub/Certificate.model' +import * as child_process from 'child_process' export class User extends ORMUser { + get pubUrl(): string { + return `https://garrettmills.dev/pub/${this.username}` + } + async toWebfinger(): Promise { + return { + subject: `acct:${this.username}@garrettmills.dev`, // fixme + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: this.pubUrl, + }, + ], + } + } + + async toPub(): Promise { + return { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + id: this.pubUrl, + type: 'Person', + preferredUsername: this.username, + inbox: `${this.pubUrl}/inbox`, + publicKey: { + id: `${this.pubUrl}#main-key`, + owner: this.pubUrl, + publicKeyPem: await this.getCertificate().then(c => c.pubkey), + }, + } + } + + certificate(): Promise> { + return Certificate.query() + .where('reltype', '=', 'user') + .where('relid', '=', this.userId) + .first() + } + + async getCertificate(): Promise { + const existing = await this.certificate() + if ( existing ) { + return existing + } + + const certificate = this.make(Certificate) + certificate.reltype = 'user' + certificate.relid = this.userId + certificate.privkey = await this.exec('openssl genrsa -out - 2048') + certificate.pubkey = await this.exec(`echo "${certificate.privkey}" | openssl rsa -outform PEM -pubout -out -`) + await certificate.save() + + return certificate + } + + /** Dirty, dirty, dirty. @fixme */ + private async exec(cmd: string): Promise { + return new Promise((res, rej) => { + child_process.exec(cmd, (err, stdout, stderr) => { + if ( err ) { + return rej(err) + } + + res(stdout.trim()) + }) + }) + } } diff --git a/src/app/models/pub/Certificate.model.ts b/src/app/models/pub/Certificate.model.ts new file mode 100644 index 0000000..92d2c0a --- /dev/null +++ b/src/app/models/pub/Certificate.model.ts @@ -0,0 +1,22 @@ +import {Injectable, Model, Field, FieldType} from '@extollo/lib' + +@Injectable() +export class Certificate extends Model { + protected static table = 'pub_certificates' + protected static key = 'certificate_id' + + @Field(FieldType.serial, 'certificate_id') + public certificateId?: number + + @Field(FieldType.varchar) + public reltype: 'user' = 'user' + + @Field(FieldType.bigint) + public relid!: number + + @Field(FieldType.text) + public pubkey!: string + + @Field(FieldType.text) + public privkey!: string +} diff --git a/src/pub/types/index.ts b/src/pub/types/index.ts new file mode 100644 index 0000000..efbc72b --- /dev/null +++ b/src/pub/types/index.ts @@ -0,0 +1,39 @@ + +export namespace Pub { + const ACCEPT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + + export interface Config { + uri: string, + } + + export interface Object { + id: string, + type: string, // FIXME + } + + export interface Actor extends Object { + ["@context"]: [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + type: 'Person', + preferredUsername: string, + inbox: string, + publicKey: { + id: string, + owner: string, + publicKeyPem: string, + }, + } + + export interface Link { + rel: string, + type: string, + href: string, + } + + export interface Webfinger { + subject: string, + links: Link[], + } +}