From cdb3426e56f49dd5dffba3c35eea383a416b1bf1 Mon Sep 17 00:00:00 2001 From: Brian Fox Date: Tue, 27 Apr 2021 12:38:31 +0200 Subject: [PATCH] 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}`]: '/', };