From e3b19937884a635bc9028610208637a69fe6b0a9 Mon Sep 17 00:00:00 2001 From: Antonio Musolino Date: Mon, 22 Aug 2022 15:12:11 +0200 Subject: [PATCH] feat(backend-common): added ports range Signed-off-by: Antonio Musolino --- .changeset/old-rockets-doubt.md | 11 ++++ .../src/reading/FetchUrlReader.test.ts | 54 +++++++++++++++++++ .../src/reading/FetchUrlReader.ts | 49 +++++++++++++++-- 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 .changeset/old-rockets-doubt.md diff --git a/.changeset/old-rockets-doubt.md b/.changeset/old-rockets-doubt.md new file mode 100644 index 0000000000..459cd2790c --- /dev/null +++ b/.changeset/old-rockets-doubt.md @@ -0,0 +1,11 @@ +--- +'@backstage/backend-common': patch +--- + +Added port ranges in allowed hosts: + +```yaml +reading: + allow: + - host: *.examples.org:900-1000 +``` diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index d2fbbab668..75172e83b0 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -74,9 +74,14 @@ describe('FetchUrlReader', () => { reading: { allow: [ { host: 'example.com' }, + { host: 'example.com:100-200' }, { host: 'example.com:700' }, { host: '*.examples.org' }, { host: '*.examples.org:700' }, + { host: '*.examples.org:900-1000' }, + { host: '*.examples.org:900-1000' }, + { host: 'https.org:443' }, + { host: 'http.org:80' }, { host: 'foobar.org', paths: ['/dir1/'], @@ -113,6 +118,55 @@ describe('FetchUrlReader', () => { expect(predicate(new URL('https://foobar.org/dir1/subpath'))).toBe(true); expect(predicate(new URL('https://foobar.org/dir12'))).toBe(false); expect(predicate(new URL('https://foobar.org/'))).toBe(false); + expect(predicate(new URL('https://a.examples.org:900/test'))).toBe(true); + expect(predicate(new URL('https://a.examples.org:1000/test'))).toBe(true); + expect(predicate(new URL('https://a.examples.org:950/test'))).toBe(true); + expect(predicate(new URL('https://a.examples.org:1050/test'))).toBe(false); + expect(predicate(new URL('https://example.com:150/test'))).toBe(true); + expect(predicate(new URL('https://example.com:4000/test'))).toBe(false); + expect(predicate(new URL('https://https.org'))).toBe(true); + expect(predicate(new URL('http://https.org'))).toBe(false); + expect(predicate(new URL('http://http.org'))).toBe(true); + expect(predicate(new URL('https://http.org'))).toBe(false); + }); + + it('factory should throw for malformed uri', async () => { + const buildFactory = (hosts: string[]) => { + return FetchUrlReader.factory({ + config: new ConfigReader({ + backend: { + reading: { + allow: hosts.map(host => ({ host })), + }, + }, + }), + logger: getVoidLogger(), + treeResponseFactory: DefaultReadTreeResponseFactory.create({ + config: new ConfigReader({}), + }), + }); + }; + expect(() => + buildFactory(['example.com:100-200', 'example.com:100-']), + ).toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:-']), + ).toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:500-']), + ).toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:100-50']), + ).toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:-330-']), + ).toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:-100-300']), + ).not.toThrow(); + expect(() => + buildFactory(['example.com:100-200', 'example.com:nb-300']), + ).toThrow(); }); describe('read', () => { diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 5734beddcf..d38cc510b1 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -27,6 +27,41 @@ import { import path from 'path'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; +const isInRange = (num: number, [start, end]: [number, number]) => { + return num >= start && num <= end; +}; + +const parsePortRange = (port: string): [number, number] => { + const isRange = port.includes('-'); + if (isRange) { + const range = port + .split('-') + .map(v => parseInt(v, 10)) + .filter(Boolean) as [number, number]; + if (range.length !== 2) throw new Error(`Port range is not valid: ${port}`); + const [start, end] = range; + if (start <= 0 || end <= 0 || start > end) + throw new Error(`Port range is not valid: [${start}, ${end}]`); + return range; + } + const parsedPort = parseInt(port, 10); + return [parsedPort, parsedPort]; +}; + +const parsePortPredicate = (port: string | undefined) => { + if (port) { + const range = parsePortRange(port); + return (url: URL) => { + if (url.port) return isInRange(parseInt(url.port, 10), range); + + if (url.protocol === 'http:') return isInRange(80, range); + if (url.protocol === 'https:') return isInRange(443, range); + return false; + }; + } + return (url: URL) => !url.port; +}; + /** * A {@link UrlReader} that does a plain fetch of the URL. * @@ -60,11 +95,17 @@ export class FetchUrlReader implements UrlReader { } : (_url: URL) => true; const host = allowConfig.getString('host'); - if (host.startsWith('*.')) { - const suffix = host.slice(1); - return (url: URL) => url.host.endsWith(suffix) && checkPath(url); + const [hostname, port] = host.split(':'); + + const checkPort = parsePortPredicate(port); + + if (hostname.startsWith('*.')) { + const suffix = hostname.slice(1); + return (url: URL) => + url.hostname.endsWith(suffix) && checkPath(url) && checkPort(url); } - return (url: URL) => url.host === host && checkPath(url); + return (url: URL) => + url.hostname === hostname && checkPath(url) && checkPort(url); }) ?? []; const reader = new FetchUrlReader();