diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java index a703f5ff630e..60ec927cfcf3 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.hbase.io.crypto.tls; -import java.net.InetAddress; import java.security.cert.Certificate; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -28,7 +27,6 @@ import java.util.Locale; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import javax.naming.InvalidNameException; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -40,12 +38,11 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.security.auth.x500.X500Principal; +import org.apache.hadoop.hbase.util.Strings; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses; - /** * When enabled in {@link X509Util}, handles verifying that the hostname of a peer matches the * certificate it presents. @@ -112,9 +109,8 @@ public boolean verify(final String host, final SSLSession session) { void verify(final String host, final X509Certificate cert) throws SSLException { final List subjectAlts = getSubjectAltNames(cert); if (subjectAlts != null && !subjectAlts.isEmpty()) { - Optional inetAddress = parseIpAddress(host); - if (inetAddress.isPresent()) { - matchIPAddress(host, inetAddress.get(), subjectAlts); + if (Strings.isInetAddress(host)) { + matchIPAddress(host, subjectAlts); } else { matchDNSName(host, subjectAlts); } @@ -131,14 +127,14 @@ void verify(final String host, final X509Certificate cert) throws SSLException { } } - private static void matchIPAddress(final String host, final InetAddress inetAddress, - final List subjectAlts) throws SSLException { + private static void matchIPAddress(final String host, final List subjectAlts) + throws SSLException { for (final SubjectName subjectAlt : subjectAlts) { - if (subjectAlt.getType() == SubjectName.IP) { - Optional parsed = parseIpAddress(subjectAlt.getValue()); - if (parsed.filter(altAddr -> altAddr.equals(inetAddress)).isPresent()) { - return; - } + if ( + subjectAlt.getType() == SubjectName.IP + && Strings.hostnamesEqual(host, subjectAlt.getValue()) + ) { + return; } } throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " @@ -230,32 +226,6 @@ private static String extractCN(final String subjectPrincipal) throws SSLExcepti } } - private static Optional parseIpAddress(String host) { - host = host.trim(); - // Uri strings only work for ipv6 and are wrapped with brackets - // Unfortunately InetAddresses can't handle a mixed input, so we - // check here and choose which parse method to use. - if (host.startsWith("[") && host.endsWith("]")) { - return parseIpAddressUriString(host); - } else { - return parseIpAddressString(host); - } - } - - private static Optional parseIpAddressUriString(String host) { - if (InetAddresses.isUriInetAddress(host)) { - return Optional.of(InetAddresses.forUriString(host)); - } - return Optional.empty(); - } - - private static Optional parseIpAddressString(String host) { - if (InetAddresses.isInetAddress(host)) { - return Optional.of(InetAddresses.forString(host)); - } - return Optional.empty(); - } - @SuppressWarnings("MixedMutabilityReturnType") private static List getSubjectAltNames(final X509Certificate cert) { try { diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java index 6759603f3aa5..647aec34a920 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java @@ -18,11 +18,13 @@ package org.apache.hadoop.hbase.util; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; @@ -30,6 +32,7 @@ import org.apache.hbase.thirdparty.com.google.common.base.Joiner; import org.apache.hbase.thirdparty.com.google.common.base.Splitter; +import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses; /** * Utility for Strings. @@ -88,6 +91,51 @@ public static String domainNamePointerToHostName(String dnPtr) { return dnPtr.endsWith(".") ? dnPtr.substring(0, dnPtr.length() - 1) : dnPtr; } + /** + * Returns whether the given string is an IP address, including bracketed IPv6 URI form. + * @param host hostname or IP + * @return {@code true} if {@code host} is an IP address + * @throws NullPointerException if {@code host} is {@code null} + */ + public static boolean isInetAddress(String host) { + Objects.requireNonNull(host, "Hostname or IP cannot be null"); + host = host.trim(); + if (host.startsWith("[") && host.endsWith("]")) { + return InetAddresses.isUriInetAddress(host); + } + return InetAddresses.isInetAddress(host); + } + + private static InetAddress parseInetAddress(String host) { + host = host.trim(); + if (host.startsWith("[") && host.endsWith("]")) { + return InetAddresses.forUriString(host); + } + return InetAddresses.forString(host); + } + + /** + * Compare two host identifiers for equality. DNS hostnames are compared case-insensitively + * because DNS labels are case-insensitive. IP address literals are compared by numeric address. + * @param left first hostname or IP + * @param right second hostname or IP + * @return {@code true} if both refer to the same host identifier + * @throws NullPointerException if either argument is {@code null} + */ + public static boolean hostnamesEqual(String left, String right) { + Objects.requireNonNull(left, "Hostname or IP cannot be null"); + Objects.requireNonNull(right, "Hostname or IP cannot be null"); + boolean leftIsIp = isInetAddress(left); + boolean rightIsIp = isInetAddress(right); + if (leftIsIp != rightIsIp) { + return false; + } + if (leftIsIp) { + return parseInetAddress(left).equals(parseInetAddress(right)); + } + return left.equalsIgnoreCase(right); + } + /** * Push the input string to the right by appending a character before it, usually a space. * @param input the string to pad diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java index da388fda9b26..c428e7a0a571 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hbase.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -57,6 +58,21 @@ public void testDomainNamePointerToHostName() { assertEquals("foo.bar.com", Strings.domainNamePointerToHostName("foo.bar.com.")); } + @Test + public void testHostnamesEqual() { + assertTrue(Strings.hostnamesEqual("HOST.example.com", "host.example.com")); + assertTrue(Strings.hostnamesEqual("rs1", "RS1")); + assertFalse(Strings.hostnamesEqual("host-a.example.com", "host-b.example.com")); + assertTrue(Strings.hostnamesEqual("10.0.0.1", "10.0.0.1")); + assertFalse(Strings.hostnamesEqual("10.0.0.1", "10.0.0.2")); + assertFalse(Strings.hostnamesEqual("HOST.example.com", "10.0.0.1")); + assertTrue(Strings.hostnamesEqual("::1", "0:0:0:0:0:0:0:1")); + assertTrue(Strings.hostnamesEqual("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334")); + assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual(null, "host")); + assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual("host", null)); + } + @Test public void testPadFront() { assertEquals("ddfoo", Strings.padFront("foo", 'd', 5)); diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index 3d7df098a37e..33a2717b07eb 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -168,6 +168,7 @@ import org.apache.hadoop.hbase.util.RetryCounter; import org.apache.hadoop.hbase.util.RetryCounterFactory; import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil; +import org.apache.hadoop.hbase.util.Strings; import org.apache.hadoop.hbase.util.Threads; import org.apache.hadoop.hbase.util.VersionInfo; import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; @@ -1432,8 +1433,9 @@ protected void handleReportForDutyResponse(final RegionServerStartupResponse c) expectedHostName = rpcServices.getSocketAddress().getAddress().getHostAddress(); } boolean isHostnameConsist = StringUtils.isBlank(useThisHostnameInstead) - ? hostnameFromMasterPOV.equals(expectedHostName) - : hostnameFromMasterPOV.equals(useThisHostnameInstead); + ? Strings.hostnamesEqual(hostnameFromMasterPOV, expectedHostName) + : Strings.hostnamesEqual(hostnameFromMasterPOV, useThisHostnameInstead); + if (!isHostnameConsist) { String msg = "Master passed us a different hostname to use; was=" + (StringUtils.isBlank(useThisHostnameInstead)