UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

149 lines (145 loc) 4.48 kB
'use strict'; var errors = require('@backstage/errors'); var dns = require('dns'); const PROTOCOL_SUFFIX = "+srv:"; class SrvResolvers { #cache; #cacheTtlMillis; #resolveSrv; constructor(options) { this.#cache = /* @__PURE__ */ new Map(); this.#cacheTtlMillis = options?.cacheTtlMillis ?? 1e3; this.#resolveSrv = options?.resolveSrv ?? ((host) => new Promise((resolve, reject) => { dns.resolveSrv(host, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); })); } isSrvUrl(url) { try { this.#parseSrvUrl(url); return true; } catch { return false; } } /** * Get a resolver function for a given SRV form URL. * * @param url An SRV form URL, e.g. `http+srv://myplugin.services.region.example.net/api/myplugin` * @returns A function that returns resolved URLs, e.g. `http://1234abcd.region.example.net:8080/api/myplugin` */ getResolver(url) { const { protocol, host, path } = this.#parseSrvUrl(url); return () => this.#resolveHost(host).then( (resolved) => `${protocol}://${resolved}${path}` ); } /** * Attempts to parse out the relevant parts of an SRV URL. */ #parseSrvUrl(url) { let parsedUrl; try { parsedUrl = new URL(url); } catch { throw new errors.InputError( `SRV resolver expected a valid URL starting with http(s)+srv:// but got '${url}'` ); } if (!parsedUrl.protocol?.endsWith(PROTOCOL_SUFFIX) || !parsedUrl.hostname) { throw new errors.InputError( `SRV resolver expected a URL with protocol http(s)+srv:// but got '${url}'` ); } if (parsedUrl.port) { throw new errors.InputError( `SRV resolver URLs cannot contain a port but got '${url}'` ); } if (parsedUrl.username || parsedUrl.password) { throw new errors.InputError( `SRV resolver URLs cannot contain username or password but got '${url}'` ); } if (parsedUrl.search || parsedUrl.hash) { throw new errors.InputError( `SRV resolver URLs cannot contain search params or a hash but got '${url}'` ); } const protocol = parsedUrl.protocol.substring( 0, parsedUrl.protocol.length - PROTOCOL_SUFFIX.length ); const host = parsedUrl.hostname; const path = parsedUrl.pathname.replace(/\/+$/, ""); if (!["http", "https"].includes(protocol)) { throw new errors.InputError( `SRV URLs must be based on http or https but got '${url}'` ); } return { protocol, host, path }; } /** * Resolves a single SRV record name to a host:port string. */ #resolveHost(host) { let records = this.#cache.get(host); if (!records) { records = this.#resolveSrv(host).then( (result) => { if (!result.length) { throw new errors.NotFoundError(`No SRV records found for ${host}`); } return result; }, (err) => { throw new errors.ForwardedError(`Failed SRV resolution for ${host}`, err); } ); this.#cache.set(host, records); setTimeout(() => { this.#cache.delete(host); }, this.#cacheTtlMillis); } return records.then((rs) => { const r = this.#pickRandomRecord(rs); return `${r.name}:${r.port}`; }); } /** * Among a set of records, pick one at random. * * This assumes that the set is not empty. * * Since this contract only ever returns a single record, the best it can do * is to pick weighted-randomly among the highest-priority records. In order * to be smarter than that, the caller would have to be able to make decisions * on the whole set of records. */ #pickRandomRecord(allRecords) { const lowestPriority = allRecords.reduce( (acc, r) => Math.min(acc, r.priority), Number.MAX_SAFE_INTEGER ); const records = allRecords.filter((r) => r.priority === lowestPriority); const totalWeight = records.reduce((acc, r) => acc + r.weight, 0); const targetWeight = Math.random() * totalWeight; let result = records[0]; let currentWeight = 0; for (const record of records) { currentWeight += record.weight; if (targetWeight <= currentWeight) { result = record; break; } } return result; } } exports.SrvResolvers = SrvResolvers; //# sourceMappingURL=SrvResolvers.cjs.js.map