From 02d9b43c770aa16bc44470edecfaeb7c17985016 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 19 Feb 2021 18:50:00 +0100 Subject: [PATCH] Merge pull request from GHSA-c4qr-gmr9-v23w * added failing test * fix first test * Fixed 2/4 * three out of four * four out of four --- index.js | 55 ++++++++++----- package.json | 1 + test/test.js | 2 +- test/ws-prefix-rewrite.js | 145 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 test/ws-prefix-rewrite.js diff --git a/index.js b/index.js index ecbae80..d096890 100644 --- a/index.js +++ b/index.js @@ -53,22 +53,8 @@ function proxyWebSockets (source, target) { target.on('unexpected-response', () => close(1011, 'unexpected response')) } -function createWebSocketUrl (options, request) { - const source = new URL(request.url, 'http://127.0.0.1') - - const target = new URL( - options.rewritePrefix || options.prefix || source.pathname, - options.upstream - ) - - target.search = source.search - - return target -} - -function setupWebSocketProxy (fastify, options) { +function setupWebSocketProxy (fastify, options, rewritePrefix) { const server = new WebSocket.Server({ - path: options.prefix, server: fastify.server, ...options.wsServerOptions }) @@ -93,13 +79,46 @@ function setupWebSocketProxy (fastify, options) { }) server.on('connection', (source, request) => { - const url = createWebSocketUrl(options, request) + if (fastify.prefix && !request.url.startsWith(fastify.prefix)) { + fastify.log.debug({ url: request.url }, 'not matching prefix') + source.close() + return + } + + const url = createWebSocketUrl(request) const target = new WebSocket(url, options.wsClientOptions) fastify.log.debug({ url: url.href }, 'proxy websocket') proxyWebSockets(source, target) }) + + function createWebSocketUrl (request) { + const source = new URL(request.url, 'http://127.0.0.1') + + const target = new URL( + source.pathname.replace(fastify.prefix, rewritePrefix), + options.upstream + ) + + target.search = source.search + + return target + } +} + +function generateRewritePrefix (prefix, opts) { + if (!prefix) { + return '' + } + + let rewritePrefix = opts.rewritePrefix || new URL(opts.upstream).pathname + + if (!prefix.endsWith('/') && rewritePrefix.endsWith('/')) { + rewritePrefix = rewritePrefix.slice(0, -1) + } + + return rewritePrefix } async function httpProxy (fastify, opts) { @@ -108,7 +127,7 @@ async function httpProxy (fastify, opts) { } const preHandler = opts.preHandler || opts.beforeHandler - const rewritePrefix = opts.rewritePrefix || '' + const rewritePrefix = generateRewritePrefix(fastify.prefix, opts) const fromOpts = Object.assign({}, opts) fromOpts.base = opts.upstream @@ -164,7 +183,7 @@ async function httpProxy (fastify, opts) { } if (opts.websocket) { - setupWebSocketProxy(fastify, opts) + setupWebSocketProxy(fastify, opts, rewritePrefix) } } diff --git a/package.json b/package.json index f7b975a..5549c7d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "express-http-proxy": "^1.6.2", "fast-proxy": "^1.7.0", "fastify": "^3.0.0", + "fastify-websocket": "^3.0.0", "got": "^11.5.1", "http-errors": "^1.8.0", "http-proxy": "^1.17.0", diff --git a/test/test.js b/test/test.js index 378af8b..035398e 100644 --- a/test/test.js +++ b/test/test.js @@ -179,7 +179,7 @@ async function run () { async preHandler (request, reply) { t.deepEqual(reply.context.config, { foo: 'bar', - url: '/*', + url: '/', method: [ 'DELETE', 'GET', diff --git a/test/ws-prefix-rewrite.js b/test/ws-prefix-rewrite.js new file mode 100644 index 0000000..ad35302 --- /dev/null +++ b/test/ws-prefix-rewrite.js @@ -0,0 +1,145 @@ +'use strict' + +const t = require('tap') +const { once } = require('events') + +const Fastify = require('fastify') +const fastifyWebSocket = require('fastify-websocket') +const proxy = require('..') +const WebSocket = require('ws') +const got = require('got') + +const level = 'warn' + +async function proxyServer (t, backendURL, backendPath, proxyOptions, wrapperOptions) { + const frontend = Fastify({ logger: { level } }) + const registerProxy = async fastify => { + fastify.register(proxy, { + upstream: backendURL + backendPath, + ...proxyOptions + }) + } + + t.comment('starting proxy to ' + backendURL + backendPath) + + if (wrapperOptions) { + await frontend.register(registerProxy, wrapperOptions) + } else { + await registerProxy(frontend) + } + + return [frontend, await frontend.listen(0)] +} + +async function processRequest (t, frontendURL, path, expected) { + const url = new URL(path, frontendURL) + t.comment('ws connecting to ' + url.toString()) + const ws = new WebSocket(url) + let wsResult, gotResult + + try { + await once(ws, 'open') + t.pass('socket connected') + + const [buf] = await Promise.race([once(ws, 'message'), once(ws, 'close')]) + if (buf instanceof Buffer) { + wsResult = buf.toString() + } else { + t.comment('websocket closed') + wsResult = 'error' + } + } catch (e) { + wsResult = 'error' + ws.terminate() + } + + try { + const result = await got(url) + gotResult = result.body + } catch (e) { + gotResult = 'error' + } + + t.is(wsResult, expected) + t.is(gotResult, expected) +} + +async function handleProxy (info, { backendPath, proxyOptions, wrapperOptions }, expected, ...paths) { + t.test(info, async function (t) { + const backend = Fastify({ logger: { level } }) + await backend.register(fastifyWebSocket) + + backend.get('/*', { + handler: (req, reply) => { + reply.send(req.url) + }, + wsHandler: (conn, req) => { + conn.write(req.url) + conn.end() + } + }) + + t.teardown(() => backend.close()) + + const backendURL = await backend.listen(0) + + const [frontend, frontendURL] = await proxyServer(t, backendURL, backendPath, proxyOptions, wrapperOptions) + + t.teardown(() => frontend.close()) + + for (const path of paths) { + await processRequest(t, frontendURL, path, expected(path)) + } + + t.end() + }) +} + +handleProxy( + 'no prefix to `/`', + { + backendPath: '/', + proxyOptions: { websocket: true } + }, + path => path, + '/', + '/pub', + '/pub/' +) + +handleProxy( + '`/pub/` to `/`', + { + backendPath: '/', + proxyOptions: { websocket: true, prefix: '/pub/' } + }, + path => path.startsWith('/pub/') ? path.replace('/pub/', '/') : 'error', + '/', + '/pub/', + '/pub/test' +) + +handleProxy( + '`/pub/` to `/public/`', + { + backendPath: '/public/', + proxyOptions: { websocket: true, prefix: '/pub/' } + }, + path => path.startsWith('/pub/') ? path.replace('/pub/', '/public/') : 'error', + '/', + '/pub/', + '/pub/test' +) + +handleProxy( + 'wrapped `/pub/` to `/public/`', + { + backendPath: '/public/', + proxyOptions: { websocket: true }, + wrapperOptions: { prefix: '/pub/' } + }, + path => path.startsWith('/pub/') ? path.replace('/pub/', '/public/') : 'error', + '/', + '/pub/', + '/pub/test' +)