diff --git a/packages/fxa-content-server/package.json b/packages/fxa-content-server/package.json index dfbf7941a0..8cf795b102 100644 --- a/packages/fxa-content-server/package.json +++ b/packages/fxa-content-server/package.json @@ -136,6 +136,8 @@ "@babel/cli": "7.7.4", "@testing-library/react": "^8.0.4", "@types/backbone": "^1.4.1", + "@types/sinon-chai": "3.2.4", + "@types/sinon-express-mock": "1.3.8", "audit-filter": "^0.5.0", "babel-eslint": "9.0.0", "babel-plugin-dynamic-import-webpack": "1.0.2", @@ -163,6 +165,8 @@ "request": "2.88.0", "request-promise": "4.2.0", "sinon": "4.5.0", + "sinon-chai": "^3.5.0", + "sinon-express-mock": "^2.2.1", "ts-node": "^8.10.1", "upng-js": "2.1.0", "url-loader": "4.1.0", diff --git a/packages/fxa-content-server/server/bin/fxa-content-server.js b/packages/fxa-content-server/server/bin/fxa-content-server.js index b4a2a0dca3..a85422150a 100755 --- a/packages/fxa-content-server/server/bin/fxa-content-server.js +++ b/packages/fxa-content-server/server/bin/fxa-content-server.js @@ -27,6 +27,7 @@ const https = require('https'); const path = require('path'); const serveStatic = require('serve-static'); +const { settingsMiddleware } = require('../lib/beta-settings'); const config = require('../lib/configuration'); const sentry = require('../lib/sentry'); const { cors, routing } = require('fxa-shared/express')(); @@ -89,11 +90,7 @@ function makeApp() { writeToDisk: true, }) ); - const { createProxyMiddleware } = require('http-proxy-middleware'); - app.use( - '/beta/settings', - createProxyMiddleware({ target: 'http://localhost:3000', ws: true }) - ); + app.use('/beta/settings', settingsMiddleware); } app.engine('html', consolidate.handlebars); diff --git a/packages/fxa-content-server/server/lib/beta-settings.js b/packages/fxa-content-server/server/lib/beta-settings.js new file mode 100644 index 0000000000..f4ff9c980a --- /dev/null +++ b/packages/fxa-content-server/server/lib/beta-settings.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { createProxyMiddleware } = require('http-proxy-middleware'); +const config = require('./configuration'); + +// Inject Beta Settings meta content +function swapBetaMeta(html, metaContent = {}) { + let result = html; + + Object.keys(metaContent).forEach((key) => { + result = result.replace( + key, + encodeURIComponent(JSON.stringify(metaContent[key])) + ); + }); + + return result; +} + +// Conditionally modify the response +function modifyResponse(proxyRes, req, res) { + const bodyChunks = []; + + proxyRes.on('data', (chunk) => { + bodyChunks.push(chunk); + }); + + proxyRes.on('end', () => { + const body = Buffer.concat(bodyChunks); + + // forward existing response data + res.status(proxyRes.statusCode); + Object.keys(proxyRes.headers).forEach((key) => { + res.append(key, proxyRes.headers[key]); + }); + + // if it's an html content type, inject server config + if ( + proxyRes.headers['content-type'] && + proxyRes.headers['content-type'].includes('text/html') + ) { + let html = body.toString(); + html = swapBetaMeta(html, { + __SERVER_CONFIG__: config, + }); + res.send(new Buffer.from(html)); + } else { + res.send(body); + } + + res.end(); + }); +} + +const settingsMiddleware = createProxyMiddleware({ + target: 'http://localhost:3000', + ws: true, + selfHandleResponse: true, // ensure res.end is not called early + onProxyRes: modifyResponse, +}); + +module.exports = { + settingsMiddleware, + swapBetaMeta, + modifyResponse, +}; diff --git a/packages/fxa-content-server/tests/server/beta-settings.js b/packages/fxa-content-server/tests/server/beta-settings.js new file mode 100644 index 0000000000..e5bf8b6183 --- /dev/null +++ b/packages/fxa-content-server/tests/server/beta-settings.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { registerSuite } = intern.getInterface('object'); +const { expect, use } = require('chai'); +const { readFileSync } = require('fs'); +const { resolve } = require('path'); +const sinonChai = require('sinon-chai'); +const { mockRes } = require('sinon-express-mock'); +const config = require('../../server/lib/configuration'); +const { + swapBetaMeta, + modifyResponse, +} = require('../../server/lib/beta-settings'); + +use(sinonChai); + +const dummyHtml = readFileSync( + resolve(__dirname, './fixtures/server-config-index.html'), + { encoding: 'utf8' } +); + +const responseOptions = { + headers: {}, +}; + +function mockedResponse(data = '', headers = {}) { + return mockRes( + Object.assign(responseOptions, { + headers, + statusCode: 200, + on(name, callback) { + const args = []; + + if (name === 'data') { + args.push(new Buffer.from(data)); + } + + callback(...args); + }, + }) + ); +} + +registerSuite('beta settings', { + tests: { + 'replaces beta meta string with config data': function () { + const config = { kenny: 'spenny' }; + const encodedConfig = encodeURIComponent(JSON.stringify(config)); + + const result = swapBetaMeta(dummyHtml, { + __SERVER_CONFIG__: config, + }); + + expect(result).to.contain(encodedConfig); + }, + 'proxies the response': function () { + const headers = { 'x-foo': 'bar' }; + const proxyingResponse = mockedResponse('', headers); + const proxiedResponse = mockRes(responseOptions); + modifyResponse(proxyingResponse, null, proxiedResponse); + proxyingResponse.send(); + + expect(proxiedResponse.send).to.be.called; + expect(proxiedResponse.end).to.be.called; + expect(proxiedResponse.statusCode).to.equal(proxyingResponse.statusCode); + expect(proxiedResponse.headers).to.equal(headers); + }, + 'modifies the response of html files': function () { + const proxyingResponse = mockedResponse(dummyHtml, { + 'content-type': 'text/html', + }); + const proxiedResponse = mockRes(responseOptions); + modifyResponse(proxyingResponse, null, proxiedResponse); + proxyingResponse.send(dummyHtml); + + const outputHtml = swapBetaMeta(dummyHtml, { + __SERVER_CONFIG__: config, + }); + + expect(proxiedResponse.send).to.be.calledWith( + new Buffer.from(outputHtml) + ); + }, + 'does not modify the response of non-html files': function () { + const data = JSON.stringify({ foo: 'bar' }); + const proxyingResponse = mockedResponse(data, { + 'content-type': 'application/json', + }); + const proxiedResponse = mockRes(responseOptions); + modifyResponse(proxyingResponse, null, proxiedResponse); + proxyingResponse.send(data); + + expect(proxiedResponse.send).to.be.calledWith(new Buffer.from(data)); + }, + }, +}); diff --git a/packages/fxa-content-server/tests/server/fixtures/server-config-index.html b/packages/fxa-content-server/tests/server/fixtures/server-config-index.html new file mode 100644 index 0000000000..699fa52103 --- /dev/null +++ b/packages/fxa-content-server/tests/server/fixtures/server-config-index.html @@ -0,0 +1,14 @@ + + + +
+ +