Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

opt to validate incoming data before proxy is executed #312

Merged
merged 6 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 59 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,58 +27,58 @@ npm i @fastify/http-proxy fastify
## Example

```js
const Fastify = require('fastify')
const server = Fastify()
const Fastify = require('fastify');
const server = Fastify();

server.register(require('@fastify/http-proxy'), {
upstream: 'http://my-api.example.com',
prefix: '/api', // optional
http2: false // optional
})
http2: false, // optional
});

server.listen({ port: 3000 })
server.listen({ port: 3000 });
```

This will proxy any request starting with `/api` to `http://my-api.example.com`. For instance `http://localhost:3000/api/users` will be proxied to `http://my-api.example.com/users`.

If you want to have different proxies on different prefixes you can register multiple instances of the plugin as shown in the following snippet:

```js
const Fastify = require('fastify')
const server = Fastify()
const proxy = require('@fastify/http-proxy')
const Fastify = require('fastify');
const server = Fastify();
const proxy = require('@fastify/http-proxy');

// /api/x will be proxied to http://my-api.example.com/x
server.register(proxy, {
upstream: 'http://my-api.example.com',
prefix: '/api', // optional
http2: false // optional
})
http2: false, // optional
});

// /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
})
http2: false, // optional
});

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

// /user will be proxied to http://single-signon.example.com/signon/user
server.register(proxy, {
upstream: 'http://single-signon.example.com',
rewritePrefix: '/signon', // optional
http2: false // optional
})
http2: false, // optional
});

server.listen({ port: 3000 })
server.listen({ port: 3000 });
```

