diff --git a/src/fixRequestBody.test.ts b/src/fixRequestBody.test.ts new file mode 100644 index 0000000..6e1c156 --- /dev/null +++ b/src/fixRequestBody.test.ts @@ -0,0 +1,63 @@ +import express from 'express'; +import http from 'http'; + +import { fixRequestBody } from './fixRequestBody'; + +const fakeProxyRequest = () => { + const proxyRequest = new http.ClientRequest('http://some-host'); + proxyRequest.emit = () => false; // Otherwise we get "Error: getaddrinfo ENOTFOUND some-host" + + return proxyRequest; +}; + +test('should not write when body is undefined', () => { + const proxyRequest = fakeProxyRequest(); + + jest.spyOn(proxyRequest, 'setHeader'); + jest.spyOn(proxyRequest, 'write'); + + fixRequestBody(proxyRequest, { body: undefined } as express.Request); + + expect(proxyRequest.setHeader).not.toHaveBeenCalled(); + expect(proxyRequest.write).not.toHaveBeenCalled(); +}); + +test('should not write when body is empty', () => { + const proxyRequest = fakeProxyRequest(); + + jest.spyOn(proxyRequest, 'setHeader'); + jest.spyOn(proxyRequest, 'write'); + + fixRequestBody(proxyRequest, { body: {} } as express.Request); + + expect(proxyRequest.setHeader).not.toHaveBeenCalled(); + expect(proxyRequest.write).not.toHaveBeenCalled(); +}); + +test('should write when body is not empty and Content-Type is application/json', () => { + const proxyRequest = fakeProxyRequest(); + proxyRequest.setHeader('content-type', 'application/json; charset=utf-8'); + + jest.spyOn(proxyRequest, 'setHeader'); + jest.spyOn(proxyRequest, 'write'); + + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as express.Request); + + const expectedBody = JSON.stringify({ someField: 'some value' }); + expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); + expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); +}); + +test('should write when body is not empty and Content-Type is application/x-www-form-urlencoded', () => { + const proxyRequest = fakeProxyRequest(); + proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded'); + + jest.spyOn(proxyRequest, 'setHeader'); + jest.spyOn(proxyRequest, 'write'); + + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as express.Request); + + const expectedBody = 'someField=some+value'; + expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); + expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); +}); diff --git a/src/fixRequestBody.ts b/src/fixRequestBody.ts new file mode 100644 index 0000000..96720b5 --- /dev/null +++ b/src/fixRequestBody.ts @@ -0,0 +1,33 @@ +import http from 'http'; + +/** + * Fix proxied body if bodyParser is involved. + * + * @see https://github.com/chimurai/http-proxy-middleware/blob/v2.0.1/src/handlers/fix-request-body.ts + * @see https://github.com/chimurai/http-proxy-middleware/issues/320 + * @see https://github.com/chimurai/http-proxy-middleware/pull/492 + */ +const fixRequestBody = (proxyReq: http.ClientRequest, req: http.IncomingMessage) => { + const requestBody = (req as unknown as Request).body; + + if (!requestBody || Object.keys(requestBody).length === 0) { + return; + } + + const contentType = proxyReq.getHeader('Content-Type') as string; + const writeBody = (bodyData: string) => { + // deepcode ignore ContentLengthInCode: bodyParser fix + proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); + proxyReq.write(bodyData); + }; + + if (contentType && contentType.includes('application/json')) { + writeBody(JSON.stringify(requestBody)); + } + + if (contentType === 'application/x-www-form-urlencoded') { + writeBody(new URLSearchParams(requestBody as unknown as string).toString()); + } +}; + +export { fixRequestBody }; diff --git a/src/proxy.ts b/src/proxy.ts index cac53c7..84c9b12 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,9 +1,10 @@ import express from 'express'; import { createProxyServer } from 'http-proxy'; -const proxy = createProxyServer({ changeOrigin: true }); +import { fixRequestBody } from './fixRequestBody'; +import { removeSecureFromSetCookie } from './removeSecureFromSetCookie'; -export const send = ( +const send = ( target: string, req: express.Request, res: express.Response, @@ -19,5 +20,15 @@ export const send = ( // // [Proxy with express.js](https://stackoverflow.com/q/10435407) + const proxy = createProxyServer({ + // Explanations: https://github.com/http-party/node-http-proxy/pull/1130 + changeOrigin: true, + cookieDomainRewrite: req.hostname + }); + + proxy.on('proxyReq', fixRequestBody); + proxy.on('proxyRes', removeSecureFromSetCookie); proxy.web(req, res, { target }, next); }; + +export { send }; diff --git a/src/removeSecureFromSetCookie.test.ts b/src/removeSecureFromSetCookie.test.ts new file mode 100644 index 0000000..3444f61 --- /dev/null +++ b/src/removeSecureFromSetCookie.test.ts @@ -0,0 +1,31 @@ +import { removeSecureFromSetCookie } from './removeSecureFromSetCookie'; + +test('should remove Secure attribute from Set-Cookie header', () => { + const proxyRes = { + headers: { + 'set-cookie': [ + 'cookie1=ZYADVSQYTDZA1; Secure; SameSite', + 'cookie2=ZYADVSQYTDZA2; Secure', + 'cookie3=ZYADVSQYTDZA3' + ] + } + }; + + removeSecureFromSetCookie(proxyRes); + + expect(proxyRes.headers['set-cookie']).toEqual([ + 'cookie1=ZYADVSQYTDZA1; SameSite', + 'cookie2=ZYADVSQYTDZA2', + 'cookie3=ZYADVSQYTDZA3' + ]); +}); + +test('do nothing if no Set-Cookie header', () => { + const proxyRes = { + headers: {} + }; + + removeSecureFromSetCookie(proxyRes); + + expect(proxyRes.headers).toEqual({}); +}); diff --git a/src/removeSecureFromSetCookie.ts b/src/removeSecureFromSetCookie.ts new file mode 100644 index 0000000..7c1ecdc --- /dev/null +++ b/src/removeSecureFromSetCookie.ts @@ -0,0 +1,20 @@ +import http from 'http'; + +/** + * http-proxy does not remove 'Secure' attribute from Set-Cookie header. + * + * @see https://github.com/http-party/node-http-proxy/issues/1165 + * @see https://github.com/http-party/node-http-proxy/pull/1166 + */ +const removeSecureFromSetCookie = (proxyRes: { headers: http.IncomingHttpHeaders }) => { + // ["Header names are lower-cased"](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#messageheaders) + + if (proxyRes.headers['set-cookie']) { + const cookies = proxyRes.headers['set-cookie'].map(cookie => cookie.replace(/; secure/gi, '')); + /* eslint-disable no-param-reassign */ + proxyRes.headers['set-cookie'] = cookies; + /* eslint-enable no-param-reassign */ + } +}; + +export { removeSecureFromSetCookie };