cloudstorm 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,192 +3,538 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  const events_1 = require("events");
6
- const zlib_sync_1 = __importDefault(require("zlib-sync"));
7
- let Erlpack;
8
- try {
9
- Erlpack = require("erlpack");
10
- }
11
- catch (e) {
12
- Erlpack = null;
13
- }
6
+ const crypto_1 = require("crypto");
7
+ const zlib_1 = require("zlib");
8
+ const https_1 = require("https");
9
+ const util_1 = __importDefault(require("util"));
10
+ const Z_SYNC_FLUSH = zlib_1.constants.Z_SYNC_FLUSH;
14
11
  const Constants_1 = require("../Constants");
15
- const ws_1 = __importDefault(require("ws"));
16
12
  const RatelimitBucket_1 = __importDefault(require("./RatelimitBucket"));
17
13
  /**
18
14
  * Helper Class for simplifying the websocket connection to Discord.
19
15
  */
20
16
  class BetterWs extends events_1.EventEmitter {
21
- /**
22
- * Create a new BetterWs instance.
23
- */
24
- constructor(address, options = {}) {
17
+ constructor(address, options) {
25
18
  super();
26
- this.zlibInflate = null;
27
- this.ws = new ws_1.default(address, options.socket);
28
- this.bindWs(this.ws);
29
19
  this.wsBucket = new RatelimitBucket_1.default(120, 60000);
30
- this.presenceBucket = new RatelimitBucket_1.default(5, 20000);
31
- if (options.compress) {
32
- this.zlibInflate = new zlib_sync_1.default.Inflate({ chunkSize: 65535 });
33
- this.compress = true;
34
- }
35
- else
36
- this.compress = false;
20
+ this.presenceBucket = new RatelimitBucket_1.default(5, 60000);
21
+ this._connecting = false;
22
+ this.encoding = options.encoding === "etf" ? "etf" : "json";
23
+ this.compress = options.compress || false;
24
+ this.address = address;
37
25
  this.options = options;
26
+ this._socket = null;
27
+ this._internal = {
28
+ closePromise: null,
29
+ zlib: null,
30
+ };
31
+ }
32
+ get status() {
33
+ const internal = this._internal;
34
+ if (this._connecting)
35
+ return 2;
36
+ if (internal.closePromise)
37
+ return 3; // closing
38
+ if (!this._socket)
39
+ return 4; // closed
40
+ return 1; // connected
38
41
  }
39
- /**
40
- * Get the raw websocket connection currently used.
41
- */
42
- get rawWs() {
43
- return this.ws;
42
+ connect() {
43
+ if (this._socket)
44
+ return Promise.resolve(void 0);
45
+ const key = (0, crypto_1.randomBytes)(16).toString("base64");
46
+ this.emit("debug", "Creating connection");
47
+ const url = new URL(this.address);
48
+ const req = (0, https_1.request)({
49
+ hostname: url.hostname,
50
+ path: `${url.pathname}${url.search}`,
51
+ headers: {
52
+ "Connection": "Upgrade",
53
+ "Upgrade": "websocket",
54
+ "Sec-WebSocket-Key": key,
55
+ "Sec-WebSocket-Version": "13",
56
+ }
57
+ });
58
+ this._connecting = true;
59
+ return new Promise((resolve, reject) => {
60
+ req.on("upgrade", (res, socket) => {
61
+ const hash = (0, crypto_1.createHash)("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
62
+ const accept = res.headers["sec-websocket-accept"];
63
+ if (hash !== accept) {
64
+ socket.end(() => {
65
+ this.emit("debug", "Failed websocket-key validation");
66
+ this._connecting = false;
67
+ reject(new Error(`Invalid Sec-Websocket-Accept | expected: ${hash} | received: ${accept}`));
68
+ });
69
+ return;
70
+ }
71
+ socket.on("error", this._onError.bind(this));
72
+ socket.on("close", this._onClose.bind(this));
73
+ socket.on("readable", this._onReadable.bind(this));
74
+ this._socket = socket;
75
+ this._connecting = false;
76
+ if (this.compress) {
77
+ const z = (0, zlib_1.createInflate)();
78
+ // @ts-ignore
79
+ z._c = z.close;
80
+ // @ts-ignore
81
+ z._h = z._handle;
82
+ // @ts-ignore
83
+ z._hc = z._handle.close;
84
+ // @ts-ignore
85
+ z._v = () => void 0;
86
+ this._internal.zlib = z;
87
+ }
88
+ this.emit("ws_open");
89
+ resolve(void 0);
90
+ });
91
+ req.on("error", e => {
92
+ this._connecting = false;
93
+ reject(e);
94
+ });
95
+ req.end();
96
+ });
44
97
  }
45
- /**
46
- * Add eventlisteners to a passed websocket connection.
47
- * @param ws Websocket.
48
- */
49
- bindWs(ws) {
50
- ws.on("message", (msg) => {
51
- this.onMessage(msg);
98
+ async close() {
99
+ const internal = this._internal;
100
+ if (internal.closePromise)
101
+ return internal.closePromise;
102
+ if (!this._socket)
103
+ return Promise.resolve(void 0);
104
+ let resolver;
105
+ const promise = new Promise(resolve => {
106
+ resolver = resolve;
107
+ this._write(Buffer.allocUnsafe(0), 8);
108
+ }).then(() => {
109
+ internal.closePromise = null;
52
110
  });
53
- ws.on("close", (code, reason) => this.onClose(code, reason.toString()));
54
- ws.on("error", (err) => {
55
- this.emit("error", err);
111
+ // @ts-ignore
112
+ promise.resolve = resolver;
113
+ internal.closePromise = promise;
114
+ }
115
+ sendMessage(data) {
116
+ if (!isValidRequest(data))
117
+ return Promise.reject(new Error("Invalid request"));
118
+ return new Promise(res => {
119
+ const presence = data.op === Constants_1.GATEWAY_OP_CODES.PRESENCE_UPDATE;
120
+ const sendMsg = () => {
121
+ this.wsBucket.queue(() => {
122
+ this.emit("debug_send", data);
123
+ if (this.encoding === "json")
124
+ this._write(Buffer.from(JSON.stringify(data)), 1);
125
+ else {
126
+ const etf = writeETF(data);
127
+ this._write(etf, 2);
128
+ }
129
+ res(void 0);
130
+ });
131
+ };
132
+ if (presence)
133
+ this.presenceBucket.queue(sendMsg);
134
+ else
135
+ sendMsg();
56
136
  });
57
- ws.on("open", () => this.onOpen());
58
137
  }
59
- /**
60
- * Create a new websocket connection if the old one was closed/destroyed.
61
- * @param address Address to connect to.
62
- * @param options Options used by the websocket connection.
63
- */
64
- recreateWs(address, options = {}) {
65
- this.ws.removeAllListeners();
66
- if (options.compress) {
67
- this.zlibInflate = new zlib_sync_1.default.Inflate({ chunkSize: 65535 });
68
- this.compress = true;
138
+ _write(packet, opcode) {
139
+ const socket = this._socket;
140
+ if (!socket || !socket.writable)
141
+ return;
142
+ const length = packet.length;
143
+ let frame;
144
+ if (length < 126) {
145
+ frame = Buffer.allocUnsafe(6 + length);
146
+ frame[1] = 128 + length;
147
+ }
148
+ else if (length < (1 << 16)) {
149
+ frame = Buffer.allocUnsafe(8 + length);
150
+ frame[1] = 254;
151
+ frame[2] = length >> 8;
152
+ frame[3] = length & 255;
69
153
  }
70
154
  else {
71
- this.zlibInflate = null;
72
- this.compress = false;
155
+ frame = Buffer.allocUnsafe(14 + length);
156
+ frame[1] = 255;
157
+ frame.writeBigUInt64BE(BigInt(length), 2);
73
158
  }
74
- this.ws = new ws_1.default(address, options.socket);
75
- this.options = options;
159
+ frame[0] = 128 + opcode;
160
+ frame.writeUInt32BE(0, frame.length - length - 4);
161
+ frame.set(packet, frame.length - length);
162
+ socket.write(frame);
163
+ }
164
+ _onError(error) {
165
+ if (!this._socket)
166
+ return;
167
+ this.emit("debug", util_1.default.inspect(error, true, 1, false));
168
+ this._write(Buffer.allocUnsafe(0), 8);
169
+ }
170
+ _onClose() {
171
+ const socket = this._socket;
172
+ const internal = this._internal;
173
+ if (!socket)
174
+ return;
175
+ this.emit("debug", "Connection closed");
176
+ socket.removeListener("data", this._onReadable);
177
+ socket.removeListener("error", this._onError);
178
+ socket.removeListener("close", this._onClose);
76
179
  this.wsBucket.dropQueue();
77
180
  this.presenceBucket.dropQueue();
78
- this.wsBucket = new RatelimitBucket_1.default(120, 60000);
79
- this.presenceBucket = new RatelimitBucket_1.default(5, 60000);
80
- this.bindWs(this.ws);
81
- }
82
- /**
83
- * Called upon opening of the websocket connection.
84
- */
85
- onOpen() {
86
- this.emit("ws_open");
181
+ this._socket = null;
182
+ if (internal.zlib) {
183
+ internal.zlib.close();
184
+ internal.zlib = null;
185
+ }
186
+ if (internal.closePromise) {
187
+ // @ts-ignore
188
+ internal.closePromise.resolve(void 0);
189
+ }
87
190
  }
88
- /**
89
- * Called once a websocket message is received,
90
- * uncompresses the message using zlib and parses it via Erlpack or JSON.parse.
91
- * @param message Message received by websocket.
92
- */
93
- onMessage(message) {
94
- let parsed;
95
- try {
96
- let msg;
97
- if (this.compress && this.zlibInflate) {
98
- const length = message.length;
99
- const flush = length >= 4 &&
100
- message[length - 4] === 0x00 &&
101
- message[length - 3] === 0x00 &&
102
- message[length - 2] === 0xFF &&
103
- message[length - 1] === 0xFF;
104
- this.zlibInflate.push(message, flush ? zlib_sync_1.default.Z_SYNC_FLUSH : false);
105
- if (!flush)
191
+ _onReadable() {
192
+ const socket = this._socket;
193
+ while (socket.readableLength > 1) {
194
+ let length = readRange(socket, 1, 1) & 127;
195
+ let bytes = 0;
196
+ if (length > 125) {
197
+ bytes = length === 126 ? 2 : 8;
198
+ if (socket.readableLength < 2 + bytes)
106
199
  return;
107
- msg = this.zlibInflate.result;
200
+ length = readRange(socket, 2, bytes);
108
201
  }
109
- else
110
- msg = message;
111
- if (Erlpack) {
112
- parsed = Erlpack.unpack(msg);
202
+ const frame = socket.read(2 + bytes + length);
203
+ if (!frame)
204
+ return;
205
+ const fin = frame[0] >> 7;
206
+ const opcode = frame[0] & 15;
207
+ if (fin !== 1 || opcode === 0)
208
+ this.emit("debug", "discord actually does send messages with fin=0. if you see this error let me know");
209
+ const payload = frame.slice(2 + bytes);
210
+ this._processFrame(opcode, payload);
211
+ }
212
+ }
213
+ _processFrame(opcode, message) {
214
+ const internal = this._internal;
215
+ switch (opcode) {
216
+ case 1: {
217
+ const packet = JSON.parse(message.toString());
218
+ this.emit("ws_message", packet);
219
+ break;
113
220
  }
114
- else {
115
- parsed = JSON.parse(String(msg));
221
+ case 2: {
222
+ let packet;
223
+ if (this.compress) {
224
+ const z = internal.zlib;
225
+ let error = null;
226
+ let data = null;
227
+ // @ts-ignore
228
+ z.close = z._handle.close = z._v;
229
+ try {
230
+ // @ts-ignore
231
+ data = z._processChunk(message, Z_SYNC_FLUSH);
232
+ }
233
+ catch (e) {
234
+ error = e;
235
+ }
236
+ const l = message.length;
237
+ if (message[l - 4] !== 0 || message[l - 3] !== 0 || message[l - 2] !== 255 || message[l - 1] !== 255)
238
+ this.emit("debug", "discord actually does send fragmented zlib messages. if you see this error let me know");
239
+ // @ts-ignore
240
+ z.close = z._c;
241
+ // @ts-ignore
242
+ z._handle = z._h;
243
+ // @ts-ignore
244
+ z._handle.close = z._hc;
245
+ // @ts-ignore
246
+ z._events.error = void 0;
247
+ // @ts-ignore
248
+ z._eventCount--;
249
+ z.removeAllListeners("error");
250
+ if (error) {
251
+ this.emit("debug", "Zlib error");
252
+ this._write(Buffer.allocUnsafe(0), 8);
253
+ return;
254
+ }
255
+ if (!data)
256
+ return; // This should never run, but TS is lame
257
+ packet = this.encoding === "json" ? JSON.parse(String(data)) : readETF(data, 1);
258
+ }
259
+ else if (this.encoding === "json") {
260
+ const data = (0, zlib_1.inflateSync)(message);
261
+ packet = JSON.parse(data.toString());
262
+ }
263
+ else
264
+ packet = readETF(message, 1);
265
+ this.emit("ws_message", packet);
266
+ break;
267
+ }
268
+ case 8: {
269
+ const code = message.length > 1 ? (message[0] << 8) + message[1] : 0;
270
+ const reason = message.length > 2 ? message.slice(2).toString() : "";
271
+ this.emit("ws_close", code, reason);
272
+ this._write(Buffer.from([code >> 8, code & 255]), 8);
273
+ break;
274
+ }
275
+ case 9: {
276
+ this._write(message, 10);
277
+ break;
116
278
  }
117
279
  }
118
- catch (e) {
119
- this.emit("error", `Message: ${message} was not parseable`);
120
- return;
121
- }
122
- this.emit("ws_message", parsed);
123
280
  }
124
- /**
125
- * Called when the websocket connection closes for some reason.
126
- * @param code Websocket close code.
127
- * @param reason Reason of the close if any.
128
- */
129
- onClose(code, reason) {
130
- this.emit("ws_close", code, reason);
131
- }
132
- /**
133
- * Send a message to the Discord gateway.
134
- * @param data Data to send.
135
- */
136
- sendMessage(data) {
137
- if (this.ws.readyState !== ws_1.default.OPEN)
138
- return Promise.reject(new Error("WS is not open"));
139
- this.emit("debug_send", data);
140
- return new Promise((res, rej) => {
141
- const presence = data.op === Constants_1.GATEWAY_OP_CODES.PRESENCE_UPDATE;
142
- try {
143
- if (Erlpack) {
144
- data = Erlpack.pack(data);
281
+ }
282
+ function isValidRequest(value) {
283
+ return value && typeof value === "object" && Number.isInteger(value.op) && typeof value.d !== "undefined";
284
+ }
285
+ function readRange(socket, index, bytes) {
286
+ // @ts-ignore
287
+ let head = socket._readableState.buffer.head;
288
+ let cursor = 0;
289
+ let read = 0;
290
+ let num = 0;
291
+ do {
292
+ for (let i = 0; i < head.data.length; i++) {
293
+ if (++cursor > index) {
294
+ num *= 256;
295
+ num += head.data[i];
296
+ if (++read === bytes)
297
+ return num;
298
+ }
299
+ }
300
+ } while ((head = head.next));
301
+ throw new Error("readRange failed?");
302
+ }
303
+ function readETF(data, start) {
304
+ let view;
305
+ let x = start;
306
+ const loop = () => {
307
+ const type = data[x++];
308
+ switch (type) {
309
+ case 97: {
310
+ return data[x++];
311
+ }
312
+ case 98: {
313
+ const int = data.readInt32BE(x);
314
+ x += 4;
315
+ return int;
316
+ }
317
+ case 100: {
318
+ const length = data.readUInt16BE(x);
319
+ let atom = "";
320
+ if (length > 30) {
321
+ // @ts-ignore
322
+ atom = data.latin1Slice(x += 2, x + length);
145
323
  }
146
324
  else {
147
- data = JSON.stringify(data);
325
+ for (let i = x += 2; i < x + length; i++) {
326
+ atom += String.fromCharCode(data[i]);
327
+ }
148
328
  }
329
+ x += length;
330
+ if (!atom)
331
+ return undefined;
332
+ if (atom === "nil" || atom === "null")
333
+ return null;
334
+ if (atom === "true")
335
+ return true;
336
+ if (atom === "false")
337
+ return false;
338
+ return atom;
149
339
  }
150
- catch (e) {
151
- return rej(e);
340
+ case 108:
341
+ case 106: {
342
+ const array = [];
343
+ if (type === 108) {
344
+ const length = data.readUInt32BE(x);
345
+ x += 4;
346
+ for (let i = 0; i < length; i++) {
347
+ array.push(loop());
348
+ }
349
+ x++;
350
+ }
351
+ return array;
152
352
  }
153
- const sendMsg = () => {
154
- // The promise from wsBucket is ignored, since the method passed to it does not return a promise
155
- this.wsBucket.queue(() => {
156
- this.ws.send(data, {}, (e) => {
157
- if (e) {
158
- return rej(e);
159
- }
160
- res();
161
- });
162
- });
163
- };
164
- if (presence) {
165
- // same here
166
- this.presenceBucket.queue(sendMsg);
353
+ case 107: {
354
+ const array = [];
355
+ const length = data.readUInt16BE(x);
356
+ x += 2;
357
+ for (let i = 0; i < length; i++) {
358
+ array.push(data[x++]);
359
+ }
360
+ return array;
167
361
  }
168
- else {
169
- sendMsg();
362
+ case 109: {
363
+ const length = data.readUInt32BE(x);
364
+ let str = "";
365
+ if (length > 30) {
366
+ // @ts-ignore
367
+ str = data.utf8Slice(x += 4, x + length);
368
+ }
369
+ else {
370
+ let i = x += 4;
371
+ const l = x + length;
372
+ while (i < l) {
373
+ const byte = data[i++];
374
+ if (byte < 128)
375
+ str += String.fromCharCode(byte);
376
+ else if (byte < 224)
377
+ str += String.fromCharCode(((byte & 31) << 6) + (data[i++] & 63));
378
+ else if (byte < 240)
379
+ str += String.fromCharCode(((byte & 15) << 12) + ((data[i++] & 63) << 6) + (data[i++] & 63));
380
+ else
381
+ str += String.fromCodePoint(((byte & 7) << 18) + ((data[i++] & 63) << 12) + ((data[i++] & 63) << 6) + (data[i++] & 63));
382
+ }
383
+ }
384
+ x += length;
385
+ return str;
170
386
  }
171
- });
172
- }
173
- /**
174
- * Close the current websocket connection.
175
- * @param code Websocket close code to use.
176
- * @param reason Reason of the disconnect.
177
- */
178
- close(code = 1000, reason = "Unknown") {
179
- if (this.ws.readyState === ws_1.default.CLOSING || this.ws.readyState === ws_1.default.CLOSED)
180
- return Promise.reject(new Error("WS is already closing or is closed"));
181
- return new Promise((res, rej) => {
182
- const timeout = setTimeout(() => {
183
- return rej("Websocket not closed within 5 seconds");
184
- }, 5 * 1000);
185
- this.ws.once("close", () => {
186
- clearTimeout(timeout);
187
- return res();
188
- });
189
- this.ws.close(code, reason);
190
- });
191
- }
387
+ case 110: {
388
+ // @ts-ignore
389
+ if (!view)
390
+ view = new DataView(data.buffer, data.offset, data.byteLength);
391
+ const length = data[x++];
392
+ const sign = data[x++];
393
+ let left = length;
394
+ let num = BigInt(0);
395
+ while (left > 0) {
396
+ if (left >= 8) {
397
+ num <<= BigInt(64);
398
+ num += view.getBigUint64(x + (left -= 8), true);
399
+ }
400
+ else if (left >= 4) {
401
+ num <<= BigInt(32);
402
+ // @ts-ignore
403
+ num += BigInt(view.getUint32(x + (left -= 4)), true);
404
+ }
405
+ else if (left >= 2) {
406
+ num <<= BigInt(16);
407
+ // @ts-ignore
408
+ num += BigInt(view.getUint16(x + (left -= 2)), true);
409
+ }
410
+ else {
411
+ num <<= BigInt(8);
412
+ num += BigInt(data[x]);
413
+ left--;
414
+ }
415
+ }
416
+ x += length;
417
+ return (sign ? -num : num).toString();
418
+ }
419
+ case 116: {
420
+ const obj = {};
421
+ const length = data.readUInt32BE(x);
422
+ x += 4;
423
+ for (let i = 0; i < length; i++) {
424
+ const key = loop();
425
+ // @ts-ignore
426
+ obj[key] = loop();
427
+ }
428
+ return obj;
429
+ }
430
+ }
431
+ throw new Error(`Missing etf type: ${type}`);
432
+ };
433
+ return loop();
434
+ }
435
+ function writeETF(data) {
436
+ const b = Buffer.allocUnsafe(1 << 12);
437
+ b[0] = 131;
438
+ let i = 1;
439
+ const loop = (obj) => {
440
+ const type = typeof obj;
441
+ switch (type) {
442
+ case "boolean": {
443
+ b[i++] = 100;
444
+ if (obj) {
445
+ b.writeUInt16BE(4, i);
446
+ // @ts-ignore
447
+ b.latin1Write("true", i += 2);
448
+ i += 4;
449
+ }
450
+ else {
451
+ b.writeUInt16BE(5, i);
452
+ // @ts-ignore
453
+ b.latin1Write("false", i += 2);
454
+ i += 5;
455
+ }
456
+ break;
457
+ }
458
+ case "string": {
459
+ const length = Buffer.byteLength(obj);
460
+ b[i++] = 109;
461
+ b.writeUInt32BE(length, i);
462
+ // @ts-ignore
463
+ b.utf8Write(obj, i += 4);
464
+ i += length;
465
+ break;
466
+ }
467
+ case "number": {
468
+ if (Number.isInteger(obj)) {
469
+ const abs = Math.abs(obj);
470
+ if (abs < 2147483648) {
471
+ b[i++] = 98;
472
+ b.writeInt32BE(obj, i);
473
+ i += 4;
474
+ }
475
+ else if (abs < Number.MAX_SAFE_INTEGER) {
476
+ b[i++] = 110;
477
+ b[i++] = 8;
478
+ b[i++] = Number(obj < 0);
479
+ b.writeBigUInt64LE(BigInt(abs), i);
480
+ i += 8;
481
+ break;
482
+ }
483
+ else {
484
+ b[i++] = 70;
485
+ b.writeDoubleBE(obj, i);
486
+ i += 8;
487
+ }
488
+ }
489
+ else {
490
+ b[i++] = 70;
491
+ b.writeDoubleBE(obj, i);
492
+ i += 8;
493
+ }
494
+ break;
495
+ }
496
+ case "bigint": {
497
+ b[i++] = 110;
498
+ b[i++] = 8;
499
+ b[i++] = Number(obj < 0);
500
+ b.writeBigUInt64LE(obj, i);
501
+ i += 8;
502
+ break;
503
+ }
504
+ case "object": {
505
+ if (obj === null) {
506
+ b[i++] = 100;
507
+ b.writeUInt16BE(3, i);
508
+ // @ts-ignore
509
+ b.latin1Write("nil", i += 2);
510
+ i += 3;
511
+ }
512
+ else if (Array.isArray(obj)) {
513
+ if (obj.length) {
514
+ b[i++] = 108;
515
+ b.writeUInt32BE(obj.length, i);
516
+ i += 4;
517
+ for (const item of obj) {
518
+ loop(item);
519
+ }
520
+ }
521
+ b[i++] = 106;
522
+ }
523
+ else {
524
+ const entries = Object.entries(obj).filter(x => typeof x[1] !== "undefined");
525
+ b[i++] = 116;
526
+ b.writeUInt32BE(entries.length, i);
527
+ i += 4;
528
+ for (const [key, value] of entries) {
529
+ loop(key);
530
+ loop(value);
531
+ }
532
+ }
533
+ break;
534
+ }
535
+ }
536
+ };
537
+ loop(data);
538
+ return Buffer.from(b.slice(0, i));
192
539
  }
193
- BetterWs.default = BetterWs;
194
540
  module.exports = BetterWs;