UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

150 lines (144 loc) 5.27 kB
'use strict'; var errors = require('@backstage/errors'); var platformPath = require('path'); var ReadUrlResponseFactory = require('./ReadUrlResponseFactory.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var platformPath__default = /*#__PURE__*/_interopDefaultCompat(platformPath); const isInRange = (num, [start, end]) => { return num >= start && num <= end; }; const parsePortRange = (port) => { const isRange = port.includes("-"); if (isRange) { const range = port.split("-").map((v) => parseInt(v, 10)).filter(Boolean); 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) => { if (port) { const range = parsePortRange(port); return (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.port; }; class FetchUrlReader { /** * The factory creates a single reader that will be used for reading any URL that's listed * in configuration at `backend.reading.allow`. The allow list contains a list of objects describing * targets to allow, containing the following fields: * * `host`: * Either full hostnames to match, or subdomain wildcard matchers with a leading '*'. * For example 'example.com' and '*.example.com' are valid values, 'prod.*.example.com' is not. * * `paths`: * An optional list of paths which are allowed. If the list is omitted all paths are allowed. */ static factory = ({ config }) => { const predicates = config.getOptionalConfigArray("backend.reading.allow")?.map((allowConfig) => { const paths = allowConfig.getOptionalStringArray("paths"); const checkPath = paths ? (url) => { const targetPath = platformPath__default.default.posix.normalize(url.pathname); return paths.some( (allowedPath) => targetPath.startsWith(allowedPath) ); } : (_url) => true; const host = allowConfig.getString("host"); const [hostname, port] = host.split(":"); const checkPort = parsePortPredicate(port); if (hostname.startsWith("*.")) { const suffix = hostname.slice(1); return (url) => url.hostname.endsWith(suffix) && checkPath(url) && checkPort(url); } return (url) => url.hostname === hostname && checkPath(url) && checkPort(url); }) ?? []; const reader = new FetchUrlReader(); const predicate = (url) => predicates.some((p) => p(url)); return [{ reader, predicate }]; }; async read(url) { const response = await this.readUrl(url); return response.buffer(); } async readUrl(url, options) { let response; try { response = await fetch(url, { headers: { ...options?.etag && { "If-None-Match": options.etag }, ...options?.lastModifiedAfter && { "If-Modified-Since": options.lastModifiedAfter.toUTCString() }, ...options?.token && { Authorization: `Bearer ${options.token}` } }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can // be removed after we support ESM for CLI dependencies and migrate to // version 3 of node-fetch. // https://github.com/backstage/backstage/issues/8242 signal: options?.signal }); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } if (response.status === 304) { throw new errors.NotModifiedError(); } if (response.ok) { return ReadUrlResponseFactory.ReadUrlResponseFactory.fromResponse(response); } const message = `could not read ${url}, ${response.status} ${response.statusText}`; if (response.status === 404) { throw new errors.NotFoundError(message); } throw new Error(message); } async readTree() { throw new Error("FetchUrlReader does not implement readTree"); } async search(url, options) { const { pathname } = new URL(url); if (pathname.match(/[*?]/)) { throw new Error("Unsupported search pattern URL"); } try { const data = await this.readUrl(url, options); return { files: [ { url, content: data.buffer, lastModifiedAt: data.lastModifiedAt } ], etag: data.etag ?? "" }; } catch (error) { errors.assertError(error); if (error.name === "NotFoundError") { return { files: [], etag: "" }; } throw error; } } toString() { return "fetch{}"; } } exports.FetchUrlReader = FetchUrlReader; //# sourceMappingURL=FetchUrlReader.cjs.js.map