diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 1874ee924..adc6a4d02 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -36,7 +36,8 @@ import { CloudflareService, WebhookService, ScheduledMessagesService, - OauthService + OauthService, + RemoteConfigService } from "@server/services"; import { EventCache } from "@server/eventCache"; import { runTerminalScript, openSystemPreferences } from "@server/api/apple/scripts"; @@ -126,6 +127,8 @@ class BlueBubblesServer extends EventEmitter { networkChecker: NetworkService; + remoteConfigService: RemoteConfigService; + caffeinate: CaffeinateService; updater: UpdateService; @@ -484,6 +487,13 @@ class BlueBubblesServer extends EventEmitter { } catch (ex: any) { this.log(`Failed to start Scheduled Message service! ${ex.message}`, "error"); } + + try { + this.log("Initializing Remote Config Service..."); + this.remoteConfigService = new RemoteConfigService(); + } catch (ex: any) { + this.log(`Failed to initialize Remote Config Service! ${ex.message}`, "error"); + } } /** @@ -544,6 +554,13 @@ class BlueBubblesServer extends EventEmitter { this.log("Starting iMessage Database listeners..."); await this.startChatListeners(); } + + try { + this.log("Starting Remote Config Service..."); + await this.remoteConfigService.start(); + } catch (ex: any) { + this.log(`Failed to start Remote Config Service! ${ex.message}`, "error"); + } } async stopServices(): Promise { @@ -593,6 +610,12 @@ class BlueBubblesServer extends EventEmitter { this.log(`Failed to stop Scheduled Messages service! ${ex?.message ?? ex}`, "error"); } + try { + this.remoteConfigService?.stop(); + } catch (ex: any) { + this.log(`Failed to stop Remote Config Service! ${ex.message}`, "error"); + } + this.log("Finished stopping services..."); } diff --git a/packages/server/src/server/services/index.ts b/packages/server/src/server/services/index.ts index 808864fba..f99d197fd 100644 --- a/packages/server/src/server/services/index.ts +++ b/packages/server/src/server/services/index.ts @@ -12,6 +12,7 @@ import { WebhookService } from "./webhookService"; import { FindMyService } from "./findMyService"; import { ScheduledMessagesService } from "./scheduledMessagesService"; import { OauthService } from "./oauthService"; +import { RemoteConfigService } from "./remoteConfigService"; export { FCMService, @@ -27,5 +28,6 @@ export { WebhookService, FindMyService, ScheduledMessagesService, - OauthService + OauthService, + RemoteConfigService }; diff --git a/packages/server/src/server/services/remoteConfigService/commands.ts b/packages/server/src/server/services/remoteConfigService/commands.ts new file mode 100644 index 000000000..d15969404 --- /dev/null +++ b/packages/server/src/server/services/remoteConfigService/commands.ts @@ -0,0 +1,107 @@ +import { isEmpty } from "@server/helpers/utils"; +import { Server } from "@server"; + + +// This whole file was bascially cribbed from main.ts. I imagine the code should +// be consolidated and extracted somwhere. + + +function quickStrConvert (val: string) { + if (val.toLowerCase() === "true") return true; + if (val.toLowerCase() === "false") return false; + return val; +} + +async function handleSet (parts: string[]) { + const configKey = parts.length > 1 ? parts[1] : null; + const configValue = parts.length > 2 ? parts[2] : null; + if (!configKey || !configValue) { + return "Empty config key/value. Ignoring..."; + } + + if (!Server().repo.hasConfig(configKey)) { + return `Configuration, '${configKey}' does not exist. Ignoring...`; + } + + try { + await Server().repo.setConfig(configKey, quickStrConvert(configValue)); + return `Successfully set config item, '${configKey}' to, '${quickStrConvert(configValue)}'`; + } catch (ex: any) { + Server().log(`Failed set config item, '${configKey}'\n${ex}`, "error"); + return `Failed set config item, '${configKey}'\n${ex}`; + } +} + +async function handleShow (parts: string[]) { + const configKey = parts.length > 1 ? parts[1] : null; + if (!configKey) { + return "Empty config key. Ignoring..."; + } + + if (!Server().repo.hasConfig(configKey)) { + return `Configuration, '${configKey}' does not exist. Ignoring...`; + } + + try { + const value = await Server().repo.getConfig(configKey); + return `${configKey} -> ${value}`; + } catch (ex: any) { + Server().log(`Failed set config item, '${configKey}'\n${ex}`, "error"); + return `Failed set config item, '${configKey}'\n${ex}`; + } +} + +const help = `[================================== Help Menu ==================================]\n +Available Commands: + - help: Show the help menu + - restart: Relaunch/Restart the app + - set: Set configuration item -> \`set \` + Available configuration items: + -> tutorial_is_done: boolean + -> socket_port: number + -> server_address: string + -> ngrok_key: string + -> password: string + -> auto_caffeinate: boolean + -> auto_start: boolean + -> enable_ngrok: boolean + -> encrypt_coms: boolean + -> hide_dock_icon: boolean + -> last_fcm_restart: number + -> start_via_terminal: boolean + - show: Show the current configuration for an item -> \`show \` + - exit: Exit the configurator +\n[===============================================================================]`; + + +export async function handleLine (line: string) : Promise<[string, boolean]> { + line = line.trim(); + if (line === "") return ['', true]; + + // Handle the standard input + const parts = line.split(" "); + if (isEmpty(parts)) return ['', true]; + + if (!Server()) { + return ["Server is not running???????", true]; + } + + switch (parts[0].toLowerCase()) { + case "help": + return [help, true]; + case "set": + return [await handleSet(parts), true]; + break; + case "show": + return [await handleShow(parts), true]; + break; + case "restart": + case "relaunch": + Server().relaunch(); + return ["Okay, restarting", true]; + case "exit": + return ["Thank you for using the Bluebubbles configurator!", false]; + default: + return ["Unrecognized command. Type 'help' for help.", true]; + } +} diff --git a/packages/server/src/server/services/remoteConfigService/index.ts b/packages/server/src/server/services/remoteConfigService/index.ts new file mode 100644 index 000000000..02503ae7c --- /dev/null +++ b/packages/server/src/server/services/remoteConfigService/index.ts @@ -0,0 +1,110 @@ +import * as net from "net"; +import { Server } from "@server"; +import { LogLevel } from "electron-log"; +import { LineExtractor } from "./lineExtractor"; +import { app } from "electron"; +import { handleLine } from "./commands"; + + +const PROMPT = 'bluebubbles-configurator>'; + + +export class RemoteConfigService { + server: net.Server; + + clients: Set = new Set(); + + socketPath: string; + + constructor () { + // not sure if this socketPath stuff belongs in fileSystem/index.ts + const isDev = process.env.NODE_ENV !== "production"; + this.socketPath = app.getPath("userData"); + if (isDev) { + this.socketPath = path.join(this.socketPath, "bluebubbles-server"); + } + this.socketPath = path.join(this.socketPath, "remote-config.sock"); + + this.log(`Remote Config Service socket path: ${this.socketPath}`, "debug"); + } + + private log(message: string, level: LogLevel = "info") { + Server().log(`[RemoteConfigService] ${String(message)}`, level); + } + + start() { + this.server = net.createServer({allowHalfOpen: false}, (client: net.Socket) => { + this.handleClient(client); + }); + + if (fs.existsSync(this.socketPath)) { + fs.unlinkSync(this.socketPath); + } + this.server.listen(this.socketPath); + + this.server.on("error", err => { + this.log("An error occured in the RemoteConfigService server socket", "error"); + this.log(err.toString(), "error"); + this.closeServer(); + }); + } + + private handleClient(client: net.Socket) { + this.log("Remote Config Service client connected", "debug"); + + this.clients.add(client); + const lineExtractor = new LineExtractor(); + + client.write(`Welcome to the BlueBubbles configurator!\nType 'help' for help.\nType 'exit' to exit.\n${PROMPT} `); + + client.on("data", async (data: Buffer) => { + for (const line of lineExtractor.feed(data)) { + await this.handleLine(client, line); + } + }); + client.on("close", () => { + this.log("Remote Config Service client closed", "debug"); + this.clients.delete(client); + }); + client.on("error", err => { + this.log("An error occured in a RemoteConfigService client", "error"); + this.log(err.toString(), "error"); + }); + } + + private async handleLine (client: net.Socket, line: string) { + this.log(`Remote Config Service client received line: ${JSON.stringify(line)}`, "debug"); + const [response, keepOpen] = await handleLine(line); + if (response) { + client.write(`${response}\n`); + } + if (keepOpen) { + client.write(`${PROMPT} `); + } + else { + client.destroy(); + } + } + + private closeServer () { + if (this.server === null) { + return; + } + this.server.close(); + this.server = null; + } + + private destroyAllClients () { + for (const client of this.clients) { + client.destroy(); + } + this.clients = new Set(); + } + + stop() { + this.log("Stopping Remote Config Service..."); + + this.destroyAllClients(); + this.closeServer(); + } +} diff --git a/packages/server/src/server/services/remoteConfigService/lineExtractor.ts b/packages/server/src/server/services/remoteConfigService/lineExtractor.ts new file mode 100644 index 000000000..651dd8e0e --- /dev/null +++ b/packages/server/src/server/services/remoteConfigService/lineExtractor.ts @@ -0,0 +1,19 @@ +export class LineExtractor { + + buffer: Buffer; + + constructor () { + this.buffer = Buffer.from([]); + } + + *feed (data: Buffer) { + this.buffer = Buffer.concat([this.buffer, data]); + while (true) { + const i = this.buffer.indexOf(10); + if (i === -1) break; + + yield this.buffer.subarray(0, i).toString('utf-8'); + this.buffer = this.buffer.subarray(i + 1); + } + } +}