wassocket 0.1.2
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.
- package/README.md +54 -0
- package/dist/adapter/RedisAdapter.d.ts +11 -0
- package/dist/binary.d.ts +41 -0
- package/dist/client/WASSocketClient.d.ts +36 -0
- package/dist/client.d.ts +1 -0
- package/dist/cluster/cluster.d.ts +5 -0
- package/dist/dashboard/dashboardPlugin.d.ts +9 -0
- package/dist/index.cjs +542 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/middleware.d.ts +15 -0
- package/dist/monitoring/metrics.d.ts +13 -0
- package/dist/monitoring/metricsPlugin.d.ts +12 -0
- package/dist/monitoring/packetInspector.d.ts +8 -0
- package/dist/namespace.d.ts +23 -0
- package/dist/plugin.d.ts +15 -0
- package/dist/protocol.d.ts +16 -0
- package/dist/security/Auth.d.ts +17 -0
- package/dist/security/authPlugin.d.ts +2 -0
- package/dist/security/autoTLS.d.ts +13 -0
- package/dist/security/rateLimit.d.ts +11 -0
- package/dist/server/RoomManager.d.ts +7 -0
- package/dist/server/WASSocketServer.d.ts +56 -0
- package/dist/transport/binary.d.ts +8 -0
- package/dist/transport/crypto.d.ts +13 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# WASSocket (CommonJS)
|
|
2
|
+
|
|
3
|
+
High-performance WebSocket framework with JWT authentication, auto TLS,
|
|
4
|
+
middleware, plugins, rooms, namespaces and Redis scaling.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
```bash
|
|
8
|
+
npm install wassocket
|
|
9
|
+
```
|
|
10
|
+
## Server Example
|
|
11
|
+
```js
|
|
12
|
+
const { WASSocketServer } = require("wassocket"); const server = new
|
|
13
|
+
WASSocketServer({ port: 3000, jwtSecret: "secret" }); server.start();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Client Example
|
|
17
|
+
```js
|
|
18
|
+
const { WASSocketClient } = require("wassocket"); const client = new
|
|
19
|
+
WASSocketClient("ws://localhost:3000", { token: "JWT" });
|
|
20
|
+
client.connect();
|
|
21
|
+
```
|
|
22
|
+
## Advanced API
|
|
23
|
+
|
|
24
|
+
### Namespace
|
|
25
|
+
```js
|
|
26
|
+
const chat = server.namespace("/chat"); chat.on("connection", client =\>
|
|
27
|
+
{});
|
|
28
|
+
```
|
|
29
|
+
### Rooms
|
|
30
|
+
```js
|
|
31
|
+
client.join("room1"); server.to("room1").emit("msg", "hello");
|
|
32
|
+
```
|
|
33
|
+
### Middleware
|
|
34
|
+
```js
|
|
35
|
+
server.middleware.usePre(async (ctx, next) =\> { await next(); });
|
|
36
|
+
```
|
|
37
|
+
### Plugin
|
|
38
|
+
```js
|
|
39
|
+
function LoggerPlugin(server) { server.on("connection", c =\>
|
|
40
|
+
console.log(c.id)); } server.use(LoggerPlugin);
|
|
41
|
+
```
|
|
42
|
+
## Cluster & Redis Scaling
|
|
43
|
+
|
|
44
|
+
Use RedisAdapter to synchronize packets across multiple instances.
|
|
45
|
+
```js
|
|
46
|
+
const { RedisAdapter } = require("wassocket/adapters");
|
|
47
|
+
server.useAdapter(new RedisAdapter({ url: "redis://localhost:6379" }));
|
|
48
|
+
```
|
|
49
|
+
Deploy multiple node processes behind a load balancer.
|
|
50
|
+
|
|
51
|
+
## Admin Dashboard JWT
|
|
52
|
+
|
|
53
|
+
Dashboard endpoint requires JWT token. Generate token with same secret
|
|
54
|
+
as server. Attach Authorization header: Bearer `<token>`{=html}.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RedisMessageHandler = (channel: string, data: any) => void;
|
|
2
|
+
export declare class RedisAdapter {
|
|
3
|
+
private pub;
|
|
4
|
+
private sub;
|
|
5
|
+
private handlers;
|
|
6
|
+
constructor(url?: string);
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
subscribe(channel: string): Promise<void>;
|
|
9
|
+
onMessage(handler: RedisMessageHandler): void;
|
|
10
|
+
publish(channel: string, data: any): void;
|
|
11
|
+
}
|
package/dist/binary.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packet opcode
|
|
3
|
+
*/
|
|
4
|
+
export declare enum OpCode {
|
|
5
|
+
PING = 1,
|
|
6
|
+
PONG = 2,
|
|
7
|
+
EVENT = 3,
|
|
8
|
+
JOIN = 4,
|
|
9
|
+
LEAVE = 5,
|
|
10
|
+
ACK = 6
|
|
11
|
+
}
|
|
12
|
+
export interface Packet {
|
|
13
|
+
op: OpCode;
|
|
14
|
+
id?: number;
|
|
15
|
+
ack?: number;
|
|
16
|
+
event?: string;
|
|
17
|
+
room?: string;
|
|
18
|
+
data?: any;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Encryption options
|
|
22
|
+
*/
|
|
23
|
+
export interface EncryptionOptions {
|
|
24
|
+
mode: "none" | "xor" | "aes";
|
|
25
|
+
secret: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Binary Transport
|
|
29
|
+
* Encode / Decode Packet to Buffer
|
|
30
|
+
*/
|
|
31
|
+
export declare class BinaryTransport {
|
|
32
|
+
private encryption?;
|
|
33
|
+
constructor(encryption?: EncryptionOptions);
|
|
34
|
+
encode(packet: Packet): Buffer;
|
|
35
|
+
decode(buffer: Buffer): Packet;
|
|
36
|
+
private encrypt;
|
|
37
|
+
private decrypt;
|
|
38
|
+
private xor;
|
|
39
|
+
private aesEncrypt;
|
|
40
|
+
private aesDecrypt;
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type Handler = (data: any) => void;
|
|
2
|
+
export interface ClientOptions {
|
|
3
|
+
encryption?: {
|
|
4
|
+
mode: "none" | "xor" | "aes";
|
|
5
|
+
secret: string;
|
|
6
|
+
};
|
|
7
|
+
reconnect?: {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
maxDelay?: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare class WASSocketClient {
|
|
13
|
+
private options?;
|
|
14
|
+
private socket?;
|
|
15
|
+
private handlers;
|
|
16
|
+
private transport;
|
|
17
|
+
private url;
|
|
18
|
+
private connected;
|
|
19
|
+
private reconnectAttempts;
|
|
20
|
+
private reconnectTimer?;
|
|
21
|
+
private offlineQueue;
|
|
22
|
+
private packetId;
|
|
23
|
+
private pendingAck;
|
|
24
|
+
constructor(options?: ClientOptions | undefined);
|
|
25
|
+
connect(url: string): void;
|
|
26
|
+
private createSocket;
|
|
27
|
+
private scheduleReconnect;
|
|
28
|
+
private flushQueue;
|
|
29
|
+
private send;
|
|
30
|
+
emit(event: string, data?: any, room?: string): Promise<void>;
|
|
31
|
+
join(room: string): void;
|
|
32
|
+
leave(room: string): void;
|
|
33
|
+
on(event: string, handler: Handler): void;
|
|
34
|
+
private handleMessage;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WASSocketClient } from "./client/WASSocketClient";
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var WebSocket = require('ws');
|
|
4
|
+
var https = require('https');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var http = require('http');
|
|
8
|
+
var greenlock = require('greenlock-express');
|
|
9
|
+
var redis = require('redis');
|
|
10
|
+
|
|
11
|
+
function createAutoTLSServer(handler, options) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
const glx = greenlock.init({
|
|
14
|
+
packageRoot: process.cwd(),
|
|
15
|
+
configDir: "./greenlock",
|
|
16
|
+
maintainerEmail: options.email,
|
|
17
|
+
cluster: false,
|
|
18
|
+
staging: options.production === false
|
|
19
|
+
});
|
|
20
|
+
const httpServer = http.createServer(glx.middleware());
|
|
21
|
+
const httpsServer = https.createServer(glx.httpsOptions, handler);
|
|
22
|
+
glx.ready(() => {
|
|
23
|
+
console.log("[AutoTLS] Greenlock ready");
|
|
24
|
+
resolve({
|
|
25
|
+
httpServer,
|
|
26
|
+
httpsServer
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.OpCode = void 0;
|
|
33
|
+
(function (OpCode) {
|
|
34
|
+
OpCode[OpCode["EVENT"] = 1] = "EVENT";
|
|
35
|
+
OpCode[OpCode["ACK"] = 2] = "ACK";
|
|
36
|
+
OpCode[OpCode["JOIN"] = 3] = "JOIN";
|
|
37
|
+
OpCode[OpCode["LEAVE"] = 4] = "LEAVE";
|
|
38
|
+
OpCode[OpCode["PING"] = 5] = "PING";
|
|
39
|
+
OpCode[OpCode["PONG"] = 6] = "PONG";
|
|
40
|
+
})(exports.OpCode || (exports.OpCode = {}));
|
|
41
|
+
|
|
42
|
+
class CryptoBox {
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
if ((options === null || options === void 0 ? void 0 : options.mode) === "aes") {
|
|
46
|
+
this.key = crypto
|
|
47
|
+
.createHash("sha256")
|
|
48
|
+
.update(options.secret)
|
|
49
|
+
.digest();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.key = Buffer.alloc(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
encrypt(data) {
|
|
56
|
+
if (!this.options || this.options.mode === "none") {
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
if (this.options.mode === "xor") {
|
|
60
|
+
return this.xor(data);
|
|
61
|
+
}
|
|
62
|
+
if (this.options.mode === "aes") {
|
|
63
|
+
const iv = crypto.randomBytes(12);
|
|
64
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.key, iv);
|
|
65
|
+
const encrypted = Buffer.concat([
|
|
66
|
+
cipher.update(data),
|
|
67
|
+
cipher.final()
|
|
68
|
+
]);
|
|
69
|
+
const tag = cipher.getAuthTag();
|
|
70
|
+
return Buffer.concat([
|
|
71
|
+
Buffer.from([iv.length]),
|
|
72
|
+
iv,
|
|
73
|
+
Buffer.from([tag.length]),
|
|
74
|
+
tag,
|
|
75
|
+
encrypted
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
decrypt(data) {
|
|
81
|
+
if (!this.options || this.options.mode === "none") {
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
if (this.options.mode === "xor") {
|
|
85
|
+
return this.xor(data);
|
|
86
|
+
}
|
|
87
|
+
if (this.options.mode === "aes") {
|
|
88
|
+
let offset = 0;
|
|
89
|
+
const ivLen = data.readUInt8(offset++);
|
|
90
|
+
const iv = data.subarray(offset, offset + ivLen);
|
|
91
|
+
offset += ivLen;
|
|
92
|
+
const tagLen = data.readUInt8(offset++);
|
|
93
|
+
const tag = data.subarray(offset, offset + tagLen);
|
|
94
|
+
offset += tagLen;
|
|
95
|
+
const encrypted = data.subarray(offset);
|
|
96
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.key, iv);
|
|
97
|
+
decipher.setAuthTag(tag);
|
|
98
|
+
return Buffer.concat([
|
|
99
|
+
decipher.update(encrypted),
|
|
100
|
+
decipher.final()
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
xor(buf) {
|
|
106
|
+
var _a;
|
|
107
|
+
const key = Buffer.from(((_a = this.options) === null || _a === void 0 ? void 0 : _a.secret) || "x");
|
|
108
|
+
const out = Buffer.allocUnsafe(buf.length);
|
|
109
|
+
for (let i = 0; i < buf.length; i++) {
|
|
110
|
+
out[i] = buf[i] ^ key[i % key.length];
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class BinaryTransport {
|
|
117
|
+
constructor(encryption) {
|
|
118
|
+
this.crypto = new CryptoBox(encryption);
|
|
119
|
+
}
|
|
120
|
+
encode(packet) {
|
|
121
|
+
const json = JSON.stringify(packet);
|
|
122
|
+
const raw = Buffer.from(json);
|
|
123
|
+
return this.crypto.encrypt(raw);
|
|
124
|
+
}
|
|
125
|
+
decode(buffer) {
|
|
126
|
+
const raw = this.crypto.decrypt(buffer);
|
|
127
|
+
return JSON.parse(raw.toString());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class RoomManager {
|
|
132
|
+
constructor() {
|
|
133
|
+
this.rooms = new Map();
|
|
134
|
+
}
|
|
135
|
+
join(room, clientId) {
|
|
136
|
+
if (!this.rooms.has(room)) {
|
|
137
|
+
this.rooms.set(room, new Set());
|
|
138
|
+
}
|
|
139
|
+
this.rooms.get(room).add(clientId);
|
|
140
|
+
}
|
|
141
|
+
leave(room, clientId) {
|
|
142
|
+
var _a;
|
|
143
|
+
(_a = this.rooms.get(room)) === null || _a === void 0 ? void 0 : _a.delete(clientId);
|
|
144
|
+
}
|
|
145
|
+
removeClient(clientId) {
|
|
146
|
+
for (const set of this.rooms.values()) {
|
|
147
|
+
set.delete(clientId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
getClients(room) {
|
|
151
|
+
var _a;
|
|
152
|
+
return (_a = this.rooms.get(room)) !== null && _a !== void 0 ? _a : new Set();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class RateLimiter {
|
|
157
|
+
constructor(options) {
|
|
158
|
+
this.options = options;
|
|
159
|
+
this.buckets = new Map();
|
|
160
|
+
}
|
|
161
|
+
allow(id) {
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
let bucket = this.buckets.get(id);
|
|
164
|
+
if (!bucket) {
|
|
165
|
+
bucket = {
|
|
166
|
+
tokens: this.options.capacity,
|
|
167
|
+
last: now
|
|
168
|
+
};
|
|
169
|
+
this.buckets.set(id, bucket);
|
|
170
|
+
}
|
|
171
|
+
const delta = (now - bucket.last) / 1000;
|
|
172
|
+
bucket.tokens = Math.min(this.options.capacity, bucket.tokens + delta * this.options.refillPerSecond);
|
|
173
|
+
bucket.last = now;
|
|
174
|
+
if (bucket.tokens < 1)
|
|
175
|
+
return false;
|
|
176
|
+
bucket.tokens -= 1;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
clear(id) {
|
|
180
|
+
this.buckets.delete(id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class PacketInspector {
|
|
185
|
+
constructor() {
|
|
186
|
+
this.listeners = new Set();
|
|
187
|
+
}
|
|
188
|
+
onPacket(cb) {
|
|
189
|
+
this.listeners.add(cb);
|
|
190
|
+
}
|
|
191
|
+
emit(packet) {
|
|
192
|
+
for (const cb of this.listeners) {
|
|
193
|
+
cb(packet);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const packetInspector = new PacketInspector();
|
|
198
|
+
|
|
199
|
+
class RedisAdapter {
|
|
200
|
+
constructor(url = "redis://127.0.0.1:6379") {
|
|
201
|
+
this.handlers = new Set();
|
|
202
|
+
this.pub = redis.createClient({ url });
|
|
203
|
+
this.sub = redis.createClient({ url });
|
|
204
|
+
}
|
|
205
|
+
async connect() {
|
|
206
|
+
await this.pub.connect();
|
|
207
|
+
await this.sub.connect();
|
|
208
|
+
console.log("[RedisAdapter] Connected");
|
|
209
|
+
}
|
|
210
|
+
async subscribe(channel) {
|
|
211
|
+
await this.sub.subscribe(channel, message => {
|
|
212
|
+
try {
|
|
213
|
+
const data = JSON.parse(message);
|
|
214
|
+
for (const h of this.handlers) {
|
|
215
|
+
h(channel, data);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch { }
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
onMessage(handler) {
|
|
222
|
+
this.handlers.add(handler);
|
|
223
|
+
}
|
|
224
|
+
publish(channel, data) {
|
|
225
|
+
this.pub.publish(channel, JSON.stringify(data));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class WASSocketServer {
|
|
230
|
+
constructor(port, options) {
|
|
231
|
+
var _a;
|
|
232
|
+
this.namespaces = new Map();
|
|
233
|
+
this.transport = new BinaryTransport(options === null || options === void 0 ? void 0 : options.encryption);
|
|
234
|
+
this.authProvider = (_a = options === null || options === void 0 ? void 0 : options.auth) === null || _a === void 0 ? void 0 : _a.provider;
|
|
235
|
+
if (options === null || options === void 0 ? void 0 : options.rateLimit) {
|
|
236
|
+
this.rateLimiter = new RateLimiter(options.rateLimit);
|
|
237
|
+
}
|
|
238
|
+
if (options === null || options === void 0 ? void 0 : options.redisUrl) {
|
|
239
|
+
this.redis = new RedisAdapter(options.redisUrl);
|
|
240
|
+
this.redis.connect();
|
|
241
|
+
this.redis.subscribe("wassocket:broadcast");
|
|
242
|
+
this.redis.onMessage((_ch, packet) => {
|
|
243
|
+
// broadcast packet from other nodes
|
|
244
|
+
for (const [, ns] of this.namespaces) {
|
|
245
|
+
for (const [, client] of ns.clients) {
|
|
246
|
+
this.send(client, packet);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (options === null || options === void 0 ? void 0 : options.autoTLS) {
|
|
252
|
+
console.log("[WASSocket] Starting AutoTLS...");
|
|
253
|
+
createAutoTLSServer((req, res) => {
|
|
254
|
+
// WebSocket upgrade handled by ws internally
|
|
255
|
+
}, {
|
|
256
|
+
email: options.autoTLS.email,
|
|
257
|
+
domain: options.autoTLS.domain,
|
|
258
|
+
production: options.autoTLS.production
|
|
259
|
+
}).then(({ httpsServer }) => {
|
|
260
|
+
this.wss = new WebSocket.WebSocketServer({ server: httpsServer });
|
|
261
|
+
httpsServer.listen(port);
|
|
262
|
+
console.log(`[WASSocket] WSS AutoTLS running on :${port}`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (options === null || options === void 0 ? void 0 : options.tls) {
|
|
266
|
+
const httpsServer = https.createServer({
|
|
267
|
+
key: fs.readFileSync(options.tls.key),
|
|
268
|
+
cert: fs.readFileSync(options.tls.cert)
|
|
269
|
+
});
|
|
270
|
+
this.wss = new WebSocket.WebSocketServer({ server: httpsServer });
|
|
271
|
+
httpsServer.listen(port);
|
|
272
|
+
console.log(`[WASSocket] WSS running on ${port}`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
this.wss = new WebSocket.WebSocketServer({ port });
|
|
276
|
+
console.log(`[WASSocket] WS running on ${port}`);
|
|
277
|
+
}
|
|
278
|
+
this.wss.on("connection", (socket, req) => this.handleConnection(socket, req.url || "/"));
|
|
279
|
+
this.namespace("/");
|
|
280
|
+
}
|
|
281
|
+
// ================= NAMESPACE =================
|
|
282
|
+
namespace(name) {
|
|
283
|
+
if (!name.startsWith("/"))
|
|
284
|
+
name = "/" + name;
|
|
285
|
+
if (!this.namespaces.has(name)) {
|
|
286
|
+
this.namespaces.set(name, {
|
|
287
|
+
name,
|
|
288
|
+
clients: new Map(),
|
|
289
|
+
rooms: new RoomManager()
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return this.namespaces.get(name);
|
|
293
|
+
}
|
|
294
|
+
// ================= CONNECTION =================
|
|
295
|
+
async handleConnection(socket, url) {
|
|
296
|
+
const path = url.split("?")[0] || "/";
|
|
297
|
+
const ns = this.namespace(path);
|
|
298
|
+
// ================= AUTH =================
|
|
299
|
+
const queryString = url.split("?")[1];
|
|
300
|
+
const query = new URLSearchParams(queryString);
|
|
301
|
+
const token = query.get("token") || undefined;
|
|
302
|
+
let user = undefined;
|
|
303
|
+
if (this.authProvider) {
|
|
304
|
+
if (!token) {
|
|
305
|
+
console.warn("[WASSocket] Missing auth token");
|
|
306
|
+
socket.close();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const verified = await this.authProvider.verifyToken(token);
|
|
311
|
+
if (!verified) {
|
|
312
|
+
console.warn("[WASSocket] Invalid token");
|
|
313
|
+
socket.close();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
user = verified;
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
console.warn("[WASSocket] Auth error", err);
|
|
320
|
+
socket.close();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ================= CLIENT =================
|
|
325
|
+
const id = crypto.randomUUID();
|
|
326
|
+
const client = {
|
|
327
|
+
id,
|
|
328
|
+
socket,
|
|
329
|
+
lastPing: Date.now(),
|
|
330
|
+
user
|
|
331
|
+
};
|
|
332
|
+
ns.clients.set(id, client);
|
|
333
|
+
// ================= EVENTS =================
|
|
334
|
+
socket.on("message", (data) => {
|
|
335
|
+
if (Buffer.isBuffer(data)) {
|
|
336
|
+
this.handleMessage(ns, client, data);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
socket.on("close", () => {
|
|
340
|
+
ns.clients.delete(id);
|
|
341
|
+
ns.rooms.removeClient(id);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// ================= MESSAGE =================
|
|
345
|
+
handleMessage(ns, client, buffer) {
|
|
346
|
+
try {
|
|
347
|
+
const packet = this.transport.decode(buffer);
|
|
348
|
+
packetInspector.emit(packet);
|
|
349
|
+
if (packet.id) {
|
|
350
|
+
this.send(client, {
|
|
351
|
+
op: exports.OpCode.ACK,
|
|
352
|
+
ack: packet.id
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
switch (packet.op) {
|
|
356
|
+
case exports.OpCode.PING:
|
|
357
|
+
client.lastPing = Date.now();
|
|
358
|
+
this.send(client, { op: exports.OpCode.PONG });
|
|
359
|
+
break;
|
|
360
|
+
case exports.OpCode.JOIN:
|
|
361
|
+
packet.room && ns.rooms.join(packet.room, client.id);
|
|
362
|
+
break;
|
|
363
|
+
case exports.OpCode.LEAVE:
|
|
364
|
+
packet.room && ns.rooms.leave(packet.room, client.id);
|
|
365
|
+
break;
|
|
366
|
+
case exports.OpCode.EVENT:
|
|
367
|
+
if (!packet.event)
|
|
368
|
+
break;
|
|
369
|
+
if (packet.room) {
|
|
370
|
+
this.broadcastRoom(ns, packet.room, packet, client.id);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.broadcast(ns, packet, client.id);
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
console.warn("[WASSocket] Message error", err);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// ================= SEND =================
|
|
383
|
+
send(client, packet) {
|
|
384
|
+
const buffer = this.transport.encode(packet);
|
|
385
|
+
client.socket.send(buffer);
|
|
386
|
+
packetInspector.emit(packet);
|
|
387
|
+
}
|
|
388
|
+
broadcast(ns, packet, exceptId) {
|
|
389
|
+
for (const [id, client] of ns.clients) {
|
|
390
|
+
if (id === exceptId)
|
|
391
|
+
continue;
|
|
392
|
+
this.send(client, packet);
|
|
393
|
+
}
|
|
394
|
+
if (this.redis) {
|
|
395
|
+
this.redis.publish("wassocket:broadcast", packet);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
broadcastRoom(ns, room, packet, exceptId) {
|
|
399
|
+
const ids = ns.rooms.getClients(room);
|
|
400
|
+
for (const id of ids) {
|
|
401
|
+
if (id === exceptId)
|
|
402
|
+
continue;
|
|
403
|
+
const client = ns.clients.get(id);
|
|
404
|
+
if (client)
|
|
405
|
+
this.send(client, packet);
|
|
406
|
+
}
|
|
407
|
+
if (this.redis) {
|
|
408
|
+
this.redis.publish("wassocket:broadcast", packet);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
class WASSocketClient {
|
|
414
|
+
constructor(options) {
|
|
415
|
+
this.options = options;
|
|
416
|
+
this.handlers = new Map();
|
|
417
|
+
this.url = "";
|
|
418
|
+
this.connected = false;
|
|
419
|
+
this.reconnectAttempts = 0;
|
|
420
|
+
this.offlineQueue = [];
|
|
421
|
+
this.packetId = 1;
|
|
422
|
+
this.pendingAck = new Map();
|
|
423
|
+
this.transport = new BinaryTransport(options === null || options === void 0 ? void 0 : options.encryption);
|
|
424
|
+
}
|
|
425
|
+
// ================= CONNECT =================
|
|
426
|
+
connect(url) {
|
|
427
|
+
this.url = url;
|
|
428
|
+
this.createSocket();
|
|
429
|
+
}
|
|
430
|
+
createSocket() {
|
|
431
|
+
this.socket = new WebSocket(this.url);
|
|
432
|
+
this.socket.on("open", () => {
|
|
433
|
+
console.log("[WASSocket] Connected");
|
|
434
|
+
this.connected = true;
|
|
435
|
+
this.reconnectAttempts = 0;
|
|
436
|
+
this.flushQueue();
|
|
437
|
+
});
|
|
438
|
+
this.socket.on("message", data => {
|
|
439
|
+
if (Buffer.isBuffer(data)) {
|
|
440
|
+
this.handleMessage(data);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
this.socket.on("close", () => {
|
|
444
|
+
console.warn("[WASSocket] Disconnected");
|
|
445
|
+
this.connected = false;
|
|
446
|
+
this.scheduleReconnect();
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
// ================= RECONNECT =================
|
|
450
|
+
scheduleReconnect() {
|
|
451
|
+
var _a, _b, _c, _d, _e;
|
|
452
|
+
if (((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.reconnect) === null || _b === void 0 ? void 0 : _b.enabled) === false)
|
|
453
|
+
return;
|
|
454
|
+
const maxDelay = (_e = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.reconnect) === null || _d === void 0 ? void 0 : _d.maxDelay) !== null && _e !== void 0 ? _e : 15000;
|
|
455
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), maxDelay);
|
|
456
|
+
this.reconnectAttempts++;
|
|
457
|
+
clearTimeout(this.reconnectTimer);
|
|
458
|
+
console.log(`[WASSocket] Reconnect in ${delay}ms`);
|
|
459
|
+
this.reconnectTimer = setTimeout(() => {
|
|
460
|
+
this.createSocket();
|
|
461
|
+
}, delay);
|
|
462
|
+
}
|
|
463
|
+
// ================= QUEUE =================
|
|
464
|
+
flushQueue() {
|
|
465
|
+
while (this.offlineQueue.length > 0) {
|
|
466
|
+
this.send(this.offlineQueue.shift());
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ================= SEND =================
|
|
470
|
+
send(packet) {
|
|
471
|
+
if (!this.connected || !this.socket) {
|
|
472
|
+
this.offlineQueue.push(packet);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const buffer = this.transport.encode(packet);
|
|
476
|
+
this.socket.send(buffer);
|
|
477
|
+
}
|
|
478
|
+
// ================= API =================
|
|
479
|
+
emit(event, data, room) {
|
|
480
|
+
const id = this.packetId++;
|
|
481
|
+
const packet = {
|
|
482
|
+
op: exports.OpCode.EVENT,
|
|
483
|
+
event,
|
|
484
|
+
data,
|
|
485
|
+
room,
|
|
486
|
+
id
|
|
487
|
+
};
|
|
488
|
+
return new Promise(resolve => {
|
|
489
|
+
const timer = setTimeout(() => {
|
|
490
|
+
console.warn("[WASSocket] ACK timeout, retry", id);
|
|
491
|
+
this.send(packet);
|
|
492
|
+
}, 3000);
|
|
493
|
+
this.pendingAck.set(id, {
|
|
494
|
+
resolve,
|
|
495
|
+
timer,
|
|
496
|
+
packet
|
|
497
|
+
});
|
|
498
|
+
this.send(packet);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
join(room) {
|
|
502
|
+
this.send({ op: exports.OpCode.JOIN, room });
|
|
503
|
+
}
|
|
504
|
+
leave(room) {
|
|
505
|
+
this.send({ op: exports.OpCode.LEAVE, room });
|
|
506
|
+
}
|
|
507
|
+
on(event, handler) {
|
|
508
|
+
if (!this.handlers.has(event)) {
|
|
509
|
+
this.handlers.set(event, new Set());
|
|
510
|
+
}
|
|
511
|
+
this.handlers.get(event).add(handler);
|
|
512
|
+
}
|
|
513
|
+
// ================= MESSAGE =================
|
|
514
|
+
handleMessage(buffer) {
|
|
515
|
+
try {
|
|
516
|
+
const packet = this.transport.decode(buffer);
|
|
517
|
+
if (packet.op === exports.OpCode.ACK && packet.ack) {
|
|
518
|
+
const pending = this.pendingAck.get(packet.ack);
|
|
519
|
+
if (pending) {
|
|
520
|
+
clearTimeout(pending.timer);
|
|
521
|
+
pending.resolve();
|
|
522
|
+
this.pendingAck.delete(packet.ack);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (packet.op === exports.OpCode.EVENT && packet.event) {
|
|
527
|
+
const set = this.handlers.get(packet.event);
|
|
528
|
+
if (!set)
|
|
529
|
+
return;
|
|
530
|
+
for (const fn of set)
|
|
531
|
+
fn(packet.data);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
console.warn("[WASSocket] Decode error", err);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
exports.WASSocketClient = WASSocketClient;
|
|
541
|
+
exports.WASSocketServer = WASSocketServer;
|
|
542
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Packet } from "./protocol";
|
|
2
|
+
export interface MiddlewareContext {
|
|
3
|
+
clientId: string;
|
|
4
|
+
packet: Packet;
|
|
5
|
+
}
|
|
6
|
+
export type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void | boolean>;
|
|
7
|
+
export declare class MiddlewareManager {
|
|
8
|
+
private pre;
|
|
9
|
+
private post;
|
|
10
|
+
usePre(fn: Middleware): void;
|
|
11
|
+
usePost(fn: Middleware): void;
|
|
12
|
+
runPre(ctx: MiddlewareContext): Promise<boolean>;
|
|
13
|
+
runPost(ctx: MiddlewareContext): Promise<boolean>;
|
|
14
|
+
private runChain;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class Metrics {
|
|
2
|
+
connections: number;
|
|
3
|
+
packetsIn: number;
|
|
4
|
+
packetsOut: number;
|
|
5
|
+
startTime: number;
|
|
6
|
+
snapshot(): {
|
|
7
|
+
uptime: number;
|
|
8
|
+
connections: number;
|
|
9
|
+
packetsIn: number;
|
|
10
|
+
packetsOut: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const metrics: Metrics;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Packet } from "../protocol";
|
|
2
|
+
export interface PacketLog {
|
|
3
|
+
time: number;
|
|
4
|
+
direction: "IN" | "OUT";
|
|
5
|
+
clientId: string;
|
|
6
|
+
packet: Packet;
|
|
7
|
+
}
|
|
8
|
+
export declare class PacketInspector {
|
|
9
|
+
private logs;
|
|
10
|
+
record(direction: "IN" | "OUT", clientId: string, packet: Packet): void;
|
|
11
|
+
snapshot(limit?: number): PacketLog[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ClientContext } from "./server/WASSocketServer";
|
|
2
|
+
export declare class RoomManager {
|
|
3
|
+
private rooms;
|
|
4
|
+
join(room: string, clientId: string): void;
|
|
5
|
+
leave(room: string, clientId: string): void;
|
|
6
|
+
getClients(room: string): Set<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare class Middleware {
|
|
9
|
+
private pre;
|
|
10
|
+
private post;
|
|
11
|
+
use(fn: Function): void;
|
|
12
|
+
runPre(ctx: any): Promise<boolean>;
|
|
13
|
+
runPost(ctx: any): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare class Namespace {
|
|
16
|
+
name: string;
|
|
17
|
+
clients: Map<string, ClientContext>;
|
|
18
|
+
rooms: RoomManager;
|
|
19
|
+
middleware: Middleware;
|
|
20
|
+
constructor(name: string);
|
|
21
|
+
addClient(client: ClientContext): void;
|
|
22
|
+
removeClient(id: string): void;
|
|
23
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WASSocketServer } from "./server/WASSocketServer";
|
|
2
|
+
import { Namespace } from "./namespace";
|
|
3
|
+
export interface WASSocketPlugin {
|
|
4
|
+
name: string;
|
|
5
|
+
install(server: WASSocketServer): void;
|
|
6
|
+
uninstall?(server: WASSocketServer): void;
|
|
7
|
+
onNamespaceCreate?(ns: Namespace): void;
|
|
8
|
+
}
|
|
9
|
+
export declare class PluginManager {
|
|
10
|
+
private plugins;
|
|
11
|
+
install(plugin: WASSocketPlugin, server: WASSocketServer): void;
|
|
12
|
+
uninstall(name: string, server: WASSocketServer): void;
|
|
13
|
+
notifyNamespaceCreate(ns: Namespace): void;
|
|
14
|
+
list(): string[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AuthUser {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
roles?: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface AuthProvider {
|
|
7
|
+
verifyToken(token: string): Promise<AuthUser | null>;
|
|
8
|
+
}
|
|
9
|
+
export interface JwtOptions {
|
|
10
|
+
secret: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class JwtAuthProvider implements AuthProvider {
|
|
13
|
+
private options;
|
|
14
|
+
constructor(options: JwtOptions);
|
|
15
|
+
verifyToken(token: string): Promise<AuthUser | null>;
|
|
16
|
+
static sign(payload: AuthUser, secret: string, expiresIn?: string): any;
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import https from "https";
|
|
3
|
+
export interface AutoTLSOptions {
|
|
4
|
+
email: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
agreeTos?: boolean;
|
|
7
|
+
production?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface AutoTLSServer {
|
|
10
|
+
httpServer: http.Server;
|
|
11
|
+
httpsServer: https.Server;
|
|
12
|
+
}
|
|
13
|
+
export declare function createAutoTLSServer(handler: (req: http.IncomingMessage, res: http.ServerResponse) => void, options: AutoTLSOptions): Promise<AutoTLSServer>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RateLimitOptions {
|
|
2
|
+
capacity: number;
|
|
3
|
+
refillPerSecond: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class RateLimiter {
|
|
6
|
+
private options;
|
|
7
|
+
private buckets;
|
|
8
|
+
constructor(options: RateLimitOptions);
|
|
9
|
+
allow(id: string): boolean;
|
|
10
|
+
clear(id: string): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { Packet } from "../protocol";
|
|
3
|
+
import { BinaryTransport } from "../transport/binary";
|
|
4
|
+
import { RoomManager } from "./RoomManager";
|
|
5
|
+
import { AuthProvider } from "../security/Auth";
|
|
6
|
+
export interface ServerOptions {
|
|
7
|
+
encryption?: {
|
|
8
|
+
mode: "none" | "xor" | "aes";
|
|
9
|
+
secret: string;
|
|
10
|
+
};
|
|
11
|
+
auth?: {
|
|
12
|
+
provider: AuthProvider;
|
|
13
|
+
};
|
|
14
|
+
rateLimit?: {
|
|
15
|
+
capacity: number;
|
|
16
|
+
refillPerSecond: number;
|
|
17
|
+
};
|
|
18
|
+
tls?: {
|
|
19
|
+
key: string;
|
|
20
|
+
cert: string;
|
|
21
|
+
};
|
|
22
|
+
autoTLS?: {
|
|
23
|
+
email: string;
|
|
24
|
+
domain: string;
|
|
25
|
+
production?: boolean;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface ClientContext {
|
|
29
|
+
id: string;
|
|
30
|
+
socket: WebSocket;
|
|
31
|
+
lastPing: number;
|
|
32
|
+
user?: any;
|
|
33
|
+
}
|
|
34
|
+
interface Namespace {
|
|
35
|
+
name: string;
|
|
36
|
+
clients: Map<string, ClientContext>;
|
|
37
|
+
rooms: RoomManager;
|
|
38
|
+
}
|
|
39
|
+
export declare class WASSocketServer {
|
|
40
|
+
private authProvider?;
|
|
41
|
+
private rateLimiter?;
|
|
42
|
+
private redis?;
|
|
43
|
+
private wss;
|
|
44
|
+
private namespaces;
|
|
45
|
+
transport: BinaryTransport;
|
|
46
|
+
constructor(port: number, options?: ServerOptions & {
|
|
47
|
+
redisUrl?: string;
|
|
48
|
+
});
|
|
49
|
+
namespace(name: string): Namespace;
|
|
50
|
+
private handleConnection;
|
|
51
|
+
private handleMessage;
|
|
52
|
+
private send;
|
|
53
|
+
private broadcast;
|
|
54
|
+
protected broadcastRoom(ns: Namespace, room: string, packet: Packet, exceptId?: string): void;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type EncryptionMode = "none" | "xor" | "aes";
|
|
2
|
+
export interface EncryptionOptions {
|
|
3
|
+
mode: EncryptionMode;
|
|
4
|
+
secret: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class CryptoBox {
|
|
7
|
+
private options?;
|
|
8
|
+
private key;
|
|
9
|
+
constructor(options?: EncryptionOptions | undefined);
|
|
10
|
+
encrypt(data: Buffer): Buffer;
|
|
11
|
+
decrypt(data: Buffer): Buffer;
|
|
12
|
+
private xor;
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wassocket",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "High performance WebSocket framework with rooms, middleware, redis adapter",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "../wassocketesm/dist/index.mjs",
|
|
8
|
+
"types": "../core/dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"types": "./core/dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ws": "^8.16.0",
|
|
21
|
+
"redis": "^4.6.0",
|
|
22
|
+
"jsonwebtoken": "^9.0.0",
|
|
23
|
+
"greenlock-express": "^4.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|