Skip to content

Commit 7934f06

Browse files
authored
Merge pull request #71 from BackendStack21/ws-support
Introducing WebSocket Support
2 parents cb3f3d1 + 936495e commit 7934f06

File tree

11 files changed

+311
-12
lines changed

11 files changed

+311
-12
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ typings/
6060
# next.js build output
6161
.next
6262

63-
.DS_Store
63+
.DS_Store

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ Example output:
196196
```
197197
> NOTE: Please see `docs` configuration entry explained above.
198198
199+
### WebSockets support
200+
WebSockets proxying is supported since `v3.1.0`. Main considerations:
201+
- WebSockets middlewares are not supported.
202+
- WebSocketRoute configuration definition:
203+
```ts
204+
interface WebSocketRoute {
205+
proxyType: 'websocket';
206+
// https://github.com/faye/faye-websocket-node#initialization-options
207+
proxyConfig?: {};
208+
prefix: string;
209+
target: string;
210+
// https://github.com/faye/faye-websocket-node#subprotocol-negotiation
211+
subProtocols?: [];
212+
hooks?: WebSocketHooks;
213+
}
214+
215+
interface WebSocketHooks {
216+
onOpen?: (ws: any, searchParams: URLSearchParams) => Promise<void>;
217+
}
218+
```
219+
- The `/` route prefix is considered the default route.
220+
221+
#### Configuration example:
222+
```js
223+
gateway({
224+
routes: [{
225+
// ... other HTTP or WebSocket routes
226+
}, {
227+
proxyType: 'websocket',
228+
prefix: '/echo',
229+
target: 'ws://ws.ifelse.io'
230+
}]
231+
}).start(PORT)
232+
```
233+
199234
## Timeouts and Unavailability
200235
We can restrict requests timeouts globally or at service level using the `timeout` configuration.
201236

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
config:
2+
target: "ws://localhost:8080/echo"
3+
phases:
4+
- duration: 15
5+
arrivalRate: 20
6+
rampTo: 500
7+
name: "Ramping up the load"
8+
scenarios:
9+
- engine: "ws"
10+
flow:
11+
- send: 'echo me'

benchmark/websocket/echo.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict'
2+
3+
const gateway = require('../../index')
4+
const WebSocket = require('faye-websocket')
5+
const http = require('http')
6+
7+
gateway({
8+
routes: [{
9+
proxyType: 'websocket',
10+
prefix: '/echo',
11+
target: 'ws://127.0.0.1:3000'
12+
}]
13+
}).start(8080)
14+
15+
const service = http.createServer()
16+
service.on('upgrade', (request, socket, body) => {
17+
if (WebSocket.isWebSocket(request)) {
18+
const ws = new WebSocket(request, socket, body)
19+
20+
ws.on('message', (event) => {
21+
ws.send(event.data)
22+
})
23+
}
24+
})
25+
service.listen(3000)

demos/ws-proxy.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const gateway = require('./../index')
4+
const PORT = process.env.PORT || 8080
5+
6+
gateway({
7+
routes: [{
8+
// ... other HTTP or WebSocket routes
9+
}, {
10+
proxyType: 'websocket',
11+
prefix: '/echo',
12+
target: 'ws://ws.ifelse.io',
13+
hooks: {
14+
onOpen: (ws, searchParams) => {
15+
16+
}
17+
}
18+
}]
19+
}).start(PORT).then(server => {
20+
console.log(`API Gateway listening on ${PORT} port!`)
21+
})

index.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ declare namespace fastgateway {
3333
hooks?: Hooks;
3434
}
3535

36+
interface WebSocketRoute {
37+
proxyType: 'websocket';
38+
proxyConfig?: {}; // https://github.com/faye/faye-websocket-node#initialization-options
39+
prefix: string;
40+
target: string;
41+
subProtocols?: []; // https://github.com/faye/faye-websocket-node#subprotocol-negotiation
42+
hooks?: WebSocketHooks;
43+
}
44+
45+
interface WebSocketHooks {
46+
onOpen?: (ws: any, searchParams: URLSearchParams) => Promise<void>;
47+
}
48+
3649
interface Hooks {
3750
onRequest?: Function,
3851
rewriteHeaders?: Function,
@@ -54,7 +67,7 @@ declare namespace fastgateway {
5467
pathRegex?: string;
5568
timeout?: number;
5669
targetOverride?: string;
57-
routes: Route[];
70+
routes: Route[] | WebSocketRoute[];
5871
}
5972
}
6073

index.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const defaultProxyHandler = (req, res, url, proxy, proxyOpts) => proxy(req, res,
88
const DEFAULT_METHODS = require('restana/libs/methods').filter(method => method !== 'all')
99
const send = require('@polka/send-type')
1010
const PROXY_TYPES = ['http', 'lambda']
11+
const registerWebSocketRoutes = require('./lib/ws-proxy')
1112

1213
const gateway = (opts) => {
1314
const proxyFactory = opts.proxyFactory || defaultProxyFactory
@@ -17,24 +18,30 @@ const gateway = (opts) => {
1718
pathRegex: '/*'
1819
}, opts)
1920

20-
const server = opts.server || restana(opts.restana)
21+
const router = opts.server || restana(opts.restana)
2122

2223
// registering global middlewares
2324
opts.middlewares.forEach(middleware => {
24-
server.use(middleware)
25+
router.use(middleware)
2526
})
2627

2728
// registering services.json
2829
const services = opts.routes.map(route => ({
2930
prefix: route.prefix,
3031
docs: route.docs
3132
}))
32-
server.get('/services.json', (req, res) => {
33+
router.get('/services.json', (req, res) => {
3334
send(res, 200, services)
3435
})
3536

36-
// processing routes
37-
opts.routes.forEach(route => {
37+
// processing websocket routes
38+
registerWebSocketRoutes({
39+
routes: opts.routes.filter(route => route.proxyType === 'websocket'),
40+
server: router.getServer()
41+
})
42+
43+
// processing non-websocket routes
44+
opts.routes.filter(route => route.proxyType !== 'websocket').forEach(route => {
3845
if (undefined === route.prefixRewrite) {
3946
route.prefixRewrite = ''
4047
}
@@ -82,13 +89,13 @@ const gateway = (opts) => {
8289

8390
methods.forEach(method => {
8491
method = method.toLowerCase()
85-
if (server[method]) {
86-
server[method].apply(server, args)
92+
if (router[method]) {
93+
router[method].apply(router, args)
8794
}
8895
})
8996
})
9097

91-
return server
98+
return router
9299
}
93100

94101
const handler = (route, proxy, proxyHandler) => async (req, res, next) => {

lib/default-hooks.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const toArray = require('stream-to-array')
55
const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'
66

77
module.exports = {
8+
websocket: {
9+
onOpenNoOp (ws, searchParams) { }
10+
},
811
lambda: {
912
onRequestNoOp (req, res) { },
1013
onResponse (req, res, response) {

lib/ws-proxy.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const WebSocket = require('faye-websocket')
4+
const { onOpenNoOp } = require('./default-hooks').websocket
5+
6+
module.exports = (config) => {
7+
const { routes, server } = config
8+
9+
const prefix2route = new Map()
10+
for (const route of routes) {
11+
prefix2route.set(route.prefix, route)
12+
}
13+
14+
server.on('upgrade', async (req, socket, body) => {
15+
if (WebSocket.isWebSocket(req)) {
16+
const url = new URL('http://fw' + req.url)
17+
const prefix = url.pathname || '/'
18+
19+
if (prefix2route.has(prefix)) {
20+
const route = prefix2route.get(prefix)
21+
const subProtocols = route.subProtocols || []
22+
route.hooks = route.hooks || {}
23+
const onOpen = route.hooks.onOpen || onOpenNoOp
24+
25+
const client = new WebSocket(req, socket, body, subProtocols)
26+
27+
try {
28+
await onOpen(client, url.searchParams)
29+
30+
const target = route.target + '?' + url.searchParams.toString()
31+
const remote = new WebSocket.Client(target, subProtocols, route.proxyConfig)
32+
33+
client.pipe(remote)
34+
remote.pipe(client)
35+
} catch (err) {
36+
client.close(err.closeEventCode || 4500, err.message)
37+
}
38+
} else {
39+
socket.end()
40+
}
41+
}
42+
})
43+
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"scripts": {
88
"test": "nyc mocha test/*.test.js",
99
"format": "npx standard --fix",
10-
"lint": "npx standard"
10+
"lint": "npx standard",
11+
"ws-bench": "npx artillery run benchmark/websocket/artillery-perf1.yml"
1112
},
1213
"repository": {
1314
"type": "git",
@@ -42,12 +43,14 @@
4243
],
4344
"devDependencies": {
4445
"@types/express": "^4.17.11",
45-
"aws-sdk": "^2.1023.0",
46+
"artillery": "^2.0.0-6",
47+
"aws-sdk": "^2.1037.0",
4648
"chai": "^4.3.4",
4749
"cors": "^2.8.5",
4850
"express": "^4.17.1",
4951
"express-jwt": "^6.1.0",
5052
"express-rate-limit": "^5.5.1",
53+
"faye-websocket": "^0.11.4",
5154
"fg-multiple-hooks": "^1.3.0",
5255
"helmet": "^4.6.0",
5356
"http-lambda-proxy": "^1.1.4",

0 commit comments

Comments
 (0)