Skip to content

Commit c9d726d

Browse files
authored
fix TunnelServer event listener leak (#103)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
1 parent a2d2172 commit c9d726d

File tree

5 files changed

+96
-71
lines changed

5 files changed

+96
-71
lines changed

src/__tests__/server.test.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
1414
MwIDAQAB
1515
-----END PUBLIC KEY-----`;
1616

17+
type IncomingSocket = {
18+
connection: "open" | "close";
19+
ws: WebSocket;
20+
};
21+
1722
describe("TunnelServer", () => {
1823
let server: TunnelServer;
1924
const port = 51515;
@@ -33,7 +38,7 @@ describe("TunnelServer", () => {
3338
* }
3439
*/
3540
const jwtToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5zLXVzZXIiLCJncm91cHMiOlsiZGV2Il0sImlhdCI6MTUxNjIzOTAyMiwiY2x1c3RlcklkIjoiYTAyNmU1MGQtZjliNC00YWE4LWJhMDItYzk3MjJmN2YwNjYzIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdC9ib3JlZC9hMDI2ZTUwZC1mOWI0LTRhYTgtYmEwMi1jOTcyMmY3ZjA2NjMifQ.jkTbX_O8UWbYdCRiTv4NEgDkewEOB9QrLOHOm_Ox8BKt7DC4696bbdOwVn_VHist0g6889ms0m8Nr_RKW5BW90ItAsfDx_0cp34_WKPuMBeXYxkfAEabBbhjATfrW1IUTVtV9R_qQ71nbqlhY9UudByfETI8CanjbDP7QYZCxmVCf2HvRML3h6mS1tqHmqZvjRAHY-cFmO8qa6xLp2c1vFMxuCoSZGoGIqoNPaLKIVBbDdjxzOEjO__gQX6ksUZxsHOy13iBre8gbBVi85lhkSCZa9OtXDEAICqsrlpHZvxIYqYMgBNG0YY4sVvvDGJgDxxTyWn8lphKrZyWWtNvjw";
36-
41+
3742
/**
3843
* {
3944
* "sub": "a026e50d-f9b4-4aa8-ba02-c9722f7f0663",
@@ -55,10 +60,7 @@ describe("TunnelServer", () => {
5560
const sleep = (amount: number) => new Promise((resolve) => setTimeout(resolve, amount));
5661
const get = async (path: string, headers?: Headers) => got(`http://localhost:${port}${path}`, { throwHttpErrors: false, headers });
5762

58-
const incomingSocket = (type = "agent", headers: { [key: string]: string } = {}, keepOpen = 10, close = true, endpoint = "connect"): Promise<{
59-
connection: "open" | "close";
60-
ws: WebSocket;
61-
}> => {
63+
const incomingSocket = (type = "agent", headers: { [key: string]: string } = {}, keepOpen = 10, close = true, endpoint = "connect"): Promise<IncomingSocket> => {
6264
return new Promise((resolve, reject) => {
6365
const ws = new WebSocket(`http://localhost:${port}/${type}/${endpoint}`, {
6466
headers
@@ -122,7 +124,7 @@ describe("TunnelServer", () => {
122124

123125
const agents = server.getAgentsForClusterId("a026e50d-f9b4-4aa8-ba02-c9722f7f0663");
124126

125-
agents.push(new Agent(ws as any, "rsa-public-key", server));
127+
agents.push(new Agent(ws as any, "rsa-public-key", server, "test-id"));
126128

127129
const res = await get("/client/public-key", { "Authorization": `Bearer ${jwtToken}`});
128130

@@ -451,7 +453,7 @@ describe("TunnelServer", () => {
451453
await expect(connect()).resolves.toHaveProperty("connection", "open");
452454
});
453455

454-
it("sends empty presence json to client presence socket when socket is open", async () => {
456+
it("sends empty presence json to client presence socket when socket is open", async (done) => {
455457
expect.assertions(1);
456458

457459
const presence = await incomingSocket("client", {
@@ -463,17 +465,14 @@ describe("TunnelServer", () => {
463465
"presence" : {
464466
"userIds" : []
465467
}
466-
})
467-
);
468+
}));
469+
presence.ws.close();
470+
done();
468471
};
469-
470-
await sleep(200); //waits until first message was sent
471-
472-
presence.ws.close();
473472
});
474473

475474

476-
it("sends presence json to client presence socket when socket is open and clients are already connected", async () => {
475+
it("sends presence json to client presence socket when socket is open and clients are already connected", async (done) => {
477476
expect.assertions(1);
478477

479478
const agent = await incomingSocket("agent", {
@@ -489,22 +488,22 @@ describe("TunnelServer", () => {
489488
}, undefined, false, "presence");
490489

491490
presence.ws.onmessage = (message) => {
492-
expect(message.data).toBe(JSON.stringify({
491+
expect(message.data).toBe(JSON.stringify({
493492
"presence" : {
494493
"userIds" : ["lens-user"]
495494
}
496495
})
497496
);
498-
};
499497

500-
await sleep(200); //waits until first message was sent
498+
presence.ws.close();
499+
client.ws.close();
500+
agent.ws.close();
501501

502-
presence.ws.close();
503-
client.ws.close();
504-
agent.ws.close();
502+
done();
503+
};
505504
});
506505

507-
it("sends userIds per agent to client presence socket after agent and client connected", async () => {
506+
it("sends userIds per agent to client presence socket after agent and client connected", async (done) => {
508507
expect.assertions(1);
509508

510509
const presence = await incomingSocket("client", {
@@ -513,31 +512,34 @@ describe("TunnelServer", () => {
513512

514513
await sleep(200); //waits until first message was sent
515514

515+
let agent: IncomingSocket | null = null;
516+
let client: IncomingSocket | null = null;
517+
516518
presence.ws.onmessage = (message) => {
517-
expect(message.data).toBe(JSON.stringify({
519+
console.log("message", message.data);
520+
expect(message.data).toBe(JSON.stringify({
518521
"presence" : {
519522
"userIds" : ["lens-user"]
520523
}
521-
})
522-
);
524+
}));
525+
526+
presence.ws.close();
527+
client?.ws.close();
528+
agent?.ws.close();
529+
530+
done();
523531
};
524532

525-
const agent = await incomingSocket("agent", {
533+
agent = await incomingSocket("agent", {
526534
"Authorization": `Bearer ${agentJwtToken}`
527535
}, undefined, false);
528536

529-
const client = await incomingSocket("client", {
537+
client = await incomingSocket("client", {
530538
"Authorization": `Bearer ${jwtToken}`
531539
}, undefined, false);
532-
533-
await sleep(100); //waits until on "ClientConnected" message was sent
534-
535-
presence.ws.close();
536-
client.ws.close();
537-
agent.ws.close();
538540
});
539541

540-
it("sends empty presence json to client presence socket after agent and client connected and disconnected", async () => {
542+
it("sends empty presence json to client presence socket after agent and client connected and disconnected", async (done) => {
541543
expect.assertions(1);
542544

543545
const presence = await incomingSocket("client", {
@@ -555,21 +557,18 @@ describe("TunnelServer", () => {
555557
}, undefined, false);
556558

557559
presence.ws.onmessage = (message) => {
558-
console.log(message.data);
559-
expect(message.data).toBe(JSON.stringify({
560+
expect(message.data).toBe(JSON.stringify({
560561
"presence" : {
561562
"userIds" : []
562563
}
563-
})
564-
);
564+
}));
565+
566+
presence.ws.close();
567+
done();
565568
};
566569

567570
agent.ws.close();
568571
client.ws.close();
569-
570-
await sleep(200); //waits until on "ClientDisconnected" message was received
571-
572-
presence.ws.close();
573572
});
574573
});
575574
});

src/agent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ export interface Client {
1111
export class Agent {
1212
public socket: WebSocket;
1313
public publicKey: string;
14+
public clusterId: string;
1415
public clients: Client[] = [];
1516
private mplex: BoredMplexClient;
1617
private server: TunnelServer;
1718

18-
constructor(socket: WebSocket, publicKey: string, server: TunnelServer) {
19+
constructor(socket: WebSocket, publicKey: string, server: TunnelServer, clusterId: string) {
1920
this.socket = socket;
2021
this.publicKey = publicKey;
2122
this.server = server;
23+
this.clusterId = clusterId;
2224

2325
const stream = WebSocket.createWebSocketStream(this.socket);
2426

@@ -77,7 +79,7 @@ export class Agent {
7779
this.removeClient(socket);
7880
});
7981

80-
this.server.emit("ClientConnected", {});
82+
this.server.emit("ClientConnected", this.clusterId);
8183
}
8284

8385
removeClient(socket: WebSocket) {
@@ -91,7 +93,7 @@ export class Agent {
9193

9294
client.socket.close(4410);
9395

94-
this.server.emit("ClientDisconnected", {});
96+
this.server.emit("ClientDisconnected", this.clusterId);
9597
console.log("SERVER: client disconnected");
9698
}
9799

src/request-handlers/agent-socket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function handleAgentSocket(req: IncomingMessage, socket: WebSocket, serve
5050

5151
console.log(`SERVER: agent connected. Cluster id: ${clusterId}`);
5252
const publicKey = Buffer.from(req.headers["x-bored-publickey"]?.toString() || "", "base64").toString("utf-8");
53-
const agent = new Agent(socket, publicKey, server);
53+
const agent = new Agent(socket, publicKey, server, clusterId);
5454

5555
agents.push(agent);
5656

src/request-handlers/client-socket.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function handleClientSocket(req: IncomingMessage, socket: WebSocket, serv
2626

2727
try {
2828
const tokenData = verifyClientToken(authorization.token, server);
29-
29+
3030
userId = tokenData.sub;
3131
clusterId = server.agentToken === "" ? tokenData.clusterId : defaultClusterId;
3232
} catch (error) {
@@ -82,29 +82,20 @@ export function handleClientPresenceSocket(req: IncomingMessage, socket: WebSock
8282
return;
8383
}
8484

85-
console.log("SERVER: client listening to user presence socket");
85+
const presenceSockets = server.getPresenceSocketsForClusterId(clusterId);
8686

87-
setTimeout(function() {
88-
sendPresenceData(socket, server, clusterId);
89-
}, firstMessageDelay);
87+
presenceSockets.push(socket);
88+
socket.on("close", () => {
89+
const index = presenceSockets.findIndex((presence) => presence === socket);
9090

91-
server.on("ClientConnected", () => {
92-
sendPresenceData(socket, server, clusterId);
91+
if (index !== -1) {
92+
presenceSockets.splice(index, 1);
93+
}
9394
});
9495

95-
server.on("ClientDisconnected", () => {
96-
sendPresenceData(socket, server, clusterId);
97-
});
98-
}
99-
100-
function sendPresenceData(socket: WebSocket, server: TunnelServer, clusterId: string) {
101-
const agents = server.getAgentsForClusterId(clusterId);
96+
console.log("SERVER: client listening to user presence socket");
10297

103-
socket.send(
104-
JSON.stringify({
105-
"presence" : {
106-
"userIds": agents.flatMap(agent => agent.clients.map(client => client.userId))
107-
}
108-
})
109-
);
98+
setTimeout(function() {
99+
server.sendPresenceData(socket, clusterId);
100+
}, firstMessageDelay);
110101
}

src/server.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,32 @@ import { EventEmitter } from "events";
1010

1111
export type ClusterId = string;
1212
export const defaultClusterId: ClusterId = "default";
13-
const eventEmitter = new EventEmitter();
1413

15-
export class TunnelServer {
14+
export class TunnelServer extends EventEmitter {
1615
private server?: HttpServer;
1716
private ws?: Server;
1817

1918
public agentToken = "";
2019
public idpPublicKey = "";
2120
public tunnelAddress?: string;
2221
public agents: Map<ClusterId, Agent[]> = new Map();
23-
emit = eventEmitter.emit;
24-
on = eventEmitter.on;
25-
off = eventEmitter.off;
22+
public presenceSockets: Map<ClusterId, WebSocket[]> = new Map();
23+
24+
constructor() {
25+
super();
26+
27+
this.on("ClientConnected", (clusterId: string) => {
28+
this.getPresenceSocketsForClusterId(clusterId).forEach((socket) => {
29+
this.sendPresenceData(socket, clusterId);
30+
});
31+
});
32+
33+
this.on("ClientDisconnected", (clusterId: string) => {
34+
this.getPresenceSocketsForClusterId(clusterId).forEach((socket) => {
35+
this.sendPresenceData(socket, clusterId);
36+
});
37+
});
38+
}
2639

2740
start(port = 8080, agentToken: string, idpPublicKey: string, tunnelAddress = process.env.TUNNEL_ADDRESS || ""): Promise<void> {
2841
this.agentToken = agentToken;
@@ -61,6 +74,14 @@ export class TunnelServer {
6174
return agents;
6275
}
6376

77+
getPresenceSocketsForClusterId(clusterId: string) {
78+
const sockets = this.presenceSockets.get(clusterId) || [];
79+
80+
if (!this.presenceSockets.has(clusterId)) this.presenceSockets.set(clusterId, sockets);
81+
82+
return sockets;
83+
}
84+
6485
handleRequest(req: IncomingMessage, res: ServerResponse) {
6586
if (!req.url) return;
6687

@@ -126,4 +147,16 @@ export class TunnelServer {
126147
});
127148
}
128149
}
150+
151+
sendPresenceData(socket: WebSocket, clusterId: string) {
152+
const agents = this.getAgentsForClusterId(clusterId);
153+
154+
socket.send(
155+
JSON.stringify({
156+
"presence" : {
157+
"userIds": agents.flatMap(agent => agent.clients.map(client => client.userId))
158+
}
159+
})
160+
);
161+
}
129162
}

0 commit comments

Comments
 (0)