+ return response.body;
+ }
+
+
+ /**
+ * Validate a url has a specific schema.
+ * @param {URL} urlObj
+ * @param {String[]} validSchemes
+ */
+ static _urlValidScheme(urlObj, validSchemes = ['http:', 'https:']) {
+ if (!validSchemes.includes(urlObj.protocol)) {
+ throw new ValidationError(`unsupported url scheme '${urlObj.protocol}'`);
+ }
+ }
+
+
+ /**
+ * Validate a url does not include some components.
+ * @param {URL} urlObj
+ * @param {String[]} disallowed
+ */
+ static _urlPartsDisallowed(urlObj, disallowed) {
+ disallowed.forEach((part) => {
+ if (urlObj[part]) { // eslint-disable-line security/detect-object-injection
+ throw new ValidationError(`url cannot contain ${common.properURLComponentName(part)}`);
+ }
+ });
+ }
+
+
+ /**
+ * Validate a url does not have relative path.
+ * @param {String} url
+ */
+ static _urlPathNoDots(url) {
+ if (noDotPathRE.test(url)) {
+ throw new ValidationError('relative path segment not valid');
+ }
+ }
+
+
+ /**
+ * Validate a url does not have a hostname which is an ip address.
+ * N.B. Sets isLoopback on urlObj
+ * @param {URL} urlObj
+ * @param {Boolean} allowLoopback
+ * @param {Boolean} resolveHostname
+ * @returns {Promise<void>}
+ */
+ static async _urlNamedHost(urlObj, allowLoopback, resolveHostname) {
+ let address;
+ if (v6HostRE.test(urlObj.hostname)) {
+ /**
+ * We do not need to worry about the Address6() failing to parse,
+ * as if it looks like an ipv6 addr but is not valid, the URL()
+ * call would already have failed.
+ */
+ address = new Address6(urlObj.hostname.slice(1, urlObj.hostname.length - 1));
+ /* succeeded parsing as ipv6, reject unless loopback */
+ urlObj.isLoopback = address.isLoopback();
+ } else {
+ try {
+ address = new Address4(urlObj.hostname);
+ /* succeeded parsing as ipv4, reject unless loopback */
+ urlObj.isLoopback = address.isInSubnet(loopback4);
+ } catch (e) {
+ /* did not parse as ip, carry on */
+ }
+ }
+
+ if (resolveHostname && !urlObj.isLoopback) {
+ /**
+ * Resolve hostname to check for localhost.
+ * This is more complicated due to SSRF mitigation:
+ * If the hostname does not end with a ., we also resolve that,
+ * and complain if the two resolutions do not match, assuming
+ * malicious intent for the server to resolve a local record.
+ */
+ const hostnames = [urlObj.hostname];
+ if (!urlObj.hostname.endsWith('.')) {
+ hostnames.push(urlObj.hostname + '.');
+ }
+ const settledResolutions = await Promise.allSettled(hostnames.map((hostname) => dns.promises.lookup(hostname, {
+ all: true,
+ verbatim: true,
+ })));
+ // If any resolution failed, bail.
+ if (settledResolutions
+ .map((resolution) => resolution.status)
+ .includes('rejected')) {
+ throw new ValidationError('could not resolve hostname');
+ }
+
+ // extract each resolution value, array of {address,family}
+ const resolutions = settledResolutions.map((resolution) => resolution.value);
+
+ // If there were two resolutions, ensure they returned identical results.
+ if (resolutions.length > 1) {
+ // create set of addresses for each resolution
+ const addressSets = resolutions.map((addrs) => {
+ return new Set((addrs || []).map((a) => a.address));
+ });
+ const differences = common.setSymmetricDifference(...addressSets);
+ if (differences.size) {
+ throw new ValidationError('inconsistent hostname resolution');
+ }
+ }
+ const resolvedHost = resolutions[0] || [];
+
+ // Persist the loopback state
+ urlObj.isLoopback = resolvedHost.reduce((acc, resolved) => {
+ let addr;
+ switch (resolved.family) {
+ case 4:
+ addr = new Address4(resolved.address);
+ return acc || addr.isInSubnet(loopback4);
+ case 6:
+ addr = new Address6(resolved.address);
+ return acc || addr.isLoopback();
+ default:
+ return acc;
+ }
+ }, false);
+ }
+
+ if (address
+ && (!urlObj.isLoopback || !allowLoopback)) {
+ throw new ValidationError('hostname cannot be IP');
+ }
+ }
+
+
+ /**
+ * Ensure a url meets the requirements to be a profile uri.
+ * @param {String} url
+ * @param {Object} validationOptions
+ * @param {Boolean=} validationOptions.allowLoopback default is false, following spec
+ * @param {Boolean=} validationOptions.resolveHostname default is false, following spec
+ * @returns {Promise<URL>}
+ */
+ async validateProfile(url, validationOptions) {
+ const _scope = _fileScope('validateProfile');
+ const errorScope = 'invalid profile url';
+
+ const options = Object.assign({
+ allowLoopback: false,
+ resolveHostname: false,
+ }, validationOptions);
+
+ let profile;