diff --git a/core/server/web/admin/controller.js b/core/server/web/admin/controller.js index 26963a4301..731b952e93 100644 --- a/core/server/web/admin/controller.js +++ b/core/server/web/admin/controller.js @@ -1,5 +1,7 @@ const debug = require('ghost-ignition').debug('web:admin:controller'); const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); const config = require('../../../shared/config'); const updateCheck = require('../../update-check'); const logging = require('../../../shared/logging'); @@ -25,6 +27,15 @@ module.exports = function adminController(req, res) { const templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate); const headers = {}; + // Generate our own ETag header + // `sendFile` by default uses filesize+lastmod date to generate an etag. + // That doesn't work for admin templates because the filesize doesn't change between versions + // and `npm pack` sets a fixed lastmod date for every file meaning the default etag never changes + const fileBuffer = fs.readFileSync(templatePath); + const hashSum = crypto.createHash('md5'); + hashSum.update(fileBuffer); + headers.ETag = hashSum.digest('hex'); + if (config.get('adminFrameProtection')) { headers['X-Frame-Options'] = 'sameorigin'; } diff --git a/test/regression/api/admin_spec.js b/test/regression/api/admin_spec.js index 7e46b506e9..422b1447e6 100644 --- a/test/regression/api/admin_spec.js +++ b/test/regression/api/admin_spec.js @@ -4,6 +4,8 @@ // But then again testing real code, rather than mock code, might be more useful... const should = require('should'); +const path = require('path'); +const fs = require('fs'); const supertest = require('supertest'); const testUtils = require('../../utils'); @@ -109,4 +111,47 @@ describe('Admin Routing', function () { .end(doEnd(done)); }); }); + + describe('built template', function () { + beforeEach(function () { + const configPaths = configUtils.config.get('paths'); + configPaths.adminViews = path.resolve('test/utils/fixtures/admin-views'); + configUtils.set('paths', configPaths); + }); + + afterEach(function () { + configUtils.restore(); + }); + + it('serves prod file in production', async function () { + configUtils.set('env', 'production'); + + const prodTemplate = fs.readFileSync(path.resolve('test/utils/fixtures/admin-views/default-prod.html')).toString(); + + const res = await request.get('/ghost/') + .set('X-Forwarded-Proto', 'https') + .expect(200); + + res.text.should.equal(prodTemplate); + }); + + it('serves dev file when not in production', async function () { + const devTemplate = fs.readFileSync(path.resolve('test/utils/fixtures/admin-views/default.html')).toString(); + + const res = await request.get('/ghost/') + .set('X-Forwarded-Proto', 'https') + .expect(200); + + res.text.should.equal(devTemplate); + }); + + it('generates it\'s own ETag header from file contents', async function () { + const res = await request.get('/ghost/') + .set('X-Forwarded-Proto', 'https') + .expect(200); + + should.exist(res.headers.etag); + res.headers.etag.should.equal('b448e5380dbfc46bc7c6da6045bf3043'); + }); + }); }); diff --git a/test/utils/fixtures/admin-views/default-prod.html b/test/utils/fixtures/admin-views/default-prod.html new file mode 100644 index 0000000000..dfd79f5dc7 --- /dev/null +++ b/test/utils/fixtures/admin-views/default-prod.html @@ -0,0 +1,4 @@ + +