From 03fff6a629bd49020569e9613094dcfb6f5f906c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Sat, 24 Dec 2022 17:36:49 -0500 Subject: [PATCH 001/275] begin move --- Mouseion/ARCHITECTURE.MD | 35 ++++++++++++++++ {Mouseion2 => Mouseion}/client.ts | 0 {Mouseion2 => Mouseion}/engine.ts | 0 {Mouseion2 => Mouseion}/server.ts | 0 {Mouseion => MouseionArtifacts}/README.md | 0 .../actors/operator.ts | 0 .../actors/resolver.ts | 0 .../actors/sync.ts | 0 .../actors/tailor.ts | 0 .../managers/cleaners.ts | 0 .../managers/folders.ts | 0 .../managers/mailbox.ts | 0 .../managers/register.ts | 0 .../pantheon/pantheon.ts | 0 .../pantheon/puppet.ts | 0 .../pantheon/puppeteer.ts | 0 .../post-office/post-office.ts | 0 .../post-office/puppet.ts | 0 .../post-office/puppeteer.ts | 0 .../post-office/types.ts | 0 .../queues/MessageQueue.ts | 0 .../queues/board-rules.ts | 0 .../queues/contacts.ts | 0 .../utils/cleaner.ts | 0 .../utils/common-actions.ts | 0 .../utils/do-in-batch.ts | 0 .../utils/logger.ts | 0 .../utils/marionette.ts | 0 .../utils/promise-lock.ts | 0 .../utils/retry.ts | 0 .../utils/sequence.ts | 0 .../utils/sleep.ts | 0 .../utils/storage.ts | 0 .../utils/types.ts | 0 package-lock.json | 40 +++++++++++++++++++ package.json | 1 + prisma/schema.prisma | 11 +++++ 37 files changed, 87 insertions(+) create mode 100644 Mouseion/ARCHITECTURE.MD rename {Mouseion2 => Mouseion}/client.ts (100%) rename {Mouseion2 => Mouseion}/engine.ts (100%) rename {Mouseion2 => Mouseion}/server.ts (100%) rename {Mouseion => MouseionArtifacts}/README.md (100%) rename {Mouseion => MouseionArtifacts}/actors/operator.ts (100%) rename {Mouseion => MouseionArtifacts}/actors/resolver.ts (100%) rename {Mouseion => MouseionArtifacts}/actors/sync.ts (100%) rename {Mouseion => MouseionArtifacts}/actors/tailor.ts (100%) rename {Mouseion => MouseionArtifacts}/managers/cleaners.ts (100%) rename {Mouseion => MouseionArtifacts}/managers/folders.ts (100%) rename {Mouseion => MouseionArtifacts}/managers/mailbox.ts (100%) rename {Mouseion => MouseionArtifacts}/managers/register.ts (100%) rename {Mouseion => MouseionArtifacts}/pantheon/pantheon.ts (100%) rename {Mouseion => MouseionArtifacts}/pantheon/puppet.ts (100%) rename {Mouseion => MouseionArtifacts}/pantheon/puppeteer.ts (100%) rename {Mouseion => MouseionArtifacts}/post-office/post-office.ts (100%) rename {Mouseion => MouseionArtifacts}/post-office/puppet.ts (100%) rename {Mouseion => MouseionArtifacts}/post-office/puppeteer.ts (100%) rename {Mouseion => MouseionArtifacts}/post-office/types.ts (100%) rename {Mouseion => MouseionArtifacts}/queues/MessageQueue.ts (100%) rename {Mouseion => MouseionArtifacts}/queues/board-rules.ts (100%) rename {Mouseion => MouseionArtifacts}/queues/contacts.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/cleaner.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/common-actions.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/do-in-batch.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/logger.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/marionette.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/promise-lock.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/retry.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/sequence.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/sleep.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/storage.ts (100%) rename {Mouseion => MouseionArtifacts}/utils/types.ts (100%) create mode 100644 prisma/schema.prisma diff --git a/Mouseion/ARCHITECTURE.MD b/Mouseion/ARCHITECTURE.MD new file mode 100644 index 00000000..a2d1583d --- /dev/null +++ b/Mouseion/ARCHITECTURE.MD @@ -0,0 +1,35 @@ +# Architecture + +## Flow + +Mouseion is a singleton now. Pantheon is also a singleton now. + +1. User signs in with a new mailbox +2. Mouseion verifies credentials +3. Mouseion spawns a Mailbox for the new mailbox. +4. Every sync cycle, Mouseion pings each Mailbox. +5. Each mailbox performs the following steps: + - Fetch configs: + - M = maximum number of messages to hold + - X = maximum number of messages to sync + - Fetch indices: + - N = newest index on server + - I = oldest index locally + - J = newest index locally + - If the mailbox does not have all historical messages + - If the mailbox has less than M messages + - Lookbehind in large pages until M is reached OR all synced + - If the mailbox has local messages + - Sync the flags/existence of last X local messages + - If J < N + - If N - J > X + - fetch the latest X messages looking behind from N + - Else + - fetch J:N + - Dump all changes into a Queue and begin Pipeline + - Pipeline: + - Custodian + - Threading + - Board Rules + - Contacts + - Attachments \ No newline at end of file diff --git a/Mouseion2/client.ts b/Mouseion/client.ts similarity index 100% rename from Mouseion2/client.ts rename to Mouseion/client.ts diff --git a/Mouseion2/engine.ts b/Mouseion/engine.ts similarity index 100% rename from Mouseion2/engine.ts rename to Mouseion/engine.ts diff --git a/Mouseion2/server.ts b/Mouseion/server.ts similarity index 100% rename from Mouseion2/server.ts rename to Mouseion/server.ts diff --git a/Mouseion/README.md b/MouseionArtifacts/README.md similarity index 100% rename from Mouseion/README.md rename to MouseionArtifacts/README.md diff --git a/Mouseion/actors/operator.ts b/MouseionArtifacts/actors/operator.ts similarity index 100% rename from Mouseion/actors/operator.ts rename to MouseionArtifacts/actors/operator.ts diff --git a/Mouseion/actors/resolver.ts b/MouseionArtifacts/actors/resolver.ts similarity index 100% rename from Mouseion/actors/resolver.ts rename to MouseionArtifacts/actors/resolver.ts diff --git a/Mouseion/actors/sync.ts b/MouseionArtifacts/actors/sync.ts similarity index 100% rename from Mouseion/actors/sync.ts rename to MouseionArtifacts/actors/sync.ts diff --git a/Mouseion/actors/tailor.ts b/MouseionArtifacts/actors/tailor.ts similarity index 100% rename from Mouseion/actors/tailor.ts rename to MouseionArtifacts/actors/tailor.ts diff --git a/Mouseion/managers/cleaners.ts b/MouseionArtifacts/managers/cleaners.ts similarity index 100% rename from Mouseion/managers/cleaners.ts rename to MouseionArtifacts/managers/cleaners.ts diff --git a/Mouseion/managers/folders.ts b/MouseionArtifacts/managers/folders.ts similarity index 100% rename from Mouseion/managers/folders.ts rename to MouseionArtifacts/managers/folders.ts diff --git a/Mouseion/managers/mailbox.ts b/MouseionArtifacts/managers/mailbox.ts similarity index 100% rename from Mouseion/managers/mailbox.ts rename to MouseionArtifacts/managers/mailbox.ts diff --git a/Mouseion/managers/register.ts b/MouseionArtifacts/managers/register.ts similarity index 100% rename from Mouseion/managers/register.ts rename to MouseionArtifacts/managers/register.ts diff --git a/Mouseion/pantheon/pantheon.ts b/MouseionArtifacts/pantheon/pantheon.ts similarity index 100% rename from Mouseion/pantheon/pantheon.ts rename to MouseionArtifacts/pantheon/pantheon.ts diff --git a/Mouseion/pantheon/puppet.ts b/MouseionArtifacts/pantheon/puppet.ts similarity index 100% rename from Mouseion/pantheon/puppet.ts rename to MouseionArtifacts/pantheon/puppet.ts diff --git a/Mouseion/pantheon/puppeteer.ts b/MouseionArtifacts/pantheon/puppeteer.ts similarity index 100% rename from Mouseion/pantheon/puppeteer.ts rename to MouseionArtifacts/pantheon/puppeteer.ts diff --git a/Mouseion/post-office/post-office.ts b/MouseionArtifacts/post-office/post-office.ts similarity index 100% rename from Mouseion/post-office/post-office.ts rename to MouseionArtifacts/post-office/post-office.ts diff --git a/Mouseion/post-office/puppet.ts b/MouseionArtifacts/post-office/puppet.ts similarity index 100% rename from Mouseion/post-office/puppet.ts rename to MouseionArtifacts/post-office/puppet.ts diff --git a/Mouseion/post-office/puppeteer.ts b/MouseionArtifacts/post-office/puppeteer.ts similarity index 100% rename from Mouseion/post-office/puppeteer.ts rename to MouseionArtifacts/post-office/puppeteer.ts diff --git a/Mouseion/post-office/types.ts b/MouseionArtifacts/post-office/types.ts similarity index 100% rename from Mouseion/post-office/types.ts rename to MouseionArtifacts/post-office/types.ts diff --git a/Mouseion/queues/MessageQueue.ts b/MouseionArtifacts/queues/MessageQueue.ts similarity index 100% rename from Mouseion/queues/MessageQueue.ts rename to MouseionArtifacts/queues/MessageQueue.ts diff --git a/Mouseion/queues/board-rules.ts b/MouseionArtifacts/queues/board-rules.ts similarity index 100% rename from Mouseion/queues/board-rules.ts rename to MouseionArtifacts/queues/board-rules.ts diff --git a/Mouseion/queues/contacts.ts b/MouseionArtifacts/queues/contacts.ts similarity index 100% rename from Mouseion/queues/contacts.ts rename to MouseionArtifacts/queues/contacts.ts diff --git a/Mouseion/utils/cleaner.ts b/MouseionArtifacts/utils/cleaner.ts similarity index 100% rename from Mouseion/utils/cleaner.ts rename to MouseionArtifacts/utils/cleaner.ts diff --git a/Mouseion/utils/common-actions.ts b/MouseionArtifacts/utils/common-actions.ts similarity index 100% rename from Mouseion/utils/common-actions.ts rename to MouseionArtifacts/utils/common-actions.ts diff --git a/Mouseion/utils/do-in-batch.ts b/MouseionArtifacts/utils/do-in-batch.ts similarity index 100% rename from Mouseion/utils/do-in-batch.ts rename to MouseionArtifacts/utils/do-in-batch.ts diff --git a/Mouseion/utils/logger.ts b/MouseionArtifacts/utils/logger.ts similarity index 100% rename from Mouseion/utils/logger.ts rename to MouseionArtifacts/utils/logger.ts diff --git a/Mouseion/utils/marionette.ts b/MouseionArtifacts/utils/marionette.ts similarity index 100% rename from Mouseion/utils/marionette.ts rename to MouseionArtifacts/utils/marionette.ts diff --git a/Mouseion/utils/promise-lock.ts b/MouseionArtifacts/utils/promise-lock.ts similarity index 100% rename from Mouseion/utils/promise-lock.ts rename to MouseionArtifacts/utils/promise-lock.ts diff --git a/Mouseion/utils/retry.ts b/MouseionArtifacts/utils/retry.ts similarity index 100% rename from Mouseion/utils/retry.ts rename to MouseionArtifacts/utils/retry.ts diff --git a/Mouseion/utils/sequence.ts b/MouseionArtifacts/utils/sequence.ts similarity index 100% rename from Mouseion/utils/sequence.ts rename to MouseionArtifacts/utils/sequence.ts diff --git a/Mouseion/utils/sleep.ts b/MouseionArtifacts/utils/sleep.ts similarity index 100% rename from Mouseion/utils/sleep.ts rename to MouseionArtifacts/utils/sleep.ts diff --git a/Mouseion/utils/storage.ts b/MouseionArtifacts/utils/storage.ts similarity index 100% rename from Mouseion/utils/storage.ts rename to MouseionArtifacts/utils/storage.ts diff --git a/Mouseion/utils/types.ts b/MouseionArtifacts/utils/types.ts similarity index 100% rename from Mouseion/utils/types.ts rename to MouseionArtifacts/utils/types.ts diff --git a/package-lock.json b/package-lock.json index 26f444fe..9c8f3609 100755 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "concurrently": "^7.4.0", "cross-env": "^7.0.3", "electron": "^20.0.0", + "prisma": "^4.8.0", "tsc-alias": "^1.7.0", "vite": "^3.0.9" } @@ -4069,6 +4070,13 @@ "@octokit/openapi-types": "^12.7.0" } }, + "node_modules/@prisma/engines": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.8.0.tgz", + "integrity": "sha512-A1Asn2rxZMlLAj1HTyfaCv0VQrLUv034jVay05QlqZg1qiHPeA3/pGTfNMijbsMYCsGVxfWEJuaZZuNxXGMCrA==", + "dev": true, + "hasInstallScript": true + }, "node_modules/@remusao/guess-url-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@remusao/guess-url-type/-/guess-url-type-1.2.1.tgz", @@ -15273,6 +15281,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prisma": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.8.0.tgz", + "integrity": "sha512-DWIhxvxt8f4h6MDd35mz7BJff+fu7HItW3WPDIEpCR3RzcOWyiHBbLQW5/DOgmf+pRLTjwXQob7kuTZVYUAw5w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.8.0" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -22145,6 +22170,12 @@ "@octokit/openapi-types": "^12.7.0" } }, + "@prisma/engines": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.8.0.tgz", + "integrity": "sha512-A1Asn2rxZMlLAj1HTyfaCv0VQrLUv034jVay05QlqZg1qiHPeA3/pGTfNMijbsMYCsGVxfWEJuaZZuNxXGMCrA==", + "dev": true + }, "@remusao/guess-url-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@remusao/guess-url-type/-/guess-url-type-1.2.1.tgz", @@ -30933,6 +30964,15 @@ "parse-ms": "^2.1.0" } }, + "prisma": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.8.0.tgz", + "integrity": "sha512-DWIhxvxt8f4h6MDd35mz7BJff+fu7HItW3WPDIEpCR3RzcOWyiHBbLQW5/DOgmf+pRLTjwXQob7kuTZVYUAw5w==", + "dev": true, + "requires": { + "@prisma/engines": "4.8.0" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index e85397e4..dc86f84b 100755 --- a/package.json +++ b/package.json @@ -204,6 +204,7 @@ "concurrently": "^7.4.0", "cross-env": "^7.0.3", "electron": "^20.0.0", + "prisma": "^4.8.0", "tsc-alias": "^1.7.0", "vite": "^3.0.9" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..d205f42a --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,11 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} From 7950572646ff5d6f55d827a4bd10b46f850e0bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Mon, 26 Dec 2022 02:06:51 -0500 Subject: [PATCH 002/275] SockPuppet 2 --- Marionette/marionette.ts | 0 Marionette/process/sockpuppet.ts | 114 +++++++++++++++++++++++++ Marionette/process/sockpuppeteer.ts | 114 +++++++++++++++++++++++++ Marionette/ws/sockpuppet.ts | 120 ++++++++++++++++++++++++++ Marionette/ws/sockpuppeteer.ts | 125 ++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+) create mode 100644 Marionette/marionette.ts create mode 100644 Marionette/process/sockpuppet.ts create mode 100644 Marionette/process/sockpuppeteer.ts create mode 100644 Marionette/ws/sockpuppet.ts create mode 100644 Marionette/ws/sockpuppeteer.ts diff --git a/Marionette/marionette.ts b/Marionette/marionette.ts new file mode 100644 index 00000000..e69de29b diff --git a/Marionette/process/sockpuppet.ts b/Marionette/process/sockpuppet.ts new file mode 100644 index 00000000..975a8d41 --- /dev/null +++ b/Marionette/process/sockpuppet.ts @@ -0,0 +1,114 @@ +import Forest from '@Iris/utils/logger' +import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import autoBind from 'auto-bind' + +interface SockPuppetProcess extends NodeJS.Process { + send: (message: any, sendHandle?: any, options?: { + swallowErrors?: boolean | undefined; + } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean +} +type SockPuppetry = {[key: string]: (...args: any[]) => Promise} + + +export default abstract class SockPuppet { + + private readonly proc: SockPuppetProcess = process;; + private deployed: boolean = false; + protected readonly Log: Logger; + abstract puppetry: SockPuppetry; + + private psucc(id: string): (payload: object) => boolean { + const proc = this.proc + return (payload: object): boolean => proc.send(JSON.stringify({ + success: true, + payload, id + })) + } + + private perr(id: string): (msg: string) => boolean { + const proc = this.proc + return (msg: string): boolean => proc.send(JSON.stringify({ + error: msg + '\n' + (new Error), + payload: {}, + success: false, + id + })) + } + + abstract checkInitialize(): boolean; + + abstract initialize(args: any[], success: (payload: object) => boolean): Promise; + + protected constructor(protected name: string, logdir?: string) { + const forest: Forest = new Forest(logdir) + const Lumberjack: LumberjackEmployer = forest.Lumberjack + this.Log = Lumberjack(this.name) + if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + process.title = "Aiko Mail | IPC | " + this.name + autoBind(this) + } + + /** Deploys the SockPuppet; you cannot redeploy (must do a complete teardown). */ + public deploy() { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + const _this = this + + this.proc.on('message', async (m: string): Promise => { + /* + ? m should be 'please ' + JSON stringified message + * object should have the following structure: + * { + * id: String, // some random string to make ipc easier + * action: String, + * args: [...] // must ALWAYS be set. for no args just do [] + * } + */ + + try { + + const { + id, + action, + args + }: { + id: string, + action: string, + args: any[] + } = JSON.parse(m.slice('please '.length)) + + if (!id) return _this.Log.error("No ID provided to sock puppet.") + if (!action) return _this.Log.error("No action provided to sock puppet.") + if (!Array.isArray(args)) return _this.Log.error("Args not provided to sock puppet as array.") + + const success = _this.psucc(id) + const error = _this.perr(id) + + if (!(_this.checkInitialize() || action === 'init')) + return error("Pantheon has not yet been initialized.") + + const attempt = async (method: (...xs: any) => Promise | any) => { + try { + const result = await method(...args) + return success(result) + } catch (e) { + _this.Log.error(e) + if (typeof e === 'string') return error(e) + else if (e instanceof Error) return error(e.message) + else return error(JSON.stringify(e)) + } + } + + if (action === 'init') return await _this.initialize(args, success) + if (action in _this.puppetry) return await attempt(_this.puppetry[action]) + else return error("No such binding: " + action) + + } catch (e) { + return _this.proc.send(JSON.stringify({ + error: e + '\n' + (new Error) + })) + } + }) + } + +} \ No newline at end of file diff --git a/Marionette/process/sockpuppeteer.ts b/Marionette/process/sockpuppeteer.ts new file mode 100644 index 00000000..6c90720c --- /dev/null +++ b/Marionette/process/sockpuppeteer.ts @@ -0,0 +1,114 @@ +import path from 'path' +import { fork, ChildProcess } from 'child_process' +import crypto from 'crypto' +import Forest from '@Iris/utils/logger' +import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import autoBind from 'auto-bind' + +type SockPuppeteerWaiterParams = { + success: boolean, + payload: any, + error?: string, + id: string +} +type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void +type SockPuppeteerListener = () => void + +type ValueType = + T extends Promise + ? U + : T;; + +type ProcessMessage = { id: string, msg: string } + +export default abstract class SockPuppeteer { + protected readonly Log: Logger + private readonly API: ChildProcess + private deployed: boolean = false; + + private readonly waiters: Record = {} + private readonly listeners: Record = {} + private readonly queue: ProcessMessage[] = [] + private rotating: boolean = false + private getID(): string { + const id = crypto.randomBytes(6).toString('hex') + if (this.waiters[id]) return this.getID() + return id + } + + protected constructor(protected name: string, logdir: string) { + const forest: Forest = new Forest(logdir) + const Lumberjack: LumberjackEmployer = forest.Lumberjack + this.Log = Lumberjack(this.name) + if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + process.title = "Aiko Mail | IPC | " + this.name + + this.API = fork(path.join(__dirname, 'puppet.js'), [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }) + this.API.stdout?.pipe(process.stdout) + this.API.stderr?.pipe(process.stderr) + autoBind(this) + } + + public deploy() { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + + //? Parses incoming messages then calls the relevant callbacks and notifies listeners + this.API.on('message', (m: string) => { + const s = JSON.parse(m) as SockPuppeteerWaiterParams + if (!(s?.id)) return this.Log.error("No ID in received message") + const cb = this.waiters[s.id] + if (!cb) return this.Log.error("No waiter set.") + const listener = this.listeners[s.id] + if (listener) listener() + cb(s) + }) + } + + private async rotate() { + if (this.queue.length > 0) { + this.rotating = true + const { id, msg } = this.queue.shift() as ProcessMessage //? TS didn't connx length > 0 to shift() != undefined + this.listeners[id] = () => { + delete this.listeners[id] + this.rotate() + } + this.API.send(msg) + } else { + this.rotating = false + } + } + + protected proxy Promise>(action: string, immediate: boolean = true) { + return (...args: Parameters): Promise>> => new Promise((s, _) => { + const id = this.getID() + const instr = { id, action, args } + + const cb: SockPuppeteerWaiter = ({ success, payload, error }: { + success: boolean, + payload: ValueType>, + error?: string + }) => { + if (error || !success) { + this.Log.error(id, '|', error || 'Failed without error.') + _() + } + else s(payload) + delete this.waiters[id] + } + + this.waiters[id] = cb + + if (!immediate) { + this.queue.push({ + id, msg: 'please ' + JSON.stringify(instr) + }) + if (!this.rotating) this.rotate() + } else { + this.API.send('please ' + JSON.stringify(instr)) + } + }) + } +} diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts new file mode 100644 index 00000000..14e9cfa0 --- /dev/null +++ b/Marionette/ws/sockpuppet.ts @@ -0,0 +1,120 @@ +import WebSocket, { Server } from 'ws' +import { unused_port, RESERVED_PORTS } from '@Iris/utils/port' +import Forest from '@Iris/utils/logger' +import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import autoBind from 'auto-bind' + +interface SockPuppetProcess extends NodeJS.Process { + send: (message: any, sendHandle?: any, options?: { + swallowErrors?: boolean | undefined; + } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean +} +type SockPuppetry = { [key: string]: (...args: any[]) => Promise } + + +export default abstract class SockPuppet { + + private readonly proc: SockPuppetProcess = process;; + private deployed: boolean = false; + protected readonly Log: Logger; + abstract puppetry: SockPuppetry; + + abstract checkInitialize(): boolean; + + abstract initialize(args: any[], success: (payload: object) => void): Promise; + + protected constructor(protected name: string, logdir?: string) { + const forest: Forest = new Forest(logdir) + const Lumberjack: LumberjackEmployer = forest.Lumberjack + this.Log = Lumberjack(this.name) + if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + process.title = "Aiko Mail | WS | " + this.name + autoBind(this) + } + + /** Deploys the SockPuppet; you cannot redeploy (must do a complete teardown). */ + public async deploy(port?: number) { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + const _this = this + + //? spawn websocket server + const _port = await unused_port(port) + const wss = new Server({ port: _port }) + this.proc.send({ port: _port, }) + wss.on("connection", (ws: WebSocket) => { + + const succ = (id: string): ((payload: object) => void) => { + return (payload: object): void => ws.send(JSON.stringify({ + success: true, + payload, id + })) + } + const err = (id: string): ((msg: string) => void) => { + return (msg: string): void => ws.send(JSON.stringify({ + error: msg + '\n' + (new Error), + payload: {}, + success: false, + id + })) + } + + ws.on('message', async (m: string): Promise => { + /* + ? m should be 'please ' + JSON stringified message + * object should have the following structure: + * { + * id: String, // some random string to make ipc easier + * action: String, + * args: [...] // must ALWAYS be set. for no args just do [] + * } + */ + + try { + + const { + id, + action, + args + }: { + id: string, + action: string, + args: any[] + } = JSON.parse(m.slice('please '.length)) + + if (!id) return _this.Log.error("No ID provided to sock puppet.") + if (!action) return _this.Log.error("No action provided to sock puppet.") + if (!Array.isArray(args)) return _this.Log.error("Args not provided to sock puppet as array.") + + const success = succ(id) + const error = err(id) + + if (!(_this.checkInitialize() || action === 'init')) + return error("Pantheon has not yet been initialized.") + + const attempt = async (method: (...xs: any) => Promise | any) => { + try { + const result = await method(...args) + return success(result) + } catch (e) { + _this.Log.error(e) + if (typeof e === 'string') return error(e) + else if (e instanceof Error) return error(e.message) + else return error(JSON.stringify(e)) + } + } + + if (action === 'init') return await _this.initialize(args, success) + if (action in _this.puppetry) return await attempt(_this.puppetry[action]) + else return error("No such binding: " + action) + + } catch (e) { + return ws.send(JSON.stringify({ + error: e + '\n' + (new Error) + })) + } + }) + }) + } + +} \ No newline at end of file diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts new file mode 100644 index 00000000..b1a86a8d --- /dev/null +++ b/Marionette/ws/sockpuppeteer.ts @@ -0,0 +1,125 @@ +import path from 'path' +import { fork, ChildProcess } from 'child_process' +import crypto from 'crypto' +import Forest from '@Iris/utils/logger' +import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import autoBind from 'auto-bind' + +type SockPuppeteerWaiterParams = { + success: boolean, + payload: any, + error?: string, + id: string +} +type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void +type SockPuppeteerListener = () => void + +type ValueType = + T extends Promise + ? U + : T;; + +type ProcessMessage = { id: string, msg: string } + +export default abstract class SockPuppeteer { + protected readonly Log: Logger + private readonly Puppet: ChildProcess + private API?: WebSocket + private deployed: boolean = false; + + private readonly waiters: Record = {} + private readonly listeners: Record = {} + private readonly queue: ProcessMessage[] = [] + private rotating: boolean = false + private getID(): string { + const id = crypto.randomBytes(6).toString('hex') + if (this.waiters[id]) return this.getID() + return id + } + + protected constructor(protected name: string, logdir: string) { + const forest: Forest = new Forest(logdir) + const Lumberjack: LumberjackEmployer = forest.Lumberjack + this.Log = Lumberjack(this.name) + if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + process.title = "Aiko Mail | WS | " + this.name + + this.Puppet = fork(path.join(__dirname, 'puppet.js'), [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }) + this.Puppet.stdout?.pipe(process.stdout) + this.Puppet.stderr?.pipe(process.stderr) + + //? Parses incoming messages then calls the relevant callbacks and notifies listeners + this.Puppet.on('message', (m: string) => { + const s = JSON.parse(m) as { port: number } + if (!(s?.port)) return this.Log.error("No PORT specified in message") + this.deploy(s.port) + }) + + autoBind(this) + } + + private async deploy(port: number) { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + // connect to websocket on port s.port + const ws = new WebSocket(`ws://localhost:${port}`) + this.API = ws + ws.binaryType = 'arraybuffer' + ws.onmessage = (m: MessageEvent): any => { + const s = JSON.parse(m.data) as SockPuppeteerWaiterParams + if (!(s?.id)) return this.Log.error("No ID specified in message") + const cb = this.waiters[s.id] + if (!cb) return this.Log.error("No waiter set.") + const listener = this.listeners[s.id] + if (listener) listener() + cb(s) + } + } + + private async rotate() { + if (this.queue.length > 0) { + this.rotating = true + const { id, msg } = this.queue.shift() as ProcessMessage //? TS didn't connx length > 0 to shift() != undefined + this.listeners[id] = () => { + delete this.listeners[id] + this.rotate() + } + this.API!.send(msg) + } else { + this.rotating = false + } + } + + protected proxy Promise>(action: string, immediate: boolean = true) { + return (...args: Parameters): Promise>> => new Promise((s, _) => { + const id = this.getID() + const instr = { id, action, args } + + const cb: SockPuppeteerWaiter = ({ success, payload, error }: { + success: boolean, + payload: ValueType>, + error?: string + }) => { + if (error || !success) { + this.Log.error(id, '|', error || 'Failed without error.') + _() + } + else s(payload) + delete this.waiters[id] + } + + this.waiters[id] = cb + + if (!immediate) { + this.queue.push({ + id, msg: 'please ' + JSON.stringify(instr) + }) + if (!this.rotating) this.rotate() + } else { + this.API!.send('please ' + JSON.stringify(instr)) + } + }) + } +} From cdacdcd87016f233c8c71d3a6fb0d8cb40685b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Mon, 26 Dec 2022 12:41:28 -0500 Subject: [PATCH 003/275] marionette stands stronger --- Marionette/ipc.ts | 134 ++++++++++++++++++++++ Marionette/process/sockpuppet.ts | 34 ++++-- Marionette/process/sockpuppeteer.ts | 12 +- Marionette/ws/sockpuppet.ts | 34 ++++-- Marionette/ws/sockpuppeteer.ts | 12 +- tsconfig.json | 1 + utils/logger.ts | 171 ++++++++++++++++++++++++++++ utils/port.ts | 26 +++++ utils/storage.ts | 132 +++++++++++++++++++++ 9 files changed, 524 insertions(+), 32 deletions(-) create mode 100644 Marionette/ipc.ts create mode 100644 utils/logger.ts create mode 100644 utils/port.ts create mode 100644 utils/storage.ts diff --git a/Marionette/ipc.ts b/Marionette/ipc.ts new file mode 100644 index 00000000..497a2288 --- /dev/null +++ b/Marionette/ipc.ts @@ -0,0 +1,134 @@ +import { ipcMain } from 'electron' +import WebSocket, { Server } from 'ws' +import { sign, verify } from 'jsonwebtoken' +import { randomBytes } from 'crypto' +import { unused_port, RESERVED_PORTS } from '@Iris/utils/port' +import autoBind from 'auto-bind' +import express from 'express' + +/** Singleton, use **extremely sparingly.** */ +export default class SecureCommunications { + private readonly key: string + readonly port: number + private readonly wss: Server + private readonly waiters: Record void> = {} + private readonly connections: WebSocket[] = [] + private readonly app: express.Express + + private constructor(port: number) { + this.port = port + + this.app = express() + this.app.listen(RESERVED_PORTS.COMMS.EXPRESS, () => console.log("Comms web relay is ACTIVE".green)) + + this.key = randomBytes(32).toString('hex') + console.log("New Secure Communications object created.") + ipcMain.handle("key exchange", async (_, q): Promise => { + const { secret } = q as { secret: string } + const token = sign({ token: secret }, this.key, { expiresIn: 60 * 60 * 24 * 7 }) + const payload = sign({ token, }, secret, { expiresIn: 60 * 60 * 24 * 7 }) + return payload + }) + ipcMain.handle("status", async (): Promise<{success: true}> => { + return { success: true } + }) + ipcMain.handle("get websocket port", () => this.port) + + this.wss = new Server({ port, }) + const _this = this + this.wss.on("connection", (ws: WebSocket) => { + this.connections.push(ws) + ws.on("message", (m: string) => { + const { stream } = JSON.parse(m) as { stream: string } + if (_this.waiters[stream]) _this.waiters[stream]() + }) + }) + + autoBind(this) + } + + tag = (): string => randomBytes(32).toString('hex') + + private _send(tag: string, data: any): Promise { + if (!(this.port || this.wss)) throw 'WebSocket server has not yet been started for IPC stream!' + return new Promise((s, _) => { + this.waiters[tag] = s + this.connections.reduceRight(_ => _).send(JSON.stringify({ tag, data })) + }) + } + + //? Returns the payload you should send back through normal IPC + async send(data: any): Promise<{stream: string}> { + const tag = this.tag() + await this._send(tag, data) + return {stream: tag} + } + + static async init(): Promise { + const port = await unused_port(RESERVED_PORTS.COMMS.WS) + const comms = new SecureCommunications(port) + return comms + } + + private verify(tok: string): string { + if (!tok) throw "Missing token" + //! FIXME: this is likely an error as the defined return type of verify is string + const { token } = verify(tok, this.key) as { token: string } + return token + } + + private sign(secret: string, payload: any): string { + return sign(payload, secret, { expiresIn: 60 * 60 * 24 * 7 }) + } + + /** @deprecated Securely handles IPC events with a custom handler callback (JWT verified). */ + on(event: string, handler: any) { + const _this = this + ipcMain.handle(event, async (_, q) => { + const { token } = q + + let client_secret: string; + try { client_secret = _this.verify(token) } catch (e) { + console.error(e) + return { error: e } + } + if (!client_secret) return { error: "Couldn't decode client secret." } + + try { + const payload = await handler(q) + if (payload?.error) return payload + return { + s: _this.sign(client_secret, { + success: true, + payload, + }) + } + } catch (e) { + return { error: e } + } + + }) + } + + /** @deprecated Handles IPC events with a custom handler callback, no signatures or other frills. */ + static unsafeOn(event: string, handler: any) { + ipcMain.handle(event, async (_, q) => { + try { + const payload = await handler(q) + if (payload?.error) return payload + return { success: true, payload } + } catch (e) { + return { error: e } + } + }) + } + + /** Handles a GET request with a custom handler callback. */ + registerGET(route: string, cb: any, {respondWithClose=true}={}) { + this.app.get(route, (q, s) => { + cb(q) //? no await -- we don't care if it actually works + if (respondWithClose) return s.redirect("https://helloaiko.com/redirect") + else return s.status(200).send("OK") + }) + } +} diff --git a/Marionette/process/sockpuppet.ts b/Marionette/process/sockpuppet.ts index 975a8d41..95b77957 100644 --- a/Marionette/process/sockpuppet.ts +++ b/Marionette/process/sockpuppet.ts @@ -1,4 +1,4 @@ -import Forest from '@Iris/utils/logger' +import { Lumberjack } from '@Iris/utils/logger' import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' import autoBind from 'auto-bind' @@ -9,12 +9,32 @@ interface SockPuppetProcess extends NodeJS.Process { } type SockPuppetry = {[key: string]: (...args: any[]) => Promise} - -export default abstract class SockPuppet { +/* + ? Usage: + * class MySockPuppet extends SockPuppet { + * puppetry = { + * foo: async (bar: string) => something, + * bar: { + * baz: async (qux: number) => something + * } + * } + * checkInitialize(): boolean { + * return true + * } + * async initialize(args: any[], success: (payload: object) => void): Promise { + * success({ foo: "bar" }) + * } + * constructor() { + * super("MySockPuppet") + * } + * } + * const puppet = new MySockPuppet() + * puppet.deploy() + */ +export default abstract class SockPuppet extends Lumberjack { private readonly proc: SockPuppetProcess = process;; private deployed: boolean = false; - protected readonly Log: Logger; abstract puppetry: SockPuppetry; private psucc(id: string): (payload: object) => boolean { @@ -40,10 +60,8 @@ export default abstract class SockPuppet { abstract initialize(args: any[], success: (payload: object) => boolean): Promise; protected constructor(protected name: string, logdir?: string) { - const forest: Forest = new Forest(logdir) - const Lumberjack: LumberjackEmployer = forest.Lumberjack - this.Log = Lumberjack(this.name) - if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + super(name, { logdir }) + if (!process.send) throw new Error("Process was spawned without IPC and is now likely in a BAD state.") process.title = "Aiko Mail | IPC | " + this.name autoBind(this) } diff --git a/Marionette/process/sockpuppeteer.ts b/Marionette/process/sockpuppeteer.ts index 6c90720c..17fed057 100644 --- a/Marionette/process/sockpuppeteer.ts +++ b/Marionette/process/sockpuppeteer.ts @@ -1,7 +1,7 @@ import path from 'path' import { fork, ChildProcess } from 'child_process' import crypto from 'crypto' -import Forest from '@Iris/utils/logger' +import Forest, { Lumberjack } from '@Iris/utils/logger' import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' import autoBind from 'auto-bind' @@ -21,8 +21,7 @@ type ValueType = type ProcessMessage = { id: string, msg: string } -export default abstract class SockPuppeteer { - protected readonly Log: Logger +export default abstract class SockPuppeteer extends Lumberjack { private readonly API: ChildProcess private deployed: boolean = false; @@ -36,11 +35,8 @@ export default abstract class SockPuppeteer { return id } - protected constructor(protected name: string, logdir: string) { - const forest: Forest = new Forest(logdir) - const Lumberjack: LumberjackEmployer = forest.Lumberjack - this.Log = Lumberjack(this.name) - if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + protected constructor(protected name: string, forest: Forest) { + super(name, { forest }) process.title = "Aiko Mail | IPC | " + this.name this.API = fork(path.join(__dirname, 'puppet.js'), [], { diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 14e9cfa0..4790135f 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -1,6 +1,6 @@ import WebSocket, { Server } from 'ws' import { unused_port, RESERVED_PORTS } from '@Iris/utils/port' -import Forest from '@Iris/utils/logger' +import { Lumberjack } from '@Iris/utils/logger' import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' import autoBind from 'auto-bind' @@ -11,12 +11,32 @@ interface SockPuppetProcess extends NodeJS.Process { } type SockPuppetry = { [key: string]: (...args: any[]) => Promise } - -export default abstract class SockPuppet { +/* + ? Usage: + * class MySockPuppet extends SockPuppet { + * puppetry = { + * foo: async (bar: string) => something, + * bar: { + * baz: async (qux: number) => something + * } + * } + * checkInitialize(): boolean { + * return true + * } + * async initialize(args: any[], success: (payload: object) => void): Promise { + * success({ foo: "bar" }) + * } + * constructor() { + * super("MySockPuppet") + * } + * } + * const puppet = new MySockPuppet() + * puppet.deploy() + */ +export default abstract class SockPuppet extends Lumberjack { private readonly proc: SockPuppetProcess = process;; private deployed: boolean = false; - protected readonly Log: Logger; abstract puppetry: SockPuppetry; abstract checkInitialize(): boolean; @@ -24,10 +44,8 @@ export default abstract class SockPuppet { abstract initialize(args: any[], success: (payload: object) => void): Promise; protected constructor(protected name: string, logdir?: string) { - const forest: Forest = new Forest(logdir) - const Lumberjack: LumberjackEmployer = forest.Lumberjack - this.Log = Lumberjack(this.name) - if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + super(name, {logdir}) + if (!process.send) throw new Error("Puppet was spawned without IPC.") process.title = "Aiko Mail | WS | " + this.name autoBind(this) } diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts index b1a86a8d..926117f9 100644 --- a/Marionette/ws/sockpuppeteer.ts +++ b/Marionette/ws/sockpuppeteer.ts @@ -1,7 +1,7 @@ import path from 'path' import { fork, ChildProcess } from 'child_process' import crypto from 'crypto' -import Forest from '@Iris/utils/logger' +import Forest, { Lumberjack } from '@Iris/utils/logger' import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' import autoBind from 'auto-bind' @@ -21,8 +21,7 @@ type ValueType = type ProcessMessage = { id: string, msg: string } -export default abstract class SockPuppeteer { - protected readonly Log: Logger +export default abstract class SockPuppeteer extends Lumberjack { private readonly Puppet: ChildProcess private API?: WebSocket private deployed: boolean = false; @@ -37,11 +36,8 @@ export default abstract class SockPuppeteer { return id } - protected constructor(protected name: string, logdir: string) { - const forest: Forest = new Forest(logdir) - const Lumberjack: LumberjackEmployer = forest.Lumberjack - this.Log = Lumberjack(this.name) - if (!process.send) this.Log.error("Process was spawned without IPC and is now likely in a BAD state.") + protected constructor(protected name: string, forest: Forest) { + super(name, { forest }) process.title = "Aiko Mail | WS | " + this.name this.Puppet = fork(path.join(__dirname, 'puppet.js'), [], { diff --git a/tsconfig.json b/tsconfig.json index 5982d179..8a9028da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "@Chiton/*":["./Chiton/*"], "@Mouseion/*": ["./Mouseion/*"], "@Veil/*": ["./Veil/*"], + "@Marionette/*": ["./Marionette/*"], "@Iris/*" : ["./*"] } } diff --git a/utils/logger.ts b/utils/logger.ts new file mode 100644 index 00000000..381ec867 --- /dev/null +++ b/utils/logger.ts @@ -0,0 +1,171 @@ +import autoBind from 'auto-bind' +import 'colors' +import crypto from 'crypto' +import path from 'path' +import Storage from '@Iris/utils/storage' +import WebSocket from 'ws' +import sleep from '@Iris/utils/sleep' +import { performance } from 'perf_hooks' + +/** Generates a string timestamp of the current date/time */ +export const Timestamp = (): string => { + const now: Date = new Date() + const date: string = now.toLocaleDateString() + const time: string = now.toTimeString().substr(0, 'HH:MM:SS'.length) + return `[${date.gray} ${time.cyan}]`.bgBlack +} + +/** + * Generates an identifier using the current time, a prefix, and a label + * @param {string} prefix - the prefix this identifier will use + * @param {string} label - a label for this identifier, will automatically be wrapped in square brackets +*/ +const Identifier = (prefix: string, label: string): string => { + const timestamp: string = Timestamp() + const signature: string = `[M]`.rainbow.bgBlack + return `${timestamp}${signature}${prefix}[${label.magenta}]` +} + +type log_fn = (...msg: any[]) => void; +export interface Logger { + log: log_fn + error: log_fn + success: log_fn, + shout: log_fn, + warn: log_fn + time: log_fn + timeEnd: log_fn +} +class UnemployedLumberjack implements Logger { + readonly label: string + readonly forest: Forest + + private timers: {[key: string]: number} = {} + + constructor(label: string, forest: Forest) { + this.label = label + this.forest = forest + } + + private readonly _log = (prefix: string) => (..._: any[]) => + this.forest.logger(prefix, this.label, ..._) + + log = this._log(Forest.prefixes.log).bind(this) + error = this._log(Forest.prefixes.error).bind(this) + shout = this._log(Forest.prefixes.shout).bind(this) + success = this._log(Forest.prefixes.success).bind(this) + warn = this._log(Forest.prefixes.warn).bind(this) + + //! Timing functions will not appear in logs (intentional) + time = (..._: any[]) => { + const label: string = [Forest.prefixes.timer, this.label, ..._].join(' ') + this.timers[label] = performance.now() + } + timeEnd = (..._: any[]) => { + const now = performance.now() + const label = [Forest.prefixes.timer, this.label, ..._].join(' ') + const start = this.timers[label] + if (!start) this._log(Forest.prefixes.warn)('No timer found for', label) + else this._log(Forest.prefixes.timer)(label, ':', now - start, 'ms') + delete this.timers[label] + } + +} +export type LumberjackEmployer = (label: string) => Logger + +//? Initialize one forest per "application" and use Lumberjacks for different labels +export default class Forest { + readonly dir: string; + readonly storage: Storage; + readonly id: string; + private readonly roots?: WebSocket + + static readonly prefixes = { + log: '[ LOG ]'.black.bgWhite, + error: '[ ERROR ]'.white.bgRed, + shout: '[ SHOUT ]'.red.bgCyan, + success: '[SUCCESS]'.green.bgBlack, + warn: '[ WARN⚠️ ]'.yellow.bgBlack, + timer: '[ TIMER ]'.red.bgWhite + } + + constructor(dir: string='logs') { + + //? initialize dir to the correct app datapath + const platform: string = process.platform + switch (platform) { + case 'darwin': dir = path.join( + process.env.HOME as string, 'Library', 'Application Support', + 'Aiko Mail', 'Forest', dir + ); break; + case 'win32': dir = path.join( + process.env.APPDATA as string, + 'Aiko Mail', 'Forest', dir + ); break; + case 'linux': dir = path.join( + process.env.HOME as string, + '.Aiko Mail', 'Forest', dir + ); break; + } + this.dir = dir + + //? logger appends strings so we want regular files + this.storage = new Storage(dir, {json: false}) + + //? randomly generate some "probably unique" identifier + this.id = crypto.randomBytes(6).toString('hex') + try { + const socket = new WebSocket('ws://localhost:4159') + this.roots = socket + } catch (e) { + console.error(e) + } + + console.log(`Forest initialized in ${this.storage.dir}/${this.id}`.green.bgBlack) + autoBind(this) + } + + logger(prefix: string, label: string, ...msg: any[]): void { + const identifier = Identifier(prefix, label) + let e: Error | null = null + if (prefix == Forest.prefixes.error) { + e = new Error("(stack trace pin)") + console.log(identifier, ...msg, e) //? dumps trace + } else if (prefix == Forest.prefixes.shout) { + console.log(identifier, ...(msg.map(m => m.toString().red.bgCyan))) + } else { + console.log(identifier, ...msg) + } + + //? remove color escape sequences and dump to log + const uncolored_msg: string = msg.map((m: string): string => JSON.stringify(m)?.stripColors).join(' ') + const clean_msg: string = e ? `${identifier?.stripColors} ${uncolored_msg}\n${e.stack}\n` : `${identifier?.stripColors} ${uncolored_msg}\n` + this.storage.append(this.id, clean_msg) + this.sendToRoots(clean_msg) + } + + async sendToRoots(msg: string, max_tries=10): Promise { + if (!(this.roots)) return; + for(let i = 0; i < max_tries; i++) { + if (this.roots.readyState === 1) return (this.roots.send(msg)) + await sleep(200) + } + } + + Lumberjack = (label: string): Logger => new UnemployedLumberjack(label, this) + +} + +export abstract class Lumberjack { + protected readonly Log: Logger; + + protected constructor(protected name: string, opts: { + forest?: Forest, + logdir?: string + }) { + const forest: Forest = opts.forest ?? new Forest(opts.logdir) + const Lumberjack: LumberjackEmployer = forest.Lumberjack + this.Log = Lumberjack(this.name) + autoBind(this) + } +} \ No newline at end of file diff --git a/utils/port.ts b/utils/port.ts new file mode 100644 index 00000000..eaceddef --- /dev/null +++ b/utils/port.ts @@ -0,0 +1,26 @@ +import net from 'net' +const DEFAULT_PORT = 41600 + +export const unused_port = async (start_port=DEFAULT_PORT): Promise => { + const look_for_port = (port: number): Promise => new Promise((s, _) => { + const serv = net.createServer() + serv.listen(port, () => { + serv.once('close', () => s(port)) + serv.close() + }) + serv.on('error', _ => { + look_for_port(port + 1).then(p => s(p)) + }) + }) + + return await look_for_port(start_port) +} + +export const RESERVED_PORTS = { + VEIL: 4160, + COMMS: { + EXPRESS: 41599, //! HARD-CODED INTO REDIRECT-URI, DO NOT CHANGE + WS: 4161, + }, + +} \ No newline at end of file diff --git a/utils/storage.ts b/utils/storage.ts new file mode 100644 index 00000000..e69dee4c --- /dev/null +++ b/utils/storage.ts @@ -0,0 +1,132 @@ +import fs from 'fs' +import fs2 from 'fs-extra' +import autoBind from 'auto-bind' + +/** A basic storage system mimicking localstorage, persisting within the filesystem */ +class Storage { + + /* + * Basic storage system that mimics localstorage by using keys that you can store, load, pop + ! However, everything is stored in [JSON] files with hard disk space, be careful! + + * store(key: String, data: [JSON-serializable] object/string/etc) => void, writes the data to the key + * load(key: String) => parsed object/string/etc, reads the data stored in key + * pop(key: String) => parsed object/string/etc, reads the data stored in key and deletes key + * append(key: String, data: string) => void, appends the data to the key, does not work when json=true + */ + + readonly dir: string //? the directory the storage works out of + readonly json: boolean //? whether or not data is stored in JSON format + readonly raw: boolean //? whether to treat file input as raw Buffers and filepaths as absolute + + constructor(dir: string, {json=true, raw=false}: {json?: boolean, raw?: boolean} ={}) { + this.dir = dir + this.json = json + this.raw = raw + autoBind(this) + } + + // read a file in one single read + static async readFile(filename: string) { + const handle = await fs.promises.open(filename, "r").catch(_ => _) + if (!handle) return null + if (handle instanceof Error) { + return null + } + let buffer: Buffer | null = null + try { + const stats = await handle.stat() + buffer = Buffer.allocUnsafe(stats.size) + const { bytesRead } = await handle.read(buffer, 0, stats.size, 0) + if (bytesRead !== stats.size) { + throw new Error("bytesRead not full file size") + } + } finally { + handle.close() + } + return buffer + } + + //? Cleans a storage key by explicitly allowing only certain characters in the filename + //! Known bug: if two different keys *clean* to the same key filepath, they will overlap + static clean_key = (key: string): string => key.replace(/[^A-z0-9/\-_]/g, '').substr(0, 86) + + //? Creates the correct filepath for a cleaned key, taking into account JSON settings + private filepath = (key: string): string => `${this.dir}/${key}`.substr(0, 122) + `.${this.json ? 'json' : 'log'}` + + /** Stores data into the relevant file for a key, stringifying it if need be */ + async store(key: string, data: any): Promise { + if (!(this.raw)) key = Storage.clean_key(key) + const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) + if (this.raw && fp.includes("/")) { + //? make the relevant directories in the path to avoid errors + const dirs = fp.split("/") + dirs.pop() + const dir = dirs.join("/") + await fs2.ensureDir(dir) + } + await fs2.ensureFile(fp) + await fs.promises.writeFile(fp, this.json ? JSON.stringify(data) : ( + this.raw ? Buffer.from(data) : data + )) + } + cache = this.store.bind(this) + + async has_key(key: string): Promise { + if (!this.raw) key = Storage.clean_key(key) + const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) + try { + return fs.promises.access(fp).then(_ => true).catch(_ => false) + } catch { + return false + } + } + + /** Loads data for a relevant key, parsing it if need be */ + async load(key: string): Promise { + if (!this.raw) key = Storage.clean_key(key) + const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) + if (this.raw) { + console.error(`Raw files are not supported for loading in this version of Mouseion.`) + return null + } else { + const buffer = await Storage.readFile(fp) + if (!buffer) return null + const s: string = buffer.toString() + try { + return !!s && (this.json ? JSON.parse(s) : s) + } catch (e) { + console.error(`Couldn't parse JSON from ${this.dir}/${key}`) + return null + } + } + } + check = this.load.bind(this) + + /** Loads data for the relevant key, parsing it if need be, then clearing the key */ + async pop(key: string): Promise { + key = Storage.clean_key(key) + const fp: string = this.filepath(key) + const buffer = await Storage.readFile(fp) + if (!buffer) return null + const s: string = buffer.toString() + fs.unlinkSync(fp) + try { + return !!s && (this.json ? JSON.parse(s) : s) + } catch (e) { + console.error(`Couldn't parse JSON from ${this.dir}/${key}`) + return null + } + } + + /** Appends a string to a file if the directory is not JSON managed */ + append(key: string, data: string): void { + if (this.json) throw new Error("Cannot append to a JSON-managed directory. Do a full read-write."); + key = Storage.clean_key(key) + const fp: string = this.filepath(key) + fs2.ensureFileSync(fp) + fs.appendFileSync(fp, data) + } +} + +export default Storage \ No newline at end of file From 8387ace6dc8b671438953bbca2b9ab9601e0cd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Wed, 28 Dec 2022 02:09:48 -0500 Subject: [PATCH 004/275] touchups --- Marionette/ipc.ts | 11 ++++++---- Marionette/marionette.ts | 0 Marionette/process/sockpuppet.ts | 3 +-- Marionette/process/sockpuppeteer.ts | 4 ++-- Marionette/ws/sockpuppet.ts | 5 ++--- Marionette/ws/sockpuppeteer.ts | 33 ++++++++++++++++------------- 6 files changed, 30 insertions(+), 26 deletions(-) delete mode 100644 Marionette/marionette.ts diff --git a/Marionette/ipc.ts b/Marionette/ipc.ts index 497a2288..a850a049 100644 --- a/Marionette/ipc.ts +++ b/Marionette/ipc.ts @@ -2,11 +2,11 @@ import { ipcMain } from 'electron' import WebSocket, { Server } from 'ws' import { sign, verify } from 'jsonwebtoken' import { randomBytes } from 'crypto' -import { unused_port, RESERVED_PORTS } from '@Iris/utils/port' +import { unused_port, RESERVED_PORTS } from '@Iris/common/port' import autoBind from 'auto-bind' import express from 'express' -/** Singleton, use **extremely sparingly.** */ +//! Singleton, use extremely sparingly. export default class SecureCommunications { private readonly key: string readonly port: number @@ -64,9 +64,12 @@ export default class SecureCommunications { return {stream: tag} } - static async init(): Promise { - const port = await unused_port(RESERVED_PORTS.COMMS.WS) + private static me?: SecureCommunications + static init(): SecureCommunications { + if (SecureCommunications.me) return SecureCommunications.me + const port = RESERVED_PORTS.COMMS.WS const comms = new SecureCommunications(port) + SecureCommunications.me = comms return comms } diff --git a/Marionette/marionette.ts b/Marionette/marionette.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/Marionette/process/sockpuppet.ts b/Marionette/process/sockpuppet.ts index 95b77957..2eac6fda 100644 --- a/Marionette/process/sockpuppet.ts +++ b/Marionette/process/sockpuppet.ts @@ -1,5 +1,4 @@ -import { Lumberjack } from '@Iris/utils/logger' -import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' interface SockPuppetProcess extends NodeJS.Process { diff --git a/Marionette/process/sockpuppeteer.ts b/Marionette/process/sockpuppeteer.ts index 17fed057..aa6e95ef 100644 --- a/Marionette/process/sockpuppeteer.ts +++ b/Marionette/process/sockpuppeteer.ts @@ -1,8 +1,7 @@ import path from 'path' import { fork, ChildProcess } from 'child_process' import crypto from 'crypto' -import Forest, { Lumberjack } from '@Iris/utils/logger' -import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import Forest, { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' type SockPuppeteerWaiterParams = { @@ -35,6 +34,7 @@ export default abstract class SockPuppeteer extends Lumberjack { return id } + /** Will fork a new process every time. */ protected constructor(protected name: string, forest: Forest) { super(name, { forest }) process.title = "Aiko Mail | IPC | " + this.name diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 4790135f..5492d42c 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -1,7 +1,6 @@ import WebSocket, { Server } from 'ws' -import { unused_port, RESERVED_PORTS } from '@Iris/utils/port' -import { Lumberjack } from '@Iris/utils/logger' -import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import { unused_port, RESERVED_PORTS } from '@Iris/common/port' +import { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' interface SockPuppetProcess extends NodeJS.Process { diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts index 926117f9..a1719bad 100644 --- a/Marionette/ws/sockpuppeteer.ts +++ b/Marionette/ws/sockpuppeteer.ts @@ -1,8 +1,7 @@ import path from 'path' import { fork, ChildProcess } from 'child_process' import crypto from 'crypto' -import Forest, { Lumberjack } from '@Iris/utils/logger' -import type { LumberjackEmployer, Logger } from '@Iris/utils/logger' +import Forest, { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' type SockPuppeteerWaiterParams = { @@ -22,7 +21,6 @@ type ValueType = type ProcessMessage = { id: string, msg: string } export default abstract class SockPuppeteer extends Lumberjack { - private readonly Puppet: ChildProcess private API?: WebSocket private deployed: boolean = false; @@ -36,22 +34,27 @@ export default abstract class SockPuppeteer extends Lumberjack { return id } - protected constructor(protected name: string, forest: Forest) { + /** Leaving port empty will create a child process. */ + protected constructor(protected name: string, forest: Forest, port?: number) { super(name, { forest }) process.title = "Aiko Mail | WS | " + this.name + autoBind(this) - this.Puppet = fork(path.join(__dirname, 'puppet.js'), [], { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'] - }) - this.Puppet.stdout?.pipe(process.stdout) - this.Puppet.stderr?.pipe(process.stderr) + if (port) this.deploy(port) + else { + const Puppet = fork(path.join(__dirname, 'puppet.js'), [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }) + Puppet.stdout?.pipe(process.stdout) + Puppet.stderr?.pipe(process.stderr) - //? Parses incoming messages then calls the relevant callbacks and notifies listeners - this.Puppet.on('message', (m: string) => { - const s = JSON.parse(m) as { port: number } - if (!(s?.port)) return this.Log.error("No PORT specified in message") - this.deploy(s.port) - }) + //? Parses incoming messages then calls the relevant callbacks and notifies listeners + Puppet.on('message', (m: string) => { + const s = JSON.parse(m) as { port: number } + if (!(s?.port)) return this.Log.error("No PORT specified in message") + this.deploy(s.port) + }) + } autoBind(this) } From 5d724f3a19b9ec7f86ed665e3cbeadf0586d4230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Wed, 28 Dec 2022 02:11:39 -0500 Subject: [PATCH 005/275] overhaul roots --- Chiton/components/preload.ts | 8 ++ Chiton/services/roots.ts | 86 +++++++++++++++++++++ Chiton/utils/comms.ts | 141 ----------------------------------- Chiton/utils/roots.ts | 96 ------------------------ {utils => common}/logger.ts | 4 +- {utils => common}/port.ts | 5 +- common/register.ts | 14 ++++ common/sleep.ts | 1 + {utils => common}/storage.ts | 0 9 files changed, 115 insertions(+), 240 deletions(-) create mode 100644 Chiton/components/preload.ts create mode 100644 Chiton/services/roots.ts delete mode 100644 Chiton/utils/comms.ts delete mode 100644 Chiton/utils/roots.ts rename {utils => common}/logger.ts (98%) rename {utils => common}/port.ts (93%) create mode 100644 common/register.ts create mode 100644 common/sleep.ts rename {utils => common}/storage.ts (100%) diff --git a/Chiton/components/preload.ts b/Chiton/components/preload.ts new file mode 100644 index 00000000..ed22de8c --- /dev/null +++ b/Chiton/components/preload.ts @@ -0,0 +1,8 @@ +import { ipcRenderer, contextBridge } from 'electron' + +contextBridge.exposeInMainWorld('platform', process.platform) + +contextBridge.exposeInMainWorld('ChitonVeryInsecureIPC', ipcRenderer) +contextBridge.exposeInMainWorld("ChitonInsecureIPC", { + ipcHandler: (event: string, cb: any) => ipcRenderer.on(event, cb), +}) \ No newline at end of file diff --git a/Chiton/services/roots.ts b/Chiton/services/roots.ts new file mode 100644 index 00000000..d6700bdd --- /dev/null +++ b/Chiton/services/roots.ts @@ -0,0 +1,86 @@ +import autoBind from 'auto-bind' +import 'colors' +import crypto from 'crypto' +import { shell } from 'electron' +import path from 'path' +import WebSocket, { Server } from 'ws' +import Storage from '@Iris/common/storage' +import fs from 'fs' +import { RESERVED_PORTS } from '@Iris/common/port' + +//! Roots requires Electron Shell and should ONLY operate within the main process. + +export default class Roots { + private readonly storage: Storage + private readonly id: string = crypto.randomBytes(6).toString('hex') + private constructor(logdir: string="logs-roots") { + //? Determine storage location + const platform: string = process.platform + switch (platform) { + case 'darwin': logdir = path.join( + process.env.HOME as string, 'Library', 'Application Support', + 'Aiko Mail', 'Mouseion', logdir + ); break; + case 'win32': logdir = path.join( + process.env.APPDATA as string, + 'Aiko Mail', 'Mouseion', logdir + ); break; + case 'linux': logdir = path.join( + process.env.HOME as string, + '.Aiko Mail', 'Mouseion', logdir + ); break; + } + this.storage = new Storage(logdir, {json: false}) + + //? Setup simple Websockets for logging + const wss_local = new Server({ port: RESERVED_PORTS.ROOTS.LOCAL }) + const wss_remote = new Server({ port: RESERVED_PORTS.ROOTS.REMOTE }) + const _this = this + wss_local.on("connection", (ws: WebSocket) => { + ws.on("message", (m: string) => { + _this.log(m) + }) + }) + wss_remote.on("connection", (ws: WebSocket) => { + ws.on("message", (m: string) => { + try { + _this.log(m, true) + } catch (e) { + console.log(e) + } + }) + setInterval(() => ws.send("ping"), 1000) + }) + + console.log(`Roots initialized in ${this.storage.dir}/${this.id}`.green.bgBlack) + autoBind(this) + } + + log(msg: string, from_remote: boolean=false) { + if (from_remote) console.log(msg.replace("\n", " ").red.bgGreen) + this.storage.append(this.id, msg) + if (msg.includes("[ ERROR ]")) { + throw new Error(msg.split("[ ERROR ]")[1]) + } + } + + async getLogs() { + const downloadFolder = (() => { + switch(process.platform) { + case "win32": return `${process.env.USERPROFILE}\\Downloads` + case "darwin": return `${process.env.HOME}/Downloads` + default: return `${process.env.HOME}/Downloads` + } + })() + const log_dest = `${downloadFolder}/aiko-mail-${this.id}.log` + await fs.promises.copyFile(`${this.storage.dir}/${this.id}.log`, log_dest) + await shell.showItemInFolder(path.normalize(log_dest)) + } + + private static me?: Roots + static init(logdir?: string) { + if (this.me) return this.me + this.me = new Roots(logdir) + return this.me + } +} \ No newline at end of file diff --git a/Chiton/utils/comms.ts b/Chiton/utils/comms.ts deleted file mode 100644 index 7f29e799..00000000 --- a/Chiton/utils/comms.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ipcMain } from 'electron' -import WebSocket, { Server } from 'ws' -import { sign, verify } from 'jsonwebtoken' -import { randomBytes } from 'crypto' -import { unused_port } from '@Mouseion/utils/marionette' -import autoBind from 'auto-bind' -import express from 'express' - -const DEFAULT_PORT = 41604 -//! NEVER, NEVER, NEVER, NEVER USE PORT 41599 FOR ANYTHING ELSE -//! WE NEEEEEED 41599 FOR EXPRESS -//! IT IS A HARD-CODED REDIRECT URI PORT FOR SECURITY IN GOOGLE SIGNIN - -//! Create a new comms object for each service, as it -//! will only support a singular websocket connection (the latest is used) -export default class SecureCommunications { - private readonly key: string - readonly port: number - private readonly wss: Server - private readonly waiters: Record void> = {} - private readonly connections: WebSocket[] = [] - private readonly app: express.Express - - private constructor(port: number) { - this.port = port - - this.app = express() - this.app.listen(41599, () => console.log("Comms web relay is ACTIVE".green)) - - this.key = randomBytes(32).toString('hex') - console.log("New Secure Communications object created.") - ipcMain.handle("key exchange", async (_, q): Promise => { - const { secret } = q as { secret: string } - const token = sign({ token: secret }, this.key, { expiresIn: 60 * 60 * 24 * 7 }) - const payload = sign({ token, }, secret, { expiresIn: 60 * 60 * 24 * 7 }) - return payload - }) - ipcMain.handle("status", async (): Promise<{success: true}> => { - return { success: true } - }) - ipcMain.handle("get websocket port", () => this.port) - - this.wss = new Server({ port, }) - const _this = this - this.wss.on("connection", (ws: WebSocket) => { - this.connections.push(ws) - ws.on("message", (m: string) => { - const { stream } = JSON.parse(m) as { stream: string } - if (_this.waiters[stream]) _this.waiters[stream]() - }) - }) - - autoBind(this) - } - - tag = (): string => randomBytes(32).toString('hex') - - private _send(tag: string, data: any): Promise { - if (!(this.port || this.wss)) throw 'WebSocket server has not yet been started for IPC stream!' - return new Promise((s, _) => { - this.waiters[tag] = s - this.connections.reduceRight(_ => _).send(JSON.stringify({ tag, data })) - }) - } - - //? Returns the payload you should send back through normal IPC - async send(data: any): Promise<{stream: string}> { - const tag = this.tag() - await this._send(tag, data) - return {stream: tag} - } - - static async init(): Promise { - const port = await unused_port(DEFAULT_PORT) - const comms = new SecureCommunications(port) - return comms - } - - private verify(tok: string): string { - if (!tok) throw "Missing token" - //! FIXME: this is likely an error as the defined return type of verify is string - const { token } = verify(tok, this.key) as { token: string } - return token - } - - private sign(secret: string, payload: any): string { - return sign(payload, secret, { expiresIn: 60 * 60 * 24 * 7 }) - } - - register(channel: string, cb: any) { - const _this = this - ipcMain.handle(channel, async (_, q) => { - const { token } = q - - let client_secret: string; - try { client_secret = _this.verify(token) } catch (e) { - console.error(e) - return { error: e } - } - if (!client_secret) return { error: "Couldn't decode client secret." } - - try { - const payload = await cb(q) - if (payload?.error) return payload - return { - s: _this.sign(client_secret, { - success: true, - payload, - }) - } - } catch (e) { - return { error: e } - } - - }) - } - - static registerBasic(channel: string, cb: any) { - ipcMain.handle(channel, async (_, q) => { - try { - const payload = await cb(q) - if (payload?.error) return payload - return { success: true, payload } - } catch (e) { - return { error: e } - } - }) - } - - registerGET(route: string, cb: any, {respondWithClose=true}={}) { - this.app.get(route, (q, s) => { - cb(q) //? no await -- we don't care if it actually works - if (respondWithClose) return s.redirect("https://helloaiko.com/redirect") - else return s.status(200).send("OK") - }) - } -} - -ipcMain.handle("start new websocket server", async (_, q) => { - throw "Why are you starting a new secure comms server? Are you okay? Do you need someone to talk to?" -}) \ No newline at end of file diff --git a/Chiton/utils/roots.ts b/Chiton/utils/roots.ts deleted file mode 100644 index 9719f4e5..00000000 --- a/Chiton/utils/roots.ts +++ /dev/null @@ -1,96 +0,0 @@ -import autoBind from 'auto-bind' -import 'colors' -import crypto from 'crypto' -import { shell } from 'electron' -import path from 'path' -import WebSocket, { Server } from 'ws' -import Storage from '@Mouseion/utils/storage' -import fs from 'fs' -import type Register from '@Mouseion/managers/register' -import type SecureCommunications from '@Chiton/utils/comms' - -//! this is the only Mouseion utility that lives outside of Mouseion -//! THIS IS ON PURPOSE. -//! Roots requires Electron Shell to work and can ONLY operate within the main process - -//! NOTHING ELSE SHOULD LIVE ON PORT 4158 OR 4159 -//! THIS IS ALSO WHY ALL OTHER PORTS IN AIKO MAIL ARE 5 DIGIT NUMBERS! -const DEFAULT_PORT = 4159 -const CLIENT_PORT = 4158 - -export default class Roots { - private readonly comms: SecureCommunications - private readonly storage: Storage - private readonly dir: string - private readonly id: string - private readonly wss: Server - private readonly wss2: Server - - constructor(dir: string="logs-roots", Registry: Register) { - this.comms = Registry.get("Communications") as SecureCommunications - this.comms.register("please get the logs", this.getLogs.bind(this)) - - //? initialize dir to the correct app datapath - const platform: string = process.platform - switch (platform) { - case 'darwin': dir = path.join( - process.env.HOME as string, 'Library', 'Application Support', - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'win32': dir = path.join( - process.env.APPDATA as string, - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'linux': dir = path.join( - process.env.HOME as string, - '.Aiko Mail', 'Mouseion', dir - ); break; - } - this.dir = dir - this.storage = new Storage(this.dir, {json: false}) - this.id = crypto.randomBytes(6).toString('hex') - this.wss = new Server({ port: DEFAULT_PORT }) - this.wss2 = new Server({ port: CLIENT_PORT }) - const _this = this - this.wss.on("connection", (ws: WebSocket) => { - ws.on("message", (m: string) => { - _this.log(m) - }) - }) - this.wss2.on("connection", (ws: WebSocket) => { - ws.on("message", (m: string) => { - try { - this.log(m) - } catch (e) { - console.log(e) - } - }) - setInterval(() => ws.send("ping"), 1000) - }) - - console.log(`Roots initialized in ${this.storage.dir}/${this.id}`.green.bgBlack) - autoBind(this) - } - - log(msg: string) { - if (msg.includes("[C]")) console.log(msg.replace("\n", "").black.bgGreen) - this.storage.append(this.id, msg) - if (msg.includes("[ ERROR ]")) { - throw new Error(msg.split("[ ERROR ]")[1]) - } - } - - async getLogs() { - const downloadFolder = (() => { - switch(process.platform) { - case "win32": return `${process.env.USERPROFILE}\\Downloads` - case "darwin": return `${process.env.HOME}/Downloads` - default: return `${process.env.HOME}/Downloads` - } - })() - const log_dest = `${downloadFolder}/aiko-mail-${this.id}.log` - await fs.promises.copyFile(`${this.storage.dir}/${this.id}.log`, log_dest) - await shell.showItemInFolder(path.normalize(log_dest)) - } - -} \ No newline at end of file diff --git a/utils/logger.ts b/common/logger.ts similarity index 98% rename from utils/logger.ts rename to common/logger.ts index 381ec867..0bb3005e 100644 --- a/utils/logger.ts +++ b/common/logger.ts @@ -2,9 +2,9 @@ import autoBind from 'auto-bind' import 'colors' import crypto from 'crypto' import path from 'path' -import Storage from '@Iris/utils/storage' +import Storage from '@Iris/common/storage' import WebSocket from 'ws' -import sleep from '@Iris/utils/sleep' +import sleep from '@Iris/common/sleep' import { performance } from 'perf_hooks' /** Generates a string timestamp of the current date/time */ diff --git a/utils/port.ts b/common/port.ts similarity index 93% rename from utils/port.ts rename to common/port.ts index eaceddef..86fbbf45 100644 --- a/utils/port.ts +++ b/common/port.ts @@ -22,5 +22,8 @@ export const RESERVED_PORTS = { EXPRESS: 41599, //! HARD-CODED INTO REDIRECT-URI, DO NOT CHANGE WS: 4161, }, - + ROOTS: { + REMOTE: 4158, + LOCAL: 4159, + } } \ No newline at end of file diff --git a/common/register.ts b/common/register.ts new file mode 100644 index 00000000..470bed45 --- /dev/null +++ b/common/register.ts @@ -0,0 +1,14 @@ +import 'colors' +import autoBind from 'auto-bind' + +export default class Register { + private defs: Record = {} + constructor() { + autoBind(this) + } + + register(key: string, def: any): void { this.defs[key] = def } + clear() { this.defs = {} } + get = (key: string): any => + this.defs[key] || console.error(`Attempted to load ${key} module, but it has not been registered.`.red) ;; +} \ No newline at end of file diff --git a/common/sleep.ts b/common/sleep.ts new file mode 100644 index 00000000..89e07a00 --- /dev/null +++ b/common/sleep.ts @@ -0,0 +1 @@ +export default (ms: number) => new Promise((s, _) => setTimeout(s, ms)) \ No newline at end of file diff --git a/utils/storage.ts b/common/storage.ts similarity index 100% rename from utils/storage.ts rename to common/storage.ts From e42c20870e0873947f5c23f198ca1a082153a695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Wed, 28 Dec 2022 02:45:14 -0500 Subject: [PATCH 006/275] cleanup + add trigger capacity to spp --- Chiton/utils/preload.ts | 6 ---- Marionette/ws/sockpuppet.ts | 56 +++++++++++++++++++++++++++------ Marionette/ws/sockpuppeteer.ts | 40 ++++++++++++++++++----- icon-darwin.png | Bin 0 -> 110433 bytes icon-win32.png | Bin 0 -> 28712 bytes prisma/schema.prisma | 11 ------- 6 files changed, 78 insertions(+), 35 deletions(-) delete mode 100644 Chiton/utils/preload.ts create mode 100644 icon-darwin.png create mode 100644 icon-win32.png delete mode 100644 prisma/schema.prisma diff --git a/Chiton/utils/preload.ts b/Chiton/utils/preload.ts deleted file mode 100644 index 72a48200..00000000 --- a/Chiton/utils/preload.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ipcRenderer, contextBridge } from 'electron' -contextBridge.exposeInMainWorld('platform', process.platform) -contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) -contextBridge.exposeInMainWorld("api", { - ipcHandler: (event: string, cb: any) => ipcRenderer.on(event, cb), -}) \ No newline at end of file diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 5492d42c..283a8538 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -1,6 +1,6 @@ import WebSocket, { Server } from 'ws' import { unused_port, RESERVED_PORTS } from '@Iris/common/port' -import { Lumberjack } from '@Iris/common/logger' +import Forest, { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' interface SockPuppetProcess extends NodeJS.Process { @@ -11,6 +11,11 @@ interface SockPuppetProcess extends NodeJS.Process { type SockPuppetry = { [key: string]: (...args: any[]) => Promise } /* + ! Warning: Until this is deployed, the socket doesn't exist. + ! Also, you can't expect port to be defined prior to deployment unless you pass it in. + ! It's left unprotected to allow for more complex use cases. + ! e.g. for Window management, the Window launch & load will take longer than the SockPuppet deployment. + ! So for Window management, you could expect port/socket to be reliably defined. ? Usage: * class MySockPuppet extends SockPuppet { * puppetry = { @@ -34,31 +39,52 @@ type SockPuppetry = { [key: string]: (...args: any[]) => Promise } */ export default abstract class SockPuppet extends Lumberjack { - private readonly proc: SockPuppetProcess = process;; + private readonly proc: SockPuppetProcess | null private deployed: boolean = false; + private readonly websockets: WebSocket[] = [] abstract puppetry: SockPuppetry; abstract checkInitialize(): boolean; abstract initialize(args: any[], success: (payload: object) => void): Promise; - protected constructor(protected name: string, logdir?: string) { - super(name, {logdir}) - if (!process.send) throw new Error("Puppet was spawned without IPC.") - process.title = "Aiko Mail | WS | " + this.name + /** should do renderer=true if you want it to run forked */ + protected constructor( + protected name: string, + opts: { + forest?: Forest | undefined, + logdir?: string | undefined, + renderer?: boolean + }, + private _port?: number, + ) { + super(name, opts) + + if (opts.renderer) { + process.title = "Aiko Mail | WS | " + this.name + this.proc = process;; + } else this.proc = null + autoBind(this) } + /** Slight safety mechanism to prevent bad accesses */ + public get port(): number { + if (!(this.deployed)) this.Log.error("Cannot get port before deployment.") + if (!(this._port)) this.Log.error("Port not defined.") + return this._port! + } + /** Deploys the SockPuppet; you cannot redeploy (must do a complete teardown). */ - public async deploy(port?: number) { + public async deploy() { if (this.deployed) return this.Log.error("Already deployed.") this.deployed = true const _this = this //? spawn websocket server - const _port = await unused_port(port) - const wss = new Server({ port: _port }) - this.proc.send({ port: _port, }) + this._port = await unused_port(this._port) + const wss = new Server({ port: this._port }) + if (this.proc) this.proc.send({ port: this._port, }) wss.on("connection", (ws: WebSocket) => { const succ = (id: string): ((payload: object) => void) => { @@ -76,6 +102,8 @@ export default abstract class SockPuppet extends Lumberjack { })) } + _this.websockets.push(ws) + ws.on('message', async (m: string): Promise => { /* ? m should be 'please ' + JSON stringified message @@ -134,4 +162,12 @@ export default abstract class SockPuppet extends Lumberjack { }) } + /** Trigger an event on all puppeteers */ + protected trigger(event: string, payload: object) { + if (!this.deployed) return this.Log.error("Cannot trigger event before deployment.") + this.websockets.map(ws => ws.send(JSON.stringify({ + event, payload + }))) + } + } \ No newline at end of file diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts index a1719bad..5b27f883 100644 --- a/Marionette/ws/sockpuppeteer.ts +++ b/Marionette/ws/sockpuppeteer.ts @@ -4,14 +4,24 @@ import crypto from 'crypto' import Forest, { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' -type SockPuppeteerWaiterParams = { +interface SockPuppeteerWaiterParams { success: boolean, payload: any, error?: string, id: string } +interface SockPuppeteerTriggerParams { + event: string + payload?: any +} +const isWaiter = (t: SockPuppeteerWaiterParams | SockPuppeteerTriggerParams): t is SockPuppeteerWaiterParams => + !!((t as SockPuppeteerWaiterParams).id); +const isTrigger = (t: SockPuppeteerWaiterParams | SockPuppeteerTriggerParams): t is SockPuppeteerTriggerParams => + !!((t as SockPuppeteerTriggerParams).event); + type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void type SockPuppeteerListener = () => void +type SockPuppeteerTrigger = (() => void) | ((_: any) => void) | (() => Promise) | ((_: any) => Promise) type ValueType = T extends Promise @@ -26,6 +36,8 @@ export default abstract class SockPuppeteer extends Lumberjack { private readonly waiters: Record = {} private readonly listeners: Record = {} + private readonly triggers: Record = {} + private readonly queue: ProcessMessage[] = [] private rotating: boolean = false private getID(): string { @@ -67,13 +79,20 @@ export default abstract class SockPuppeteer extends Lumberjack { this.API = ws ws.binaryType = 'arraybuffer' ws.onmessage = (m: MessageEvent): any => { - const s = JSON.parse(m.data) as SockPuppeteerWaiterParams - if (!(s?.id)) return this.Log.error("No ID specified in message") - const cb = this.waiters[s.id] - if (!cb) return this.Log.error("No waiter set.") - const listener = this.listeners[s.id] - if (listener) listener() - cb(s) + const s = JSON.parse(m.data) as (SockPuppeteerWaiterParams | SockPuppeteerTriggerParams) + if (isTrigger(s)) { + const cb = this.triggers[s.event] + if (!cb) return this.Log.error("No trigger set for", s.event) + cb(s.payload) + } else if (isWaiter(s)) { + const cb = this.waiters[s.id] + if (!cb) return this.Log.error("No waiter set.") + const listener = this.listeners[s.id] + if (listener) listener() + cb(s) + } else { + this.Log.error("Unknown message type (no id or event)") + } } } @@ -121,4 +140,9 @@ export default abstract class SockPuppeteer extends Lumberjack { } }) } + + protected register(event: string, trigger: SockPuppeteerTrigger) { + this.triggers[event] = trigger + } + } diff --git a/icon-darwin.png b/icon-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..60c045b0b02bf2d46074a4417196c7e795a71436 GIT binary patch literal 110433 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 Date: Wed, 28 Dec 2022 03:00:46 -0500 Subject: [PATCH 007/275] shuffle ports --- common/port.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/port.ts b/common/port.ts index 86fbbf45..4eacc53d 100644 --- a/common/port.ts +++ b/common/port.ts @@ -17,13 +17,14 @@ export const unused_port = async (start_port=DEFAULT_PORT): Promise => { } export const RESERVED_PORTS = { + ROOTS: { + REMOTE: 4158, + LOCAL: 4159, + }, VEIL: 4160, + GUIDEPOST: 4161, COMMS: { EXPRESS: 41599, //! HARD-CODED INTO REDIRECT-URI, DO NOT CHANGE - WS: 4161, + WS: 4162, }, - ROOTS: { - REMOTE: 4158, - LOCAL: 4159, - } } \ No newline at end of file From 6148bea6b0ef2ed0e320344b594c5e2cf225bb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Wed, 28 Dec 2022 03:07:59 -0500 Subject: [PATCH 008/275] tolerance for spp --- Marionette/ws/sockpuppet.ts | 2 +- Marionette/ws/sockpuppeteer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 283a8538..efe79c93 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -163,7 +163,7 @@ export default abstract class SockPuppet extends Lumberjack { } /** Trigger an event on all puppeteers */ - protected trigger(event: string, payload: object) { + protected trigger(event: string, payload: any) { if (!this.deployed) return this.Log.error("Cannot trigger event before deployment.") this.websockets.map(ws => ws.send(JSON.stringify({ event, payload diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts index 5b27f883..27fe0315 100644 --- a/Marionette/ws/sockpuppeteer.ts +++ b/Marionette/ws/sockpuppeteer.ts @@ -82,7 +82,7 @@ export default abstract class SockPuppeteer extends Lumberjack { const s = JSON.parse(m.data) as (SockPuppeteerWaiterParams | SockPuppeteerTriggerParams) if (isTrigger(s)) { const cb = this.triggers[s.event] - if (!cb) return this.Log.error("No trigger set for", s.event) + if (!cb) return this.Log.warn("No trigger set for", s.event) cb(s.payload) } else if (isWaiter(s)) { const cb = this.waiters[s.id] From 7a600b5e0bf026b0728b7920f59863cd98e56b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Wed, 28 Dec 2022 03:27:29 -0500 Subject: [PATCH 009/275] window --- Chiton/components/window.ts | 108 +++++++++++++++++++++++ Chiton/utils/window-manager.ts | 153 --------------------------------- 2 files changed, 108 insertions(+), 153 deletions(-) create mode 100644 Chiton/components/window.ts delete mode 100644 Chiton/utils/window-manager.ts diff --git a/Chiton/components/window.ts b/Chiton/components/window.ts new file mode 100644 index 00000000..1982d95c --- /dev/null +++ b/Chiton/components/window.ts @@ -0,0 +1,108 @@ +import autoBind from 'auto-bind' +import { ipcMain, shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' +import { Lumberjack } from '@Iris/common/logger' +import type Register from '@Iris/common/register' +import SecureCommunications from '@Marionette/ipc' +import path from 'path' +import type { Chiton } from '@Chiton/app' +import SockPuppet from '@Marionette/ws/sockpuppet' + +export enum WindowEvents { + FULLSCREEN="fullscreen", + MAXIMIZE="maximize", + BEFORE_QUIT="before-quit", + WOKE_FROM_SLEEP="woke-from-sleep", + RESIZE="resize", + OS_THEME_CHANGED="os-theme-changed", +} + +export abstract class Window extends SockPuppet { + + protected readonly win: BrowserWindow + maximize() { this.win.maximize() } + unmaximize() { this.win.unmaximize() } + minimize() { this.win.minimize() } + setFullScreen(s: boolean) { this.win.setFullScreen(s) } + getFullScreen(): boolean { return this.win.isFullScreen() } + close() { this.win.close() } + hide() { this.win.hide() } + focus() { this.win.show(); this.win.focus() } + findInWindow() { this.win.webContents.findInPage("") } + + protected constructor( + private readonly chiton: Chiton, + name: string, + { + closable=true, + spellcheck=false, + winArgs={}, + }: { + closable?: boolean, + spellcheck?: boolean, + winArgs?: Partial + } ={}, + ) { + super(name, { + forest: chiton.forest, + renderer: false + }) + const _this = this + + this.win = new BrowserWindow({ + show: false, + frame: process.platform == 'darwin', + titleBarStyle: 'hidden', + backgroundColor: nativeTheme.shouldUseDarkColors ? '#0c0e13' : '#ffffff', + webPreferences: { + nodeIntegration: false, + spellcheck, + backgroundThrottling: false, + preload: path.join(__dirname, 'preload.js'), //! FIXME: migrate away from preload + }, + icon: process.platform == 'darwin' ? './icon-darwin.png' : './icon-win32.png', + roundedCorners: true, + ...winArgs + }) + + this.win.on('enter-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, true)) + this.win.on('leave-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, false)) + this.win.on('enter-html-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, true)) + this.win.on('leave-html-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, false)) + this.win.on('maximize', () => _this.trigger(WindowEvents.MAXIMIZE, true)) + this.win.on('unmaximize', () => _this.trigger(WindowEvents.MAXIMIZE, false)) + let quitting = false + app.on("before-quit", _ => { + quitting = true + _this.trigger(WindowEvents.BEFORE_QUIT, {}) + }) + this.win.on("close", e => { + if (!closable && !quitting && process.platform === "darwin") { + _this.Log.log("Preventing window from closing (hiding instead).") + e.preventDefault() + _this.win.hide() + return false + } + }) + powerMonitor.on('resume', () => _this.trigger(WindowEvents.WOKE_FROM_SLEEP, {})) + this.win.on('resize', () => _this.trigger(WindowEvents.RESIZE, {})) + nativeTheme.on('updated', () => _this.trigger(WindowEvents.OS_THEME_CHANGED, {})) + + //! force OS to handle links in default browser + this.win.webContents.setWindowOpenHandler(details => { + shell.openExternal(details.url) + return { action: "deny" } + }) + + this.deploy() + autoBind(this) + } + + loadURL(url: string, args?: Electron.LoadURLOptions) { + if (!this.win) throw "Window has not been set. Cannot load URL." + this.win.loadURL(url, { + userAgent: this.chiton.config.user_agent, + ...args + }) + } + +} \ No newline at end of file diff --git a/Chiton/utils/window-manager.ts b/Chiton/utils/window-manager.ts deleted file mode 100644 index e1d10750..00000000 --- a/Chiton/utils/window-manager.ts +++ /dev/null @@ -1,153 +0,0 @@ -import autoBind from 'auto-bind' -import { ipcMain, shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' -import type { Logger, LumberjackEmployer } from '@Mouseion/utils/logger' -import type Register from '@Mouseion/managers/register' -import SecureCommunications from '@Chiton/utils/comms' -import path from 'path' - -export default class WindowManager { - private fullscreened: boolean = false - private readonly Log: Logger - public quitting: boolean = false - - constructor( - private readonly Registry: Register, - private win: BrowserWindow | null, - private readonly hash='', - private closable=true, - ) { - const _this = this - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("Window Manager #" + hash) - this.handler("minimize window", () => _this.minimize()) - this.handler("maximize window", () => _this.maximize()) - this.handler("unmaximize window", () => _this.unmaximize()) - this.handler("fullscreen window", () => _this.setFullScreen(true)) - this.handler("close window", () => _this.close()) - this.handler("hide window", () => _this.hide()) - this.handler("find in window", () => this.findInWindow()) - this.handler("focus window", () => this.focus()) - this.handler("get the platform", () => process.platform) - - autoBind(this) - } - - private handler(action: string, cb: any) { - SecureCommunications.registerBasic(this.hash + ': please ' + action, (_: any) => { - cb() - return true - }) - } - - maximize() { if (this.win) this.win.maximize() } - unmaximize() { if (this.win) this.win.unmaximize() } - minimize() { if (this.win) this.win.minimize() } - setFullScreen(s: boolean) { if (this.win) this.win.setFullScreen(s) } - close() { if (this.win) this.win.close() } - hide() { if (this.win) this.win.hide() } - focus() { if (this.win) { this.win.show(); this.win.focus() }} - findInWindow() { if (this.win) this.win.webContents.findInPage("") } - - set window(win: BrowserWindow | null) { - this.win = win - this.addListeners() - } - - get window() { - if (!this.win) return null; - return this.win - } - - loadURL(url: string, args?: Electron.LoadURLOptions) { - if (!this.win) throw "Window has not been set. Cannot load URL." - this.win.loadURL(url, { - userAgent: this.Registry.get("user agent"), - ...args - }) - } - - private addListeners() { - if (!(this.win)) throw "No window." - const _this = this - - const updateFullscreenStatus = (status: boolean) => { - if (_this.win) { - _this.win.webContents.send(_this.hash + ': please fullscreen status changed', status) - _this.fullscreened = status - } - } - const updateMaximizedStatus = (status: boolean) => { - if (_this.win) { - _this.win.webContents.send(_this.hash + ': please maximized status changed', status) - } - } - - this.win.on("enter-full-screen", () => updateFullscreenStatus(true)) - this.win.on("enter-html-full-screen", () => updateFullscreenStatus(true)) - this.win.on("leave-full-screen", () => updateFullscreenStatus(false)) - this.win.on("leave-html-full-screen", () => updateFullscreenStatus(false)) - - this.win.on("maximize", () => updateMaximizedStatus(true)) - this.win.on("unmaximize", () => updateMaximizedStatus(false)) - - app.on("before-quit", e => _this.quitting = true) - this.win.on("close", (e) => { - _this.Log.log(e) - if (!this.closable && !this.quitting && process.platform == "darwin") { - _this.Log.log("Preventing window from closing (hiding instead).") - e.preventDefault() - _this.win?.hide() - return false - } - }) - - powerMonitor.on("resume", () => { - try { - //? I don't think the below is necessary anymore. - // if (_this.win) _this.win.reload() - } catch (e) { - _this.Log.error(e) - } - }) - - this.win.webContents.setWindowOpenHandler(details => { - shell.openExternal(details.url) - return { action: "deny" } - }) - - //? Detect when OS color scheme changes. - nativeTheme.on("updated", () => { - _this.Log.shout("OS color scheme changed.") - _this.triggerEvent(_this.hash + ': please update color scheme', {}) - }) - - ipcMain.removeHandler(this.hash + ": please get fullscreen status") - this.handler("get fullscreen status", () => updateFullscreenStatus(_this.fullscreened)) - } - - triggerEvent(channel: string, data: any) { - if (!this.win) return this.Log.error("Tried to trigger event but window has been destroyed.") - this.win.webContents.send(channel, data) - } - - static newWindow(args: Partial, { - spellcheck=false - } ={}) { - console.log(path.join(__dirname, './public/assets/js/common/preload.js')) - return new BrowserWindow({ - show: false, - frame: process.platform == 'darwin', - titleBarStyle: 'hidden', - backgroundColor: nativeTheme.shouldUseDarkColors ? '#0c0e13' : '#ffffff', - webPreferences: { - nodeIntegration: true, //! FIXME: migrate fully to websockets - spellcheck, - backgroundThrottling: false, - preload: path.join(__dirname, 'preload.js'), - }, - icon: process.platform == 'darwin' ? './public/assets/img/icon.png' : './public/assets/img/app-icon/square-icon-shadow.png', - roundedCorners: true, - ...args - }) - } -} \ No newline at end of file From fb06ee22fbf3e9b8ca174eb217aa0742376b9192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 16:44:26 -0500 Subject: [PATCH 010/275] add port for guidepost and bump comms ws --- common/port.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/port.ts b/common/port.ts index 4eacc53d..8df6eaf5 100644 --- a/common/port.ts +++ b/common/port.ts @@ -22,9 +22,10 @@ export const RESERVED_PORTS = { LOCAL: 4159, }, VEIL: 4160, - GUIDEPOST: 4161, + CHITON: 4161, + GUIDEPOST: 4162, COMMS: { EXPRESS: 41599, //! HARD-CODED INTO REDIRECT-URI, DO NOT CHANGE - WS: 4162, + WS: 4163, }, } \ No newline at end of file From e082c1a1f347d885972bf8df7e9e4608543ad638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 17:16:41 -0500 Subject: [PATCH 011/275] optionality --- Marionette/ws/sockpuppet.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index efe79c93..2ca63507 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -8,7 +8,10 @@ interface SockPuppetProcess extends NodeJS.Process { swallowErrors?: boolean | undefined; } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean } -type SockPuppetry = { [key: string]: (...args: any[]) => Promise } +type SockPuppetry = { + [key: string]: + (...args: any[]) => Promise | any | void +} /* ! Warning: Until this is deployed, the socket doesn't exist. @@ -44,9 +47,9 @@ export default abstract class SockPuppet extends Lumberjack { private readonly websockets: WebSocket[] = [] abstract puppetry: SockPuppetry; - abstract checkInitialize(): boolean; + protected abstract checkInitialize(): boolean; - abstract initialize(args: any[], success: (payload: object) => void): Promise; + protected abstract initialize(args: any[], success: (payload: object) => void): Promise; /** should do renderer=true if you want it to run forked */ protected constructor( @@ -87,10 +90,11 @@ export default abstract class SockPuppet extends Lumberjack { if (this.proc) this.proc.send({ port: this._port, }) wss.on("connection", (ws: WebSocket) => { - const succ = (id: string): ((payload: object) => void) => { - return (payload: object): void => ws.send(JSON.stringify({ + const succ = (id: string): ((payload?: object) => void) => { + return (payload?: object): void => ws.send(JSON.stringify({ success: true, - payload, id + payload: payload ?? {}, + id })) } const err = (id: string): ((msg: string) => void) => { From ff2d5b0584ac5fb491a74ece462bd0c151dea40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 17:44:31 -0500 Subject: [PATCH 012/275] templatize dwarfstar, lock down settings, fix semantics --- Chiton/cache/dwarf-star.ts | 100 --------------------------- Chiton/{cache => store}/gas-giant.ts | 2 +- Chiton/store/generic/dwarf-star.ts | 67 ++++++++++++++++++ Chiton/store/settings.ts | 42 +++++++++++ Chiton/{cache => store}/templates.ts | 4 +- Marionette/process/sockpuppet.ts | 8 +-- Marionette/ws/sockpuppet.ts | 2 +- 7 files changed, 117 insertions(+), 108 deletions(-) delete mode 100644 Chiton/cache/dwarf-star.ts rename Chiton/{cache => store}/gas-giant.ts (97%) create mode 100644 Chiton/store/generic/dwarf-star.ts create mode 100644 Chiton/store/settings.ts rename Chiton/{cache => store}/templates.ts (96%) diff --git a/Chiton/cache/dwarf-star.ts b/Chiton/cache/dwarf-star.ts deleted file mode 100644 index dff4c313..00000000 --- a/Chiton/cache/dwarf-star.ts +++ /dev/null @@ -1,100 +0,0 @@ -import path from 'path' -import fs2 from 'fs-extra' -import type SecureCommunications from '@Chiton/utils/comms' -import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' - -interface Settings { - - version: number - - auth: { - authenticated: boolean - token: string - credentials: { - email: string - password: string - } - } - - meta: { - firstTime: boolean - } - -} -const isSettings = (x: any):x is Settings => !!(x.version > 0) - -export default class DwarfStar { - - private readonly fp: string - settings: Settings - private comms: SecureCommunications - - constructor(Registry: Register, fp: string) { - switch (process.platform) { - case 'darwin': fp = path.join(process.env.HOME || "~", "Library", "Application Support", "Aiko Mail", fp); break - case 'win32': fp = path.join(process.env.APPDATA || "/c/", "Aiko Mail", fp); break - case 'linux': fp = path.join(process.env.HOME || "~", ".Aiko Mail", fp); break - } - this.fp = fp - this.comms = Registry.get("Communications") as SecureCommunications - - this.settings = DwarfStar.defaultSettings - - this.comms.register("save preferences", this.set.bind(this)) - this.comms.register("clear preferences", this.reset.bind(this)) - this.comms.register("get preferences", this.copy.bind(this)) - - autoBind(this) - } - - reset(_: {}={}) { - fs2.ensureFileSync(this.fp) - const s = fs2.readFileSync(this.fp, {encoding: "utf-8"}) - if (!(s?.length > 0)) { - this.settings = DwarfStar.defaultSettings - } - else { - const d = JSON.parse(s) - if (isSettings(d)) this.settings = d - } - return this.save() - } - - private set(d: Partial) { - this.settings = { - ...this.settings, - ...d - } - return this.save() - } - - private copy(_: {}={}) { - return JSON.parse(JSON.stringify(this.settings)) - } - - save() { - const s = JSON.stringify(this.settings) - fs2.writeFileSync(this.fp, s) - return this.settings - } - - static get defaultSettings(): Settings { - return { - - version: 1, - - auth: { - authenticated: false, - token: "", - credentials: { - email: "", - password: "" - } - }, - meta: { - firstTime: true - } - } - } -} \ No newline at end of file diff --git a/Chiton/cache/gas-giant.ts b/Chiton/store/gas-giant.ts similarity index 97% rename from Chiton/cache/gas-giant.ts rename to Chiton/store/gas-giant.ts index 9f86be60..25f8382a 100644 --- a/Chiton/cache/gas-giant.ts +++ b/Chiton/store/gas-giant.ts @@ -4,7 +4,7 @@ import type SecureCommunications from '@Chiton/utils/comms' import path from 'path' import fs2 from 'fs-extra' import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' +import type Register from '@Iris/common/register' export default class GasGiant { private readonly storage: Storage diff --git a/Chiton/store/generic/dwarf-star.ts b/Chiton/store/generic/dwarf-star.ts new file mode 100644 index 00000000..81f7ddb1 --- /dev/null +++ b/Chiton/store/generic/dwarf-star.ts @@ -0,0 +1,67 @@ +import path from 'path' +import fs2 from 'fs-extra' +import autoBind from 'auto-bind' +import SockPuppet from '@Marionette/ws/sockpuppet' +import type { Chiton } from '@Chiton/app' + +/** Persistent data-store for small state (e.g. settings) */ +export default class DwarfStar extends SockPuppet { + puppetry = { + get: this.clone, + set: this.set, + reset: this.reset, + } + + protected state: T | null = null + protected checkInitialize(): boolean { return !!(this.state) } + protected async initialize(args: any[], success: (payload: object) => void) { + if (this.state) return this.Log.error("Already initialized.") + this.state = args[0] as T + success(this.save()) + } + + protected save(): T { + fs2.writeFileSync(this.fp, JSON.stringify(this.state)) + return JSON.parse(fs2.readFileSync(this.fp, { encoding: "utf-8" })) as T + } + private set(state: Partial): T { + this.state = { + ...(this.state!), + ...state, + } + return this.save() + } + private reset(): T { + fs2.ensureFileSync(this.fp) + const state = JSON.parse(fs2.readFileSync(this.fp, {encoding: "utf-8"})) as T + if (!(this.checkInitialize())) { + this.Log.error("Reset failed: state is empty.") + throw new Error("Cannot reset against empty state.") + } + this.state = state + return this.save() + } + private clone(): T { + return JSON.parse(JSON.stringify(this.state)); + } + + constructor( + chiton: Chiton, + name: string, + private readonly fp: string, + ) { + super(name + ' (DwarfStar)', { + forest: chiton.forest, + renderer: false + }) + + switch (process.platform) { + case 'darwin': fp = path.join(process.env.HOME || "~", "Library", "Application Support", "Aiko Mail", fp); break + case 'win32': fp = path.join(process.env.APPDATA || "/c/", "Aiko Mail", fp); break + case 'linux': fp = path.join(process.env.HOME || "~", ".Aiko Mail", fp); break + } + + autoBind(this) + } + +} diff --git a/Chiton/store/settings.ts b/Chiton/store/settings.ts new file mode 100644 index 00000000..b54bbb63 --- /dev/null +++ b/Chiton/store/settings.ts @@ -0,0 +1,42 @@ +import type { Chiton } from "@Chiton/app" +import DwarfStar from "./generic/dwarf-star" + +interface ISettings { + + version: number + + auth: { + authenticated: boolean + token: string + credentials: { + email: string + password: string + } + } + + meta: { + firstTime: boolean + } + +} + +export class Settings extends DwarfStar { + constructor(chiton: Chiton) { + super(chiton, 'Settings', 'settings.json') + this.state = { + version: 1, + auth: { + authenticated: false, + token: "", + credentials: { + email: "", + password: "" + } + }, + meta: { + firstTime: true + } + } + this.save() + } +} \ No newline at end of file diff --git a/Chiton/cache/templates.ts b/Chiton/store/templates.ts similarity index 96% rename from Chiton/cache/templates.ts rename to Chiton/store/templates.ts index 08e55639..2dad46e8 100644 --- a/Chiton/cache/templates.ts +++ b/Chiton/store/templates.ts @@ -1,10 +1,10 @@ -import Storage from '@Mouseion/utils/storage' +import Storage from '@Iris/common/storage' import { session } from 'electron' import type SecureCommunications from '@Chiton/utils/comms' import path from 'path' import fs2 from 'fs-extra' import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' +import type Register from '@Iris/common/register' const HTML2Text = require('html-to-text') export interface Template { diff --git a/Marionette/process/sockpuppet.ts b/Marionette/process/sockpuppet.ts index 2eac6fda..5b632b45 100644 --- a/Marionette/process/sockpuppet.ts +++ b/Marionette/process/sockpuppet.ts @@ -6,7 +6,7 @@ interface SockPuppetProcess extends NodeJS.Process { swallowErrors?: boolean | undefined; } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean } -type SockPuppetry = {[key: string]: (...args: any[]) => Promise} +type SockPuppetry = {[key: string]: (...args: any[]) => Promise | any | void} /* ? Usage: @@ -54,9 +54,9 @@ export default abstract class SockPuppet extends Lumberjack { })) } - abstract checkInitialize(): boolean; + protected abstract checkInitialize(): boolean; - abstract initialize(args: any[], success: (payload: object) => boolean): Promise; + protected abstract initialize(args: any[], success: (payload: object) => boolean): Promise; protected constructor(protected name: string, logdir?: string) { super(name, { logdir }) @@ -102,7 +102,7 @@ export default abstract class SockPuppet extends Lumberjack { const error = _this.perr(id) if (!(_this.checkInitialize() || action === 'init')) - return error("Pantheon has not yet been initialized.") + return error("Puppet has not yet been initialized.") const attempt = async (method: (...xs: any) => Promise | any) => { try { diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 2ca63507..f72a6511 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -139,7 +139,7 @@ export default abstract class SockPuppet extends Lumberjack { const error = err(id) if (!(_this.checkInitialize() || action === 'init')) - return error("Pantheon has not yet been initialized.") + return error("Puppet has not yet been initialized.") const attempt = async (method: (...xs: any) => Promise | any) => { try { From f0871352cea0a216ba77fe10ce70f59dd607e786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 17:45:07 -0500 Subject: [PATCH 013/275] clear out artifacts --- MouseionArtifacts/utils/logger.ts | 157 ----------------------------- MouseionArtifacts/utils/sleep.ts | 1 - MouseionArtifacts/utils/storage.ts | 133 ------------------------ 3 files changed, 291 deletions(-) delete mode 100644 MouseionArtifacts/utils/logger.ts delete mode 100644 MouseionArtifacts/utils/sleep.ts delete mode 100644 MouseionArtifacts/utils/storage.ts diff --git a/MouseionArtifacts/utils/logger.ts b/MouseionArtifacts/utils/logger.ts deleted file mode 100644 index 8b19f4ed..00000000 --- a/MouseionArtifacts/utils/logger.ts +++ /dev/null @@ -1,157 +0,0 @@ -import autoBind from 'auto-bind' -import 'colors' -import crypto from 'crypto' -import path from 'path' -import Storage from '@Mouseion/utils/storage' -import WebSocket from 'ws' -import sleep from '@Mouseion/utils/sleep' -import { performance } from 'perf_hooks' - -/** Generates a string timestamp of the current date/time */ -export const Timestamp = (): string => { - const now: Date = new Date() - const date: string = now.toLocaleDateString() - const time: string = now.toTimeString().substr(0, 'HH:MM:SS'.length) - return `[${date.gray} ${time.cyan}]`.bgBlack -} - -/** - * Generates an identifier using the current time, a prefix, and a label - * @param {string} prefix - the prefix this identifier will use - * @param {string} label - a label for this identifier, will automatically be wrapped in square brackets -*/ -const Identifier = (prefix: string, label: string): string => { - const timestamp: string = Timestamp() - const signature: string = `[M]`.rainbow.bgBlack - return `${timestamp}${signature}${prefix}[${label.magenta}]` -} - -type log_fn = (...msg: any[]) => void; -export interface Logger { - log: log_fn - error: log_fn - success: log_fn, - shout: log_fn, - warn: log_fn - time: log_fn - timeEnd: log_fn -} -class UnemployedLumberjack implements Logger { - readonly label: string - readonly forest: Forest - - private timers: {[key: string]: number} = {} - - constructor(label: string, forest: Forest) { - this.label = label - this.forest = forest - } - - private readonly _log = (prefix: string) => (..._: any[]) => - this.forest.logger(prefix, this.label, ..._) - - log = this._log(Forest.prefixes.log).bind(this) - error = this._log(Forest.prefixes.error).bind(this) - shout = this._log(Forest.prefixes.shout).bind(this) - success = this._log(Forest.prefixes.success).bind(this) - warn = this._log(Forest.prefixes.warn).bind(this) - - //! Timing functions will not appear in logs (intentional) - time = (..._: any[]) => { - const label: string = [Forest.prefixes.timer, this.label, ..._].join(' ') - this.timers[label] = performance.now() - } - timeEnd = (..._: any[]) => { - const now = performance.now() - const label = [Forest.prefixes.timer, this.label, ..._].join(' ') - const start = this.timers[label] - if (!start) this._log(Forest.prefixes.warn)('No timer found for', label) - else this._log(Forest.prefixes.timer)(label, ':', now - start, 'ms') - delete this.timers[label] - } - -} -export type LumberjackEmployer = (label: string) => Logger - -//? Initialize one forest per "application" and use Lumberjacks for different labels -export default class Forest { - readonly dir: string; - readonly storage: Storage; - readonly id: string; - private readonly roots?: WebSocket - - static readonly prefixes = { - log: '[ LOG ]'.black.bgWhite, - error: '[ ERROR ]'.white.bgRed, - shout: '[ SHOUT ]'.red.bgCyan, - success: '[SUCCESS]'.green.bgBlack, - warn: '[ WARN⚠️ ]'.yellow.bgBlack, - timer: '[ TIMER ]'.red.bgWhite - } - - constructor(dir: string='logs') { - - //? initialize dir to the correct app datapath - const platform: string = process.platform - switch (platform) { - case 'darwin': dir = path.join( - process.env.HOME as string, 'Library', 'Application Support', - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'win32': dir = path.join( - process.env.APPDATA as string, - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'linux': dir = path.join( - process.env.HOME as string, - '.Aiko Mail', 'Mouseion', dir - ); break; - } - this.dir = dir - - //? logger appends strings so we want regular files - this.storage = new Storage(dir, {json: false}) - - //? randomly generate some "probably unique" identifier - this.id = crypto.randomBytes(6).toString('hex') - try { - const socket = new WebSocket('ws://localhost:4159') - this.roots = socket - } catch (e) { - console.error(e) - } - - console.log(`Forest initialized in ${this.storage.dir}/${this.id}`.green.bgBlack) - autoBind(this) - } - - logger(prefix: string, label: string, ...msg: any[]): void { - const identifier = Identifier(prefix, label) - let e: Error | null = null - if (prefix == Forest.prefixes.error) { - e = new Error("(stack trace pin)") - console.log(identifier, ...msg, e) //? dumps trace - } else if (prefix == Forest.prefixes.shout) { - console.log(identifier, ...(msg.map(m => m.toString().red.bgCyan))) - } else { - console.log(identifier, ...msg) - } - - //? remove color escape sequences and dump to log - const uncolored_msg: string = msg.map((m: string): string => JSON.stringify(m)?.stripColors).join(' ') - const clean_msg: string = e ? `${identifier?.stripColors} ${uncolored_msg}\n${e.stack}\n` : `${identifier?.stripColors} ${uncolored_msg}\n` - this.storage.append(this.id, clean_msg) - this.sendToRoots(clean_msg) - } - - async sendToRoots(msg: string, max_tries=10): Promise { - if (!(this.roots)) return; - for(let i = 0; i < max_tries; i++) { - if (this.roots.readyState === 1) return (this.roots.send(msg)) - await sleep(200) - } - } - - Lumberjack = (label: string): Logger => new UnemployedLumberjack(label, this) - -} \ No newline at end of file diff --git a/MouseionArtifacts/utils/sleep.ts b/MouseionArtifacts/utils/sleep.ts deleted file mode 100644 index 89e07a00..00000000 --- a/MouseionArtifacts/utils/sleep.ts +++ /dev/null @@ -1 +0,0 @@ -export default (ms: number) => new Promise((s, _) => setTimeout(s, ms)) \ No newline at end of file diff --git a/MouseionArtifacts/utils/storage.ts b/MouseionArtifacts/utils/storage.ts deleted file mode 100644 index 8cccd643..00000000 --- a/MouseionArtifacts/utils/storage.ts +++ /dev/null @@ -1,133 +0,0 @@ -import path from 'path' -import fs from 'fs' -import fs2 from 'fs-extra' -import autoBind from 'auto-bind' -import { performance } from 'perf_hooks' -/** A basic storage system mimicking localstorage, persisting within the filesystem */ -class Storage { - - /* - * Basic storage system that mimics localstorage by using keys that you can store, load, pop - ! However, everything is stored in [JSON] files with hard disk space, be careful! - - * store(key: String, data: [JSON-serializable] object/string/etc) => void, writes the data to the key - * load(key: String) => parsed object/string/etc, reads the data stored in key - * pop(key: String) => parsed object/string/etc, reads the data stored in key and deletes key - * append(key: String, data: string) => void, appends the data to the key, does not work when json=true - */ - - readonly dir: string //? the directory the storage works out of - readonly json: boolean //? whether or not data is stored in JSON format - readonly raw: boolean //? whether to treat file input as raw Buffers and filepaths as absolute - - constructor(dir: string, {json=true, raw=false}: {json?: boolean, raw?: boolean} ={}) { - this.dir = dir - this.json = json - this.raw = raw - autoBind(this) - } - - // read a file in one single read - static async readFile(filename: string) { - const handle = await fs.promises.open(filename, "r").catch(_ => _) - if (!handle) return null - if (handle instanceof Error) { - return null - } - let buffer: Buffer | null = null - try { - const stats = await handle.stat() - buffer = Buffer.allocUnsafe(stats.size) - const { bytesRead } = await handle.read(buffer, 0, stats.size, 0) - if (bytesRead !== stats.size) { - throw new Error("bytesRead not full file size") - } - } finally { - handle.close() - } - return buffer - } - - //? Cleans a storage key by explicitly allowing only certain characters in the filename - //! Known bug: if two different keys *clean* to the same key filepath, they will overlap - static clean_key = (key: string): string => key.replace(/[^A-z0-9/\-_]/g, '').substr(0, 86) - - //? Creates the correct filepath for a cleaned key, taking into account JSON settings - private filepath = (key: string): string => `${this.dir}/${key}`.substr(0, 122) + `.${this.json ? 'json' : 'log'}` - - /** Stores data into the relevant file for a key, stringifying it if need be */ - async store(key: string, data: any): Promise { - if (!(this.raw)) key = Storage.clean_key(key) - const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) - if (this.raw && fp.includes("/")) { - //? make the relevant directories in the path to avoid errors - const dirs = fp.split("/") - dirs.pop() - const dir = dirs.join("/") - await fs2.ensureDir(dir) - } - await fs2.ensureFile(fp) - await fs.promises.writeFile(fp, this.json ? JSON.stringify(data) : ( - this.raw ? Buffer.from(data) : data - )) - } - cache = this.store.bind(this) - - async has_key(key: string): Promise { - if (!this.raw) key = Storage.clean_key(key) - const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) - try { - return fs.promises.access(fp).then(_ => true).catch(_ => false) - } catch { - return false - } - } - - /** Loads data for a relevant key, parsing it if need be */ - async load(key: string): Promise { - if (!this.raw) key = Storage.clean_key(key) - const fp: string = (this.raw) ? `${this.dir}/${key}` : this.filepath(key) - if (this.raw) { - console.error(`Raw files are not supported for loading in this version of Mouseion.`) - return null - } else { - const buffer = await Storage.readFile(fp) - if (!buffer) return null - const s: string = buffer.toString() - try { - return !!s && (this.json ? JSON.parse(s) : s) - } catch (e) { - console.error(`Couldn't parse JSON from ${this.dir}/${key}`) - return null - } - } - } - check = this.load.bind(this) - - /** Loads data for the relevant key, parsing it if need be, then clearing the key */ - async pop(key: string): Promise { - key = Storage.clean_key(key) - const fp: string = this.filepath(key) - const buffer = await Storage.readFile(fp) - if (!buffer) return null - const s: string = buffer.toString() - fs.unlinkSync(fp) - try { - return !!s && (this.json ? JSON.parse(s) : s) - } catch (e) { - console.error(`Couldn't parse JSON from ${this.dir}/${key}`) - return null - } - } - - /** Appends a string to a file if the directory is not JSON managed */ - append(key: string, data: string): void { - if (this.json) throw new Error("Cannot append to a JSON-managed directory. Do a full read-write."); - key = Storage.clean_key(key) - const fp: string = this.filepath(key) - fs2.ensureFileSync(fp) - fs.appendFileSync(fp, data) - } -} - -export default Storage \ No newline at end of file From 6357d39ed50a1f2bfaa3a065d4cfc8e75f544f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 17:47:00 -0500 Subject: [PATCH 014/275] rename --- Chiton/store/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chiton/store/settings.ts b/Chiton/store/settings.ts index b54bbb63..b89a80cc 100644 --- a/Chiton/store/settings.ts +++ b/Chiton/store/settings.ts @@ -20,7 +20,7 @@ interface ISettings { } -export class Settings extends DwarfStar { +export default class SettingsStore extends DwarfStar { constructor(chiton: Chiton) { super(chiton, 'Settings', 'settings.json') this.state = { From ceaca7d11324671ced3796d30a7d4eb9d26b0350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 21:42:07 -0500 Subject: [PATCH 015/275] inbox window --- Chiton/components/inbox.ts | 54 ++++++++++++++++++++++++++++++ Chiton/components/window.ts | 36 ++++++++++++++------ Chiton/store/generic/dwarf-star.ts | 1 + Chiton/store/settings.ts | 21 +++++++++++- Marionette/ws/sockpuppet.ts | 3 +- 5 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 Chiton/components/inbox.ts diff --git a/Chiton/components/inbox.ts b/Chiton/components/inbox.ts new file mode 100644 index 00000000..8c42402a --- /dev/null +++ b/Chiton/components/inbox.ts @@ -0,0 +1,54 @@ +import type { Chiton } from "@Chiton/app"; +import { Window } from "@Chiton/components/window"; +import { RESERVED_PORTS } from "@Iris/common/port"; + +export class Inbox extends Window { + + puppetry = { + window: { + ...(this.windowPuppetry), + setFullScreen: this.setFullScreen + } + } + + checkInitialize(): boolean { + return true + } + async initialize(args: any[], success: (payload: object) => void) { + success({}) + } + + //? persist fullscreen status + setFullScreen(s: boolean) { + super.setFullScreen(s) + const settings = this.chiton.settingsStore.settings + settings.inbox.appearance.fullscreen = s + this.chiton.settingsStore.settings = settings + return true + } + + constructor(chiton: Chiton, { + demoMode=false + }: { + demoMode?: boolean, + }={}) { + super(chiton, "Inbox", { + closable: false + }) + + if (demoMode || chiton.settingsStore.get().auth.authenticated) { + if (demoMode) this.Log.shout("Env:", process.env.NODE_ENV, "[Demo]") + else this.Log.shout("Env:", process.env.NODE_ENV) + if (chiton.devMode) { + this.loadURL(`http://localhost:${RESERVED_PORTS.VEIL}#${chiton.version_hash}`) + this.win.webContents.openDevTools() + } else { + this.loadURL(`file://${__dirname}/../Veil/index.html`) + } + } else { + this.Log.warn("User is not signed in, initiating login flow.") + this.loadURL("https://aikomail.com/email/signin") //! FIXME: replace with Ovid + } + } + +} \ No newline at end of file diff --git a/Chiton/components/window.ts b/Chiton/components/window.ts index 1982d95c..599f215f 100644 --- a/Chiton/components/window.ts +++ b/Chiton/components/window.ts @@ -18,19 +18,35 @@ export enum WindowEvents { export abstract class Window extends SockPuppet { + windowPuppetry = { + maximize: this.maximize, + unmaximize: this.unmaximize, + minimize: this.minimize, + setFullScreen: this.setFullScreen, + getFullScreen: this.getFullScreen, + close: this.close, + hide: this.hide, + focus: this.focus, + findInWindow: this.findInWindow, + } + + puppetry = { + window: this.windowPuppetry + } + protected readonly win: BrowserWindow - maximize() { this.win.maximize() } - unmaximize() { this.win.unmaximize() } - minimize() { this.win.minimize() } - setFullScreen(s: boolean) { this.win.setFullScreen(s) } - getFullScreen(): boolean { return this.win.isFullScreen() } - close() { this.win.close() } - hide() { this.win.hide() } - focus() { this.win.show(); this.win.focus() } - findInWindow() { this.win.webContents.findInPage("") } + maximize() { this.win.maximize(); return true } + unmaximize() { this.win.unmaximize(); return true } + minimize() { this.win.minimize(); return true } + setFullScreen(s: boolean) { this.win.setFullScreen(s); return true } + getFullScreen(): boolean { return this.win.isFullScreen(); return true } + close() { this.win.close(); return true } + hide() { this.win.hide(); return true } + focus() { this.win.show(); this.win.focus(); return true } + findInWindow() { this.win.webContents.findInPage(""); return true } protected constructor( - private readonly chiton: Chiton, + protected readonly chiton: Chiton, name: string, { closable=true, diff --git a/Chiton/store/generic/dwarf-star.ts b/Chiton/store/generic/dwarf-star.ts index 81f7ddb1..6e4e9eb6 100644 --- a/Chiton/store/generic/dwarf-star.ts +++ b/Chiton/store/generic/dwarf-star.ts @@ -64,4 +64,5 @@ export default class DwarfStar extends SockPuppet { autoBind(this) } + public get(): T { return this.state! } } diff --git a/Chiton/store/settings.ts b/Chiton/store/settings.ts index b89a80cc..65f9113b 100644 --- a/Chiton/store/settings.ts +++ b/Chiton/store/settings.ts @@ -18,9 +18,23 @@ interface ISettings { firstTime: boolean } + inbox: { + appearance: { + fullscreen: boolean + } + } + } export default class SettingsStore extends DwarfStar { + + // TODO: refactor into mutation observer + get settings() { return this.state! } + set settings(s: ISettings) { + this.state = s + this.save() + } + constructor(chiton: Chiton) { super(chiton, 'Settings', 'settings.json') this.state = { @@ -35,7 +49,12 @@ export default class SettingsStore extends DwarfStar { }, meta: { firstTime: true - } + }, + inbox: { + appearance: { + fullscreen: false + } + } } this.save() } diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index f72a6511..170f354b 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -10,7 +10,8 @@ interface SockPuppetProcess extends NodeJS.Process { } type SockPuppetry = { [key: string]: - (...args: any[]) => Promise | any | void + ((...args: any[]) => Promise | any | void) + | SockPuppetry } /* From d5ebc9eff88cd1b7a559bfd498ec1e232e2e6155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 22:15:32 -0500 Subject: [PATCH 016/275] refactor google oauth --- Chiton/components/window.ts | 5 +- Chiton/oauth/google.ts | 101 ------------------------------- Chiton/services/oauth/google.ts | 102 ++++++++++++++++++++++++++++++++ Marionette/ipc.ts | 2 +- 4 files changed, 104 insertions(+), 106 deletions(-) delete mode 100644 Chiton/oauth/google.ts create mode 100644 Chiton/services/oauth/google.ts diff --git a/Chiton/components/window.ts b/Chiton/components/window.ts index 599f215f..e15b0ea4 100644 --- a/Chiton/components/window.ts +++ b/Chiton/components/window.ts @@ -1,8 +1,5 @@ import autoBind from 'auto-bind' -import { ipcMain, shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' -import { Lumberjack } from '@Iris/common/logger' -import type Register from '@Iris/common/register' -import SecureCommunications from '@Marionette/ipc' +import { shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' import path from 'path' import type { Chiton } from '@Chiton/app' import SockPuppet from '@Marionette/ws/sockpuppet' diff --git a/Chiton/oauth/google.ts b/Chiton/oauth/google.ts deleted file mode 100644 index 26b525f6..00000000 --- a/Chiton/oauth/google.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { shell } from "electron" -import request from 'request' -import type Register from "@Mouseion/managers/register" -import type SecureCommunications from "@Chiton/utils/comms" -import autoBind from "auto-bind" -import { google } from 'googleapis' -import type { OAuth2Client } from "google-auth-library" -import type { Request } from 'express-serve-static-core' -import type WindowManager from "@Chiton/utils/window-manager" -import type { Logger, LumberjackEmployer } from "@Mouseion/utils/logger" - -export default class GOauth { - - private readonly comms: SecureCommunications - private readonly Log: Logger - private readonly client: OAuth2Client - private readonly windowManager: WindowManager - private tmpListener: ((code: string) => void | any) | null = null - - constructor( - private readonly Registry: Register, - private readonly clientId: string, - private readonly clientSecret: string | undefined, - private scopes: string[] - ) { - this.comms = Registry.get("Communications") as SecureCommunications - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("Mailman") - this.windowManager = Registry.get("Window Manager") as WindowManager - - if (!scopes.includes("profile")) scopes.push("profile") - if (!scopes.includes("email")) scopes.push("email") - - this.comms.register("please get google oauth token", this.newToken.bind(this)) - this.comms.register("please refresh google oauth token", this.refreshToken.bind(this)) - this.comms.registerGET("/oauth/google", this.getCode.bind(this)) - - this.client = new google.auth.OAuth2( - clientId, clientSecret, "http://127.0.0.1:41599/oauth/google" - ) - - autoBind(this) - } - - private getCode(req: Request) { - const codeParam = req.query.code - const code: string = codeParam ? (typeof codeParam == "string" ? codeParam : "") : "" - this.windowManager.focus() - if (this.tmpListener) return this.tmpListener(code) - else return this.Log.error("OAuth code was not caught by listener.") - } - - - private newToken({login_hint}: {login_hint?: string}) { - const _this = this - return new Promise(async (s, _) => { - const finish = (code: string) => _this.client.getToken(code).then(res => _this.client.setCredentials(res.tokens)) - - _this.client.on("tokens", tokens => { - s(tokens) - }) - - const url = _this.client.generateAuthUrl({ - access_type: "offline", - scope: _this.scopes, - login_hint, - }) - - - this.tmpListener = finish - - shell.openExternal(url) - }) - } - - private refreshToken({r_token}: {r_token: string}) { - const _this = this - return new Promise(async (s, _) => { - const opts = { - method: 'POST', - url: 'https://oauth2.googleapis.com/token', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - form: { - client_id: _this.clientId, - client_secret: _this.clientSecret, - refresh_token: r_token, - grant_type: 'refresh_token' - } - } - - request(opts, (e, _, b) => { - if (e) return s({ error: e }) - const d = JSON.parse(b) - s(d) - }) - }) - } - -} \ No newline at end of file diff --git a/Chiton/services/oauth/google.ts b/Chiton/services/oauth/google.ts new file mode 100644 index 00000000..a6cd72a5 --- /dev/null +++ b/Chiton/services/oauth/google.ts @@ -0,0 +1,102 @@ +import { shell } from "electron" +import request from 'request' +import autoBind from "auto-bind" +import { google } from 'googleapis' +import type { OAuth2Client } from "google-auth-library" +import type { Request } from 'express-serve-static-core' +import SockPuppet from "@Marionette/ws/sockpuppet" +import type { Chiton } from "@Chiton/app" + +export default class GOAuth extends SockPuppet { + puppetry = { + authorize: this.authorize, + refresh: this.refresh + } + + protected checkInitialize(): boolean { + throw new Error("Method not implemented.") + } + protected initialize(args: any[], success: (payload: object) => void): Promise { + throw new Error("Method not implemented.") + } + + private readonly client: OAuth2Client + private callback?: (code: string) => void | any + + constructor( + private readonly chiton: Chiton, + private readonly scopes: string[] + ) { + super("Google OAuth", { + forest: chiton.forest, + renderer: false, + }) + + if (!(this.scopes.includes('profile'))) this.scopes.push('profile') + if (!(this.scopes.includes('email'))) this.scopes.push('email') + + this.client = new google.auth.OAuth2( + this.chiton.config.secrets.googleClientId, undefined, "http://127.0.0.1:41599/oauth/google" + ) + + this.chiton.comms.get("/oauth/google", this.loopback.bind(this), { + respondWithClose: true + }) + + autoBind(this) + } + + private loopback(req: Request) { + const code = (typeof (req.query.code) === "string") ? req.query.code : ""; + this.chiton.inbox.focus() + this.callback!(code) + } + + // TODO: stronger types for authorize and refresh + + private authorize(login_hint?: string): Promise { + const _this = this + return new Promise(async (s, _) => { + _this.client.on('tokens', s) + + const url = _this.client.generateAuthUrl({ + access_type: "offline", + scope: _this.scopes, + login_hint, + }) + + _this.callback = async (code: string) => { + const { tokens } = await _this.client.getToken(code) + _this.client.setCredentials(tokens) + } + + shell.openExternal(url) + }) + } + + private refresh(refresh_token: string): Promise { + const _this = this + return new Promise(async (s, _) => { + const opts = { + method: 'POST', + url: 'https://oauth2.googleapis.com/token', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + form: { + client_id: _this.chiton.config.secrets.googleClientId, + client_secret: undefined, + refresh_token: refresh_token, + grant_type: 'refresh_token' + } + } + + request(opts, (e, _, b) => { + if (e) return s({ error: e }) + const d = JSON.parse(b) + s(d) + }) + }) + } + +} diff --git a/Marionette/ipc.ts b/Marionette/ipc.ts index a850a049..1c430d2a 100644 --- a/Marionette/ipc.ts +++ b/Marionette/ipc.ts @@ -127,7 +127,7 @@ export default class SecureCommunications { } /** Handles a GET request with a custom handler callback. */ - registerGET(route: string, cb: any, {respondWithClose=true}={}) { + get(route: string, cb: any, {respondWithClose=true}={}) { this.app.get(route, (q, s) => { cb(q) //? no await -- we don't care if it actually works if (respondWithClose) return s.redirect("https://helloaiko.com/redirect") From 225aa520f685fe89bd8fe991d79410f44b4157eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 22:31:53 -0500 Subject: [PATCH 017/275] fix typing issue --- Marionette/ws/sockpuppet.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts index 170f354b..1d5028b2 100644 --- a/Marionette/ws/sockpuppet.ts +++ b/Marionette/ws/sockpuppet.ts @@ -8,10 +8,9 @@ interface SockPuppetProcess extends NodeJS.Process { swallowErrors?: boolean | undefined; } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean } +type SockPuppetryMethod = ((...args: any[]) => Promise | any | void) type SockPuppetry = { - [key: string]: - ((...args: any[]) => Promise | any | void) - | SockPuppetry + [key: string]: SockPuppetryMethod | SockPuppetry } /* @@ -155,7 +154,7 @@ export default abstract class SockPuppet extends Lumberjack { } if (action === 'init') return await _this.initialize(args, success) - if (action in _this.puppetry) return await attempt(_this.puppetry[action]) + if (action in _this.puppetry) return await attempt(_this.puppetry[action] as SockPuppetryMethod) else return error("No such binding: " + action) } catch (e) { From 71e7d3f44919bd6ed229b6078d25b62a74e38f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=94?= Date: Fri, 30 Dec 2022 22:32:03 -0500 Subject: [PATCH 018/275] remove unused import --- Marionette/ws/sockpuppeteer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts index 27fe0315..69c3f751 100644 --- a/Marionette/ws/sockpuppeteer.ts +++ b/Marionette/ws/sockpuppeteer.ts @@ -1,5 +1,5 @@ import path from 'path' -import { fork, ChildProcess } from 'child_process' +import { fork } from 'child_process' import crypto from 'crypto' import Forest, { Lumberjack } from '@Iris/common/logger' import autoBind from 'auto-bind' From e4e88040082a8da526537b2ed48464aa951e199f Mon Sep 17 00:00:00 2001 From: Ruben Touitou Date: Wed, 4 Jan 2023 18:31:47 +0700 Subject: [PATCH 019/275] Added sidebar button for voice --- Veil/components/Sidebar/Sidebar.vue | 84 ++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/Veil/components/Sidebar/Sidebar.vue b/Veil/components/Sidebar/Sidebar.vue index 01fa60f2..06384bc2 100644 --- a/Veil/components/Sidebar/Sidebar.vue +++ b/Veil/components/Sidebar/Sidebar.vue @@ -17,13 +17,18 @@ const toggleSidebarCollapse = () => (Sidebar.collapsed = !Sidebar.collapsed); diff --git a/Veil/components/Base/Voice.vue b/Veil/components/Base/Voice.vue index 32de3782..b05721f3 100644 --- a/Veil/components/Base/Voice.vue +++ b/Veil/components/Base/Voice.vue @@ -1,24 +1,31 @@