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 {
|
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