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", | ||||
|       "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", | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
							
								
								
									
										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 {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() | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue