From 6c38f5e92031b5574e20d8da28ffeedd89608177 Mon Sep 17 00:00:00 2001 From: Brian Fox Date: Tue, 27 Apr 2021 10:55:57 +0200 Subject: [PATCH 1/2] tests(proxy-backend): align tests with default backend configuration The pathPrefix is not suffixed with `/` by default as noted at https://github.com/backstage/backstage/blob/df897a6126d1641d111b015c527cc8d40c47bd06/packages/backend-common/src/discovery/types.ts#L47 and seen in the implementation at https://github.com/backstage/backstage/blob/df897a6126d1641d111b015c527cc8d40c47bd06/packages/backend-common/src/discovery/SingleHostDiscovery.ts#L81. Currently the tests work in the opposite way - the pathPrefix is suffixed by / whereas the routes are **not** prefixed by /. This commit brings the tests in line with the default configuration prior to any further work. Signed-off-by: Brian Fox --- .../proxy-backend/src/service/router.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 6c47043602..3a0a25f164 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -60,7 +60,7 @@ describe('buildMiddleware', () => { }); it('accepts strings', async () => { - buildMiddleware('/api/', logger, 'test', 'http://mocked'); + buildMiddleware('/proxy', logger, '/test', 'http://mocked'); expect(createProxyMiddleware).toHaveBeenCalledTimes(1); @@ -74,13 +74,13 @@ describe('buildMiddleware', () => { expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); - expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' }); + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); expect(fullConfig.changeOrigin).toBe(true); expect(fullConfig.logProvider!(logger)).toBe(logger); }); it('limits allowedMethods', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedMethods: ['GET', 'DELETE'], }); @@ -97,13 +97,13 @@ describe('buildMiddleware', () => { expect(filter('', { method: 'PATCH', headers: {} })).toBe(false); expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); - expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' }); + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); expect(fullConfig.changeOrigin).toBe(true); expect(fullConfig.logProvider!(logger)).toBe(logger); }); it('permits default headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', }); @@ -143,7 +143,7 @@ describe('buildMiddleware', () => { }); it('permits default and configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', headers: { Authorization: 'my-token', @@ -176,7 +176,7 @@ describe('buildMiddleware', () => { }); it('permits configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedHeaders: ['authorization', 'cookie'], }); @@ -208,7 +208,7 @@ describe('buildMiddleware', () => { }); it('responds default headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', }); @@ -251,7 +251,7 @@ describe('buildMiddleware', () => { }); it('responds configured headers', async () => { - buildMiddleware('/api/', logger, 'test', { + buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', allowedHeaders: ['set-cookie'], }); @@ -282,10 +282,10 @@ describe('buildMiddleware', () => { it('rejects malformed target URLs', async () => { expect(() => - buildMiddleware('/api/', logger, 'test', 'backstage.io'), + buildMiddleware('/proxy', logger, '/test', 'backstage.io'), ).toThrowError(/Proxy target is not a valid URL/); expect(() => - buildMiddleware('/api/', logger, 'test', { target: 'backstage.io' }), + buildMiddleware('/proxy', logger, '/test', { target: 'backstage.io' }), ).toThrowError(/Proxy target is not a valid URL/); }); }); From cdb3426e56f49dd5dffba3c35eea383a416b1bf1 Mon Sep 17 00:00:00 2001 From: Brian Fox Date: Tue, 27 Apr 2021 12:38:31 +0200 Subject: [PATCH 2/2] fix(proxy-backend): prefix routes with `/` if not present in config This commit ensures that the default `pathRewrite` configuration for a proxy element is more resilient to the combination of whether: - `route` is prefixed with `/` - `pathPrefix` on the plugin is suffixed with `/` (which at present it will never be - see comments). Signed-off-by: Brian Fox --- .changeset/thick-cobras-switch.md | 5 +++ docs/plugins/proxying.md | 11 ++--- .../proxy-backend/src/service/router.test.ts | 42 ++++++++++++++++++- plugins/proxy-backend/src/service/router.ts | 16 ++++++- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 .changeset/thick-cobras-switch.md diff --git a/.changeset/thick-cobras-switch.md b/.changeset/thick-cobras-switch.md new file mode 100644 index 0000000000..002c724421 --- /dev/null +++ b/.changeset/thick-cobras-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +Prefix proxy routes with `/` if not present in configuration diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index 50c56af7a6..3e20e37cad 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -36,7 +36,7 @@ Example: ```yaml # in app-config.yaml proxy: - '/simple-example': http://simple.example.com:8080 + simple-example: http://simple.example.com:8080 '/larger-example/v1': target: http://larger.example.com:8080/svc.v1 headers: @@ -46,10 +46,11 @@ proxy: ``` Each key under the proxy configuration entry is a route to match, below the -prefix that the proxy plugin is mounted on. It must start with a slash. For -example, if the backend mounts the proxy plugin as `/proxy`, the above -configuration will lead to the proxy acting on backend requests to -`/api/proxy/simple-example/...` and `/api/proxy/larger-example/v1/...`. +prefix that the proxy plugin is mounted on. If it does not start with a slash, +one will be prefixed automatically. For example, if the backend mounts the proxy +plugin as `/proxy`, the above configuration will lead to the proxy acting on +backend requests to `/api/proxy/simple-example/...` and +`/api/proxy/larger-example/v1/...`. The value inside each route is either a simple URL string, or an object on the format accepted by diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 3a0a25f164..571de96ac4 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -59,7 +59,7 @@ describe('buildMiddleware', () => { mockCreateProxyMiddleware.mockClear(); }); - it('accepts strings', async () => { + it('accepts strings prefixed by /', async () => { buildMiddleware('/proxy', logger, '/test', 'http://mocked'); expect(createProxyMiddleware).toHaveBeenCalledTimes(1); @@ -79,6 +79,46 @@ describe('buildMiddleware', () => { expect(fullConfig.logProvider!(logger)).toBe(logger); }); + it('accepts routes not prefixed with / when path is not suffixed with /', async () => { + buildMiddleware('/proxy', logger, 'test', 'http://mocked'); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ + (pathname: string, req: Partial) => boolean, + ProxyMiddlewareConfig, + ]; + expect(filter('', { method: 'GET', headers: {} })).toBe(true); + expect(filter('', { method: 'POST', headers: {} })).toBe(true); + expect(filter('', { method: 'PUT', headers: {} })).toBe(true); + expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); + expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); + + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); + expect(fullConfig.changeOrigin).toBe(true); + expect(fullConfig.logProvider!(logger)).toBe(logger); + }); + + it('accepts routes prefixed with / when path is suffixed with /', async () => { + buildMiddleware('/proxy/', logger, '/test', 'http://mocked'); + + expect(createProxyMiddleware).toHaveBeenCalledTimes(1); + + const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [ + (pathname: string, req: Partial) => boolean, + ProxyMiddlewareConfig, + ]; + expect(filter('', { method: 'GET', headers: {} })).toBe(true); + expect(filter('', { method: 'POST', headers: {} })).toBe(true); + expect(filter('', { method: 'PUT', headers: {} })).toBe(true); + expect(filter('', { method: 'PATCH', headers: {} })).toBe(true); + expect(filter('', { method: 'DELETE', headers: {} })).toBe(true); + + expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/': '/' }); + expect(fullConfig.changeOrigin).toBe(true); + expect(fullConfig.logProvider!(logger)).toBe(logger); + }); + it('limits allowedMethods', async () => { buildMiddleware('/proxy', logger, '/test', { target: 'http://mocked', diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 5c306de966..71ed82c23b 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -77,10 +77,24 @@ export function buildMiddleware( `Proxy target is not a valid URL: ${fullConfig.target ?? ''}`, ); } + // Default is to do a path rewrite that strips out the proxy's path prefix // and the rest of the route. if (fullConfig.pathRewrite === undefined) { - const routeWithSlash = route.endsWith('/') ? route : `${route}/`; + let routeWithSlash = route.endsWith('/') ? route : `${route}/`; + + if (!pathPrefix.endsWith('/') && !routeWithSlash.startsWith('/')) { + // Need to insert a / between pathPrefix and routeWithSlash + routeWithSlash = `/${routeWithSlash}`; + } else if (pathPrefix.endsWith('/') && routeWithSlash.startsWith('/')) { + // Never expect this to happen at this point in time as + // pathPrefix is set using `getExternalBaseUrl` which "Returns the + // external HTTP base backend URL for a given plugin, + // **without a trailing slash.**". But in case this changes in future, we + // need to drop a / on either pathPrefix or routeWithSlash + routeWithSlash = routeWithSlash.substring(1); + } + fullConfig.pathRewrite = { [`^${pathPrefix}${routeWithSlash}`]: '/', };