Notice that in this case it is important to use the `prefix` option to tell the proxy how to properly route the requests across different upstreams.
Expand All @@ -92,20 +92,22 @@ For other examples, see [`example.js`](examples/example.js).
`@fastify/http-proxy` can track and pipe the `request-id` across the upstreams. Using the [`hyperid`](https://www.npmjs.com/package/hyperid) module and the [`@fastify/reply-from`](https://github.com/fastify/fastify-reply-from) built-in options a fairly simple example would look like this:

```js
const Fastify = require('fastify')
const proxy = require('@fastify/http-proxy')
const hyperid = require('hyperid')
const Fastify = require('fastify');
const proxy = require('@fastify/http-proxy');
const hyperid = require('hyperid');

const server = Fastify()
const uuid = hyperid()
const server = Fastify();
const uuid = hyperid();

server.register(proxy, {
upstream: 'http://localhost:4001',
replyOptions: {
rewriteRequestHeaders: (originalReq, headers) => ({...headers, 'request-id': uuid()})
}
})

rewriteRequestHeaders: (originalReq, headers) => ({
...headers,
'request-id': uuid(),
}),
},
});

server.listen({ port: 3000 });
```
Expand All @@ -115,8 +117,8 @@ server.listen({ port: 3000 });
This `fastify` plugin supports _all_ the options of
[`@fastify/reply-from`](https://github.com/fastify/fastify-reply-from) plus the following.

*Note that this plugin is fully encapsulated, and non-JSON payloads will
be streamed directly to the destination.*
_Note that this plugin is fully encapsulated, and non-JSON payloads will
be streamed directly to the destination._

### `upstream`

Expand Down Expand Up @@ -147,9 +149,24 @@ For example, if you are expecting a payload of type `application/xml`, then you

```javascript
fastify.addContentTypeParser('application/xml', (req, done) => {
const parsedBody = parsingCode(req)
done(null, parsedBody)
})
const parsedBody = parsingCode(req);
done(null, parsedBody);
});
```

### `preValidation`

Specify preValidation function to perform the validation of the request before the proxy is executed (e.g. check request payload).

```javascript
fastify.register(proxy, {
upstream: `http://your-target-upstream.com`,
preValidation: async (request, reply) => {
if (request.body.method === 'invalid_method') {
reply.code(400).send({ message: 'payload contains invalid method' });
}
},
});
```

### `config`
Expand All @@ -164,6 +181,7 @@ configuration passed to the route.
Object with [reply options](https://github.com/fastify/fastify-reply-from#replyfromsource-opts) for `@fastify/reply-from`.

### `internalRewriteLocationHeader`

By default, `@fastify/http-proxy` will rewrite the `location` header when a request redirects to a relative path.
In other words, the [prefix](https://github.com/fastify/fastify-http-proxy#prefix) will be added to the relative path.

Expand All @@ -172,12 +190,13 @@ If you want to preserve the original path, this option will disable this interna
Note that the [rewriteHeaders](https://github.com/fastify/fastify-reply-from#rewriteheadersheaders-request) option of [`@fastify/reply-from`](http://npm.im/fastify-reply-from) will retrieve headers modified (reminder: only `location` is updated among all headers) in parameter but with this option, the headers are unchanged.

### `httpMethods`

An array that contains the types of the methods. Default: `['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']`.

### `websocket`

This module has _partial_ support for forwarding websockets by passing a
`websocket` boolean option.
`websocket` boolean option.

A few things are missing:

Expand All @@ -188,6 +207,7 @@ A few things are missing:
Pull requests are welcome to finish this feature.

### `wsUpstream`

Working only if property `websocket` is `true`.

An URL (including protocol) that represents the target websockets to use for proxying websockets.
Expand All @@ -214,19 +234,19 @@ The default implementation forwards the `cookie` header.

The following benchmarks where generated on a dedicated server with an Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz and 64GB of RAM:

| __Framework__ | req/sec |
| :----------------- | :------------------------- |
| `express-http-proxy` | 2557 |
| `http-proxy` | 9519 |
| `@fastify/http-proxy` | 15919 |
| **Framework** | req/sec |
| :-------------------- | :------ |
| `express-http-proxy` | 2557 |
| `http-proxy` | 9519 |
| `@fastify/http-proxy` | 15919 |

The results were gathered on the second run of `autocannon -c 100 -d 5
URL`.

## TODO

* [ ] Perform validations for incoming data
* [ ] Finish implementing websocket
- [ ] Perform validations for incoming data
- [ ] Finish implementing websocket

## License

Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ async function fastifyHttpProxy (fastify, opts) {

fastify.register(From, fromOpts)

if (opts.proxyPayloads !== false) {
if (opts.preValidation) {
fastify.addHook('preValidation', opts.preValidation)
} else if (opts.proxyPayloads !== false) {
fastify.addContentTypeParser('application/json', bodyParser)
fastify.addContentTypeParser('*', bodyParser)
}
Expand Down
56 changes: 56 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,62 @@ async function run () {
t.same(resultRoot.body, { something: 'posted' })
})

test('preValidation post payload contains invalid data', async t => {
const server = Fastify()
server.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
preValidation: async (request, reply) => {
if (request.body.hello !== 'world') {
reply.code(400).send({ message: 'invalid body.hello value' })
}
}
})

await server.listen({ port: 0 })
t.teardown(server.close.bind(server))

try {
await got(
`http://localhost:${server.server.address().port}/this-has-data`,
{
method: 'POST',
json: { hello: 'invalid' },
responseType: 'json'
}
)
} catch (err) {
t.equal(err.response.statusCode, 400)
t.same(err.response.body, { message: 'invalid body.hello value' })
return
}
t.fail()
})

test('preValidation post payload contains valid data', async t => {
const server = Fastify()
server.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
preValidation: async (request, reply) => {
if (request.body.hello !== 'world') {
reply.code(400).send({ message: 'invalid body.hello value' })
}
}
})

await server.listen({ port: 0 })
t.teardown(server.close.bind(server))

const resultRoot = await got(
`http://localhost:${server.server.address().port}/this-has-data`,
{
method: 'POST',
json: { hello: 'world' },
responseType: 'json'
}
)
t.same(resultRoot.body, { something: 'posted' })
})

test('dynamic upstream for posting stuff', async t => {
const server = Fastify()
server.register(proxy, {
Expand Down
3 changes: 2 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types='node' />

import { FastifyPluginCallback, preHandlerHookHandler } from 'fastify';
import { FastifyPluginCallback, preHandlerHookHandler, preValidationHookHandler } from 'fastify';

import {
FastifyReplyFromOptions,
Expand Down Expand Up @@ -31,6 +31,7 @@ declare namespace fastifyHttpProxy {
proxyPayloads?: boolean;
preHandler?: preHandlerHookHandler;
beforeHandler?: preHandlerHookHandler;
preValidation?: preValidationHookHandler;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here: 0c0e505

config?: Object;
replyOptions?: FastifyReplyFromHooks;
wsClientOptions?: ClientOptions;
Expand Down