From 471d1d6b28f8a6b62680408756e389e8a0b71be0 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:08:08 +0100 Subject: [PATCH] :sparkles: Base Server --- .vscode/launch.json | 17 ++++++++++++++ client.js | 33 +++++++++++++++++++++++++-- package-lock.json | Bin 105348 -> 109013 bytes src/Server.ts | 16 +++++++++++++ src/events/Close.ts | 6 +++++ src/events/Connection.ts | 46 ++++++++++++++++++++++++++++++++++++++ src/events/Message.ts | 36 ++++++++++++++++++++++++++++++ src/index.ts | 47 +++++---------------------------------- src/util/Constants.ts | 1 + src/util/Send.ts | 21 +++++++++++++++++ src/util/WebSocket.ts | 13 +++++++++++ src/util/setHeartbeat.ts | 10 +++++++++ 12 files changed, 203 insertions(+), 43 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/Server.ts create mode 100644 src/events/Close.ts create mode 100644 src/events/Connection.ts create mode 100644 src/events/Message.ts create mode 100644 src/util/Send.ts create mode 100644 src/util/WebSocket.ts create mode 100644 src/util/setHeartbeat.ts 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 7d75869ac9c42b95f5fa9b44d6efc40448b37b71..ce1ccfa3df3a524dd6e2ba22792e86575804be96 100644 GIT binary patch delta 5500 zcmeHLeQZYzMlQ_YN9Xqz;q!jk-_iR7M_&vY( zY{xDnw2E5NjdpOfJ{7go_=8QWCiUY@v&K4!Zm`lqx3O+o={B0G-K4D=q}qpAr#5@G zL!_mo4Ju*(@ZZzB=bm%!x##@O@7$?e`ETDWf8onAc;y}po|nsE|D}4sPc(bOkr6Uw zr-mY!$2O)Nu%w6Fqk~3|hKjk(gK1L_Ze%=Wo6Tn%9rSX;c49O-s<#>XZMuPkT}%1x zgd>2P&H5hGu$daOv?j*2-Lwu|;bxe4Tn<~`QNzr-YWQmTy{H`?ey0~S>zaMtgN(o5 z=}UB_D9$&|#{#KrD3ciCdPvuJjPD7=lA{iHoahUfc-CyEI^!db{wy2RICM+EFMAz_ex2E5^%f-9kMf*C*kX$6?v$CUA3i$BJ`^2}mtVQl^s}u@_k|9a9 zi)6?+LB@j=$qkS^uIy-jD8Hysa!H1vS)Nk`=@`u}{8uPxhNtK_9+Q6YY?3TUAAq_I z8(^+-6`ZQS7d|<9Ui4J+C6h%vp=6w(;$cbtfr4<+fI`V<7*hJF3{!k08Bi94CBsQJ zuUZ-5d4}u2u;OZ4klu%J3?1iamcv3MOLLsEXu7y>u~G)-*-4U)FQqn}PPgS{B$0yd zWvC2F){`+lf0QMBEh=fF*)Wz5U#%3sc_;I(J_zr&){B>FPsqe%gTHk0{}z{GSSIt= z{Hbu?inuI)vA06Gn|*r`6+xp`9Sj`d}!aQyUEgj+;UTgI0^T z>q))NbmTi)*rU?HuZb38KI^ zs+Y?vi>E1+8N9M-?P_wIkzC~R*}X^KrZMD|!QVRMQ1g`R{7j&AyRVJ7uQ)-R5XtYup_0@d1z1 zN!Zn$=61r6_SuG#!8G4FmU6ZB_N9D;#~NaMZbq%?>9T6|$%qwmX@~tTQ$Lf?VOdGC z-N~3;X*lBD2yL&+;kk)_&9|t}ZiRT7(@RBrt;Jlo0TyaNp$u?zTt$xa%M>4*B-uc| znJrU(9~}?VL_igcQDmG~Nqw8m6x#6(?w34_N}eh^?8`b87snYU^LaMF!ZpE;LwCTr z$*b3eN7y?h7iSK>SqgJcu3IrCQuN|Jxc`t`JUMk10WledZ%^F;zpQV_Qxj=ni+JkM znG%VB9iJ``Ft5p@4|oiu)z)boNLcsj)wC}Xk9aVRC1WAdWNSvlj(8(POc#rCW1Oy& z8OfT`k~u%AoOKD>8V0^{1p{e1?^?;kYqS)A9$*!n7!v}Z0r`;1q)Op2iop58sJN$HTGtk4sslw({p`*i zNQR`DY+n1)MQ&0kit40sXtB+ucS)P_W3Tx0iwDbY(K7;?ZGuyO$Uh_aR{Z%u+%e;k z3B&c$MDzBRN_gz@R;X^dQ~1Dz8em7u9^qmw+6XVVR0zINv{h=X3V817R=}VEKEB!j z55R8WL>;OVpZOEWMcEZgEnwEuP&vB>n&v75$0({0o^qnQVDOf!ke7SS?pbe$hSn^cv`4%)QhbyYr^GD4+q(y;-#kAJDBD7Z66hKYi z6?Y+-Fff8PeEyx)<$mBquOeafYP9}(*OyDU(ETeO&E$MY3>4d{Ws3P}<7gZu*t)h?vZ~2hpR`lWA?{*{gCaB#m z%V+i4oa`X_W;N2o!>ckPar}#zZog#t!}&FFb1 ky23yU+Fh2%JDB@RWA6PYkYd%XKz=k^FID34r_p=pzi0#5MgRZ+ delta 2485 zcmchXYiv{J8OQY`zQnN&1QUl#(p>0hG=q0_CYJ(|v`{1bNjw2RRWW}IoKP21;X`*hl%S)&LWrNY5VdoFfhPr{SSQc)XYiWc<&oxLzM zYmQ}A?x<&`8($mQi2Frijun3_}h`Xx-z`IH* zHmu!-zQu2FJ)0u6`0k}9?guS)0sbVfn90cM=tPp%>yug&o-efEE-j6dgIjQ)MgbIllWpX##a{=G z*NTMIVx(CK49*S8Ih~Hy_yoP`v6Lm_8P?^~Vaoj-n&9Y3=?J&`rTp&dIGZR6|HH+HKZCKKFklsf?z}2xEc5C?!zbEtHtiQRt^uJ2K;5Y347*5_^qefxgX76sR126E>;uo ze%Oqs=GsCpv9P1ycj&xK$z!31cSj6GClw1aG^L_T{nN27Z@EZO z{cOsuE9=q$L&+FUl`hubgbcvq!~T8l`|2 zW}3Lp;~xoe z!wVPJAbfy_Ck4_ZPew zbHZYX+WaMhf6C}_Izmo^WvtZYnF-o#tk>jcN3EKC(q$dap#$IJ#}rO}}fv&Z6h7ko}y706EHykW|nh%kGGY_Yz)$H~}s_m89b zx0;p6rGGak_>ZUxuL`RCcmPncJ1ZDk-rq zz?V_8y@gy?LlZe7gMFy`q?H^@!|SB!IIQ7r{-d(|rNBGn10N`Gx$^5PPfPfB<*-z9 z-*6(&u7gF?-fkzicfxvdX9U)ebKM}QhF)6@^+ROG$x8XTT`T1`;*7R^C4++P^%4oz zvn{m}Ne7;qZ^Ul40o`m1rq~C_q35BA&oscJ^|;hmhr_oO|3eaAk#rXLYt0}4{?is% ztiA6U@GrH)0cquVGrB&IlFkKa0-E$LfD>;HORJMpkh?T=kPXM7f$!V_4+CG)z=O5_ z4QhO=4*o3ue-Mpal|jpAkk%_XRC zt)6%9tq$w@2ypyqw^a7=!0 zy_zcUggUiGO_Qr?XkRVpZLb9Gt&@ { + 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); +}