diff --git a/.secrets.baseline b/.secrets.baseline index 9363d40f3..8f76e74bd 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -75,6 +75,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -114,7 +118,7 @@ "filename": "app/controllers/web-payments/apple-pay/merchant-validation.controller.js", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 20 + "line_number": 19 } ], "test/controllers/web-payments/apple-pay/normalise-apple-pay-payload.test.js": [ @@ -385,5 +389,5 @@ } ] }, - "generated_at": "2024-07-30T15:02:23Z" + "generated_at": "2024-08-09T08:40:47Z" } diff --git a/app/controllers/web-payments/apple-pay/merchant-validation.controller.js b/app/controllers/web-payments/apple-pay/merchant-validation.controller.js index fcda5d175..2d3d96e37 100644 --- a/app/controllers/web-payments/apple-pay/merchant-validation.controller.js +++ b/app/controllers/web-payments/apple-pay/merchant-validation.controller.js @@ -4,8 +4,7 @@ const request = require('requestretry') // to be removed once axios is in use const logger = require('../../../utils/logger')(__filename) const { getLoggingFields } = require('../../../utils/logging-fields-helper') const axios = require('axios') -const https = require('https') -const { HttpsProxyAgent } = require('https-proxy-agent') +const { HttpsProxyAgent } = require('hpagent') const proxyUrl = process.env.HTTPS_PROXY const applePayMerchantValidationViaAxios = process.env.APPLE_PAY_MERCHANT_VALIDATION_VIA_AXIOS === 'true' @@ -55,10 +54,13 @@ module.exports = async (req, res) => { return res.sendStatus(400) } - const httpsAgent = new https.Agent({ - cert: merchantIdentityVars.cert, - key: merchantIdentityVars.key - }); + const httpsAgent = new HttpsProxyAgent({ + proxy: proxyUrl, + cert: merchantIdentityVars.cert, + key: merchantIdentityVars.key + }) + + const axiosInstance = axios.create({ httpsAgent, proxy: false }); if (proxyUrl) { logger.info('Using proxy URL') @@ -95,59 +97,42 @@ module.exports = async (req, res) => { if (applePayMerchantValidationViaAxios) { if (proxyUrl) { - logger.info('Generating Apple Pay session via axios and https proxy agent') + logger.info('Generating Apple Pay session via axios and https proxy agent (hpagent)') const data = { - cert: merchantIdentityVars.cert, - key: merchantIdentityVars.key, merchantIdentifier: merchantIdentityVars.merchantIdentifier, displayName: 'GOV.UK Pay', initiative: 'web', initiativeContext: process.env.APPLE_PAY_MERCHANT_DOMAIN } - - const httpsProxyAgent = new HttpsProxyAgent(proxyUrl, { - cert: merchantIdentityVars.cert, - key: merchantIdentityVars.key - }); - - // const httpsAgent = new https.Agent({ - // proxy: httpsProxyAgent - // }); - - const axiosInstance = axios.create({ - httpsAgent: httpsProxyAgent - }); - - try { const response = await axiosInstance.post(url, data, { headers: { 'Content-Type': 'application/json; charset=utf-8' } }) - logger.info('Apple Pay session successfully generated via axios and https proxy agent') + logger.info('Apple Pay session successfully generated via axios and https proxy agent (hpagent)') res.status(200).send(response.data) } catch (error) { - logger.info('Error generating Apple Pay session', { + logger.info('Error generating Apple Pay session with axios and https proxy agent (hpagent)', { ...getLoggingFields(req), error: error.message, status: error.response ? error.response.status : 'No status' }) - logger.info('Apple Pay session via axios and https proxy agent failed', 'Apple Pay Error') + logger.info('Apple Pay session via axios and https proxy agent (hpagent) failed', 'Apple Pay Error') res.status(500).send('Apple Pay Error') } } else { - logger.info('Generating Apple Pay session via axios and https agent (local machine)') + logger.info('Generating Apple Pay session via axios and https proxy agent (hpagent) (NO PROXY)') try { const response = await axios(options) - logger.info('Apple Pay session successfully generated via axios and https agent') + logger.info('Apple Pay session successfully generated via axios and https proxy agent (hpagent) (NO PROXY)') res.status(200).send(response.data) } catch (error) { - logger.info('Error generating Apple Pay session', { + logger.info('Error generating Apple Pay session (NO PROXY)', { ...getLoggingFields(req), error: error.message }) - logger.info('Apple Pay session via axios and https agent failed', 'Apple Pay Error') + logger.info('Apple Pay session via axios and https proxy agent (hpagent) with no proxy failed', 'Apple Pay Error') res.status(500).send('Apple Pay Error') } } diff --git a/package-lock.json b/package-lock.json index 8272f5cde..bf92e861a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "gaap-analytics": "^3.1.0", "govuk-frontend": "^4.8.0", "helmet": "^7.1.0", - "https-proxy-agent": "^7.0.5", + "hpagent": "^1.2.0", "i18n": "0.15.x", "lodash": "4.17.x", "mailcheck": "^1.1.1", @@ -2831,6 +2831,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, "dependencies": { "debug": "^4.3.4" }, @@ -8879,6 +8880,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -9003,18 +9012,6 @@ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", "dev": true }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -14782,9 +14779,9 @@ } }, "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/sshpk": { @@ -19130,6 +19127,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, "requires": { "debug": "^4.3.4" } @@ -23848,6 +23846,11 @@ } } }, + "hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==" + }, "html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -23941,15 +23944,6 @@ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", "dev": true }, - "https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -28420,9 +28414,9 @@ "dev": true }, "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "sshpk": { diff --git a/package.json b/package.json index 59546e04b..6b1e31cc6 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "gaap-analytics": "^3.1.0", "govuk-frontend": "^4.8.0", "helmet": "^7.1.0", - "https-proxy-agent": "^7.0.5", + "hpagent": "^1.2.0", "i18n": "0.15.x", "lodash": "4.17.x", "mailcheck": "^1.1.1", diff --git a/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js b/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js index c786dac1a..e69e5be50 100644 --- a/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js +++ b/test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js @@ -2,7 +2,6 @@ const sinon = require('sinon') const proxyquire = require('proxyquire') -const https = require('https') const merchantDomain = 'www.pymnt.uk' const worldpayMerchantId = 'worldpay.merchant.id' @@ -15,9 +14,14 @@ const url = 'https://fakeapple.url' const appleResponse = { status: 200, data: { foo: 'bar' } } +const hpagentMock = { + HttpsProxyAgent: sinon.stub().returns({}) +} + function getControllerWithMocks (axiosMock) { return proxyquire('../../../../app/controllers/web-payments/apple-pay/merchant-validation.controller', { - axios: axiosMock + axios: axiosMock, + hpagent: hpagentMock }) } @@ -47,6 +51,7 @@ describe('Validate with Apple the merchant is legitimate', () => { describe('when running locally with no proxy', () => { it('should return a payload for a Worldpay payment if Merchant is valid', async () => { + const axiosStub = sinon.stub().resolves(appleResponse) const controller = getControllerWithMocks(axiosStub) const req = { @@ -57,23 +62,21 @@ describe('Validate with Apple the merchant is legitimate', () => { } await controller(req, res) - sinon.assert.calledWith(axiosStub, sinon.match({ - url, + sinon.assert.calledOnce(axiosStub) + const axiosCallArg = axiosStub.getCall(0).args[0] + + sinon.assert.match(axiosCallArg, { + url: url, method: 'post', + cert: sinon.match(cert => cert.includes(worldpayCertificate)), + key: sinon.match(key => key.includes(worldpayKey)), headers: { 'Content-Type': 'application/json' }, data: { merchantIdentifier: worldpayMerchantId, displayName: 'GOV.UK Pay', initiative: 'web', initiativeContext: merchantDomain - }, - httpsAgent: sinon.match.instanceOf(https.Agent) - })) - - const httpsAgentArg = axiosStub.getCall(0).args[0].httpsAgent - sinon.assert.match(httpsAgentArg.options, { - cert: sinon.match(cert => cert.includes(worldpayCertificate)), - key: sinon.match(key => key.includes(worldpayKey)) + } }) sinon.assert.calledWith(res.status, 200) @@ -82,13 +85,15 @@ describe('Validate with Apple the merchant is legitimate', () => { }) describe('when there is a proxy', () => { - it('should return a payload for a Worldpay payment if Merchant is valid', async () => { + beforeEach(() => { process.env.HTTPS_PROXY = 'https://fakeproxy.com' + }) + + it('should return a payload for a Worldpay payment if Merchant is valid', async () => { const axiosPostStub = sinon.stub().resolves(appleResponse) const axiosCreateStub = sinon.stub().returns({ post: axiosPostStub }) const axiosStub = { - create: axiosCreateStub, - post: sinon.stub().resolves(appleResponse) + create: axiosCreateStub } const controller = getControllerWithMocks(axiosStub) @@ -100,9 +105,16 @@ describe('Validate with Apple the merchant is legitimate', () => { } await controller(req, res) - // sinon.assert.calledWith(axiosCreateStub, sinon.match({ - // httpsAgent: sinon.match.instanceOf(https.Agent) - // })) + sinon.assert.calledWith(hpagentMock.HttpsProxyAgent, sinon.match({ + proxy: 'https://fakeproxy.com', + cert: sinon.match(cert => cert.includes(worldpayCertificate)), + key: sinon.match(key => key.includes(worldpayKey)) + })) + + sinon.assert.calledWith(axiosCreateStub, sinon.match({ + httpsAgent: sinon.match.any, + proxy: false + })) sinon.assert.calledWith(axiosPostStub, sinon.match(url), @@ -122,12 +134,10 @@ describe('Validate with Apple the merchant is legitimate', () => { }) it('should return a payload for a Stripe payment if Merchant is valid', async () => { - process.env.HTTPS_PROXY = 'https://fakeproxy.com' const axiosPostStub = sinon.stub().resolves({ data: appleResponse.data, status: 200 }) const axiosCreateStub = sinon.stub().returns({ post: axiosPostStub }) const axiosStub = { - create: axiosCreateStub, - post: sinon.stub().resolves({ data: appleResponse.data, status: 200 }) + create: axiosCreateStub } const controller = getControllerWithMocks(axiosStub) @@ -139,9 +149,16 @@ describe('Validate with Apple the merchant is legitimate', () => { } await controller(req, res) - // sinon.assert.calledWith(axiosCreateStub, sinon.match({ - // httpsAgent: sinon.match.instanceOf(https.Agent) - // })) + sinon.assert.calledWith(hpagentMock.HttpsProxyAgent, sinon.match({ + proxy: 'https://fakeproxy.com', + cert: sinon.match(cert => cert.includes(stripeCertificate)), + key: sinon.match(key => key.includes(stripeKey)) + })) + + sinon.assert.calledWith(axiosCreateStub, sinon.match({ + httpsAgent: sinon.match.any, + proxy: false + })) sinon.assert.calledWith(axiosPostStub, sinon.match(url), @@ -160,27 +177,11 @@ describe('Validate with Apple the merchant is legitimate', () => { sinon.assert.calledWith(sendSpy, appleResponse.data) }) - it('should return 400 if no url is provided', async () => { - const axiosStub = sinon.stub().resolves({ data: appleResponse.data, status: 200 }) - const controller = getControllerWithMocks(axiosStub) - - const req = { - body: { - paymentProvider: 'worldpay' - } - } - await controller(req, res) - sinon.assert.calledWith(res.sendStatus, 400) - sinon.assert.notCalled(axiosStub) - }) - it('should return a payload for a Sandbox payment if Merchant is valid', async () => { - process.env.HTTPS_PROXY = 'https://fakeproxy.com' const axiosPostStub = sinon.stub().resolves(appleResponse) const axiosCreateStub = sinon.stub().returns({ post: axiosPostStub }) const axiosStub = { - create: axiosCreateStub, - post: sinon.stub().resolves(appleResponse) + create: axiosCreateStub } const controller = getControllerWithMocks(axiosStub) @@ -193,9 +194,16 @@ describe('Validate with Apple the merchant is legitimate', () => { await controller(req, res) - // sinon.assert.calledWith(axiosCreateStub, sinon.match({ - // httpsAgent: sinon.match.instanceOf(https.Agent) - // })) + sinon.assert.calledWith(hpagentMock.HttpsProxyAgent, sinon.match({ + proxy: 'https://fakeproxy.com', + cert: sinon.match(cert => cert.includes(worldpayCertificate)), + key: sinon.match(key => key.includes(worldpayKey)) + })) + + sinon.assert.calledWith(axiosCreateStub, sinon.match({ + httpsAgent: sinon.match.any, + proxy: false + })) sinon.assert.calledWith(axiosPostStub, sinon.match(url), @@ -213,35 +221,53 @@ describe('Validate with Apple the merchant is legitimate', () => { sinon.assert.calledWith(res.status, 200) sinon.assert.calledWith(sendSpy, appleResponse.data) }) + }) - it('should return 400 for unexpected payment provider', async () => { - const axiosStub = sinon.stub().resolves({ data: appleResponse.data, status: 200 }) - const controller = getControllerWithMocks(axiosStub) + it('should return 400 if no url is provided', async () => { + const axiosStub = sinon.stub().resolves({ data: appleResponse.data, status: 200 }) + const controller = getControllerWithMocks(axiosStub) - const req = { - body: { - url, - paymentProvider: 'mystery' - } + const req = { + body: { + paymentProvider: 'worldpay' } - await controller(req, res) - sinon.assert.calledWith(res.sendStatus, 400) - sinon.assert.notCalled(axiosStub) - }) + } + await controller(req, res) + sinon.assert.calledWith(res.sendStatus, 400) + sinon.assert.notCalled(axiosStub) + }) - it('should return an error if Apple Pay returns an error', async () => { - const axiosErrorStub = sinon.stub().rejects(new Error('Whatever error from Apple Pay')) - const controller = getControllerWithMocks(axiosErrorStub) + it('should return 400 for unexpected payment provider', async () => { + const axiosStub = sinon.stub().resolves({ data: appleResponse.data, status: 200 }) + const controller = getControllerWithMocks(axiosStub) - const req = { - body: { - url, - paymentProvider: 'worldpay' - } + const req = { + body: { + url, + paymentProvider: 'mystery' } - await controller(req, res) - sinon.assert.calledWith(res.status, 500) - sinon.assert.calledWith(sendSpy, 'Apple Pay Error') - }) + } + await controller(req, res) + sinon.assert.calledWith(res.sendStatus, 400) + sinon.assert.notCalled(axiosStub) + }) + + it('should return an error if Apple Pay returns an error', async () => { + const axiosPostStub = sinon.stub().rejects(new Error('Whatever error from Apple Pay')) + const axiosCreateStub = sinon.stub().returns({ post: axiosPostStub }) + const axiosStub = { + create: axiosCreateStub + } + const controller = getControllerWithMocks(axiosStub) + + const req = { + body: { + url, + paymentProvider: 'worldpay' + } + } + await controller(req, res) + sinon.assert.calledWith(res.status, 500) + sinon.assert.calledWith(sendSpy, 'Apple Pay Error') }) })