feat: Discord's local RPC servers
arRPC (https://github.com/OpenAsar/arrpc)
This commit is contained in:
parent
23d59d70ea
commit
fbcbe77c14
18
Document/DiscordClientRPC.md
Normal file
18
Document/DiscordClientRPC.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
## Discord's local RPC servers (~ discord app)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Client, RichPresence, DiscordRPCServer } = require('discord.js-selfbot-v13');
|
||||||
|
|
||||||
|
const client = new Client();
|
||||||
|
|
||||||
|
client.once('ready', async () => {
|
||||||
|
const server = await new DiscordRPCServer(client, false)
|
||||||
|
server.on('activity', async data => {
|
||||||
|
if (!data.activity) return;
|
||||||
|
const activity = new RichPresence(client, data.activity);
|
||||||
|
client.user.setActivity(activity);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login('token');
|
||||||
|
```
|
@ -9,6 +9,7 @@ exports.ShardingManager = require('./sharding/ShardingManager');
|
|||||||
exports.WebhookClient = require('./client/WebhookClient');
|
exports.WebhookClient = require('./client/WebhookClient');
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
exports.DiscordRPCServer = require('./util/arRPC/index');
|
||||||
exports.ActivityFlags = require('./util/ActivityFlags');
|
exports.ActivityFlags = require('./util/ActivityFlags');
|
||||||
exports.ApplicationFlags = require('./util/ApplicationFlags');
|
exports.ApplicationFlags = require('./util/ApplicationFlags');
|
||||||
exports.BaseManager = require('./managers/BaseManager');
|
exports.BaseManager = require('./managers/BaseManager');
|
||||||
|
229
src/util/arRPC/index.js
Normal file
229
src/util/arRPC/index.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { EventEmitter } = require('node:events');
|
||||||
|
const ProcessServer = require('./process/index.js');
|
||||||
|
const IPCServer = require('./transports/ipc.js');
|
||||||
|
const WSServer = require('./transports/websocket.js');
|
||||||
|
const { RichPresence } = require('../../structures/RichPresence.js');
|
||||||
|
const { NitroType } = require('../Constants.js');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
const checkUrl = url => /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test(url);
|
||||||
|
|
||||||
|
let socketId = 0;
|
||||||
|
module.exports = class RPCServer extends EventEmitter {
|
||||||
|
constructor(client, debug = false) {
|
||||||
|
super();
|
||||||
|
Object.defineProperty(this, 'client', { value: client });
|
||||||
|
return (async () => {
|
||||||
|
this.debug = debug;
|
||||||
|
this.onConnection = this.onConnection.bind(this);
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
this.onClose = this.onClose.bind(this);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
connection: this.onConnection,
|
||||||
|
message: this.onMessage,
|
||||||
|
close: this.onClose,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ipc = await new IPCServer(handlers, this.debug);
|
||||||
|
this.ws = await new WSServer(handlers, this.debug);
|
||||||
|
this.process = await new ProcessServer(handlers, this.debug);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnection(socket) {
|
||||||
|
socket.send({
|
||||||
|
cmd: 'DISPATCH',
|
||||||
|
evt: 'READY',
|
||||||
|
|
||||||
|
data: {
|
||||||
|
v: 1,
|
||||||
|
// Needed otherwise some stuff errors out parsing json strictly
|
||||||
|
user: {
|
||||||
|
// Mock user data using arRPC app/bot
|
||||||
|
id: this.client?.user?.id ?? '1045800378228281345',
|
||||||
|
username: this.client?.user?.username ?? 'arRPC',
|
||||||
|
discriminator: this.client?.user?.discriminator ?? '0000',
|
||||||
|
avatar: this.client?.user?.avatar,
|
||||||
|
flags: this.client?.user?.flags?.bitfield ?? 0,
|
||||||
|
premium_type: this.client?.user?.nitroType ? NitroType[this.client?.user?.nitroType] : 0,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
api_endpoint: '//discord.com/api',
|
||||||
|
cdn_host: 'cdn.discordapp.com',
|
||||||
|
environment: 'production',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.socketId = socketId++;
|
||||||
|
|
||||||
|
this.emit('connection', socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(socket) {
|
||||||
|
this.emit('activity', {
|
||||||
|
activity: null,
|
||||||
|
pid: socket.lastPid,
|
||||||
|
socketId: socket.socketId.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('close', socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(socket, { cmd, args, nonce }) {
|
||||||
|
this.emit('message', { socket, cmd, args, nonce });
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'SET_ACTIVITY':
|
||||||
|
if (!socket.clientInfo || !socket.clientAssets) {
|
||||||
|
// https://discord.com/api/v9/oauth2/applications/:id/rpc
|
||||||
|
socket.clientInfo = await this.client.api.oauth2.applications(socket.clientId).rpc.get();
|
||||||
|
socket.clientAssets = await this.client.api.oauth2.applications(socket.clientId).assets.get();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const { activity, pid } = args; // Translate given parameters into what discord dispatch expects
|
||||||
|
|
||||||
|
if (!activity) {
|
||||||
|
return this.emit('activity', {
|
||||||
|
activity: null,
|
||||||
|
pid,
|
||||||
|
socketId: socket.socketId.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const { buttons, timestamps, instance, assets } = activity;
|
||||||
|
|
||||||
|
socket.lastPid = pid ?? socket.lastPid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const metadata = {};
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const extra = {};
|
||||||
|
if (buttons) {
|
||||||
|
// Map buttons into expected metadata
|
||||||
|
metadata.button_urls = buttons.map(x => x.url);
|
||||||
|
extra.buttons = buttons.map(x => x.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets?.large_image) {
|
||||||
|
if (checkUrl(assets.large_image)) {
|
||||||
|
assets.large_image = assets.large_image
|
||||||
|
.replace('https://cdn.discordapp.com/', 'mp:')
|
||||||
|
.replace('http://cdn.discordapp.com/', 'mp:')
|
||||||
|
.replace('https://media.discordapp.net/', 'mp:')
|
||||||
|
.replace('http://media.discordapp.net/', 'mp:');
|
||||||
|
if (!assets.large_image.startsWith('mp:')) {
|
||||||
|
// Fetch
|
||||||
|
const data = await RichPresence.getExternal(this.client, socket.clientId, assets.large_image);
|
||||||
|
assets.large_image = data[0].external_asset_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/^[0-9]{17,19}$/.test(assets.large_image)) {
|
||||||
|
// ID Assets
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
assets.large_image.startsWith('mp:') ||
|
||||||
|
assets.large_image.startsWith('youtube:') ||
|
||||||
|
assets.large_image.startsWith('spotify:')
|
||||||
|
) {
|
||||||
|
// Image
|
||||||
|
}
|
||||||
|
if (assets.large_image.startsWith('external/')) {
|
||||||
|
assets.large_image = `mp:${assets.large_image}`;
|
||||||
|
} else {
|
||||||
|
const l = socket.clientAssets.find(o => o.name == assets.large_image);
|
||||||
|
if (l) assets.large_image = l.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets?.small_image) {
|
||||||
|
if (checkUrl(assets.small_image)) {
|
||||||
|
assets.small_image = assets.small_image
|
||||||
|
.replace('https://cdn.discordapp.com/', 'mp:')
|
||||||
|
.replace('http://cdn.discordapp.com/', 'mp:')
|
||||||
|
.replace('https://media.discordapp.net/', 'mp:')
|
||||||
|
.replace('http://media.discordapp.net/', 'mp:');
|
||||||
|
if (!assets.small_image.startsWith('mp:')) {
|
||||||
|
// Fetch
|
||||||
|
const data = await RichPresence.getExternal(this.client, socket.clientId, assets.small_image);
|
||||||
|
assets.small_image = data[0].external_asset_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/^[0-9]{17,19}$/.test(assets.small_image)) {
|
||||||
|
// ID Assets
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
assets.small_image.startsWith('mp:') ||
|
||||||
|
assets.small_image.startsWith('youtube:') ||
|
||||||
|
assets.small_image.startsWith('spotify:')
|
||||||
|
) {
|
||||||
|
// Image
|
||||||
|
}
|
||||||
|
if (assets.small_image.startsWith('external/')) {
|
||||||
|
assets.small_image = `mp:${assets.small_image}`;
|
||||||
|
} else {
|
||||||
|
const l = socket.clientAssets.find(o => o.name == assets.small_image);
|
||||||
|
if (l) assets.small_image = l.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamps) {
|
||||||
|
for (const x in timestamps) {
|
||||||
|
// Translate s -> ms timestamps
|
||||||
|
if (Date.now().toString().length - timestamps[x].toString().length > 2) {
|
||||||
|
timestamps[x] = Math.floor(1000 * timestamps[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('activity', {
|
||||||
|
activity: {
|
||||||
|
application_id: socket.clientId,
|
||||||
|
type: 0,
|
||||||
|
name: socket.clientInfo.name,
|
||||||
|
metadata,
|
||||||
|
assets,
|
||||||
|
flags: instance ? 1 << 0 : 0,
|
||||||
|
...activity,
|
||||||
|
...extra,
|
||||||
|
},
|
||||||
|
pid,
|
||||||
|
socketId: socket.socketId.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.send?.({
|
||||||
|
cmd,
|
||||||
|
data: null,
|
||||||
|
evt: null,
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'GUILD_TEMPLATE_BROWSER':
|
||||||
|
case 'INVITE_BROWSER':
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const { code } = args;
|
||||||
|
socket.send({
|
||||||
|
cmd,
|
||||||
|
data: {
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit(cmd === 'INVITE_BROWSER' ? 'invite' : 'guild-template', code);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DEEP_LINK':
|
||||||
|
this.emit('link', args.params);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
1
src/util/arRPC/process/detectable.json
Normal file
1
src/util/arRPC/process/detectable.json
Normal file
File diff suppressed because one or more lines are too long
102
src/util/arRPC/process/index.js
Normal file
102
src/util/arRPC/process/index.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
|
||||||
|
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}]`, ...args);
|
||||||
|
const { setInterval } = require('node:timers');
|
||||||
|
const process = require('process');
|
||||||
|
const DetectableDB = require('./detectable.json');
|
||||||
|
const Natives = require('./native/index.js');
|
||||||
|
|
||||||
|
const Native = Natives[process.platform];
|
||||||
|
|
||||||
|
const timestamps = {},
|
||||||
|
names = {},
|
||||||
|
pids = {};
|
||||||
|
module.exports = class ProcessServer {
|
||||||
|
constructor(handlers, debug = false) {
|
||||||
|
this.debug = debug;
|
||||||
|
if (!Native) return; // Log('unsupported platform:', process.platform);
|
||||||
|
|
||||||
|
this.handlers = handlers;
|
||||||
|
|
||||||
|
this.scan = this.scan.bind(this);
|
||||||
|
|
||||||
|
this.scan();
|
||||||
|
setInterval(this.scan, 5000);
|
||||||
|
|
||||||
|
if (this.debug) log('started');
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan() {
|
||||||
|
const processes = await Native.getProcesses();
|
||||||
|
const ids = [];
|
||||||
|
|
||||||
|
for (const [pid, _path] of processes) {
|
||||||
|
const path = _path.toLowerCase().replaceAll('\\', '/');
|
||||||
|
const toCompare = [path.split('/').pop(), path.split('/').slice(-2).join('/')];
|
||||||
|
|
||||||
|
for (const p of toCompare.slice()) {
|
||||||
|
// Add more possible tweaked paths for less false negatives
|
||||||
|
toCompare.push(p.replace('64', '')); // Remove 64bit identifiers-ish
|
||||||
|
toCompare.push(p.replace('.x64', ''));
|
||||||
|
toCompare.push(p.replace('x64', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { executables, id, name } of DetectableDB) {
|
||||||
|
if (executables?.some(x => !x.isLauncher && toCompare.some(y => x.name === y))) {
|
||||||
|
names[id] = name;
|
||||||
|
pids[id] = pid;
|
||||||
|
|
||||||
|
ids.push(id);
|
||||||
|
if (!timestamps[id]) {
|
||||||
|
// eslint-disable-next-line max-depth
|
||||||
|
if (this.debug) log('detected game!', name);
|
||||||
|
timestamps[id] = Date.now();
|
||||||
|
|
||||||
|
this.handlers.message(
|
||||||
|
{
|
||||||
|
socketId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: 'SET_ACTIVITY',
|
||||||
|
args: {
|
||||||
|
activity: {
|
||||||
|
application_id: id,
|
||||||
|
name,
|
||||||
|
timestamps: {
|
||||||
|
start: timestamps[id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id in timestamps) {
|
||||||
|
if (!ids.includes(id)) {
|
||||||
|
if (this.debug) log('lost game!', names[id]);
|
||||||
|
delete timestamps[id];
|
||||||
|
|
||||||
|
this.handlers.message(
|
||||||
|
{
|
||||||
|
socketId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: 'SET_ACTIVITY',
|
||||||
|
args: {
|
||||||
|
activity: null,
|
||||||
|
pid: pids[id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If (this.debug) log(`finished scan in ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
// process.stdout.write(`\r${' '.repeat(100)}\r[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}] scanned (took ${(performance.now() - startTime).toFixed(2)}ms)`);
|
||||||
|
}
|
||||||
|
};
|
5
src/util/arRPC/process/native/index.js
Normal file
5
src/util/arRPC/process/native/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const linux = require('./linux.js');
|
||||||
|
const win32 = require('./win32.js');
|
||||||
|
module.exports = { win32, linux };
|
37
src/util/arRPC/process/native/linux.js
Normal file
37
src/util/arRPC/process/native/linux.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const { readlink } = require('fs/promises');
|
||||||
|
|
||||||
|
const getProcesses = () =>
|
||||||
|
new Promise(res =>
|
||||||
|
exec(`ps a -o "%p;%c;%a"`, async (e, out) => {
|
||||||
|
res(
|
||||||
|
(
|
||||||
|
await Promise.all(
|
||||||
|
out
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.slice(1, -1)
|
||||||
|
|
||||||
|
.map(async x => {
|
||||||
|
const split = x.trim().split(';');
|
||||||
|
// If (split.length === 1) return;
|
||||||
|
|
||||||
|
const pid = parseInt(split[0].trim());
|
||||||
|
/* Unused
|
||||||
|
const cmd = split[1].trim();
|
||||||
|
const argv = split.slice(2).join(';').trim();
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = await readlink(`/proc/${pid}/exe`).catch(() => {}); // Read path from /proc/{pid}/exe symlink
|
||||||
|
|
||||||
|
return [pid, path];
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter(x => x && x[1]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = { getProcesses };
|
25
src/util/arRPC/process/native/win32.js
Normal file
25
src/util/arRPC/process/native/win32.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
|
const getProcesses = () =>
|
||||||
|
new Promise(res =>
|
||||||
|
exec(`wmic process get ProcessID,ExecutablePath /format:csv`, (e, out) => {
|
||||||
|
res(
|
||||||
|
out
|
||||||
|
.toString()
|
||||||
|
.split('\r\n')
|
||||||
|
.slice(2)
|
||||||
|
|
||||||
|
.map(x => {
|
||||||
|
// eslint-disable-next-line newline-per-chained-call
|
||||||
|
const parsed = x.trim().split(',').slice(1).reverse();
|
||||||
|
parsed[0] = parseInt(parsed[0]) || parsed[0]; // Pid to int
|
||||||
|
return parsed;
|
||||||
|
})
|
||||||
|
.filter(x => x[1]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = { getProcesses };
|
281
src/util/arRPC/transports/ipc.js
Normal file
281
src/util/arRPC/transports/ipc.js
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
|
||||||
|
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(254, 231, 92, 'ipc')}]`, ...args);
|
||||||
|
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
const { unlinkSync } = require('fs');
|
||||||
|
const { createServer, createConnection } = require('net');
|
||||||
|
const { setTimeout } = require('node:timers');
|
||||||
|
const { join } = require('path');
|
||||||
|
const { platform, env } = require('process');
|
||||||
|
|
||||||
|
const SOCKET_PATH =
|
||||||
|
platform === 'win32'
|
||||||
|
? '\\\\?\\pipe\\discord-ipc'
|
||||||
|
: join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || '/tmp', 'discord-ipc');
|
||||||
|
|
||||||
|
// Enums for various constants
|
||||||
|
const Types = {
|
||||||
|
// Types of packets
|
||||||
|
HANDSHAKE: 0,
|
||||||
|
FRAME: 1,
|
||||||
|
CLOSE: 2,
|
||||||
|
PING: 3,
|
||||||
|
PONG: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CloseCodes = {
|
||||||
|
// Codes for closures
|
||||||
|
CLOSE_NORMAL: 1000,
|
||||||
|
CLOSE_UNSUPPORTED: 1003,
|
||||||
|
CLOSE_ABNORMAL: 1006,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorCodes = {
|
||||||
|
// Codes for errors
|
||||||
|
INVALID_CLIENTID: 4000,
|
||||||
|
INVALID_ORIGIN: 4001,
|
||||||
|
RATELIMITED: 4002,
|
||||||
|
TOKEN_REVOKED: 4003,
|
||||||
|
INVALID_VERSION: 4004,
|
||||||
|
INVALID_ENCODING: 4005,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uniqueId = 0;
|
||||||
|
|
||||||
|
const encode = (type, data) => {
|
||||||
|
data = JSON.stringify(data);
|
||||||
|
const dataSize = Buffer.byteLength(data);
|
||||||
|
|
||||||
|
const buf = Buffer.alloc(dataSize + 8);
|
||||||
|
buf.writeInt32LE(type, 0); // Type
|
||||||
|
buf.writeInt32LE(dataSize, 4); // Data size
|
||||||
|
buf.write(data, 8, dataSize); // Data
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
|
|
||||||
|
const read = socket => {
|
||||||
|
let resp = socket.read(8);
|
||||||
|
if (!resp) return;
|
||||||
|
|
||||||
|
resp = Buffer.from(resp);
|
||||||
|
const type = resp.readInt32LE(0);
|
||||||
|
const dataSize = resp.readInt32LE(4);
|
||||||
|
|
||||||
|
if (type < 0 || type >= Object.keys(Types).length) throw new Error('invalid type');
|
||||||
|
|
||||||
|
let data = socket.read(dataSize);
|
||||||
|
if (!data) throw new Error('failed reading data');
|
||||||
|
|
||||||
|
data = JSON.parse(Buffer.from(data).toString());
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case Types.PING:
|
||||||
|
socket.emit('ping', data);
|
||||||
|
socket.write(encode(Types.PONG, data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Types.PONG:
|
||||||
|
socket.emit('pong', data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Types.HANDSHAKE:
|
||||||
|
if (socket._handshook) throw new Error('already handshook');
|
||||||
|
|
||||||
|
socket._handshook = true;
|
||||||
|
socket.emit('handshake', data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Types.FRAME:
|
||||||
|
if (!socket._handshook) throw new Error('need to handshake first');
|
||||||
|
|
||||||
|
socket.emit('request', data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Types.CLOSE:
|
||||||
|
socket.end();
|
||||||
|
socket.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
read(socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
const socketIsAvailable = async socket => {
|
||||||
|
socket.pause();
|
||||||
|
socket.on('readable', () => {
|
||||||
|
try {
|
||||||
|
read(socket);
|
||||||
|
} catch (e) {
|
||||||
|
// Debug: log('error whilst reading', e);
|
||||||
|
socket.end(
|
||||||
|
encode(Types.CLOSE, {
|
||||||
|
code: CloseCodes.CLOSE_UNSUPPORTED,
|
||||||
|
message: e.message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
try {
|
||||||
|
socket.end();
|
||||||
|
socket.destroy();
|
||||||
|
} catch {
|
||||||
|
// Debug
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const possibleOutcomes = Promise.race([
|
||||||
|
new Promise(res => socket.on('error', res)), // Errored
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
new Promise((res, rej) => socket.on('pong', () => rej('socket ponged'))), // Ponged
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
new Promise((res, rej) => setTimeout(() => rej('timed out'), 1000)), // Timed out
|
||||||
|
]).then(
|
||||||
|
() => true,
|
||||||
|
e => e,
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.write(encode(Types.PING, ++uniqueId));
|
||||||
|
|
||||||
|
const outcome = await possibleOutcomes;
|
||||||
|
stop();
|
||||||
|
// Debug: log('checked if socket is available:', outcome === true, outcome === true ? '' : `- reason: ${outcome}`);
|
||||||
|
|
||||||
|
return outcome === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableSocket = async (tries = 0) => {
|
||||||
|
if (tries > 9) {
|
||||||
|
throw new Error('ran out of tries to find socket', tries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = `${SOCKET_PATH}-${tries}`;
|
||||||
|
const socket = createConnection(path);
|
||||||
|
|
||||||
|
// Debug: log('checking', path);
|
||||||
|
|
||||||
|
if (await socketIsAvailable(socket)) {
|
||||||
|
if (platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
unlinkSync(path);
|
||||||
|
} catch {
|
||||||
|
// Debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: log(`not available, trying again (attempt ${tries + 1})`);
|
||||||
|
return getAvailableSocket(tries + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = class IPCServer {
|
||||||
|
constructor(handers, debug = false) {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async res => {
|
||||||
|
this.debug = debug;
|
||||||
|
this.handlers = handers;
|
||||||
|
|
||||||
|
this.onConnection = this.onConnection.bind(this);
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
const server = createServer(this.onConnection);
|
||||||
|
server.on('error', e => {
|
||||||
|
if (this.debug) log('server error', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketPath = await getAvailableSocket();
|
||||||
|
server.listen(socketPath, () => {
|
||||||
|
if (this.debug) log('listening at', socketPath);
|
||||||
|
this.server = server;
|
||||||
|
|
||||||
|
res(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnection(socket) {
|
||||||
|
if (this.debug) log('new connection!');
|
||||||
|
|
||||||
|
socket.pause();
|
||||||
|
socket.on('readable', () => {
|
||||||
|
try {
|
||||||
|
read(socket);
|
||||||
|
} catch (e) {
|
||||||
|
if (this.debug) log('error whilst reading', e);
|
||||||
|
|
||||||
|
socket.end(
|
||||||
|
encode(Types.CLOSE, {
|
||||||
|
code: CloseCodes.CLOSE_UNSUPPORTED,
|
||||||
|
message: e.message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once('handshake', params => {
|
||||||
|
if (this.debug) log('handshake:', params);
|
||||||
|
|
||||||
|
const ver = parseInt(params.v ?? 1);
|
||||||
|
const clientId = params.client_id ?? '';
|
||||||
|
// Encoding is always json for ipc
|
||||||
|
|
||||||
|
socket.close = (code = CloseCodes.CLOSE_NORMAL, message = '') => {
|
||||||
|
socket.end(
|
||||||
|
encode(Types.CLOSE, {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ver !== 1) {
|
||||||
|
if (this.debug) log('unsupported version requested', ver);
|
||||||
|
|
||||||
|
socket.close(ErrorCodes.INVALID_VERSION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId === '') {
|
||||||
|
if (this.debug) log('client id required');
|
||||||
|
|
||||||
|
socket.close(ErrorCodes.INVALID_CLIENTID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('error', e => {
|
||||||
|
if (this.debug) log('socket error', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('close', e => {
|
||||||
|
if (this.debug) log('socket closed', e);
|
||||||
|
this.handlers.close(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('request', this.onMessage.bind(this, socket));
|
||||||
|
|
||||||
|
socket._send = socket.send;
|
||||||
|
socket.send = msg => {
|
||||||
|
if (this.debug) log('sending', msg);
|
||||||
|
socket.write(encode(Types.FRAME, msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.clientId = clientId;
|
||||||
|
|
||||||
|
this.handlers.connection(socket);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(socket, msg) {
|
||||||
|
if (this.debug) log('message', msg);
|
||||||
|
this.handlers.message(socket, msg);
|
||||||
|
}
|
||||||
|
};
|
128
src/util/arRPC/transports/websocket.js
Normal file
128
src/util/arRPC/transports/websocket.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
|
||||||
|
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(235, 69, 158, 'websocket')}]`, ...args);
|
||||||
|
|
||||||
|
const { createServer } = require('http');
|
||||||
|
const { parse } = require('querystring');
|
||||||
|
const { WebSocketServer } = require('ws');
|
||||||
|
|
||||||
|
const portRange = [6463, 6472]; // Ports available/possible: 6463-6472
|
||||||
|
|
||||||
|
module.exports = class WSServer {
|
||||||
|
constructor(handlers, debug = false) {
|
||||||
|
return (async () => {
|
||||||
|
this.debug = debug;
|
||||||
|
|
||||||
|
this.handlers = handlers;
|
||||||
|
|
||||||
|
this.onConnection = this.onConnection.bind(this);
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
let port = portRange[0];
|
||||||
|
|
||||||
|
let http, wss;
|
||||||
|
while (port <= portRange[1]) {
|
||||||
|
if (this.debug) log('trying port', port);
|
||||||
|
|
||||||
|
if (
|
||||||
|
await new Promise(res => {
|
||||||
|
http = createServer();
|
||||||
|
http.on('error', e => {
|
||||||
|
// Log('http error', e);
|
||||||
|
|
||||||
|
if (e.code === 'EADDRINUSE') {
|
||||||
|
if (this.debug) log(port, 'in use!');
|
||||||
|
res(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wss = new WebSocketServer({ server: http });
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
wss.on('error', e => {
|
||||||
|
// Debug: Log('wss error', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on('connection', this.onConnection);
|
||||||
|
|
||||||
|
http.listen(port, '127.0.0.1', () => {
|
||||||
|
if (this.debug) log('listening on', port);
|
||||||
|
|
||||||
|
this.http = http;
|
||||||
|
this.wss = wss;
|
||||||
|
|
||||||
|
res(true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
port++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnection(socket, req) {
|
||||||
|
const params = parse(req.url.split('?')[1]);
|
||||||
|
const ver = parseInt(params.v ?? 1);
|
||||||
|
const encoding = params.encoding ?? 'json'; // Json | etf (erlpack)
|
||||||
|
const clientId = params.client_id ?? '';
|
||||||
|
|
||||||
|
const origin = req.headers.origin ?? '';
|
||||||
|
|
||||||
|
if (this.debug) log(`new connection! origin:`, origin, JSON.parse(JSON.stringify(params)));
|
||||||
|
|
||||||
|
if (
|
||||||
|
origin !== '' &&
|
||||||
|
!['https://discord.com', 'https://ptb.discord.com', 'https://canary.discord.com/'].includes(origin)
|
||||||
|
) {
|
||||||
|
if (this.debug) log('disallowed origin', origin);
|
||||||
|
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding !== 'json') {
|
||||||
|
if (this.debug) log('unsupported encoding requested', encoding);
|
||||||
|
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ver !== 1) {
|
||||||
|
if (this.debug) log('unsupported version requested', ver);
|
||||||
|
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.clientId = clientId;
|
||||||
|
socket.encoding = encoding;
|
||||||
|
|
||||||
|
socket.on('error', e => {
|
||||||
|
if (this.debug) log('socket error', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('close', (e, r) => {
|
||||||
|
if (this.debug) log('socket closed', e, r);
|
||||||
|
this.handlers.close(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('message', this.onMessage.bind(this, socket));
|
||||||
|
|
||||||
|
socket._send = socket.send;
|
||||||
|
socket.send = msg => {
|
||||||
|
if (this.debug) log('sending', msg);
|
||||||
|
socket._send(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handlers.connection(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(socket, msg) {
|
||||||
|
if (this.debug) log('message', JSON.parse(msg));
|
||||||
|
this.handlers.message(socket, JSON.parse(msg));
|
||||||
|
}
|
||||||
|
};
|
14
typings/index.d.ts
vendored
14
typings/index.d.ts
vendored
@ -239,6 +239,20 @@ export class DiscordAuthWebsocket extends EventEmitter {
|
|||||||
public on(event: 'closed', listener: (token?: string) => void): this;
|
public on(event: 'closed', listener: (token?: string) => void): this;
|
||||||
public on(event: string, listener: (...args: any[]) => Awaitable<void>): this;
|
public on(event: string, listener: (...args: any[]) => Awaitable<void>): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DiscordRPCServer extends EventEmitter {
|
||||||
|
constructor(client: Client, debug?: boolean);
|
||||||
|
public debug?: boolean;
|
||||||
|
public client: Client;
|
||||||
|
public on(event: 'activity', listener: (data: RPCActivityData) => void): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RPCActivityData {
|
||||||
|
activity?: RichPresence;
|
||||||
|
pid: number;
|
||||||
|
socketId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiscordAuthWebsocketOptions {
|
export interface DiscordAuthWebsocketOptions {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
hiddenLog: boolean;
|
hiddenLog: boolean;
|
||||||
|
Loading…
Reference in New Issue
Block a user