From 92d543dbbd3f439239cdd7b2c13b33dd070fa058 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 21 Oct 2024 06:20:41 +0200 Subject: [PATCH] Keys seem to be working! This is wild though: https://fosstodon.org/@hongminhee/113343168981354525 --- .vscode/launch.json | 18 +- src/database/index.ts | 176 ++++++++++++++++++ src/database/init/04-create-actor.sql | 8 +- src/database/init/05-create-actor_service.sql | 5 +- src/database/init/08-insert-actor_service.sql | 13 ++ src/database/init/index.ts | 41 ---- src/database/kv.ts | 12 -- src/database/postgres.ts | 22 --- src/database/query/get-actor_guild.sql | 12 ++ src/database/query/get-actor_player.sql | 12 ++ src/database/query/get-actor_service.sql | 8 + src/dv/dotinoVeloce.ts | 65 +++++-- src/entry/dbInit.ts | 12 ++ src/entry/server.ts | 21 +++ src/main.ts | 23 --- 15 files changed, 315 insertions(+), 133 deletions(-) create mode 100644 src/database/index.ts create mode 100644 src/database/init/08-insert-actor_service.sql delete mode 100644 src/database/init/index.ts delete mode 100644 src/database/kv.ts delete mode 100644 src/database/postgres.ts create mode 100644 src/database/query/get-actor_guild.sql create mode 100644 src/database/query/get-actor_player.sql create mode 100644 src/database/query/get-actor_service.sql create mode 100644 src/entry/dbInit.ts create mode 100644 src/entry/server.ts delete mode 100644 src/main.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c0e5980..0b30c2a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,9 +6,9 @@ "configurations": [ { "request": "launch", - "name": "Dev Server", + "name": "Reinitialize database", "type": "node", - "program": "${workspaceFolder}/main.ts", + "program": "${workspaceFolder}/src/entry/dbInit.ts", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", @@ -17,28 +17,26 @@ "--watch", "--no-prompt", "--allow-all", - "--inspect-wait", - "src/main.ts" + "--inspect-wait" ], "attachSimplePort": 9229 }, { - "request": "launch", - "name": "Init database", + "name": "Dev Server", "type": "node", - "program": "${workspaceFolder}/main.ts", + "program": "${workspaceFolder}/src/entry/server.ts", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/local.env", "runtimeExecutable": "/usr/bin/deno", "runtimeArgs": [ "run", + "--watch", "--no-prompt", "--allow-all", - "--inspect-wait", - "src/database/init/index.ts" + "--inspect-wait" ], "attachSimplePort": 9229 - }, + } ] } \ No newline at end of file diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..4bce443 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,176 @@ +import Postgres from "@@npm/postgres" +import { getLogger } from "@logtape/logtape" +import { exportJwk, generateCryptoKeyPair, KvStore } from "@fedify/fedify" +import { PostgresKvStore } from "@fedify/postgres/kv" + + +const l = getLogger(["dotino-veloce", "database", "postgres"]) + + +export type DbActor = { + handle: string, + public_rsa: JsonWebKey, + public_ed: JsonWebKey, + private_rsa: JsonWebKey, + private_ed: JsonWebKey, +} + +export type DbActorService = DbActor + +export type DbActorPlayer = DbActor & { + steam_id: bigint, +} + +export type DbActorGuild = DbActor & { + guild_id: number, +} + + +export class Database { + sql: Postgres.Sql + + constructor(connString: string) { + l.info`Creating Postgres object with string: ${connString}` + this.sql = Postgres(connString) + } + + static fromEnv() { + l.debug`Getting connection string from environment variable DOTINO_POSTGRES_STRING...` + const connString = Deno.env.get("DOTINO_POSTGRES_STRING") + if(!connString) { + l.error`DOTINO_POSTGRES_STRING is unset.` + throw new Error("DOTINO_POSTGRES_STRING is unset.") + } + + return new Database(connString) + } + + useAsKvStore(): KvStore { + l.info`Creating Postgres key-value store...` + return new PostgresKvStore(this.sql, {}) + } + + async #doQueryFile(path: string, variables: Postgres.ParameterOrJSON[] = []): Promise> { + try { + l.debug`Running query at ${path}...` + var results = await this.sql.file(path, variables, undefined) + } + catch (e) { + l.error`Query at ${path} errored out with: ${e}` + throw e + } + l.debug`Query was successful, results are: ${results}` + return results + } + + async reinitializeDev() { + l.warn`Re-initializing database from scratch for development!!!` + + l.info`01/XX Dropping public schema from database...` + await this.#doQueryFile("src/database/init/01-drop-schema.sql") + + l.info`02/XX Recreating public schema...` + await this.#doQueryFile("src/database/init/02-create-schema.sql") + + l.info`03/XX Creating applied migrations table...` + await this.#doQueryFile("src/database/init/03-create-migration_applied.sql") + + l.info`04/XX Creating actor table...` + await this.#doQueryFile("src/database/init/04-create-actor.sql") + + l.info`05/XX Creating service table...` + await this.#doQueryFile("src/database/init/05-create-actor_service.sql") + + l.info`06/XX Creating player table...` + await this.#doQueryFile("src/database/init/06-create-actor_player.sql") + + l.info`07/XX Creating guild table...` + await this.#doQueryFile("src/database/init/07-create-actor_guild.sql") + + l.info`08/XX Creating service actor...` + l.debug`Creating RSA key pair...` + const {publicKey: publicRsa, privateKey: privateRsa} = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5") + l.debug`Creating elliptic curve key pair...` + const {publicKey: publicEd, privateKey: privateEd} = await generateCryptoKeyPair("Ed25519") + l.debug`Exporting to JWK format...` + const publicRsaJwk = await exportJwk(publicRsa) + const publicEdJwk = await exportJwk(publicEd) + const privateRsaJwk = await exportJwk(privateRsa) + const privateEdJwk = await exportJwk(privateEd) + l.debug`Running SQL query...` + await this.#doQueryFile( + "src/database/init/08-insert-actor_service.sql", + [ + publicRsaJwk, + publicEdJwk, + privateRsaJwk, + privateEdJwk, + ] + ) + + l.info("Done!") + } + + async getActorService(): Promise { + l.info`Getting service actor entity...` + + const results = await this.#doQueryFile("src/database/query/get-actor_service.sql") + + if(results.count == 0) { + l.error`No service actor entity exists.` + throw new Error("No service actor entity exists.") + } + else if(results.count > 1) { + l.error`Multiple service actor entities exist.` + throw new Error("Multiple service actor entities exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } + + async getActorPlayer(steamId: number): Promise { + l.info`Getting player actor entity with id: ${steamId}` + + const results = await this.#doQueryFile("src/database/query/get-actor_player.sql", [steamId]) + + if(results.count == 0) { + l.debug`No such player actor entity exists.` + return null + } + else if(results.count > 1) { + l.error`Multiple player actor entities having the same SteamID exist.` + throw new Error("Multiple player actor entities having the same SteamID exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } + + async getActorGuild(guildId: number): Promise { + l.info`Getting guild actor entity with id: ${guildId}` + + const results = await this.#doQueryFile("src/database/query/get-actor_guild.sql", [guildId]) + + if(results.count == 0) { + l.debug`No such guild actor entity exists.` + return null + } + else if(results.count > 1) { + l.error`Multiple guild actor entities having the same Guild ID exist.` + throw new Error("Multiple guild actor entities having the same Guild ID exist.") + } + + l.debug`Attempting to get only result...` + const result = results[0] + l.debug`Filtered down to result: ${result}` + + return result + } +} diff --git a/src/database/init/04-create-actor.sql b/src/database/init/04-create-actor.sql index cf073bc..a313e49 100644 --- a/src/database/init/04-create-actor.sql +++ b/src/database/init/04-create-actor.sql @@ -1,9 +1,3 @@ CREATE TABLE actor ( - handle VARCHAR PRIMARY KEY, - - public_rsassa BYTEA NOT NULL, - private_rsassa BYTEA NOT NULL, - - public_jwk BYTEA NOT NULL, - private_jwk BYTEA NOT NULL + handle VARCHAR PRIMARY KEY ); diff --git a/src/database/init/05-create-actor_service.sql b/src/database/init/05-create-actor_service.sql index e4318d7..e8c5c5e 100644 --- a/src/database/init/05-create-actor_service.sql +++ b/src/database/init/05-create-actor_service.sql @@ -1,5 +1,8 @@ CREATE TABLE actor_service ( - service_id SERIAL UNIQUE NOT NULL + public_rsa JSON NOT NULL, + public_ed JSON NOT NULL, + private_rsa JSON NOT NULL, + private_ed JSON NOT NULL ) INHERITS ( actor ); diff --git a/src/database/init/08-insert-actor_service.sql b/src/database/init/08-insert-actor_service.sql new file mode 100644 index 0000000..e44ae36 --- /dev/null +++ b/src/database/init/08-insert-actor_service.sql @@ -0,0 +1,13 @@ +INSERT INTO actor_service ( + handle, + public_rsa, + public_ed, + private_rsa, + private_ed +) VALUES ( + 'service', + $1, + $2, + $3, + $4 +); diff --git a/src/database/init/index.ts b/src/database/init/index.ts deleted file mode 100644 index a41c07a..0000000 --- a/src/database/init/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" -import { createPostgresFromEnv } from "../postgres.ts" -import { initLogging } from "../../deno/logging.ts" - - -const l = getLogger(["dotino-veloce", "database", "init"]) - - -async function main() { - await initLogging() - - l.debug`Creating Postgres instance...` - const postgres = createPostgresFromEnv() - - l.info`01/XX Dropping public schema from database...` - await postgres.file("src/database/init/01-drop-schema.sql") - - l.info`02/XX Recreating public schema...` - await postgres.file("src/database/init/02-create-schema.sql") - - l.info`03/XX Creating applied migrations table...` - await postgres.file("src/database/init/03-create-migration_applied.sql") - - l.info`04/XX Creating actor table...` - await postgres.file("src/database/init/04-create-actor.sql") - - l.info`05/XX Creating service table...` - await postgres.file("src/database/init/05-create-actor_service.sql") - - l.info`06/XX Creating player table...` - await postgres.file("src/database/init/06-create-actor_player.sql") - - l.info`07/XX Creating guild table...` - await postgres.file("src/database/init/07-create-actor_guild.sql") - - l.info("Done!") - Deno.exit(0) -} - - -await main() diff --git a/src/database/kv.ts b/src/database/kv.ts deleted file mode 100644 index 97d6aff..0000000 --- a/src/database/kv.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PostgresKvStore } from "@fedify/postgres/kv" -import { getLogger } from "@logtape/logtape" -import Postgres from "@@npm/postgres" - - -const l = getLogger(["dotino-veloce", "database", "kv"]) - - -export function createPostgresKvStore(postgres: Postgres.Sql): PostgresKvStore { - l.info`Creating Postgres key-value store...` - return new PostgresKvStore(postgres, {}) -} diff --git a/src/database/postgres.ts b/src/database/postgres.ts deleted file mode 100644 index 9380ad9..0000000 --- a/src/database/postgres.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Postgres from "@@npm/postgres" -import { getLogger } from "@logtape/logtape" - - -const l = getLogger(["dotino-veloce", "database", "postgres"]) - - -export function createPostgres(connString: string): Postgres.Sql { - l.info`Creating Postgres object with string: ${connString}` - return Postgres(connString) -} - -export function createPostgresFromEnv(): Postgres.Sql { - l.debug`Getting connection string from environment variable DOTINO_POSTGRES_STRING...` - const connString = Deno.env.get("DOTINO_POSTGRES_STRING") - if(!connString) { - l.error`DOTINO_POSTGRES_STRING is unset.` - throw new Error("DOTINO_POSTGRES_STRING is unset.") - } - - return createPostgres(connString) -} diff --git a/src/database/query/get-actor_guild.sql b/src/database/query/get-actor_guild.sql new file mode 100644 index 0000000..8c85971 --- /dev/null +++ b/src/database/query/get-actor_guild.sql @@ -0,0 +1,12 @@ +SELECT + actor_guild.handle, + actor_guild.guild_id, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_guild, + actor_service +WHERE + actor_guild.steam_id = $1; diff --git a/src/database/query/get-actor_player.sql b/src/database/query/get-actor_player.sql new file mode 100644 index 0000000..6a65da9 --- /dev/null +++ b/src/database/query/get-actor_player.sql @@ -0,0 +1,12 @@ +SELECT + actor_player.handle, + actor_player.steam_id, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_player, + actor_service +WHERE + actor_player.steam_id = $1; diff --git a/src/database/query/get-actor_service.sql b/src/database/query/get-actor_service.sql new file mode 100644 index 0000000..c1997bf --- /dev/null +++ b/src/database/query/get-actor_service.sql @@ -0,0 +1,8 @@ +SELECT + actor_service.handle, + actor_service.public_rsa, + actor_service.public_ed, + actor_service.private_rsa, + actor_service.private_ed +FROM + actor_service; diff --git a/src/dv/dotinoVeloce.ts b/src/dv/dotinoVeloce.ts index 64ad4ed..b172d2b 100644 --- a/src/dv/dotinoVeloce.ts +++ b/src/dv/dotinoVeloce.ts @@ -1,8 +1,9 @@ // deno-lint-ignore-file require-await -import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints } from "@fedify/fedify" +import { createFederation, Person, Application, Image, PropertyValue, Organization, Federation, KvStore, Context, Actor, Follow, Endpoints, importSpki, importJwk } from "@fedify/fedify" import { getLogger } from "https://jsr.io/@logtape/logtape/0.6.3/logtape/logger.ts" import { escapeHtml } from "@@x/escape" import { StratzAPI } from "../stratz/api.ts" +import { Database } from "../database/index.ts" const l = getLogger(["dotino-veloce", "ap", "federation"]) @@ -12,39 +13,39 @@ type ContextData = undefined export class DotinoVeloce { + db: Database stratz: StratzAPI federation: Federation - constructor(kv: KvStore, stratz: StratzAPI) { + constructor(db: Database, stratz: StratzAPI) { + this.db = db this.stratz = stratz - this.federation = createFederation({ kv }) + this.federation = createFederation({ kv: db.useAsKvStore() }) this.federation .setActorDispatcher("/users/{identifier}", this.#actorHandler.bind(this)) + .setKeyPairsDispatcher(this.#actorKeys.bind(this)) .mapHandle(this.#actorMapper.bind(this)) - // .setKeyPairsDispatcher(this.#actorKeys.bind(this)) this.federation .setInboxListeners("/inbox/{identifier}", "/inbox") .on(Follow, this.#followHandler.bind(this)) } - #commonActorProperties(ctx: Context, handle: string): Partial { + async #commonActorProperties(ctx: Context, handle: string): Promise> { l.debug`Generating common actor properties for ${handle}...` const properties = { + preferredUsername: handle, id: ctx.getActorUri(handle), + publicKeys: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.cryptographicKey), + assertionMethods: (await ctx.getActorKeyPairs(handle)).map(keyPair => keyPair.multikey), + // Akkoma requires inboxes to be setup to display profiles + // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 inbox: ctx.getInboxUri(handle), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), - preferredUsername: handle, - // Akkoma expects URL to be equal to ID - // Or does it? This makes no sense to me... - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/object/fetcher.ex#L287 - // url: id, - // Akkoma requires inboxes to be setup to display profiles - // https://akkoma.dev/AkkomaGang/akkoma/src/commit/f1018867097e6f293d8b2b5b6935f0a7ebf99bd0/lib/pleroma/web/activity_pub/object_validators/user_validator.ex#L72 } l.debug`Generated common actor properties for ${handle}: ${properties}` return properties @@ -79,7 +80,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for player ${steamId}...` const actor = new Person({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: `[TEST] ${escapeHtml(player.name)}`, icon: new Image({ @@ -140,7 +141,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for guild ${guildId}...` const actor = new Organization({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: `[TEST] ${escapeHtml(guild.name)}`, summary: escapeHtml(guild.description), @@ -181,7 +182,7 @@ export class DotinoVeloce { l.debug`Generating ActivityPub actor for service...` const actor = new Application({ - ...this.#commonActorProperties(ctx, handle), + ...(await this.#commonActorProperties(ctx, handle)), name: "[TEST] Dotino Veloce", summary: "Service account of a Dotino Veloce instance.", @@ -210,11 +211,41 @@ export class DotinoVeloce { } async #actorMapper(_ctx: Context, handle: string): Promise { + l.debug`Getting handle mapping for: ${handle}` return handle } - async #actorKeys(ctx: Context, handle: string): Promise { - throw "TODO: Not implemented" + async #actorKeys(_ctx: Context, handle: string): Promise { + l.debug`Getting keys for: ${handle}` + + l.debug`Requesting keys from the database...` + const { + public_rsa: publicRsaJwk, + public_ed: publicEdJwk, + private_rsa: privateRsaJwk, + private_ed: privateEdJwk, + } = await this.db.getActorService() + + l.debug`Importing keys from JWK format...` + const publicRsa = await importJwk(publicRsaJwk, "public") + const publicEd = await importJwk(publicEdJwk, "public") + const privateRsa = await importJwk(privateRsaJwk, "private") + const privateEd = await importJwk(privateEdJwk, "private") + + l.debug`Bundling keys...` + const result = [ + { + publicKey: publicRsa, + privateKey: privateRsa, + }, + { + publicKey: publicEd, + privateKey: privateEd, + } + ] + l.debug`Bundled keys are: ${result}` + + return result } async #followHandler(ctx: Context, follow: Follow) { diff --git a/src/entry/dbInit.ts b/src/entry/dbInit.ts new file mode 100644 index 0000000..a4220c0 --- /dev/null +++ b/src/entry/dbInit.ts @@ -0,0 +1,12 @@ +import { Database } from "../database/index.ts" +import { initLogging } from "../deno/logging.ts" + + +async function main() { + await initLogging() + const db = Database.fromEnv() + await db.reinitializeDev() +} + + +main() diff --git a/src/entry/server.ts b/src/entry/server.ts new file mode 100644 index 0000000..eb9bc68 --- /dev/null +++ b/src/entry/server.ts @@ -0,0 +1,21 @@ +import { doServe } from "../deno/server.ts" +import { initLogging } from "../deno/logging.ts" +import { StratzAPI } from "../stratz/api.ts" +import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" +import { createRouter } from "../deno/router.ts" +import { DotinoVeloce } from "../dv/dotinoVeloce.ts" +import { Database } from "../database/index.ts" + + +async function main() { + await initLogging() + const db = Database.fromEnv() + const stratz = StratzAPI.fromEnv() + const ap = new DotinoVeloce(db, stratz) + const router = createRouter(ap) + const routerBehindProxy = behindProxy(router as Fetch) + await doServe(routerBehindProxy) +} + + +main() diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index ca598a0..0000000 --- a/src/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { doServe } from "./deno/server.ts" -import { initLogging } from "./deno/logging.ts" -import { StratzAPI } from "./stratz/api.ts" -import { behindProxy, Fetch } from "@hongminhee/x-forwarded-fetch" -import { createRouter } from "./deno/router.ts" -import { DotinoVeloce } from "./dv/dotinoVeloce.ts" -import { createPostgresKvStore } from "./database/kv.ts" -import { createPostgresFromEnv } from "./database/postgres.ts" - - -async function main() { - await initLogging() - const postgres = createPostgresFromEnv() - const kv = createPostgresKvStore(postgres) - const stratz = StratzAPI.fromEnv() - const ap = new DotinoVeloce(kv, stratz) - const router = createRouter(ap) - const routerBehindProxy = behindProxy(router as Fetch) - await doServe(routerBehindProxy) -} - - -main()