Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@fastify/http-proxy

Package Overview
Dependencies
Maintainers
18
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fastify/http-proxy - npm Package Compare versions

Comparing version 8.4.0 to 9.0.0

142

index.js
'use strict'
const From = require('@fastify/reply-from')
const { ServerResponse } = require('http')
const WebSocket = require('ws')
const { convertUrlToWebSocket } = require('./utils')
const fp = require('fastify-plugin')
const httpMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
const urlPattern = /^https?:\/\//
const kWs = Symbol('ws')
const kWsHead = Symbol('wsHead')
function liftErrorCode (code) {
/* istanbul ignore next */
if (typeof code !== 'number') {

@@ -35,6 +40,8 @@ // Sometimes "close" event emits with a non-numeric value

function isExternalUrl (url = '') {
function isExternalUrl (url) {
return urlPattern.test(url)
};
}
function noop () {}
function proxyWebSockets (source, target) {

@@ -47,6 +54,10 @@ function close (code, reason) {

source.on('message', (data, binary) => waitConnection(target, () => target.send(data, { binary })))
/* istanbul ignore next */
source.on('ping', data => waitConnection(target, () => target.ping(data)))
/* istanbul ignore next */
source.on('pong', data => waitConnection(target, () => target.pong(data)))
source.on('close', close)
/* istanbul ignore next */
source.on('error', error => close(1011, error.message))
/* istanbul ignore next */
source.on('unexpected-response', () => close(1011, 'unexpected response'))

@@ -56,6 +67,10 @@

target.on('message', (data, binary) => source.send(data, { binary }))
/* istanbul ignore next */
target.on('ping', data => source.ping(data))
/* istanbul ignore next */
target.on('pong', data => source.pong(data))
target.on('close', close)
/* istanbul ignore next */
target.on('error', error => close(1011, error.message))
/* istanbul ignore next */
target.on('unexpected-response', () => close(1011, 'unexpected response'))

@@ -69,6 +84,27 @@ }

const wss = new WebSocket.Server({
server: fastify.server,
noServer: true,
...wsServerOptions
})
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
const rawResponse = new ServerResponse(rawRequest)
rawResponse.assignSocket(socket)
fastify.routing(rawRequest, rawResponse)
rawResponse.on('finish', () => {
socket.destroy()
})
})
this.handleUpgrade = (request, cb) => {
wss.handleUpgrade(request.raw, request.raw[kWs], request.raw[kWsHead], (socket) => {
this.handleConnection(socket, request)
cb()
})
}
// To be able to close the HTTP server,

@@ -79,16 +115,23 @@ // all WebSocket clients need to be disconnected.

// to monkeypatching for now.
const oldClose = fastify.server.close
fastify.server.close = function (done) {
for (const client of wss.clients) {
client.close()
{
const oldClose = fastify.server.close
fastify.server.close = function (done) {
wss.close(() => {
oldClose.call(this, (err) => {
/* istanbul ignore next */
done && done(err)
})
})
for (const client of wss.clients) {
client.close()
}
}
oldClose.call(this, done)
}
/* istanbul ignore next */
wss.on('error', (err) => {
/* istanbul ignore next */
this.logger.error(err)
})
wss.on('connection', this.handleConnection.bind(this))
this.wss = wss

@@ -98,6 +141,2 @@ this.prefixList = []

close (done) {
this.wss.close(done)
}
addUpstream (prefix, rewritePrefix, upstream, wsClientOptions) {

@@ -125,3 +164,4 @@ this.prefixList.push({

return undefined
/* 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`)
}

@@ -131,8 +171,5 @@

const upstream = this.findUpstream(request)
if (!upstream) {
this.logger.debug({ url: request.url }, 'not matching prefix')
source.close()
return
}
const { target: url, wsClientOptions } = upstream
const rewriteRequestHeaders = wsClientOptions?.rewriteRequestHeaders || defaultWsHeadersRewrite
const headersToRewrite = wsClientOptions?.headers || {}

@@ -144,9 +181,4 @@ const subprotocols = []

let optionsWs = {}
if (request.headers.cookie) {
const headers = { cookie: request.headers.cookie }
optionsWs = { ...wsClientOptions, headers }
} else {
optionsWs = wsClientOptions
}
const headers = rewriteRequestHeaders(headersToRewrite, request)
const optionsWs = { ...(wsClientOptions || {}), headers }

@@ -159,2 +191,9 @@ const target = new WebSocket(url, subprotocols, optionsWs)

function defaultWsHeadersRewrite (headers, request) {
if (request.headers.cookie) {
return { ...headers, cookie: request.headers.cookie }
}
return { ...headers }
}
const httpWss = new WeakMap() // http.Server => WebSocketProxy

@@ -167,7 +206,2 @@

httpWss.set(fastify.server, wsProxy)
fastify.addHook('onClose', (instance, done) => {
httpWss.delete(fastify.server)
wsProxy.close(done)
})
}

@@ -177,3 +211,4 @@

wsProxy.addUpstream(fastify.prefix, rewritePrefix, options.upstream, options.wsClientOptions)
} else if (typeof options.replyOptions.getUpstream === 'function') {
// The else block is validate earlier in the code
} else {
wsProxy.findUpstream = function (request) {

@@ -183,2 +218,3 @@ const source = new URL(request.url, 'ws://127.0.0.1')

const target = new URL(source.pathname, upstream)
/* istanbul ignore next */
target.protocol = upstream.indexOf('http:') === 0 ? 'ws:' : 'wss'

@@ -189,5 +225,6 @@ target.search = source.search

}
return wsProxy
}
function generateRewritePrefix (prefix = '', opts) {
function generateRewritePrefix (prefix, opts) {
let rewritePrefix = opts.rewritePrefix || (opts.upstream ? new URL(opts.upstream).pathname : '/')

@@ -259,3 +296,19 @@

let wsProxy
if (opts.websocket) {
wsProxy = setupWebSocketProxy(fastify, opts, rewritePrefix)
}
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 queryParamIndex = request.raw.url.indexOf('?')

@@ -267,3 +320,9 @@ let dest = request.raw.url.slice(0, queryParamIndex !== -1 ? queryParamIndex : undefined)

const prefixPathWithVariables = this.prefix.split('/').map((_, index) => requestedPathElements[index]).join('/')
dest = dest.replace(prefixPathWithVariables, rewritePrefix)
let rewritePrefixWithVariables = rewritePrefix
for (const [name, value] of Object.entries(request.params)) {
rewritePrefixWithVariables = rewritePrefixWithVariables.replace(`:${name}`, value)
}
dest = dest.replace(prefixPathWithVariables, rewritePrefixWithVariables)
} else {

@@ -274,15 +333,10 @@ dest = dest.replace(this.prefix, rewritePrefix)

}
if (opts.websocket) {
setupWebSocketProxy(fastify, opts, rewritePrefix)
}
}
fastifyHttpProxy[Symbol.for('plugin-meta')] = {
module.exports = fp(fastifyHttpProxy, {
fastify: '4.x',
name: '@fastify/http-proxy'
}
module.exports = fastifyHttpProxy
name: '@fastify/http-proxy',
encapsulate: true
})
module.exports.default = fastifyHttpProxy
module.exports.fastifyHttpProxy = fastifyHttpProxy
{
"name": "@fastify/http-proxy",
"version": "8.4.0",
"version": "9.0.0",
"description": "proxy http requests, for Fastify",

@@ -50,8 +50,9 @@ "main": "index.js",

"tap": "^16.0.0",
"tsd": "^0.24.1",
"typescript": "^4.5.4",
"tsd": "^0.28.0",
"typescript": "^5.0.2",
"why-is-node-running": "^2.2.2"
},
"dependencies": {
"@fastify/reply-from": "^8.0.0",
"@fastify/reply-from": "^9.0.0",
"fastify-plugin": "^4.5.0",
"ws": "^8.4.2"

@@ -58,0 +59,0 @@ },

@@ -58,2 +58,10 @@ # @fastify/http-proxy

// /rest-api/123/endpoint will be proxied to http://my-rest-api.example.com/123/endpoint
server.register(proxy, {
upstream: 'http://my-rest-api.example.com',
prefix: '/rest-api/:id/endpoint', // optional
rewritePrefix: '/:id/endpoint', // optional
http2: false // optional
})
// /auth/user will be proxied to http://single-signon.example.com/signon/user

@@ -114,22 +122,24 @@ server.register(proxy, {

### upstream
### `upstream`
An URL (including protocol) that represents the target server to use for proxying.
### prefix
### `prefix`
The prefix to mount this plugin on. All the requests to the current server starting with the given prefix will be proxied to the provided upstream.
Parametric path is supported. To register a parametric path, use the colon before the parameter name.
The prefix will be removed from the URL when forwarding the HTTP
request.
### rewritePrefix
### `rewritePrefix`
Rewrite the prefix to the specified string. Default: `''`.
### preHandler
### `preHandler`
A `preHandler` to be applied on all routes. Useful for performing actions before the proxy is executed (e.g. check for authentication).
### proxyPayloads
### `proxyPayloads`

@@ -147,3 +157,3 @@ When this option is `false`, you will be able to access the body but it will also disable direct pass through of the payload. As a result, it is left up to the implementation to properly parse and proxy the payload correctly.

### config
### `config`

@@ -155,28 +165,37 @@ An object accessible within the `preHandler` via `reply.context.config`.

### replyOptions
### `replyOptions`
Object with [reply options](https://github.com/fastify/fastify-reply-from#replyfromsource-opts) for `@fastify/reply-from`.
### httpMethods
### `httpMethods`
An array that contains the types of the methods. Default: `['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']`.
## websocket
### `websocket`
This module has _partial_ support for forwarding websockets by passing a
`websocket` option. All those options are going to be forwarded to
[`@fastify/websocket`](https://github.com/fastify/fastify-websocket).
`websocket` boolean option.
Multiple websocket proxies may be attached to the same HTTP server at different paths.
In this case, only the first `wsServerOptions` is applied.
A few things are missing:
1. forwarding headers as well as `rewriteHeaders`. Note: Only cookie headers are being forwarded
2. request id logging
3. support `ignoreTrailingSlash`
4. forwarding more than one subprotocols. Note: Only the first subprotocol is being forwarded
1. request id logging
2. support `ignoreTrailingSlash`
3. forwarding more than one subprotocols. Note: Only the first subprotocol is being forwarded
Pull requests are welcome to finish this feature.
### `wsServerOptions`
The options passed to [`new ws.Server()`](https://github.com/websockets/ws/blob/HEAD/doc/ws.md#class-websocketserver).
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`
The options passed to the [`WebSocket` constructor](https://github.com/websockets/ws/blob/HEAD/doc/ws.md#class-websocket) for outgoing websockets.
It also supports an additional `rewriteRequestHeaders(headers, request)` function that can be used to write the headers before
opening the WebSocket connection. This function should return an object with the given headers.
The default implementation forwards the `cookie` header.
## Benchmarks

@@ -196,4 +215,5 @@

## TODO
* [ ] Perform validations for incoming data
* [ ] Finish implementing websocket (follow TODO)
* [ ] Finish implementing websocket

@@ -200,0 +220,0 @@ ## License

@@ -36,2 +36,6 @@ 'use strict'

origin.get('/variable-api/:id/endpoint', async (request, reply) => {
return `this is "variable-api" endpoint with id ${request.params.id}`
})
origin.get('/timeout', async (request, reply) => {

@@ -457,3 +461,3 @@ await new Promise((resolve) => setTimeout(resolve, 600))

test('rewritePrefix with variables', async t => {
test('prefix with variables', async t => {
const proxyServer = Fastify()

@@ -479,2 +483,65 @@

test('prefix and rewritePrefix with variables', async t => {
const proxyServer = Fastify()
proxyServer.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
prefix: '/api/:id',
rewritePrefix: '/variable-api/:id'
})
await proxyServer.listen({ port: 0 })
t.teardown(() => {
proxyServer.close()
})
const firstProxyPrefix = await got(
`http://localhost:${proxyServer.server.address().port}/api/123/endpoint`
)
t.equal(firstProxyPrefix.body, 'this is "variable-api" endpoint with id 123')
})
test('prefix (complete path) and rewritePrefix with variables and similar path', async t => {
const proxyServer = Fastify()
proxyServer.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
prefix: '/api/:id/static',
rewritePrefix: '/variable-api/:id/endpoint'
})
await proxyServer.listen({ port: 0 })
t.teardown(() => {
proxyServer.close()
})
const firstProxyPrefix = await got(
`http://localhost:${proxyServer.server.address().port}/api/123/static`
)
t.equal(firstProxyPrefix.body, 'this is "variable-api" endpoint with id 123')
})
test('prefix and rewritePrefix with variables with different paths', async t => {
const proxyServer = Fastify()
proxyServer.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
prefix: '/:id',
rewritePrefix: '/variable-api/:id/endpoint'
})
await proxyServer.listen({ port: 0 })
t.teardown(() => {
proxyServer.close()
})
const firstProxyPrefix = await got(
`http://localhost:${proxyServer.server.address().port}/123`
)
t.equal(firstProxyPrefix.body, 'this is "variable-api" endpoint with id 123')
})
test('rewrite location headers', async t => {

@@ -481,0 +548,0 @@ const proxyServer = Fastify()

@@ -129,4 +129,4 @@ 'use strict'

test('getWebSocketStream', async (t) => {
t.plan(7)
test('getUpstream', async (t) => {
t.plan(9)

@@ -152,6 +152,14 @@ const origin = createServer()

const server = Fastify()
let _req
server.server.on('upgrade', (req) => {
_req = req
})
server.register(proxy, {
upstream: '',
replyOptions: {
getUpstream: function (original, base) {
getUpstream: function (original) {
t.not(original, _req)
t.equal(original.raw, _req)
return `http://localhost:${origin.address().port}`

@@ -190,1 +198,215 @@ }

})
test('websocket proxy trigger hooks', async (t) => {
t.plan(8)
const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))
const serverMessages = []
wss.on('connection', (ws, request) => {
t.equal(ws.protocol, subprotocolValue)
t.equal(request.headers.cookie, cookieValue)
ws.on('message', (message, binary) => {
serverMessages.push([message.toString(), binary])
// echo
ws.send(message, { binary })
})
})
await promisify(origin.listen.bind(origin))({ port: 0 })
const server = Fastify()
server.addHook('onRequest', (request, reply, done) => {
t.pass('onRequest')
done()
})
server.register(proxy, {
upstream: `ws://localhost:${origin.address().port}`,
websocket: true
})
await server.listen({ port: 0 })
t.teardown(server.close.bind(server))
const options = { headers: { cookie: cookieValue } }
const ws = new WebSocket(`ws://localhost:${server.server.address().port}`, [subprotocolValue], options)
await once(ws, 'open')
ws.send('hello', { binary: false })
const [reply0, binary0] = await once(ws, 'message')
t.equal(reply0.toString(), 'hello')
t.equal(binary0, false)
ws.send(Buffer.from('fastify'), { binary: true })
const [reply1, binary1] = await once(ws, 'message')
t.equal(reply1.toString(), 'fastify')
t.equal(binary1, true)
t.strictSame(serverMessages, [
['hello', false],
['fastify', true]
])
await Promise.all([
once(ws, 'close'),
server.close()
])
})
test('websocket proxy with rewriteRequestHeaders', async (t) => {
t.plan(7)
const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))
const serverMessages = []
wss.on('connection', (ws, request) => {
t.equal(ws.protocol, subprotocolValue)
t.equal(request.headers.myauth, 'myauth')
ws.on('message', (message, binary) => {
serverMessages.push([message.toString(), binary])
// echo
ws.send(message, { binary })
})
})
await promisify(origin.listen.bind(origin))({ port: 0 })
const server = Fastify()
server.register(proxy, {
upstream: `ws://localhost:${origin.address().port}`,
websocket: true,
wsClientOptions: {
rewriteRequestHeaders: (headers, request) => {
return {
...headers,
myauth: 'myauth'
}
}
}
})
await server.listen({ port: 0 })
t.teardown(server.close.bind(server))
const ws = new WebSocket(`ws://localhost:${server.server.address().port}`, [subprotocolValue])
await once(ws, 'open')
ws.send('hello', { binary: false })
const [reply0, binary0] = await once(ws, 'message')
t.equal(reply0.toString(), 'hello')
t.equal(binary0, false)
ws.send(Buffer.from('fastify'), { binary: true })
const [reply1, binary1] = await once(ws, 'message')
t.equal(reply1.toString(), 'fastify')
t.equal(binary1, true)
t.strictSame(serverMessages, [
['hello', false],
['fastify', true]
])
await Promise.all([
once(ws, 'close'),
server.close()
])
})
test('websocket proxy custom headers', async (t) => {
t.plan(7)
const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))
const serverMessages = []
wss.on('connection', (ws, request) => {
t.equal(ws.protocol, subprotocolValue)
t.equal(request.headers.myauth, 'myauth')
ws.on('message', (message, binary) => {
serverMessages.push([message.toString(), binary])
// echo
ws.send(message, { binary })
})
})
await promisify(origin.listen.bind(origin))({ port: 0 })
const server = Fastify()
server.register(proxy, {
upstream: `ws://localhost:${origin.address().port}`,
websocket: true,
wsClientOptions: {
headers: {
myauth: 'myauth'
}
}
})
await server.listen({ port: 0 })
t.teardown(server.close.bind(server))
const ws = new WebSocket(`ws://localhost:${server.server.address().port}`, [subprotocolValue])
await once(ws, 'open')
ws.send('hello', { binary: false })
const [reply0, binary0] = await once(ws, 'message')
t.equal(reply0.toString(), 'hello')
t.equal(binary0, false)
ws.send(Buffer.from('fastify'), { binary: true })
const [reply1, binary1] = await once(ws, 'message')
t.equal(reply1.toString(), 'fastify')
t.equal(binary1, true)
t.strictSame(serverMessages, [
['hello', false],
['fastify', true]
])
await Promise.all([
once(ws, 'close'),
server.close()
])
})
test('Should gracefully close when clients attempt to connect after calling close', async (t) => {
const origin = createServer()
const wss = new WebSocket.Server({ server: origin })
t.teardown(wss.close.bind(wss))
t.teardown(origin.close.bind(origin))
await promisify(origin.listen.bind(origin))({ port: 0 })
const server = Fastify({ logger: false })
await server.register(proxy, {
upstream: `ws://localhost:${origin.address().port}`,
websocket: true
})
const oldClose = server.server.close
let p
server.server.close = function (cb) {
const ws = new WebSocket('ws://localhost:' + server.server.address().port)
p = once(ws, 'unexpected-response').then(([req, res]) => {
t.equal(res.statusCode, 503)
oldClose.call(this, cb)
})
}
await server.listen({ port: 0 })
const ws = new WebSocket('ws://localhost:' + server.server.address().port)
await once(ws, 'open')
await server.close()
await p
})

@@ -85,3 +85,6 @@ 'use strict'

t.teardown(() => backend.close())
t.teardown(async () => {
await backend.close()
t.comment('backend closed')
})

@@ -92,3 +95,6 @@ const backendURL = await backend.listen({ port: 0, host: '127.0.0.1' })

t.teardown(() => frontend.close())
t.teardown(async () => {
await frontend.close()
t.comment('frontend closed')
})

@@ -95,0 +101,0 @@ for (const path of paths) {

@@ -33,3 +33,3 @@ import fastify, { RawReplyDefaultExpression, RawRequestDefaultExpression } from 'fastify';

keepAliveTimeout: 60 * 1000,
tls: {
connect: {
rejectUnauthorized: false

@@ -36,0 +36,0 @@ }

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc