Skip to content

Commit bb0ada8

Browse files
authored
feat: namespace scoped middlewares (#511)
1 parent 58725f0 commit bb0ada8

File tree

8 files changed

+336
-9
lines changed

8 files changed

+336
-9
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,20 @@ export class CompressionMiddleware implements MiddlewareInterface {
342342
}
343343
```
344344

345+
You can limit middlewares to namespaces providing either a `string`, `RegExp` or `Array<string | RegExp>` to the `namespace` parameter:
346+
347+
```typescript
348+
import { Middleware, MiddlewareInterface } from 'socket-controllers';
349+
350+
@Middleware({namespace: '/test'})
351+
export class CompressionMiddleware implements MiddlewareInterface {
352+
use(socket: any, next: (err?: any) => any) {
353+
console.log('do something, for example get authorization token and check authorization');
354+
next();
355+
}
356+
}
357+
```
358+
345359
## Don't forget to load your controllers and middlewares
346360

347361
Controllers and middlewares should be loaded:

src/SocketControllerExecutor.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ParameterParseJsonError } from './error/ParameterParseJsonError';
77
import { ParamTypes } from './metadata/types/ParamTypes';
88
import { ControllerMetadata } from './metadata/ControllerMetadata';
99
import { pathToRegexp } from 'path-to-regexp';
10+
import { Namespace } from 'socket.io';
11+
import { MiddlewareMetadata } from './metadata/MiddlewareMetadata';
1012

1113
/**
1214
* Registers controllers and actions in the given server framework.
@@ -66,18 +68,44 @@ export class SocketControllerExecutor {
6668
*/
6769
private registerMiddlewares(classes?: Function[]): this {
6870
const middlewares = this.metadataBuilder.buildMiddlewareMetadata(classes);
71+
middlewares.sort((middleware1, middleware2) => (middleware1.priority || 0) - (middleware2.priority || 0));
6972

70-
middlewares
71-
.sort((middleware1, middleware2) => (middleware1.priority || 0) - (middleware2.priority || 0))
72-
.forEach(middleware => {
73-
this.io.use((socket: any, next: (err?: any) => any) => {
74-
middleware.instance.use(socket, next);
73+
const middlewaresWithoutNamespace = middlewares.filter(middleware => !middleware.namespace);
74+
const middlewaresWithNamespace = middlewares.filter(middleware => !!middleware.namespace);
75+
76+
for (const middleware of middlewaresWithoutNamespace) {
77+
this.registerMiddleware(this.io as Namespace, middleware);
78+
}
79+
80+
this.io.on('new_namespace', (namespace: Namespace) => {
81+
for (const middleware of middlewaresWithNamespace) {
82+
const middlewareNamespaces = Array.isArray(middleware.namespace)
83+
? middleware.namespace
84+
: [middleware.namespace];
85+
86+
const shouldApply = middlewareNamespaces.some(nsp => {
87+
const nspRegexp = nsp instanceof RegExp ? nsp : pathToRegexp(nsp as string);
88+
return nspRegexp.test(namespace.name);
7589
});
76-
});
90+
91+
if (shouldApply) {
92+
this.registerMiddleware(namespace, middleware);
93+
}
94+
}
95+
});
7796

7897
return this;
7998
}
8099

100+
/**
101+
* Registers middleware.
102+
*/
103+
private registerMiddleware(namespace: Namespace, middleware: MiddlewareMetadata) {
104+
namespace.use((socket: any, next: (err?: any) => any) => {
105+
middleware.instance.use(socket, next);
106+
});
107+
}
108+
81109
/**
82110
* Registers controllers.
83111
*/

src/decorators.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,15 @@ export function SocketRooms() {
226226
/**
227227
* Registers a new middleware to be registered in the socket.io.
228228
*/
229-
export function Middleware(options?: { priority?: number }): Function {
229+
export function Middleware(options?: {
230+
priority?: number;
231+
namespace?: string | RegExp | Array<RegExp | string>;
232+
}): Function {
230233
return function (object: Function) {
231234
const metadata: MiddlewareMetadataArgs = {
232235
target: object,
233-
priority: options && options.priority ? options.priority : undefined,
236+
priority: options?.priority,
237+
namespace: options?.namespace,
234238
};
235239
defaultMetadataArgsStorage().middlewares.push(metadata);
236240
};

src/metadata/MiddlewareMetadata.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class MiddlewareMetadata {
99

1010
target: Function;
1111
priority?: number;
12+
namespace?: string | RegExp | Array<RegExp | string>;
1213

1314
// -------------------------------------------------------------------------
1415
// Constructor
@@ -17,6 +18,7 @@ export class MiddlewareMetadata {
1718
constructor(args: MiddlewareMetadataArgs) {
1819
this.target = args.target;
1920
this.priority = args.priority;
21+
this.namespace = args.namespace;
2022
}
2123

2224
// -------------------------------------------------------------------------

src/metadata/args/MiddlewareMetadataArgs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ export interface MiddlewareMetadataArgs {
88
* Middleware priority.
99
*/
1010
priority?: number;
11+
12+
/**
13+
* Limits usage of the middleware to the given namespaces
14+
*/
15+
namespace?: string | RegExp | Array<RegExp | string>;
1116
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { Server } from 'socket.io';
2+
import { Socket, io } from 'socket.io-client';
3+
import {
4+
ConnectedSocket,
5+
defaultMetadataArgsStorage,
6+
Middleware,
7+
MiddlewareInterface,
8+
OnConnect,
9+
SocketController,
10+
useContainer,
11+
useSocketServer,
12+
} from '../../src';
13+
import { Container, Service } from 'typedi';
14+
import { waitForEvent } from '../utilities/waitForEvent';
15+
import { createServer, Server as HttpServer } from 'http';
16+
17+
describe('Middlewares', () => {
18+
const PORT = 8080;
19+
const PATH_FOR_CLIENT = `ws://localhost:${PORT}`;
20+
21+
let httpServer: HttpServer;
22+
let wsApp: Server;
23+
let wsClient: Socket;
24+
let testResult;
25+
26+
beforeEach(done => {
27+
httpServer = createServer();
28+
wsApp = new Server(httpServer, {
29+
cors: {
30+
origin: '*',
31+
},
32+
});
33+
httpServer.listen(PORT, () => {
34+
done();
35+
});
36+
useContainer(Container);
37+
});
38+
39+
afterEach(() => {
40+
testResult = undefined;
41+
42+
Container.reset();
43+
wsClient.close();
44+
wsClient = null;
45+
defaultMetadataArgsStorage().reset();
46+
return new Promise(resolve => {
47+
if (wsApp)
48+
return wsApp.close(() => {
49+
resolve(null);
50+
});
51+
resolve(null);
52+
});
53+
});
54+
55+
it('no namespace', async () => {
56+
@Middleware()
57+
@Service()
58+
class GlobalMiddleware implements MiddlewareInterface {
59+
use(socket: any, next: (err?: any) => any): any {
60+
testResult = 'global middleware';
61+
next();
62+
}
63+
}
64+
65+
@SocketController()
66+
@Service()
67+
class Controller {
68+
@OnConnect()
69+
connected(@ConnectedSocket() socket: Socket) {
70+
socket.emit('connected');
71+
}
72+
}
73+
74+
useSocketServer(wsApp, {
75+
middlewares: [GlobalMiddleware],
76+
controllers: [Controller],
77+
});
78+
wsClient = io(PATH_FOR_CLIENT, { reconnection: false, timeout: 5000, forceNew: true });
79+
80+
await waitForEvent(wsClient, 'connected');
81+
expect(testResult).toEqual('global middleware');
82+
});
83+
84+
describe('string namespace', () => {
85+
it('correct namespace', async () => {
86+
@Middleware({ namespace: '/string' })
87+
@Service()
88+
class StringNamespaceMiddleware implements MiddlewareInterface {
89+
use(socket: any, next: (err?: any) => any): any {
90+
testResult = 'string middleware';
91+
next();
92+
}
93+
}
94+
95+
@SocketController('/string')
96+
@Service()
97+
class StringNamespaceController {
98+
@OnConnect()
99+
connected(@ConnectedSocket() socket: Socket) {
100+
socket.emit('connected');
101+
}
102+
}
103+
104+
useSocketServer(wsApp, {
105+
middlewares: [StringNamespaceMiddleware],
106+
controllers: [StringNamespaceController],
107+
});
108+
wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true });
109+
110+
await waitForEvent(wsClient, 'connected');
111+
expect(testResult).toEqual('string middleware');
112+
});
113+
114+
it('incorrect namespace', async () => {
115+
@Middleware({ namespace: '/string' })
116+
@Service()
117+
class StringNamespaceMiddleware implements MiddlewareInterface {
118+
use(socket: any, next: (err?: any) => any): any {
119+
testResult = 'string middleware';
120+
next();
121+
}
122+
}
123+
124+
@SocketController('/string2')
125+
@Service()
126+
class String2NamespaceController {
127+
@OnConnect()
128+
connected(@ConnectedSocket() socket: Socket) {
129+
socket.emit('connected');
130+
}
131+
}
132+
133+
useSocketServer(wsApp, {
134+
middlewares: [StringNamespaceMiddleware],
135+
controllers: [String2NamespaceController],
136+
});
137+
wsClient = io(PATH_FOR_CLIENT + '/string2', { reconnection: false, timeout: 5000, forceNew: true });
138+
139+
await waitForEvent(wsClient, 'connected');
140+
expect(testResult).toEqual(undefined);
141+
});
142+
});
143+
144+
describe('regexp namespace', () => {
145+
it('correct namespace', async () => {
146+
@Middleware({ namespace: /^\/dynamic-\d+$/ })
147+
@Service()
148+
class RegexpNamespaceMiddleware implements MiddlewareInterface {
149+
use(socket: any, next: (err?: any) => any): any {
150+
testResult = socket.nsp.name;
151+
next();
152+
}
153+
}
154+
155+
@SocketController(/^\/dynamic-\d+$/)
156+
@Service()
157+
class RegexpNamespaceController {
158+
@OnConnect()
159+
connected(@ConnectedSocket() socket: Socket) {
160+
socket.emit('connected');
161+
}
162+
}
163+
164+
useSocketServer(wsApp, {
165+
middlewares: [RegexpNamespaceMiddleware],
166+
controllers: [RegexpNamespaceController],
167+
});
168+
wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true });
169+
170+
await waitForEvent(wsClient, 'connected');
171+
expect(testResult).toEqual('/dynamic-1');
172+
});
173+
174+
it('incorrect namespace', async () => {
175+
@Middleware({ namespace: /^\/dynamic-\s+$/ })
176+
@Service()
177+
class RegexpNamespaceMiddleware implements MiddlewareInterface {
178+
use(socket: any, next: (err?: any) => any): any {
179+
testResult = socket.nsp.name;
180+
next();
181+
}
182+
}
183+
184+
@SocketController(/^\/dynamic-\d+$/)
185+
@Service()
186+
class RegexpNamespaceController {
187+
@OnConnect()
188+
connected(@ConnectedSocket() socket: Socket) {
189+
socket.emit('connected');
190+
}
191+
}
192+
193+
useSocketServer(wsApp, {
194+
middlewares: [RegexpNamespaceMiddleware],
195+
controllers: [RegexpNamespaceController],
196+
});
197+
wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true });
198+
199+
await waitForEvent(wsClient, 'connected');
200+
expect(testResult).toEqual(undefined);
201+
});
202+
});
203+
204+
describe('array namespace', () => {
205+
it('correct namespace', async () => {
206+
@Middleware({ namespace: [/^\/dynamic-\d+$/] })
207+
@Service()
208+
class RegexpArrayNamespaceMiddleware implements MiddlewareInterface {
209+
use(socket: any, next: (err?: any) => any): any {
210+
testResult = socket.nsp.name;
211+
next();
212+
}
213+
}
214+
215+
@SocketController(/^\/dynamic-\d+$/)
216+
@Service()
217+
class RegexpNamespaceController {
218+
@OnConnect()
219+
connected(@ConnectedSocket() socket: Socket) {
220+
socket.emit('connected');
221+
}
222+
}
223+
224+
useSocketServer(wsApp, {
225+
middlewares: [RegexpArrayNamespaceMiddleware],
226+
controllers: [RegexpNamespaceController],
227+
});
228+
wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true });
229+
230+
await waitForEvent(wsClient, 'connected');
231+
expect(testResult).toEqual('/dynamic-1');
232+
});
233+
234+
it('incorrect namespace', async () => {
235+
@Middleware({ namespace: [/^\/dynamic-\s+$/] })
236+
@Service()
237+
class RegexpArrayNamespaceMiddleware implements MiddlewareInterface {
238+
use(socket: any, next: (err?: any) => any): any {
239+
testResult = socket.nsp.name;
240+
next();
241+
}
242+
}
243+
244+
@SocketController(/^\/dynamic-\d+$/)
245+
@Service()
246+
class RegexpNamespaceController {
247+
@OnConnect()
248+
connected(@ConnectedSocket() socket: Socket) {
249+
socket.emit('connected');
250+
}
251+
}
252+
253+
useSocketServer(wsApp, {
254+
middlewares: [RegexpArrayNamespaceMiddleware],
255+
controllers: [RegexpNamespaceController],
256+
});
257+
wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true });
258+
259+
await waitForEvent(wsClient, 'connected');
260+
expect(testResult).toEqual(undefined);
261+
});
262+
});
263+
});

test/utilities/waitForEvent.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Socket } from 'socket.io-client';
2+
import { Server } from 'socket.io';
3+
4+
export const waitForEvent = (socket: Socket | Server, event: string): Promise<unknown> => {
5+
return new Promise(resolve => {
6+
socket.on(event, data => {
7+
resolve(data);
8+
});
9+
});
10+
};

0 commit comments

Comments
 (0)