diff --git a/test/nginx/src/mocha/request.js b/test/nginx/src/mocha/request.js index 4941c8e30..509a8eff3 100644 --- a/test/nginx/src/mocha/request.js +++ b/test/nginx/src/mocha/request.js @@ -1,3 +1,4 @@ +const { isIPv6 } = require('node:net'); const { Readable } = require('node:stream'); module.exports = request; @@ -12,7 +13,7 @@ function request(url, { body, ...options }={}) { return new Promise((resolve, reject) => { try { - const req = getProtocolImplFrom(url).request(url, options, res => { + const req = getProtocolImplFrom(url).request({ ...options, ...preserve(url) }, res => { res.on('error', reject); const body = new Readable({ read:() => {} }); @@ -56,3 +57,24 @@ function getProtocolImplFrom(url) { default: throw new Error(`Unsupported protocol: ${protocol}`); } } + +/** + * Prevent URL path normalisation. + * @see https://nodejs.org/api/http.html#httprequesturl-options-callback + * @see https://nodejs.org/api/url.html#new-urlinput-base + */ +function preserve(urlString) { + const url = new URL(urlString); + if(url.username || url.password) throw new Error('Basic auth creds not yet supported.'); + + const host = safeIpv6(url.hostname); + const port = url.port; + const path = urlString.replace(/^http(s?):\/\/[^/]*/, '') || '/'; + + return { host, port, path }; +} + +function safeIpv6(hostname) { + const maybeV6 = hostname.replace(/^\[(.*)\]$/, (_, $1) => $1); + return isIPv6(maybeV6) ? maybeV6 : hostname; +} diff --git a/test/nginx/src/mocha/request.spec.js b/test/nginx/src/mocha/request.spec.js index cc4929af1..bcb0a075c 100644 --- a/test/nginx/src/mocha/request.spec.js +++ b/test/nginx/src/mocha/request.spec.js @@ -15,8 +15,8 @@ describe('request()', () => { const app = express(); app.use((req, res, next) => { - const { method, path, headers } = req; - requestsReceived.push({ method, path, headers }); + const { method, originalUrl:target, headers } = req; + requestsReceived.push({ method, target, headers }); next(); }); app.get('/redirect-302', (req, res) => { @@ -46,7 +46,7 @@ describe('request()', () => { assert.equal(res.status, 302); assert.equal(res.headers.get('location'), 'http://example.test/redirected'); assert.deepEqual(stripHeaders(requestsReceived), [ - { method:'GET', path:'/redirect-302' }, + { method:'GET', target:'/redirect-302' }, ]); }); @@ -62,10 +62,21 @@ describe('request()', () => { // then assert.equal(res.status, 200); assert.deepEqual(stripHeaders(requestsReceived), [ - { method:'GET', path:'/' }, + { method:'GET', target:'/' }, ]); assert.equal(requestsReceived[0].headers['host'], 'not-a-host'); }); + + it('should not normalise URLs', async () => { + // when + const res = await request(`http://127.0.0.1:${port}/a/../b.html?x=1`); + + // then + assert.equal(res.status, 200); + assert.deepEqual(stripHeaders(requestsReceived), [ + { method:'GET', target:'/a/../b.html?x=1' }, + ]); + }); }); function stripHeaders(arr) {