diff --git a/lib/configuration/hooks/gzip/index.js b/lib/configuration/hooks/gzip/index.js index 354d5f67e4..63ae1539c7 100644 --- a/lib/configuration/hooks/gzip/index.js +++ b/lib/configuration/hooks/gzip/index.js @@ -21,7 +21,7 @@ module.exports = function (strapi) { initialize: function (cb) { if (strapi.config.gzip === true) { - strapi.app.use(strapi.middlewares.gzip()); + strapi.app.use(strapi.middlewares.compress()); } cb(); diff --git a/lib/configuration/hooks/responses/index.js b/lib/configuration/hooks/responses/index.js new file mode 100644 index 0000000000..d9bf10a1df --- /dev/null +++ b/lib/configuration/hooks/responses/index.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = function () { + const hook = { + + /** + * Default options + */ + + defaults: { + enabled: true + }, + + /** + * Initialize the hook + */ + + initialize: function (cb) { + cb(); + } + }; + + return hook; +}; diff --git a/lib/configuration/hooks/responses/policies/responses.js b/lib/configuration/hooks/responses/policies/responses.js new file mode 100644 index 0000000000..4950574f31 --- /dev/null +++ b/lib/configuration/hooks/responses/policies/responses.js @@ -0,0 +1,22 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +// Local utilities. +const responses = require('../responses/index'); + +/** + * Policy used to add responses in the `this.response` object. + */ + +module.exports = function * (next) { + // Add the custom responses to the `this.response` object. + this.response = _.merge(this.response, responses); + + yield next; +}; diff --git a/lib/configuration/hooks/responses/responses/badRequest.js b/lib/configuration/hooks/responses/responses/badRequest.js new file mode 100644 index 0000000000..3db16c028a --- /dev/null +++ b/lib/configuration/hooks/responses/responses/badRequest.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * Default `badRequest` response. + */ + +module.exports = function * badRequest(data) { + // Set the status. + this.status = 400; + + // Delete the `data` object if the app is used in production environment. + if (strapi.config.environment === 'production') { + data = undefined; + } + + // Finally send the response. + this.body = data; +}; diff --git a/lib/configuration/hooks/responses/responses/created.js b/lib/configuration/hooks/responses/responses/created.js new file mode 100644 index 0000000000..0fc30654a9 --- /dev/null +++ b/lib/configuration/hooks/responses/responses/created.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * Default `created` response. + */ + +module.exports = function * created(data) { + // Set the status. + this.status = 201; + + // Finally send the response. + this.body = data; +}; diff --git a/lib/configuration/hooks/responses/responses/forbidden.js b/lib/configuration/hooks/responses/responses/forbidden.js new file mode 100644 index 0000000000..d7fcf42ffa --- /dev/null +++ b/lib/configuration/hooks/responses/responses/forbidden.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * Default `forbidden` response. + */ + +module.exports = function * forbidden(data) { + // Set the status. + this.status = 403; + + // Delete the `data` object if the app is used in production environment. + if (strapi.config.environment === 'production') { + data = undefined; + } + + // Finally send the response. + this.body = data; +}; diff --git a/lib/configuration/hooks/responses/responses/index.js b/lib/configuration/hooks/responses/responses/index.js new file mode 100644 index 0000000000..cc310714d4 --- /dev/null +++ b/lib/configuration/hooks/responses/responses/index.js @@ -0,0 +1,14 @@ +'use strict'; + +/** + * Index of the responses hook responses. + */ + +module.exports = { + badRequest: require('./badRequest'), + created: require('./created'), + forbidden: require('./forbidden'), + notFound: require('./notFound'), + ok: require('./ok'), + serverError: require('./serverError') +}; diff --git a/lib/configuration/hooks/responses/responses/notFound.js b/lib/configuration/hooks/responses/responses/notFound.js new file mode 100644 index 0000000000..73d5074c8d --- /dev/null +++ b/lib/configuration/hooks/responses/responses/notFound.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * Default `notFound` response. + */ + +module.exports = function * notFound(data) { + // Set the status. + this.status = 404; + + // Delete the `data` object if the app is used in production environment. + if (strapi.config.environment === 'production') { + data = undefined; + } + + // Finally send the response. + this.body = data; +}; diff --git a/lib/configuration/hooks/responses/responses/ok.js b/lib/configuration/hooks/responses/responses/ok.js new file mode 100644 index 0000000000..1185ceccde --- /dev/null +++ b/lib/configuration/hooks/responses/responses/ok.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * Default `ok` response. + */ + +module.exports = function sendOk(data) { + // Set the status. + this.status = 200; + + // Finally send the response. + this.body = data; +}; diff --git a/lib/configuration/hooks/responses/responses/serverError.js b/lib/configuration/hooks/responses/responses/serverError.js new file mode 100644 index 0000000000..be0e962e4e --- /dev/null +++ b/lib/configuration/hooks/responses/responses/serverError.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * Default `serverError` response. + */ + +module.exports = function * serverError(data) { + // Set the status. + this.status = 500; + + // Delete the `data` object if the app is used in production environment. + if (strapi.config.environment === 'production') { + data = undefined; + } + + // Finally send the response. + this.body = data; +}; diff --git a/package.json b/package.json index 87d74f0877..3b65bf006f 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,10 @@ "koa": "~1.1.2", "koa-bodyparser": "~2.0.1", "koa-compose": "~2.3.0", + "koa-compress": "^1.0.9", "koa-cors": "~0.0.16", "koa-favicon": "~1.2.0", "koa-graphql": "^0.4.5", - "koa-gzip": "~0.1.0", "koa-i18n": "~1.2.0", "koa-ip": "~0.1.0", "koa-load-middlewares": "~1.0.0", diff --git a/test/middlewares/compress/index.js b/test/middlewares/compress/index.js new file mode 100644 index 0000000000..2ce5eb3c03 --- /dev/null +++ b/test/middlewares/compress/index.js @@ -0,0 +1,262 @@ +'use strict'; + +const fs = require('fs'); + +const should = require('should'); +const request = require('supertest'); + +const assert = require('assert'); +const http = require('http'); +const Stream = require('stream'); +const crypto = require('crypto'); +const path = require('path'); + +const strapi = require('../../..'); +const Koa = strapi.server; + +describe('compress', function() { + + describe('strapi.middlewares.compress()', function() { + it('should work with no options', function() { + strapi.middlewares.compress(); + }); + }); + + const buffer = crypto.randomBytes(1024); + const string = buffer.toString('hex'); + + function* sendString(next) { + this.body = string + } + + function* sendBuffer(next) { + this.compress = true; + this.body = buffer + } + + it('should compress strings', function(done) { + + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendString); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) return done(err); + + //res.should.have.header('Content-Encoding', 'gzip') + res.should.have.header('Transfer-Encoding', 'chunked'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-length'); + res.text.should.equal(string); + + done() + }) + }); + + it('should not compress strings below threshold', function(done) { + + let app = strapi.server(); + app.use(strapi.middlewares.compress({ + threshold: '1mb' + })); + app.use(sendString); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) return done(err); + + res.should.have.header('Content-Length', '2048'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-encoding'); + res.headers.should.not.have.property('transfer-encoding'); + res.text.should.equal(string); + + done() + }) + }); + + it('should compress JSON body', function(done) { + var jsonBody = { + 'status': 200, + 'message': 'ok', + 'data': string + }; + + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(function*(next) { + this.body = jsonBody + }); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + res.should.have.header('Transfer-Encoding', 'chunked'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-length'); + res.text.should.equal(JSON.stringify(jsonBody)); + + done() + }) + }); + + it('should not compress JSON body below threshold', function(done) { + var jsonBody = { + 'status': 200, + 'message': 'ok' + }; + + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(function* sendJSON(next) { + this.body = jsonBody + }); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-encoding'); + res.headers.should.not.have.property('transfer-encoding'); + res.text.should.equal(JSON.stringify(jsonBody)); + + done() + }) + }); + + it('should compress buffers', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendBuffer); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + //res.should.have.header('Content-Encoding', 'gzip') + res.should.have.header('Transfer-Encoding', 'chunked'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-length'); + + done() + }) + }); + + it('should compress streams', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + + app.use(function*(next) { + this.type = 'application/javascript'; + this.body = fs.createReadStream(path.join(__dirname, 'index.js')) + }); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + //res.should.have.header('Content-Encoding', 'gzip') + res.should.have.header('Transfer-Encoding', 'chunked'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-length'); + + done() + }) + }); + + it('should compress when this.compress === true', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendBuffer); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + //res.should.have.header('Content-Encoding', 'gzip') + res.should.have.header('Transfer-Encoding', 'chunked'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-length'); + + done() + }) + }); + + it('should not compress when this.compress === false', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(function*(next) { + this.compress = false; + this.body = buffer + }); + + request(app.listen()).get('/').expect(200).end(function(err, res) { + if (err) + return done(err); + + res.should.have.header('Content-Length', '1024'); + res.should.have.header('Vary', 'Accept-Encoding'); + res.headers.should.not.have.property('content-encoding'); + res.headers.should.not.have.property('transfer-encoding'); + + done() + }) + }); + + it('should not compress HEAD requests', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendString); + + request(app.listen()).head('/').expect(200).expect('', function(err, res) { + if (err) + return done(err); + + res.headers.should.not.have.property('content-encoding'); + + done() + }) + }); + + it('should not crash even if accept-encoding: sdch', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendBuffer); + + request(app.listen()).get('/').set('Accept-Encoding', 'sdch, gzip, deflate').expect(200, done) + }); + + it('should not crash if no accept-encoding is sent', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(sendBuffer); + + request(app.listen()).get('/').expect(200, done) + }); + + it('should not crash if a type does not pass the filter', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress()); + app.use(function*() { + this.type = 'image/png'; + this.body = new Buffer(2048) + }); + + request(app.listen()).get('/').expect(200, done) + }); + + it('should not compress when transfer-encoding is already set', function(done) { + let app = strapi.server(); + app.use(strapi.middlewares.compress({ + threshold: 0 + })); + app.use(function*() { + this.set('Content-Encoding', 'identity'); + this.type = 'text'; + this.body = 'asdf' + }); + + request(app.listen()).get('/').expect('asdf', done) + }); +}); diff --git a/test/middlewares/gzip/index.js b/test/middlewares/gzip/index.js deleted file mode 100644 index 9ec573cb8e..0000000000 --- a/test/middlewares/gzip/index.js +++ /dev/null @@ -1,172 +0,0 @@ -'use strict'; - -const fs = require('fs'); - -const should = require('should'); -const request = require('supertest'); - -const strapi = require('../../..'); -const Koa = strapi.server; - -describe('gzip', function () { - const options = {}; - const BODY = 'foo bar string, foo bar string, foo bar string, foo bar string, \ - foo bar string, foo bar string, foo bar string, foo bar string, foo bar string, foo bar string, \ - foo bar string, foo bar string, foo bar string, foo bar string, foo bar string, foo bar string'; - - let app = strapi.server(); - - app.use(strapi.middlewares.gzip(options)); - app.use(strapi.middlewares.gzip(options)); - app.use(function * (next) { - if (this.url === '/404') { - return yield next; - } - if (this.url === '/small') { - return this.body = 'foo bar string'; - } - if (this.url === '/string') { - return this.body = BODY; - } - if (this.url === '/buffer') { - return this.body = new Buffer(BODY); - } - if (this.url === '/object') { - return this.body = { - foo: BODY - }; - } - if (this.url === '/number') { - return this.body = 1984; - } - if (this.url === '/stream') { - const stat = fs.statSync(__filename); - this.set('content-length', stat.size); - return this.body = fs.createReadStream(__filename); - } - if (this.url === '/exists-encoding') { - this.set('content-encoding', 'gzip'); - return this.body = new Buffer('gzip'); - } - if (this.url === '/error') { - return this.throw(new Error('mock error')); - } - }); - - before(function (done) { - app = app.listen(0, done); - }); - - describe('strapi.middlewares.gzip()', function () { - it('should work with no options', function () { - strapi.middlewares.gzip(); - }); - }); - - describe('when status 200 and request accept-encoding include gzip', function () { - it('should return gzip string body', function (done) { - request(app) - .get('/string') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(200) - .expect('content-encoding', 'gzip') - .expect('content-length', '46') - .expect(BODY, done); - }); - - it('should return raw string body if body smaller than minLength', function (done) { - request(app) - .get('/small') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(200) - .expect('content-length', '14') - .expect('foo bar string', function (err, res) { - should.not.exist(err); - should.not.exist(res.headers['content-encoding']); - done(); - }); - }); - - it('should return gzip buffer body', function (done) { - request(app) - .get('/buffer') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(200) - .expect('content-encoding', 'gzip') - .expect('content-length', '46') - .expect(BODY, done); - }); - - it('should return gzip stream body', function (done) { - request(app) - .get('/stream') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(200) - .expect('Content-Encoding', 'gzip') - .expect(fs.readFileSync(__filename, 'utf8'), - function (err, res) { - should.not.exist(err); - should.not.exist(res.headers['content-length']); - done(); - }); - }); - - it('should return gzip json body', function (done) { - request(app) - .get('/object') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(200) - .expect('Content-Encoding', 'gzip') - .expect('content-length', '52') - .expect({ - foo: BODY - }, done); - }); - - it('should return number body', function (done) { - request(app) - .get('/number') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect('content-length', '4') - .expect(200, function (err, res) { - should.not.exist(err); - res.body.should.equal(1984); - done(); - }); - }); - }); - - describe('when status 200 and request accept-encoding exclude gzip', function () { - it('should return raw body', function (done) { - request(app) - .get('/string') - .set('Accept-Encoding', 'deflate,sdch') - .expect(200) - .expect('content-length', '' + BODY.length) - .expect(BODY, - function (err, res) { - should.not.exist(err); - should.not.exist(res.headers['content-encoding']); - done(); - }); - }); - }); - - describe('when status non 200', function () { - it('should return 404', function (done) { - request(app) - .get('/404') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(404) - .expect('Not Found', done); - }); - - it('should return 500', function (done) { - request(app) - .get('/error') - .set('Accept-Encoding', 'gzip,deflate,sdch') - .expect(500) - .expect('Internal Server Error', done); - }); - }); -});