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

Allow custom encoders to validate compression options #3319

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions API.md
Expand Up @@ -29,7 +29,7 @@
- [`server.decorate(type, property, method, [options])`](#serverdecoratetype-property-method-options)
- [`server.dependency(dependencies, [after])`](#serverdependencydependencies-after)
- [`server.emit(criteria, data, [callback])`](#serveremitcriteria-data-callback)
- [`server.encoder(encoding, encoder)`](#serverencoderencoding-encoder)
- [`server.encoder(encoding, encoder, [schema])`](#serverencoderencoding-encoder-schema)
- [`server.event(events)`](#servereventevents)
- [`server.expose(key, value)`](#serverexposekey-value)
- [`server.expose(obj)`](#serverexposeobj)
Expand Down Expand Up @@ -1071,14 +1071,16 @@ server.on('test', (update) => console.log(update));
server.emit('test', 'hello');
```

### `server.encoder(encoding, encoder)`
### `server.encoder(encoding, encoder, [schema])`

Registers a custom content encoding compressor to extend the built-in support for `'gzip'` and
'`deflate`' where:
- `encoding` - the encoder name string.
- `encoder` - a function using the signature `function(options)` where `options` are the encoding specific options configured in
the route `compression` configuration option, and the return value is an object compatible with the output of node's
[`zlib.createGzip()`](https://nodejs.org/dist/latest-v6.x/docs/api/zlib.html#zlib_zlib_creategzip_options).
- `schema` - an optional [Joi](http://github.com/hapijs/joi) validation object, which is applied to the encoding compression
settings object on the route.

```js
const Zlib = require('zlib');
Expand Down
25 changes: 24 additions & 1 deletion lib/compression.js
Expand Up @@ -5,13 +5,19 @@
const Zlib = require('zlib');
const Accept = require('accept');
const Hoek = require('hoek');
const Joi = require('joi');


// Declare internals

const internals = {};


internals.schema = Joi.object({
identity: Joi.object().unknown(false).optional()
}).unknown();


exports = module.exports = internals.Compression = function () {

this.encodings = ['identity', 'gzip', 'deflate'];
Expand All @@ -25,15 +31,24 @@ exports = module.exports = internals.Compression = function () {
gzip: (options) => Zlib.createGunzip(options),
deflate: (options) => Zlib.createInflate(options)
};

this._schema = internals.schema;
};


internals.Compression.prototype.addEncoder = function (encoding, encoder) {
internals.Compression.prototype.addEncoder = function (encoding, encoder, schema) {

Hoek.assert(this._encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`);
Hoek.assert(schema === undefined || typeof schema === 'object', `Invalid encoder options schema for ${encoding}`);
this._encoders[encoding] = encoder;
this.encodings.push(encoding);

if (schema) {
this._schema = this._schema.keys({
[encoding]: schema
});
}
};


Expand All @@ -45,6 +60,14 @@ internals.Compression.prototype.addDecoder = function (encoding, decoder) {
};


internals.Compression.prototype.validate = function (routeSettings, message) {

const result = Joi.validate(routeSettings.compression, this._schema);
Hoek.assert(!result.error, `Invalid compression options (${message})`, result.error && result.error.annotate());
routeSettings.compression = result.value;
};


internals.Compression.prototype.accept = function (request) {

return Accept.encoding(request.headers['accept-encoding'], this.encodings);
Expand Down
4 changes: 2 additions & 2 deletions lib/connection.js
Expand Up @@ -371,9 +371,9 @@ internals.Connection.prototype.decoder = function (encoding, decoder) {
};


internals.Connection.prototype.encoder = function (encoding, encoder) {
internals.Connection.prototype.encoder = function (encoding, encoder, schema) {

return this._compression.addEncoder(encoding, encoder);
return this._compression.addEncoder(encoding, encoder, schema);
};


Expand Down
4 changes: 2 additions & 2 deletions lib/plugin.js
Expand Up @@ -412,9 +412,9 @@ internals.Plugin.prototype.emit = function (criteria, data, callback) {
};


internals.Plugin.prototype.encoder = function (encoding, encoder) {
internals.Plugin.prototype.encoder = function (encoding, encoder, schema) {

this._apply('encoder', Connection.prototype.encoder, [encoding, encoder]);
this._apply('encoder', Connection.prototype.encoder, [encoding, encoder, schema]);
};


Expand Down
1 change: 1 addition & 0 deletions lib/route.js
Expand Up @@ -63,6 +63,7 @@ exports = module.exports = internals.Route = function (route, connection, plugin
this.settings = Hoek.applyToDefaultsWithShallow(base, config || {}, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query']);
this.settings.handler = handler;
this.settings = Schema.apply('routeConfig', this.settings, routeDisplay);
connection._compression.validate(this.settings, routeDisplay);

const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', routeDisplay);
Expand Down
58 changes: 58 additions & 0 deletions test/plugin.js
Expand Up @@ -2872,6 +2872,64 @@ describe('Plugin', () => {
});
});
});

it('validates custom schema', (done) => {

const data = '{"test":"true"}';

const server = new Hapi.Server();
server.connection({ routes: { compression: { test: { some: 'option' } } } });

const encoder = (options) => {

expect(options).to.equal({ some: 'option' });
return Zlib.createGzip();
};

server.encoder('test', encoder, { some: 'option', other: false });

const handler = function (request, reply) {

return reply(request.payload);
};

server.route({ method: 'POST', path: '/', handler });
server.start((err) => {

expect(err).to.not.exist();

const uri = 'http://localhost:' + server.info.port;

Zlib.gzip(new Buffer(data), (err, zipped) => {

expect(err).to.not.exist();

Wreck.post(uri, { headers: { 'accept-encoding': 'test' }, payload: data }, (err, res, body) => {

expect(err).to.not.exist();
expect(res.headers['content-encoding']).to.equal('test');
expect(body.toString()).to.equal(zipped.toString());
server.stop(done);
});
});
});
});

it('throws error when custom schema fails validation', (done) => {

const server = new Hapi.Server();
server.connection({ routes: { compression: { test: { some: 'bad' } } } });

server.encoder('test', Hoek.ignore, { some: 'option', other: false });

const fn = () => {

server.route({ method: 'POST', path: '/', handler: Hoek.ignore });
};

expect(fn).to.throw(/Invalid compression options/);
done();
});
});

describe('event()', () => {
Expand Down