-
-
Notifications
You must be signed in to change notification settings - Fork 75
/
index.js
241 lines (196 loc) · 7.49 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
'use strict'
const { ServerResponse } = require('node:http')
const { PassThrough } = require('node:stream')
const { randomBytes } = require('node:crypto')
const fp = require('fastify-plugin')
const WebSocket = require('ws')
const Duplexify = require('duplexify')
const kWs = Symbol('ws-socket')
const kWsHead = Symbol('ws-head')
function fastifyWebsocket (fastify, opts, next) {
fastify.decorateRequest('ws', null)
let errorHandler = defaultErrorHandler
if (opts.errorHandler) {
if (typeof opts.errorHandler !== 'function') {
return next(new Error('invalid errorHandler function'))
}
errorHandler = opts.errorHandler
}
let preClose = defaultPreClose
if (opts && opts.preClose) {
if (typeof opts.preClose !== 'function') {
return next(new Error('invalid preClose function'))
}
preClose = opts.preClose
}
if (opts.options && opts.options.noServer) {
return next(new Error("fastify-websocket doesn't support the ws noServer option. If you want to create a websocket server detatched from fastify, use the ws library directly."))
}
const wssOptions = Object.assign({ noServer: true }, opts.options)
if (wssOptions.path) {
fastify.log.warn('ws server path option shouldn\'t be provided, use a route instead')
}
// We always handle upgrading ourselves in this library so that we can dispatch through the fastify stack before actually upgrading
// For this reason, we run the WebSocket.Server in noServer mode, and prevent the user from passing in a http.Server instance for it to attach to.
// Usually, we listen to the upgrade event of the `fastify.server`, but we do still support this server option by just listening to upgrades on it if passed.
const websocketListenServer = wssOptions.server || fastify.server
delete wssOptions.server
const wss = new WebSocket.Server(wssOptions)
fastify.decorate('websocketServer', wss)
async function injectWS (path = '/', upgradeContext = {}) {
const server2Client = new PassThrough()
const client2Server = new PassThrough()
const serverStream = new Duplexify(server2Client, client2Server)
const clientStream = new Duplexify(client2Server, server2Client)
const ws = new WebSocket(null, undefined, { isServer: false })
const head = Buffer.from([])
let resolve, reject
const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject })
ws.on('open', () => {
clientStream.removeListener('data', onData)
resolve(ws)
})
const onData = (chunk) => {
if (chunk.toString().includes('HTTP/1.1 101 Switching Protocols')) {
ws._isServer = false
ws.setSocket(clientStream, head, { maxPayload: 0 })
} else {
clientStream.removeListener('data', onData)
const statusCode = Number(chunk.toString().match(/HTTP\/1.1 (\d+)/)[1])
reject(new Error('Unexpected server response: ' + statusCode))
}
}
clientStream.on('data', onData)
const req = {
...upgradeContext,
method: 'GET',
headers: {
...upgradeContext.headers,
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-version': 13,
'sec-websocket-key': randomBytes(16).toString('base64')
},
httpVersion: '1.1',
url: path,
[kWs]: serverStream,
[kWsHead]: head
}
websocketListenServer.emit('upgrade', req, req[kWs], req[kWsHead])
return promise
}
fastify.decorate('injectWS', injectWS)
function onUpgrade (rawRequest, socket, head) {
// Save a reference to the socket and then dispatch the request through the normal fastify router so that it will invoke hooks and then eventually a route handler that might upgrade the socket.
rawRequest[kWs] = socket
rawRequest[kWsHead] = head
const rawResponse = new ServerResponse(rawRequest)
try {
rawResponse.assignSocket(socket)
fastify.routing(rawRequest, rawResponse)
} catch (err) {
fastify.log.warn({ err }, 'websocket upgrade failed')
}
}
websocketListenServer.on('upgrade', onUpgrade)
const handleUpgrade = (rawRequest, callback) => {
wss.handleUpgrade(rawRequest, rawRequest[kWs], rawRequest[kWsHead], (socket) => {
wss.emit('connection', socket, rawRequest)
socket.on('error', (error) => {
fastify.log.error(error)
})
callback(socket)
})
}
fastify.addHook('onRequest', (request, reply, done) => { // this adds req.ws to the Request object
if (request.raw[kWs]) {
request.ws = true
} else {
request.ws = false
}
done()
})
fastify.addHook('onResponse', (request, reply, done) => {
if (request.ws) {
request.raw[kWs].destroy()
}
done()
})
fastify.addHook('onRoute', routeOptions => {
let isWebsocketRoute = false
let wsHandler = routeOptions.wsHandler
let handler = routeOptions.handler
if (routeOptions.websocket || routeOptions.wsHandler) {
if (routeOptions.method === 'HEAD') {
return
} else if (routeOptions.method !== 'GET') {
throw new Error('websocket handler can only be declared in GET method')
}
isWebsocketRoute = true
if (routeOptions.websocket) {
wsHandler = routeOptions.handler
handler = function (_, reply) {
reply.code(404).send()
}
}
if (typeof wsHandler !== 'function') {
throw new Error('invalid wsHandler function')
}
}
// we always override the route handler so we can close websocket connections to routes to handlers that don't support websocket connections
// This is not an arrow function to fetch the encapsulated this
routeOptions.handler = function (request, reply) {
// within the route handler, we check if there has been a connection upgrade by looking at request.raw[kWs]. we need to dispatch the normal HTTP handler if not, and hijack to dispatch the websocket handler if so
if (request.raw[kWs]) {
reply.hijack()
handleUpgrade(request.raw, socket => {
let result
try {
if (isWebsocketRoute) {
result = wsHandler.call(this, socket, request)
} else {
result = noHandle.call(this, socket, request)
}
} catch (err) {
return errorHandler.call(this, err, socket, request, reply)
}
if (result && typeof result.catch === 'function') {
result.catch(err => errorHandler.call(this, err, socket, request, reply))
}
})
} else {
return handler.call(this, request, reply)
}
}
})
// Fastify is missing a pre-close event, or the ability to
// add a hook before the server.close call. We need to resort
// to monkeypatching for now.
fastify.addHook('preClose', preClose)
function defaultPreClose (done) {
const server = this.websocketServer
if (server.clients) {
for (const client of server.clients) {
client.close()
}
}
fastify.server.removeListener('upgrade', onUpgrade)
server.close(done)
done()
}
function noHandle (socket, rawRequest) {
this.log.info({ path: rawRequest.url }, 'closed incoming websocket connection for path with no websocket handler')
socket.close()
}
function defaultErrorHandler (error, socket, request) {
request.log.error(error)
socket.terminate()
}
next()
}
module.exports = fp(fastifyWebsocket, {
fastify: '5.x',
name: '@fastify/websocket'
})
module.exports.default = fastifyWebsocket
module.exports.fastifyWebsocket = fastifyWebsocket