From 3d0fd7071b27aa5257470eeef68e34f1955a7864 Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:56:04 +0200 Subject: [PATCH 1/7] Add websocket for invitation link requests --- package-lock.json | 59 +++++++++++---- package.json | 5 +- src/socket.ts | 179 ++++++++++++++++++++++++++++++++++++++++++++++ src/window.ts | 19 ++++- 4 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 src/socket.ts diff --git a/package-lock.json b/package-lock.json index 3e32787..1ffb1de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,13 @@ "license": "OSL-3.0", "dependencies": { "electron-context-menu": "^3.1.2", - "electron-tabs": "^0.17.0", - "v8-compile-cache": "^2.3.0" + "v8-compile-cache": "^2.3.0", + "ws": "^8.5.0" }, "devDependencies": { "@types/electron-json-storage": "^4.5.0", "@types/node": "^17.0.24", + "@types/ws": "^8.5.3", "copyfiles": "^2.4.1", "electron": "^18.0.4", "electron-builder": "^22.5.1", @@ -121,6 +122,15 @@ "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -1119,11 +1129,6 @@ "node": ">= 10.0.0" } }, - "node_modules/electron-tabs": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/electron-tabs/-/electron-tabs-0.17.0.tgz", - "integrity": "sha512-jFv6WOeumSR5q2Cf6WOghE7CTdxPB0mSuPw8dGwz1OAG8MJMQn/kd/ghmvRPwoOYK77v4d9YligqjXIQc2oPcg==" - }, "node_modules/electron/node_modules/@types/node": { "version": "16.11.26", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", @@ -2907,6 +2912,26 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", @@ -3057,6 +3082,15 @@ "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", "dev": true }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -3885,11 +3919,6 @@ } } }, - "electron-tabs": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/electron-tabs/-/electron-tabs-0.17.0.tgz", - "integrity": "sha512-jFv6WOeumSR5q2Cf6WOghE7CTdxPB0mSuPw8dGwz1OAG8MJMQn/kd/ghmvRPwoOYK77v4d9YligqjXIQc2oPcg==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5285,6 +5314,12 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", diff --git a/package.json b/package.json index 1ca810a..c121ac4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@types/electron-json-storage": "^4.5.0", "@types/node": "^17.0.24", + "@types/ws": "^8.5.3", "copyfiles": "^2.4.1", "electron": "^18.0.4", "electron-builder": "^22.5.1", @@ -33,8 +34,8 @@ }, "dependencies": { "electron-context-menu": "^3.1.2", - "electron-tabs": "^0.17.0", - "v8-compile-cache": "^2.3.0" + "v8-compile-cache": "^2.3.0", + "ws": "^8.5.0" }, "build": { "appId": "com.smartfridge.armcord", diff --git a/src/socket.ts b/src/socket.ts new file mode 100644 index 0000000..2e73c7c --- /dev/null +++ b/src/socket.ts @@ -0,0 +1,179 @@ +// MIT License + +// Copyright (c) 2020-2022 Dawid Papiewski "SpacingBat3" + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +import type {Server, WebSocket} from "ws"; +import {inviteWindow, createInviteWindow} from "./window"; + +async function wsLog(message: string, ...args: unknown[]) { + console.log("WebSocket" + +message, ...args); +} + +/** Generates an inclusive range (as `Array`) from `start` to `end`. */ +function range(start: number, end: number) { + return Array.from({length: end - start + 1}, (_v, k) => start + k); +} + +interface InviteResponse { + /** Response type/command. */ + cmd: "INVITE_BROWSER"; + /** Response arguments. */ + args: { + /** An invitation code. */ + code: string; + }; + /** Nonce indentifying the communication. */ + nonce: string; +} + +function isInviteResponse(data: unknown): data is InviteResponse { + if (!(data instanceof Object)) return false; + if ((data as Partial)?.cmd !== "INVITE_BROWSER") return false; + if (typeof (data as Partial)?.args?.code !== "string") return false; + if (typeof (data as Partial)?.nonce !== "string") return false; + return true; +} + +const messages = { + /** + * A fake, hard-coded Discord command to spoof the presence of + * official Discord client (which makes browser to actually start a + * communication with the ArmCord). + */ + handShake: { + /** Message command. */ + cmd: "DISPATCH", + /** Message data. */ + data: { + /** Message scheme version. */ + v: 1, + /** Client properties. */ + config: { + /** Discord CDN host (hard-coded for `dicscord.com` instance). */ + cdn_host: "cdn.discordapp.com", + /** API endpoint (hard-coded for `dicscord.com` instance). */ + api_endpoint: "//discord.com/api", + /** Client type. Can be (probably) `production` or `canary`. */ + environment: "production" + } + }, + evt: "READY", + nonce: null + } +}; + +/** + * Tries to reserve the server at given port. + * + * @returns `Promise`, which always resolves (either to `Server` on + * success or `null` on failure). + */ +async function getServer(port: number) { + const {WebSocketServer} = await import("ws"); + return new Promise | null>((resolve) => { + const wss = new WebSocketServer({host: "127.0.0.1", port}); + wss.once("listening", () => resolve(wss)); + wss.once("error", () => resolve(null)); + }); +} + +/** + * Tries to start a WebSocket server at given port range. If it suceed, it will + * listen to the browser requests which are meant to be sent to official + * Discord client. + * + * Currently it supports only the invitation link requests. + * + */ +export default async function startServer() { + function isJsonSyntaxCorrect(string: string) { + try { + JSON.parse(string); + } catch { + return false; + } + return true; + } + /** Known Discord instances, including the official ones. */ + const knownInstancesList = [ + ["Discord", new URL("https://discord.com/app")], + ["Discord Canary", new URL("https://canary.discord.com/app")], + ["Discord PTB", new URL("https://ptb.discord.com/app")], + ["Fosscord", new URL("https://dev.fosscord.com/app")] + ] as const; + + let wss = null, wsPort = 6463; + for(const port of range(6463, 6472)) { + wss = await getServer(port); + if(wss !== null) { + void wsLog("ArmCord is listening at " + (port.toString())); + wsPort = port; + break; + } + } + if(wss === null) return; + let lock = false; + wss.on('connection', (wss, request) => { + const origin = request.headers.origin??'https://discord.com'; + let known = false; + for(const instance of knownInstancesList) { + if(instance[1].origin === origin) + known = true; + } + if(!known) return; + wss.send(JSON.stringify(messages.handShake)); + wss.once('message', (data, isBinary) => { + if(lock) return; + lock = true; + let parsedData:unknown = data; + if(!isBinary) + parsedData = data.toString(); + if(isJsonSyntaxCorrect(parsedData as string)) + parsedData = JSON.parse(parsedData as string); + if(isInviteResponse(parsedData)) { + // Replies to browser, so it finds the communication successful. + wss.send(JSON.stringify({ + cmd: parsedData.cmd, + data: { + invite: null, + code: parsedData.args.code + }, + evt: null, + nonce: parsedData.nonce + })); + createInviteWindow() + const child = inviteWindow; + if(child === undefined) return; + void child.loadURL(origin+'/invite/'+parsedData.args.code); + child.webContents.once("did-finish-load", () => { + child.show(); + }); + child.webContents.once("will-navigate", () => { + lock = false; + child.close(); + }) + // Blocks requests to ArmCord's WS, to prevent loops. + child.webContents.session.webRequest.onBeforeRequest({ + urls: ['ws://127.0.0.1:'+wsPort.toString()+'/*'] + }, (_details,callback) => callback({cancel: true})); + } + }) + }) +} diff --git a/src/window.ts b/src/window.ts index bc00139..cd1c59d 100644 --- a/src/window.ts +++ b/src/window.ts @@ -6,9 +6,10 @@ import {BrowserWindow, shell, app, ipcMain, dialog} from "electron"; import path from "path"; import {checkIfConfigIsBroken, firstRun, getConfig, contentPath} from "./utils"; import {registerIpc} from "./ipc"; +import startServer from "./socket" import contextMenu from "electron-context-menu"; export let mainWindow: BrowserWindow; - +export let inviteWindow: BrowserWindow; let guestWindows: BrowserWindow[] = []; contextMenu({ showSaveImageAs: true, @@ -39,6 +40,7 @@ function doAfterDefiningTheWindow() { } }); console.log(contentPath); + startServer() try { mainWindow.loadFile(contentPath); } catch (e) { @@ -165,3 +167,18 @@ export function createTabsGuest(number: number) { guestWindows[number].loadURL("https://discord.com/app"); } } +export function createInviteWindow() { + inviteWindow = new BrowserWindow({ + width: 800, + height: 600, + title: "ArmCord Invite Manager", + darkTheme: true, + icon: path.join(__dirname, "/assets/icon_transparent.png"), + frame: true, + autoHideMenuBar: true, + webPreferences: { + spellcheck: true + } + }); + inviteWindow.hide() +} \ No newline at end of file From eda6621989ebda28d0053cdafc46adf687d24313 Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 16:02:30 +0200 Subject: [PATCH 2/7] Fix websocket logger --- src/socket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket.ts b/src/socket.ts index 2e73c7c..0155947 100644 --- a/src/socket.ts +++ b/src/socket.ts @@ -23,7 +23,7 @@ import type {Server, WebSocket} from "ws"; import {inviteWindow, createInviteWindow} from "./window"; async function wsLog(message: string, ...args: unknown[]) { - console.log("WebSocket" + +message, ...args); + console.log("[WebSocket]" + message, ...args); } /** Generates an inclusive range (as `Array`) from `start` to `end`. */ From 37966b985a6f3f3033d71321e15247c8d19398bd Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 16:10:12 +0200 Subject: [PATCH 3/7] Fix websocket not unlocking when window is closed manually --- src/socket.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/socket.ts b/src/socket.ts index 0155947..0c2e0a9 100644 --- a/src/socket.ts +++ b/src/socket.ts @@ -169,6 +169,9 @@ export default async function startServer() { lock = false; child.close(); }) + child.on("close", (e) => { + lock = false; + }) // Blocks requests to ArmCord's WS, to prevent loops. child.webContents.session.webRequest.onBeforeRequest({ urls: ['ws://127.0.0.1:'+wsPort.toString()+'/*'] From 7a7dd051634aaf8be52b21e5b83d8291c0ed7d7f Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 19:59:52 +0200 Subject: [PATCH 4/7] Make invite websocket togglable --- src/content/setup.html | 12 +++++++++--- src/settings/settings.html | 4 +++- src/utils.ts | 2 ++ src/window.ts | 6 ++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/content/setup.html b/src/content/setup.html index cdca768..22f3f98 100644 --- a/src/content/setup.html +++ b/src/content/setup.html @@ -46,7 +46,9 @@ minimizeToTray: true, automaticPatches: false, mods: "cumcord", - blurType: "acrylic" + blurType: "acrylic", + inviteWebsocket: true, + doneSetup: true }); fade(document.getElementById("setup")); setTimeout(function () { @@ -98,7 +100,9 @@ minimizeToTray: true, automaticPatches: false, mods: mod, - blurType: "acrylic" + blurType: "acrylic", + inviteWebsocket: true, + doneSetup: true }); fade(document.getElementById("setup")); setTimeout(function () { @@ -113,7 +117,9 @@ minimizeToTray: true, automaticPatches: false, mods: "none", - blurType: "acrylic" + blurType: "acrylic", + inviteWebsocket: true, + doneSetup: true }); fade(document.getElementById("setup")); setTimeout(function () { diff --git a/src/settings/settings.html b/src/settings/settings.html index 2b8151f..b74d94f 100644 --- a/src/settings/settings.html +++ b/src/settings/settings.html @@ -74,7 +74,9 @@ minimizeToTray: document.getElementById("tray").checked, automaticPatches: document.getElementById("patches").checked, mods: document.getElementById("mod").value, - blurType: "acrylic" + blurType: "acrylic", + inviteWebsocket: true, + doneSetup: true }); }); diff --git a/src/utils.ts b/src/utils.ts index 7e48a87..6b12f38 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,6 +41,7 @@ export function setup() { automaticPatches: false, mods: "cumcord", blurType: "acrylic", + inviteWebsocket: true, doneSetup: false }; setConfigBulk({ @@ -72,6 +73,7 @@ export interface Settings { automaticPatches: boolean; mods: string; blurType: string; + inviteWebsocket: boolean; doneSetup: boolean; } export async function getConfig(object: string) { diff --git a/src/window.ts b/src/window.ts index cd1c59d..c5e4b9c 100644 --- a/src/window.ts +++ b/src/window.ts @@ -17,7 +17,7 @@ contextMenu({ showSearchWithGoogle: true }); -function doAfterDefiningTheWindow() { +async function doAfterDefiningTheWindow() { checkIfConfigIsBroken(); registerIpc(); mainWindow.webContents.userAgent = @@ -40,7 +40,9 @@ function doAfterDefiningTheWindow() { } }); console.log(contentPath); - startServer() + if (await getConfig("inviteWebsocket") == true) { + startServer() + } try { mainWindow.loadFile(contentPath); } catch (e) { From 1f01ae67dd5bdd0122cd520f16b357eada9a271c Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:13:46 +0200 Subject: [PATCH 5/7] Add dev build CI --- .github/release.md | 2 ++ .github/workflows/dev.yml | 57 +++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 60 insertions(+) create mode 100644 .github/release.md create mode 100644 .github/workflows/dev.yml diff --git a/.github/release.md b/.github/release.md new file mode 100644 index 0000000..639ec3e --- /dev/null +++ b/.github/release.md @@ -0,0 +1,2 @@ +# Thanks for checking out ArmCord dev build! +Make sure to join our [Discord server](https://discord.gg/uaW5vMY3V6) to share opinions, or to chat with ArmCord developers! \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..7de6e45 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,57 @@ +name: Dev build +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + - run: npm install + - id: vars + shell: bash + run: | + echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.vars.outputs.sha_short }} + release_name: Release ${{ steps.vars.outputs.sha_short }} + draft: true + prerelease: true + body_path: ./.github/release.md + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0.zip + asset_name: ArmCordLinux.zip + asset_content_type: application/octet-stream + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0-win.zip + asset_name: ArmCordWindows.zip + asset_content_type: application/octet-stream + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0-mac.zip + asset_name: ArmCordWindows.zip + asset_content_type: application/octet-stream + - run: | + curl --request PATCH \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: application/json" \ + https://api.github.com/repos/${{ github.repository }}/releases/${{steps.create_release.outputs.id}} \ + -d '{"draft":false}' \ No newline at end of file diff --git a/package.json b/package.json index c121ac4..4d66138 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "npm run build && electron ./ts-out/main.js", "package": "npm run build && electron-builder", "format": "prettier --write src/**/*", + "CIbuild": "npm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip", "postinstall": "husky install" }, "repository": { From d68cec6396c9c2e667d24b5a5ce91a916337d56a Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:14:55 +0200 Subject: [PATCH 6/7] I forgor --- .github/workflows/dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 7de6e45..0c6640e 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -9,6 +9,7 @@ jobs: with: node-version: '18' - run: npm install + - run: npm run CIbuild - id: vars shell: bash run: | From 3854e5fa39a50c7bac2590de05594b6b7a322d76 Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:19:00 +0200 Subject: [PATCH 7/7] Add windows build --- .github/workflows/dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0c6640e..11bd34a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -8,6 +8,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: '18' + - run: sudo apt install -y wine - run: npm install - run: npm run CIbuild - id: vars