mirror of
				https://github.com/smartfrigde/armcord.git
				synced 2024-08-14 23:56:58 +00:00 
			
		
		
		
	Add websocket for invitation link requests
This commit is contained in:
		
							parent
							
								
									bac604a7cc
								
							
						
					
					
						commit
						3d0fd7071b
					
				
					 4 changed files with 247 additions and 15 deletions
				
			
		
							
								
								
									
										59
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -11,12 +11,13 @@ | ||||||
|       "license": "OSL-3.0", |       "license": "OSL-3.0", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "electron-context-menu": "^3.1.2", |         "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": { |       "devDependencies": { | ||||||
|         "@types/electron-json-storage": "^4.5.0", |         "@types/electron-json-storage": "^4.5.0", | ||||||
|         "@types/node": "^17.0.24", |         "@types/node": "^17.0.24", | ||||||
|  |         "@types/ws": "^8.5.3", | ||||||
|         "copyfiles": "^2.4.1", |         "copyfiles": "^2.4.1", | ||||||
|         "electron": "^18.0.4", |         "electron": "^18.0.4", | ||||||
|         "electron-builder": "^22.5.1", |         "electron-builder": "^22.5.1", | ||||||
|  | @ -121,6 +122,15 @@ | ||||||
|       "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", |       "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", | ||||||
|       "dev": true |       "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": { |     "node_modules/@types/yargs": { | ||||||
|       "version": "15.0.14", |       "version": "15.0.14", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", |       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", | ||||||
|  | @ -1119,11 +1129,6 @@ | ||||||
|         "node": ">= 10.0.0" |         "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": { |     "node_modules/electron/node_modules/@types/node": { | ||||||
|       "version": "16.11.26", |       "version": "16.11.26", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", |       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", | ||||||
|  | @ -2907,6 +2912,26 @@ | ||||||
|       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", |       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", | ||||||
|       "dev": true |       "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": { |     "node_modules/xdg-basedir": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", | ||||||
|  | @ -3057,6 +3082,15 @@ | ||||||
|       "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", |       "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", | ||||||
|       "dev": true |       "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": { |     "@types/yargs": { | ||||||
|       "version": "15.0.14", |       "version": "15.0.14", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", |       "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": { |     "emoji-regex": { | ||||||
|       "version": "8.0.0", |       "version": "8.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", |       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | ||||||
|  | @ -5285,6 +5314,12 @@ | ||||||
|       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", |       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|  |     "ws": { | ||||||
|  |       "version": "8.5.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", | ||||||
|  |       "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", | ||||||
|  |       "requires": {} | ||||||
|  |     }, | ||||||
|     "xdg-basedir": { |     "xdg-basedir": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/electron-json-storage": "^4.5.0", |     "@types/electron-json-storage": "^4.5.0", | ||||||
|     "@types/node": "^17.0.24", |     "@types/node": "^17.0.24", | ||||||
|  |     "@types/ws": "^8.5.3", | ||||||
|     "copyfiles": "^2.4.1", |     "copyfiles": "^2.4.1", | ||||||
|     "electron": "^18.0.4", |     "electron": "^18.0.4", | ||||||
|     "electron-builder": "^22.5.1", |     "electron-builder": "^22.5.1", | ||||||
|  | @ -33,8 +34,8 @@ | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "electron-context-menu": "^3.1.2", |     "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": { |   "build": { | ||||||
|     "appId": "com.smartfridge.armcord", |     "appId": "com.smartfridge.armcord", | ||||||
|  |  | ||||||
							
								
								
									
										179
									
								
								src/socket.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/socket.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<InviteResponse>)?.cmd !== "INVITE_BROWSER") return false; | ||||||
|  |     if (typeof (data as Partial<InviteResponse>)?.args?.code !== "string") return false; | ||||||
|  |     if (typeof (data as Partial<InviteResponse>)?.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<WebSocket>` on | ||||||
|  |  *          success or `null` on failure). | ||||||
|  |  */ | ||||||
|  | async function getServer(port: number) { | ||||||
|  |     const {WebSocketServer} = await import("ws"); | ||||||
|  |     return new Promise<Server<WebSocket> | 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})); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | @ -6,9 +6,10 @@ import {BrowserWindow, shell, app, ipcMain, dialog} from "electron"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import {checkIfConfigIsBroken, firstRun, getConfig, contentPath} from "./utils"; | import {checkIfConfigIsBroken, firstRun, getConfig, contentPath} from "./utils"; | ||||||
| import {registerIpc} from "./ipc"; | import {registerIpc} from "./ipc"; | ||||||
|  | import startServer from "./socket" | ||||||
| import contextMenu from "electron-context-menu"; | import contextMenu from "electron-context-menu"; | ||||||
| export let mainWindow: BrowserWindow; | export let mainWindow: BrowserWindow; | ||||||
| 
 | export let inviteWindow: BrowserWindow; | ||||||
| let guestWindows: BrowserWindow[] = []; | let guestWindows: BrowserWindow[] = []; | ||||||
| contextMenu({ | contextMenu({ | ||||||
|     showSaveImageAs: true, |     showSaveImageAs: true, | ||||||
|  | @ -39,6 +40,7 @@ function doAfterDefiningTheWindow() { | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|     console.log(contentPath); |     console.log(contentPath); | ||||||
|  |     startServer() | ||||||
|     try { |     try { | ||||||
|         mainWindow.loadFile(contentPath); |         mainWindow.loadFile(contentPath); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  | @ -165,3 +167,18 @@ export function createTabsGuest(number: number) { | ||||||
|         guestWindows[number].loadURL("https://discord.com/app"); |         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() | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue