diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..07fd32ac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "sourceMaps": true, + "type": "node", + "request": "launch", + "name": "Launch Server", + "program": "${workspaceFolder}/dist/index.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + } + ] +} diff --git a/client.js b/client.js index 7e8c6d5c..ceb97bdd 100644 --- a/client.js +++ b/client.js @@ -1,9 +1,11 @@ +require("missing-native-js-functions"); const WebSocket = require("ws"); +const Constants = require("./dist/util/Constants"); const ws = new WebSocket("ws://127.0.0.1:8080"); ws.on("open", () => { - ws.send(JSON.stringify({ req_type: "new_auth" })); + // ws.send(JSON.stringify({ req_type: "new_auth" })); // ws.send(JSON.stringify({ req_type: "check_auth", token: "" })); // op: 0, // d: {}, @@ -11,6 +13,33 @@ ws.on("open", () => { // t: "GATEWAY_EVENT_NAME", }); -ws.on("message", (data) => { +function send(data) { + ws.send(JSON.stringify(data)); +} + +ws.on("message", (buffer) => { + let data = JSON.parse(buffer.toString()); console.log(data); + + switch (data.op) { + case 10: + setIntervalNow(() => { + send({ op: 1 }); + }, data.d.heartbeat_interval); + + send({ + op: 2, + d: { + token: "", + intents: 0n, + properties: {}, + }, + }); + + break; + } +}); + +ws.on("close", (code, reason) => { + console.log(code, reason, Constants.CLOSECODES[code]); }); diff --git a/package-lock.json b/package-lock.json index 7d75869a..ce1ccfa3 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/src/Server.ts b/src/Server.ts new file mode 100644 index 00000000..3598c8e1 --- /dev/null +++ b/src/Server.ts @@ -0,0 +1,16 @@ +import { db } from "discord-server-util"; +import { Server as WebSocketServer } from "ws"; +import { Connection } from "./events/Connection"; + +export class Server { + public ws: WebSocketServer; + constructor() { + this.ws = new WebSocketServer({ port: 8080, maxPayload: 4096 }); + this.ws.on("connection", Connection); + } + + async listen(): Promise { + await db.init(); + console.log("listening"); + } +} diff --git a/src/events/Close.ts b/src/events/Close.ts new file mode 100644 index 00000000..f819b064 --- /dev/null +++ b/src/events/Close.ts @@ -0,0 +1,6 @@ +import WebSocket from "ws"; +import { Message } from "./Message"; + +export function Close(this: WebSocket, code: number, reason: string) { + this.off("message", Message); +} diff --git a/src/events/Connection.ts b/src/events/Connection.ts new file mode 100644 index 00000000..815d84cf --- /dev/null +++ b/src/events/Connection.ts @@ -0,0 +1,46 @@ +import WebSocket, { Server } from "../util/WebSocket"; +import { IncomingMessage } from "http"; +import { Close } from "./Close"; +import { Message } from "./Message"; +import { setHeartbeat } from "../util/setHeartbeat"; +import { Send } from "../util/Send"; +import { CLOSECODES, OPCODES } from "../util/Constants"; + +// TODO: check rate limit +// TODO: specify rate limit in config + +export function Connection(this: Server, socket: WebSocket, request: IncomingMessage) { + try { + socket.on("close", Close); + socket.on("message", Message); + + const { searchParams } = new URL(`http://localhost${request.url}`); + // @ts-ignore + socket.encoding = searchParams.get("encoding") || "json"; + if (!["json", "etf"].includes(socket.encoding)) return socket.close(CLOSECODES.Decode_error); + + // @ts-ignore + socket.version = Number(searchParams.get("version")) || 8; + if (socket.version != 8) return socket.close(CLOSECODES.Invalid_API_version); + + // @ts-ignore + socket.compression = searchParams.get("compress") || ""; + // TODO: compression + + setHeartbeat(socket); + + Send(socket, { + op: OPCODES.Hello, + d: { + heartbeat_interval: 1000 * 30, + }, + }); + + socket.readyTimeout = setTimeout(() => { + return socket.close(CLOSECODES.Session_timed_out); + }, 1000 * 30); + } catch (error) { + console.error(error); + return socket.close(CLOSECODES.Unknown_error); + } +} diff --git a/src/events/Message.ts b/src/events/Message.ts new file mode 100644 index 00000000..bc497e94 --- /dev/null +++ b/src/events/Message.ts @@ -0,0 +1,36 @@ +import WebSocket, { Data } from "../util/WebSocket"; +import erlpack from "erlpack"; +import OPCodeHandlers from "../opcodes"; +import { Payload, CLOSECODES } from "../util/Constants"; +import { instanceOf } from "lambert-server"; + +const PayloadSchema = { + op: Number, + $d: Object, + $s: Number, + $t: String, +}; + +export async function Message(this: WebSocket, buffer: Data) { + // TODO: compression + var data: Payload; + + try { + if (this.encoding === "etf" && buffer instanceof Buffer) data = erlpack.unpack(buffer); + else if (this.encoding === "json" && typeof buffer === "string") data = JSON.parse(buffer); + if (!instanceOf(PayloadSchema, data)) throw "invalid data"; + } catch (error) { + return this.close(CLOSECODES.Decode_error); + } + + // @ts-ignore + const OPCodeHandler = OPCodeHandlers[data.op]; + if (!OPCodeHandler) return this.close(CLOSECODES.Unknown_opcode); + + try { + return await OPCodeHandler.call(this, data); + } catch (error) { + console.error(error); + return this.close(CLOSECODES.Unknown_error); + } +} diff --git a/src/index.ts b/src/index.ts index 45374721..b267bbfb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,8 @@ -import WebSocket from "ws"; -import DB from "./extras/Database"; -import { message_dev } from "./assets/datastru"; -import { v4 } from "uuid"; +process.on("uncaughtException", console.error); +process.on("unhandledRejection", console.error); +setTimeout(() => {}, 100000000); -class Server { - db: any; - constructor() { - this.db = DB; - } +import { Server } from "./Server"; - async listen(): Promise { - await this.db.init(); - const wss = new WebSocket.Server({ port: 8080 }); - - wss.on("connection", (ws) => { - ws.on("message", async (msg: any) => { - const message: message_dev = msg; - - if (message.req_type) { - switch (message.req_type) { - case "new_auth": - const token = v4(); - await this.db.data.auth.push({ token }); - return ws.send({ new_token: token }); - case "check_auth": - if (!message.token) { - return ws.send({ error: "token not providen" }); - } - return this.db.data.auth({ token: message.token }).get(); - } - } else { - ws.send({ error: "req_type not providen" }); - } - }); - - ws.send("connected"); - }); - } -} - -const s = new Server(); -s.listen(); +const server = new Server(); +server.listen(); diff --git a/src/util/Constants.ts b/src/util/Constants.ts index cdc0d07d..e8e91d69 100644 --- a/src/util/Constants.ts +++ b/src/util/Constants.ts @@ -4,6 +4,7 @@ export enum OPCODES { Identify, Presence_Update, Voice_State_Update, + Dummy_Value, // ? What is opcode 5? Resume, Reconnect, Request_Guild_Members, diff --git a/src/util/Send.ts b/src/util/Send.ts new file mode 100644 index 00000000..d38865b3 --- /dev/null +++ b/src/util/Send.ts @@ -0,0 +1,21 @@ +import erlpack from "erlpack"; +import { promisify } from "util"; +import { Payload } from "../util/Constants"; + +import WebSocket from "./WebSocket"; + +export async function Send(socket: WebSocket, data: Payload) { + let buffer: Buffer | string; + if (socket.encoding === "etf") buffer = erlpack.pack(data); + // TODO: encode circular object + else if (socket.encoding === "json") buffer = JSON.stringify(data); + + // TODO: compression + + return new Promise((res, rej) => { + socket.send(buffer, (err) => { + if (err) return rej(err); + return res(null); + }); + }); +} diff --git a/src/util/WebSocket.ts b/src/util/WebSocket.ts new file mode 100644 index 00000000..41ce8851 --- /dev/null +++ b/src/util/WebSocket.ts @@ -0,0 +1,13 @@ +import WS, { Server, Data } from "ws"; + +interface WebSocket extends WS { + version: number; + userid: bigint; + encoding: "etf" | "json"; + compress?: "zlib-stream"; + heartbeatTimeout: NodeJS.Timeout; + readyTimeout: NodeJS.Timeout; +} + +export default WebSocket; +export { Server, Data }; diff --git a/src/util/setHeartbeat.ts b/src/util/setHeartbeat.ts new file mode 100644 index 00000000..1fe657a8 --- /dev/null +++ b/src/util/setHeartbeat.ts @@ -0,0 +1,10 @@ +import WebSocket from "./WebSocket"; + +// TODO: make heartbeat timeout configurable +export function setHeartbeat(socket: WebSocket) { + if (socket.heartbeatTimeout) clearTimeout(socket.heartbeatTimeout); + + socket.heartbeatTimeout = setTimeout(() => { + return socket.close(4009); + }, 1000 * 30); +}