Spaces:
Runtime error
Runtime error
File size: 7,688 Bytes
23ac194 | 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 242 243 | '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')
const statusCodeReg = /HTTP\/1.1 (\d+)/u
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?.preClose) {
if (typeof opts.preClose !== 'function') {
return next(new Error('invalid preClose function'))
}
preClose = opts.preClose
}
if (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(statusCodeReg.exec(chunk.toString())[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 TypeError('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
|