2022-04-19 13:56:04 +00:00
|
|
|
// 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[]) {
|
2022-07-05 16:34:53 +00:00
|
|
|
console.log("[WebSocket] " + message, ...args);
|
2022-04-19 13:56:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** 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;
|
2022-05-22 11:52:26 +00:00
|
|
|
|
|
|
|
let wss = null,
|
|
|
|
wsPort = 6463;
|
|
|
|
for (const port of range(6463, 6472)) {
|
2022-04-19 13:56:04 +00:00
|
|
|
wss = await getServer(port);
|
2022-05-22 11:52:26 +00:00
|
|
|
if (wss !== null) {
|
|
|
|
void wsLog("ArmCord is listening at " + port.toString());
|
2022-04-19 13:56:04 +00:00
|
|
|
wsPort = port;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-05-22 11:52:26 +00:00
|
|
|
if (wss === null) return;
|
2022-04-19 13:56:04 +00:00
|
|
|
let lock = false;
|
2022-05-22 11:52:26 +00:00
|
|
|
wss.on("connection", (wss, request) => {
|
|
|
|
const origin = request.headers.origin ?? "https://discord.com";
|
2022-04-19 13:56:04 +00:00
|
|
|
let known = false;
|
2022-05-22 11:52:26 +00:00
|
|
|
for (const instance of knownInstancesList) {
|
|
|
|
if (instance[1].origin === origin) known = true;
|
2022-04-19 13:56:04 +00:00
|
|
|
}
|
2022-05-22 11:52:26 +00:00
|
|
|
if (!known) return;
|
2022-04-19 13:56:04 +00:00
|
|
|
wss.send(JSON.stringify(messages.handShake));
|
2022-05-22 11:52:26 +00:00
|
|
|
wss.once("message", (data, isBinary) => {
|
|
|
|
if (lock) return;
|
2022-04-19 13:56:04 +00:00
|
|
|
lock = true;
|
2022-05-22 11:52:26 +00:00
|
|
|
let parsedData: unknown = data;
|
|
|
|
if (!isBinary) parsedData = data.toString();
|
|
|
|
if (isJsonSyntaxCorrect(parsedData as string)) parsedData = JSON.parse(parsedData as string);
|
|
|
|
if (isInviteResponse(parsedData)) {
|
2022-04-19 13:56:04 +00:00
|
|
|
// Replies to browser, so it finds the communication successful.
|
2022-05-22 11:52:26 +00:00
|
|
|
wss.send(
|
|
|
|
JSON.stringify({
|
|
|
|
cmd: parsedData.cmd,
|
|
|
|
data: {
|
|
|
|
invite: null,
|
|
|
|
code: parsedData.args.code
|
|
|
|
},
|
|
|
|
evt: null,
|
|
|
|
nonce: parsedData.nonce
|
|
|
|
})
|
|
|
|
);
|
|
|
|
createInviteWindow();
|
2022-04-19 13:56:04 +00:00
|
|
|
const child = inviteWindow;
|
2022-05-22 11:52:26 +00:00
|
|
|
if (child === undefined) return;
|
|
|
|
void child.loadURL(origin + "/invite/" + parsedData.args.code);
|
2022-04-19 13:56:04 +00:00
|
|
|
child.webContents.once("did-finish-load", () => {
|
|
|
|
child.show();
|
|
|
|
});
|
|
|
|
child.webContents.once("will-navigate", () => {
|
|
|
|
lock = false;
|
|
|
|
child.close();
|
2022-05-22 11:52:26 +00:00
|
|
|
});
|
2022-04-19 14:10:12 +00:00
|
|
|
child.on("close", (e) => {
|
|
|
|
lock = false;
|
2022-05-22 11:52:26 +00:00
|
|
|
});
|
2022-04-19 13:56:04 +00:00
|
|
|
// Blocks requests to ArmCord's WS, to prevent loops.
|
2022-05-22 11:52:26 +00:00
|
|
|
child.webContents.session.webRequest.onBeforeRequest(
|
|
|
|
{
|
|
|
|
urls: ["ws://127.0.0.1:" + wsPort.toString() + "/*"]
|
|
|
|
},
|
|
|
|
(_details, callback) => callback({cancel: true})
|
|
|
|
);
|
2022-04-19 13:56:04 +00:00
|
|
|
}
|
2022-05-22 11:52:26 +00:00
|
|
|
});
|
|
|
|
});
|
2022-04-19 13:56:04 +00:00
|
|
|
}
|