Start VERY basic ActivityPub implementation - user endpoint and webfinger acct: support
This commit is contained in:
parent
41cd00a413
commit
258abeb13a
41
src/app/http/controllers/pub/Webfinger.controller.ts
Normal file
41
src/app/http/controllers/pub/Webfinger.controller.ts
Normal file
@ -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<User>()
|
||||
.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<User>()
|
||||
.where('username', '=', username)
|
||||
.first()
|
||||
|
||||
if ( !user ) {
|
||||
return http(HTTPStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
return json(await user.toWebfinger())
|
||||
}
|
||||
|
||||
}
|
10
src/app/http/routes/pub.routes.ts
Normal file
10
src/app/http/routes/pub.routes.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {Route} from '@extollo/lib'
|
||||
import {Webfinger} from '../controllers/pub/Webfinger.controller'
|
||||
|
||||
Route.get('/.well-known/webfinger')
|
||||
.calls<Webfinger>(Webfinger, w => w.getWebfinger)
|
||||
|
||||
Route.group('/pub', () => {
|
||||
Route.get('/:username')
|
||||
.calls<Webfinger>(Webfinger, w => w.getUser)
|
||||
})
|
@ -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<void> {
|
||||
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<void> {
|
||||
const schema = this.db.get().schema()
|
||||
const table = await schema.table('pub_certificates')
|
||||
|
||||
table.dropIfExists()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
}
|
@ -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<Pub.Webfinger> {
|
||||
return {
|
||||
subject: `acct:${this.username}@garrettmills.dev`, // fixme
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: this.pubUrl,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async toPub(): Promise<Pub.Actor> {
|
||||
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<Maybe<Certificate>> {
|
||||
return Certificate.query<Certificate>()
|
||||
.where('reltype', '=', 'user')
|
||||
.where('relid', '=', this.userId)
|
||||
.first()
|
||||
}
|
||||
|
||||
async getCertificate(): Promise<Certificate> {
|
||||
const existing = await this.certificate()
|
||||
if ( existing ) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const certificate = this.make<Certificate>(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<string> {
|
||||
return new Promise<string>((res, rej) => {
|
||||
child_process.exec(cmd, (err, stdout, stderr) => {
|
||||
if ( err ) {
|
||||
return rej(err)
|
||||
}
|
||||
|
||||
res(stdout.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
22
src/app/models/pub/Certificate.model.ts
Normal file
22
src/app/models/pub/Certificate.model.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Injectable, Model, Field, FieldType} from '@extollo/lib'
|
||||
|
||||
@Injectable()
|
||||
export class Certificate extends Model<Certificate> {
|
||||
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
|
||||
}
|
39
src/pub/types/index.ts
Normal file
39
src/pub/types/index.ts
Normal file
@ -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[],
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user