@fastify/http-proxy
Advanced tools
Comparing version 9.3.0 to 9.4.0
@@ -14,3 +14,3 @@ 'use strict' | ||
origin.get('/redirect', async (request, reply) => { | ||
return reply.redirect(302, 'https://fastify.io') | ||
return reply.redirect(302, 'https://fastify.dev') | ||
}) | ||
@@ -17,0 +17,0 @@ |
159
index.js
@@ -13,2 +13,3 @@ 'use strict' | ||
const kWsHead = Symbol('wsHead') | ||
const kWsUpgradeListener = Symbol('wsUpgradeListener') | ||
@@ -78,5 +79,27 @@ function liftErrorCode (code) { | ||
function handleUpgrade (fastify, 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) | ||
rawResponse.assignSocket(socket) | ||
fastify.routing(rawRequest, rawResponse) | ||
rawResponse.on('finish', () => { | ||
socket.destroy() | ||
}) | ||
} | ||
class WebSocketProxy { | ||
constructor (fastify, wsServerOptions) { | ||
constructor (fastify, { wsServerOptions, wsClientOptions, upstream, wsUpstream, replyOptions: { getUpstream } = {} }) { | ||
this.logger = fastify.log | ||
this.wsClientOptions = { | ||
rewriteRequestHeaders: defaultWsHeadersRewrite, | ||
headers: {}, | ||
...wsClientOptions | ||
} | ||
this.upstream = convertUrlToWebSocket(upstream) | ||
this.wsUpstream = wsUpstream ? convertUrlToWebSocket(wsUpstream) : '' | ||
this.getUpstream = getUpstream | ||
@@ -88,19 +111,11 @@ const wss = new WebSocket.Server({ | ||
fastify.server.on('upgrade', (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 | ||
if (!fastify.server[kWsUpgradeListener]) { | ||
fastify.server[kWsUpgradeListener] = (rawRequest, socket, head) => | ||
handleUpgrade(fastify, rawRequest, socket, head) | ||
fastify.server.on('upgrade', fastify.server[kWsUpgradeListener]) | ||
} | ||
const rawResponse = new ServerResponse(rawRequest) | ||
rawResponse.assignSocket(socket) | ||
fastify.routing(rawRequest, rawResponse) | ||
rawResponse.on('finish', () => { | ||
socket.destroy() | ||
}) | ||
}) | ||
this.handleUpgrade = (request, cb) => { | ||
this.handleUpgrade = (request, dest, cb) => { | ||
wss.handleUpgrade(request.raw, request.raw[kWs], request.raw[kWsHead], (socket) => { | ||
this.handleConnection(socket, request) | ||
this.handleConnection(socket, request, dest) | ||
cb() | ||
@@ -140,41 +155,29 @@ }) | ||
addUpstream (prefix, rewritePrefix, upstream, wsUpstream, wsClientOptions) { | ||
this.prefixList.push({ | ||
prefix: new URL(prefix, 'ws://127.0.0.1').pathname, | ||
rewritePrefix, | ||
upstream: convertUrlToWebSocket(upstream), | ||
wsUpstream: wsUpstream ? convertUrlToWebSocket(wsUpstream) : '', | ||
wsClientOptions | ||
}) | ||
findUpstream (request, dest) { | ||
const search = new URL(request.url, 'ws://127.0.0.1').search | ||
// sort by decreasing prefix length, so that findUpstreamUrl() does longest prefix match | ||
this.prefixList.sort((a, b) => b.prefix.length - a.prefix.length) | ||
} | ||
if (typeof this.wsUpstream === 'string' && this.wsUpstream !== '') { | ||
const target = new URL(this.wsUpstream) | ||
target.search = search | ||
return target | ||
} | ||
findUpstream (request) { | ||
const source = new URL(request.url, 'ws://127.0.0.1') | ||
for (const { prefix, rewritePrefix, upstream, wsUpstream, wsClientOptions } of this.prefixList) { | ||
if (wsUpstream) { | ||
const target = new URL(wsUpstream) | ||
target.search = source.search | ||
return { target, wsClientOptions } | ||
} | ||
if (source.pathname.startsWith(prefix)) { | ||
const target = new URL(source.pathname.replace(prefix, rewritePrefix), upstream) | ||
target.search = source.search | ||
return { target, wsClientOptions } | ||
} | ||
if (typeof this.upstream === 'string' && this.upstream !== '') { | ||
const target = new URL(dest, this.upstream) | ||
target.search = search | ||
return target | ||
} | ||
const upstream = this.getUpstream(request, '') | ||
const target = new URL(dest, upstream) | ||
/* istanbul ignore next */ | ||
throw new Error(`no upstream found for ${request.url}. this should not happened. Please report to https://github.com/fastify/fastify-http-proxy`) | ||
target.protocol = upstream.indexOf('http:') === 0 ? 'ws:' : 'wss' | ||
target.search = search | ||
return target | ||
} | ||
handleConnection (source, request) { | ||
const upstream = this.findUpstream(request) | ||
const { target: url, wsClientOptions } = upstream | ||
const rewriteRequestHeaders = wsClientOptions?.rewriteRequestHeaders || defaultWsHeadersRewrite | ||
const headersToRewrite = wsClientOptions?.headers || {} | ||
handleConnection (source, request, dest) { | ||
const url = this.findUpstream(request, dest) | ||
const rewriteRequestHeaders = this.wsClientOptions.rewriteRequestHeaders | ||
const headersToRewrite = this.wsClientOptions.headers | ||
@@ -187,3 +190,3 @@ const subprotocols = [] | ||
const headers = rewriteRequestHeaders(headersToRewrite, request) | ||
const optionsWs = { ...(wsClientOptions || {}), headers } | ||
const optionsWs = { ...this.wsClientOptions, headers } | ||
@@ -203,37 +206,2 @@ const target = new WebSocket(url, subprotocols, optionsWs) | ||
const httpWss = new WeakMap() // http.Server => WebSocketProxy | ||
function setupWebSocketProxy (fastify, options, rewritePrefix) { | ||
let wsProxy = httpWss.get(fastify.server) | ||
if (!wsProxy) { | ||
wsProxy = new WebSocketProxy(fastify, options.wsServerOptions) | ||
httpWss.set(fastify.server, wsProxy) | ||
} | ||
if ( | ||
(typeof options.wsUpstream === 'string' && options.wsUpstream !== '') || | ||
(typeof options.upstream === 'string' && options.upstream !== '') | ||
) { | ||
wsProxy.addUpstream( | ||
fastify.prefix, | ||
rewritePrefix, | ||
options.upstream, | ||
options.wsUpstream, | ||
options.wsClientOptions | ||
) | ||
// The else block is validate earlier in the code | ||
} else { | ||
wsProxy.findUpstream = function (request) { | ||
const source = new URL(request.url, 'ws://127.0.0.1') | ||
const upstream = options.replyOptions.getUpstream(request, '') | ||
const target = new URL(source.pathname, upstream) | ||
/* istanbul ignore next */ | ||
target.protocol = upstream.indexOf('http:') === 0 ? 'ws:' : 'wss' | ||
target.search = source.search | ||
return { target, wsClientOptions: options.wsClientOptions } | ||
} | ||
} | ||
return wsProxy | ||
} | ||
function generateRewritePrefix (prefix, opts) { | ||
@@ -312,3 +280,3 @@ let rewritePrefix = opts.rewritePrefix || (opts.upstream ? new URL(opts.upstream).pathname : '/') | ||
if (opts.websocket) { | ||
wsProxy = setupWebSocketProxy(fastify, opts, rewritePrefix) | ||
wsProxy = new WebSocketProxy(fastify, opts) | ||
} | ||
@@ -331,12 +299,2 @@ | ||
function handler (request, reply) { | ||
if (request.raw[kWs]) { | ||
reply.hijack() | ||
try { | ||
wsProxy.handleUpgrade(request, noop) | ||
} catch (err) { | ||
/* istanbul ignore next */ | ||
request.log.warn({ err }, 'websocket proxy error') | ||
} | ||
return | ||
} | ||
const { path, queryParams } = extractUrlComponents(request.url) | ||
@@ -361,2 +319,13 @@ let dest = path | ||
} | ||
if (request.raw[kWs]) { | ||
reply.hijack() | ||
try { | ||
wsProxy.handleUpgrade(request, dest || '/', noop) | ||
} catch (err) { | ||
/* istanbul ignore next */ | ||
request.log.warn({ err }, 'websocket proxy error') | ||
} | ||
return | ||
} | ||
reply.from(dest || '/', replyOpts) | ||
@@ -363,0 +332,0 @@ } |
{ | ||
"name": "@fastify/http-proxy", | ||
"version": "9.3.0", | ||
"version": "9.4.0", | ||
"description": "proxy http requests, for Fastify", | ||
@@ -51,3 +51,3 @@ "main": "index.js", | ||
"tap": "^16.0.0", | ||
"tsd": "^0.29.0", | ||
"tsd": "^0.30.0", | ||
"typescript": "^5.0.2", | ||
@@ -54,0 +54,0 @@ "why-is-node-running": "^2.2.2" |
@@ -8,3 +8,3 @@ # @fastify/http-proxy | ||
Proxy your HTTP requests to another server, with hooks. | ||
This [`fastify`](https://www.fastify.io) plugin forwards all requests | ||
This [`fastify`](https://fastify.dev) plugin forwards all requests | ||
received with a given prefix (or none) to an upstream. All Fastify hooks are still applied. | ||
@@ -167,3 +167,3 @@ | ||
if (request.body.method === 'invalid_method') { | ||
reply.code(400).send({ message: 'payload contains invalid method' }); | ||
return reply.code(400).send({ message: 'payload contains invalid method' }); | ||
} | ||
@@ -177,3 +177,3 @@ }, | ||
An object accessible within the `preHandler` via `reply.context.config`. | ||
See [Config](https://www.fastify.io/docs/v4.8.x/Reference/Routes/#config) in the Fastify | ||
See [Config](https://fastify.dev/docs/v4.8.x/Reference/Routes/#config) in the Fastify | ||
documentation for information on this option. Note: this is merged with other | ||
@@ -225,5 +225,2 @@ configuration passed to the route. | ||
In case multiple websocket proxies are attached to the same HTTP server at different paths. | ||
In this case, only the first `wsServerOptions` is applied. | ||
### `wsClientOptions` | ||
@@ -230,0 +227,0 @@ |
@@ -22,3 +22,3 @@ 'use strict' | ||
origin.get('/redirect', async (request, reply) => { | ||
return reply.redirect(302, 'https://fastify.io') | ||
return reply.redirect(302, 'https://fastify.dev') | ||
}) | ||
@@ -125,3 +125,3 @@ | ||
) | ||
t.equal(location, 'https://fastify.io') | ||
t.equal(location, 'https://fastify.dev') | ||
t.equal(statusCode, 302) | ||
@@ -152,3 +152,3 @@ }) | ||
) | ||
t.equal(location, 'https://fastify.io') | ||
t.equal(location, 'https://fastify.dev') | ||
t.equal(statusCode, 302) | ||
@@ -155,0 +155,0 @@ }) |
@@ -477,1 +477,101 @@ 'use strict' | ||
}) | ||
test('multiple websocket upstreams with host constraints', async (t) => { | ||
t.plan(4) | ||
const server = Fastify() | ||
for (const name of ['foo', 'bar']) { | ||
const origin = createServer() | ||
const wss = new WebSocket.Server({ server: origin }) | ||
t.teardown(wss.close.bind(wss)) | ||
t.teardown(origin.close.bind(origin)) | ||
wss.once('connection', (ws) => { | ||
ws.once('message', message => { | ||
t.equal(message.toString(), `hello ${name}`) | ||
// echo | ||
ws.send(message) | ||
}) | ||
}) | ||
await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' }) | ||
server.register(proxy, { | ||
upstream: `ws://127.0.0.1:${origin.address().port}`, | ||
websocket: true, | ||
constraints: { host: name } | ||
}) | ||
} | ||
await server.listen({ port: 0, host: '127.0.0.1' }) | ||
t.teardown(server.close.bind(server)) | ||
const wsClients = [] | ||
for (const name of ['foo', 'bar']) { | ||
const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, { headers: { host: name } }) | ||
await once(ws, 'open') | ||
ws.send(`hello ${name}`) | ||
const [reply] = await once(ws, 'message') | ||
t.equal(reply.toString(), `hello ${name}`) | ||
wsClients.push(ws) | ||
} | ||
await Promise.all([ | ||
...wsClients.map(ws => once(ws, 'close')), | ||
server.close() | ||
]) | ||
}) | ||
test('multiple websocket upstreams with distinct server options', async (t) => { | ||
t.plan(4) | ||
const server = Fastify() | ||
for (const name of ['foo', 'bar']) { | ||
const origin = createServer() | ||
const wss = new WebSocket.Server({ server: origin }) | ||
t.teardown(wss.close.bind(wss)) | ||
t.teardown(origin.close.bind(origin)) | ||
wss.once('connection', (ws, req) => { | ||
t.equal(req.url, `/?q=${name}`) | ||
ws.once('message', message => { | ||
// echo | ||
ws.send(message) | ||
}) | ||
}) | ||
await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' }) | ||
server.register(proxy, { | ||
upstream: `ws://127.0.0.1:${origin.address().port}`, | ||
websocket: true, | ||
constraints: { host: name }, | ||
wsServerOptions: { | ||
verifyClient: ({ req }) => { | ||
t.equal(req.url, `/?q=${name}`) | ||
return true | ||
} | ||
} | ||
}) | ||
} | ||
await server.listen({ port: 0, host: '127.0.0.1' }) | ||
t.teardown(server.close.bind(server)) | ||
const wsClients = [] | ||
for (const name of ['foo', 'bar']) { | ||
const ws = new WebSocket( | ||
`ws://127.0.0.1:${server.server.address().port}/?q=${name}`, | ||
{ headers: { host: name } } | ||
) | ||
await once(ws, 'open') | ||
ws.send(`hello ${name}`) | ||
await once(ws, 'message') | ||
wsClients.push(ws) | ||
} | ||
await Promise.all([ | ||
...wsClients.map(ws => once(ws, 'close')), | ||
server.close() | ||
]) | ||
}) |
79975
2087
251