diff --git a/.baseline/checkstyle/checkstyle-suppressions.xml b/.baseline/checkstyle/checkstyle-suppressions.xml
index ca12ca6ea..7fc2d7ecc 100644
--- a/.baseline/checkstyle/checkstyle-suppressions.xml
+++ b/.baseline/checkstyle/checkstyle-suppressions.xml
@@ -30,4 +30,6 @@
+
+
diff --git a/.gitignore b/.gitignore
index 9be4cac7d..ff67d8ba9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ build/
.project
.settings
bin/
+generated/
# Idea
*.iml
diff --git a/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfiguration.java b/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfiguration.java
index f3df6b84a..f5a423753 100644
--- a/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfiguration.java
+++ b/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfiguration.java
@@ -58,6 +58,12 @@ public interface ClientConfiguration {
/** See {@link com.palantir.conjure.java.api.config.service.PartialServiceConfiguration#enableGcmCipherSuites}. */
boolean enableGcmCipherSuites();
+ /**
+ * See {@link
+ * com.palantir.conjure.java.api.config.service.PartialServiceConfiguration#fallbackToCommonNameVerification}.
+ */
+ boolean fallbackToCommonNameVerification();
+
/** The proxy to use for the HTTP connection. */
ProxySelector proxy();
diff --git a/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfigurations.java b/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfigurations.java
index 29c79feae..05ef593fe 100644
--- a/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfigurations.java
+++ b/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfigurations.java
@@ -43,6 +43,7 @@ public final class ClientConfigurations {
private static final Duration DEFAULT_BACKOFF_SLOT_SIZE = Duration.ofMillis(250);
private static final Duration DEFAULT_FAILED_URL_COOLDOWN = Duration.ZERO;
private static final boolean DEFAULT_ENABLE_GCM_CIPHERS = false;
+ private static final boolean DEFAULT_FALLBACK_TO_COMMON_NAME_VERIFICATION = false;
private static final NodeSelectionStrategy DEFAULT_NODE_SELECTION_STRATEGY = NodeSelectionStrategy.PIN_UNTIL_ERROR;
private static final int DEFAULT_MAX_NUM_RETRIES = 4;
@@ -61,6 +62,8 @@ public static ClientConfiguration of(ServiceConfiguration config) {
.readTimeout(config.readTimeout().orElse(DEFAULT_READ_TIMEOUT))
.writeTimeout(config.writeTimeout().orElse(DEFAULT_WRITE_TIMEOUT))
.enableGcmCipherSuites(config.enableGcmCipherSuites().orElse(DEFAULT_ENABLE_GCM_CIPHERS))
+ .fallbackToCommonNameVerification(config.fallbackToCommonNameVerification()
+ .orElse(DEFAULT_FALLBACK_TO_COMMON_NAME_VERIFICATION))
.proxy(config.proxy().map(ClientConfigurations::createProxySelector).orElse(ProxySelector.getDefault()))
.proxyCredentials(config.proxy().flatMap(ProxyConfiguration::credentials))
.meshProxy(meshProxy(config.proxy()))
@@ -85,6 +88,7 @@ public static ClientConfiguration of(
.readTimeout(DEFAULT_READ_TIMEOUT)
.writeTimeout(DEFAULT_WRITE_TIMEOUT)
.enableGcmCipherSuites(DEFAULT_ENABLE_GCM_CIPHERS)
+ .fallbackToCommonNameVerification(DEFAULT_FALLBACK_TO_COMMON_NAME_VERIFICATION)
.proxy(ProxySelector.getDefault())
.proxyCredentials(Optional.empty())
.maxNumRetries(DEFAULT_MAX_NUM_RETRIES)
diff --git a/client-config/src/test/java/com/palantir/conjure/java/client/config/ClientConfigurationsTest.java b/client-config/src/test/java/com/palantir/conjure/java/client/config/ClientConfigurationsTest.java
index fa7c6d7d0..a586ab1bc 100644
--- a/client-config/src/test/java/com/palantir/conjure/java/client/config/ClientConfigurationsTest.java
+++ b/client-config/src/test/java/com/palantir/conjure/java/client/config/ClientConfigurationsTest.java
@@ -54,6 +54,7 @@ public void testFromServiceConfig_fillsInDefaults() {
assertThat(actual.readTimeout()).isEqualTo(Duration.ofMinutes(10));
assertThat(actual.writeTimeout()).isEqualTo(Duration.ofMinutes(10));
assertThat(actual.enableGcmCipherSuites()).isFalse();
+ assertThat(actual.fallbackToCommonNameVerification()).isFalse();
assertThat(actual.proxy().select(URI.create("https://foo"))).containsExactly(Proxy.NO_PROXY);
}
@@ -70,6 +71,7 @@ public void testFromParameters_fillsInDefaults() {
assertThat(actual.readTimeout()).isEqualTo(Duration.ofMinutes(10));
assertThat(actual.writeTimeout()).isEqualTo(Duration.ofMinutes(10));
assertThat(actual.enableGcmCipherSuites()).isFalse();
+ assertThat(actual.fallbackToCommonNameVerification()).isFalse();
assertThat(actual.proxy().select(URI.create("https://foo"))).containsExactly(Proxy.NO_PROXY);
}
diff --git a/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/DistinguishedNameParser.java b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/DistinguishedNameParser.java
new file mode 100644
index 000000000..cf009e8b4
--- /dev/null
+++ b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/DistinguishedNameParser.java
@@ -0,0 +1,414 @@
+/*
+ * 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 com.palantir.conjure.java.okhttp;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A distinguished name (DN) parser. This parser only supports extracting a string value from a DN.
+ * It doesn't support values in the hex-string style.
+ *
+ * Copied from https://raw.githubusercontent.com/square/okhttp/parent-3.9.1/okhttp/src/main/java/okhttp3/internal/tls/DistinguishedNameParser.java
+ * Original license preserved. Changes made:
+ *
+ *
+ * - Changed indentation from 2 spaces to 4 spaces
+ * - Suppressed checkstyle
+ *
+ */
+final class DistinguishedNameParser {
+ private final String dn;
+ private final int length;
+ private int pos;
+ private int beg;
+ private int end;
+
+ /** Temporary variable to store positions of the currently parsed item. */
+ private int cur;
+
+ /** Distinguished name characters. */
+ private char[] chars;
+
+ DistinguishedNameParser(X500Principal principal) {
+ // RFC2253 is used to ensure we get attributes in the reverse
+ // order of the underlying ASN.1 encoding, so that the most
+ // significant values of repeated attributes occur first.
+ this.dn = principal.getName(X500Principal.RFC2253);
+ this.length = this.dn.length();
+ }
+
+ // gets next attribute type: (ALPHA 1*keychar) / oid
+ private String nextAT() {
+ // skip preceding space chars, they can present after
+ // comma or semicolon (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+ if (pos == length) {
+ return null; // reached the end of DN
+ }
+
+ // mark the beginning of attribute type
+ beg = pos;
+
+ // attribute type chars
+ pos++;
+ for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
+ // we don't follow exact BNF syntax here:
+ // accept any char except space and '='
+ }
+ if (pos >= length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ // mark the end of attribute type
+ end = pos;
+
+ // skip trailing space chars between attribute type and '='
+ // (compatibility with RFC 1779)
+ if (chars[pos] == ' ') {
+ for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
+ }
+
+ if (chars[pos] != '=' || pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+ }
+
+ pos++; //skip '=' char
+
+ // skip space chars between '=' and attribute value
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+
+ // in case of oid attribute type skip its prefix: "oid." or "OID."
+ // (compatibility with RFC 1779)
+ if ((end - beg > 4) && (chars[beg + 3] == '.')
+ && (chars[beg] == 'O' || chars[beg] == 'o')
+ && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
+ && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
+ beg += 4;
+ }
+
+ return new String(chars, beg, end - beg);
+ }
+
+ // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
+ private String quotedAV() {
+ pos++;
+ beg = pos;
+ end = beg;
+ while (true) {
+
+ if (pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ if (chars[pos] == '"') {
+ // enclosing quotation was found
+ pos++;
+ break;
+ } else if (chars[pos] == '\\') {
+ chars[end] = getEscaped();
+ } else {
+ // shift char: required for string with escaped chars
+ chars[end] = chars[pos];
+ }
+ pos++;
+ end++;
+ }
+
+ // skip trailing space chars before comma or semicolon.
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+
+ return new String(chars, beg, end - beg);
+ }
+
+ // gets hex string attribute value: "#" hexstring
+ private String hexAV() {
+ if (pos + 4 >= length) {
+ // encoded byte array must be not less then 4 c
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ beg = pos; // store '#' position
+ pos++;
+ while (true) {
+
+ // check for end of attribute value
+ // looks for space and component separators
+ if (pos == length || chars[pos] == '+' || chars[pos] == ','
+ || chars[pos] == ';') {
+ end = pos;
+ break;
+ }
+
+ if (chars[pos] == ' ') {
+ end = pos;
+ pos++;
+ // skip trailing space chars before comma or semicolon.
+ // (compatibility with RFC 1779)
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ }
+ break;
+ } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
+ chars[pos] += 32; //to low case
+ }
+
+ pos++;
+ }
+
+ // verify length of hex string
+ // encoded byte array must be not less then 4 and must be even number
+ int hexLen = end - beg; // skip first '#' char
+ if (hexLen < 5 || (hexLen & 1) == 0) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ // get byte encoding from string representation
+ byte[] encoded = new byte[hexLen / 2];
+ for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
+ encoded[i] = (byte) getByte(p);
+ }
+
+ return new String(chars, beg, hexLen);
+ }
+
+ // gets string attribute value: *( stringchar / pair )
+ private String escapedAV() {
+ beg = pos;
+ end = pos;
+ while (true) {
+ if (pos >= length) {
+ // the end of DN has been found
+ return new String(chars, beg, end - beg);
+ }
+
+ switch (chars[pos]) {
+ case '+':
+ case ',':
+ case ';':
+ // separator char has been found
+ return new String(chars, beg, end - beg);
+ case '\\':
+ // escaped char
+ chars[end++] = getEscaped();
+ pos++;
+ break;
+ case ' ':
+ // need to figure out whether space defines
+ // the end of attribute value or not
+ cur = end;
+
+ pos++;
+ chars[end++] = ' ';
+
+ for (; pos < length && chars[pos] == ' '; pos++) {
+ chars[end++] = ' ';
+ }
+ if (pos == length || chars[pos] == ',' || chars[pos] == '+'
+ || chars[pos] == ';') {
+ // separator char or the end of DN has been found
+ return new String(chars, beg, cur - beg);
+ }
+ break;
+ default:
+ chars[end++] = chars[pos];
+ pos++;
+ }
+ }
+ }
+
+ // returns escaped char
+ private char getEscaped() {
+ pos++;
+ if (pos == length) {
+ throw new IllegalStateException("Unexpected end of DN: " + dn);
+ }
+
+ switch (chars[pos]) {
+ case '"':
+ case '\\':
+ case ',':
+ case '=':
+ case '+':
+ case '<':
+ case '>':
+ case '#':
+ case ';':
+ case ' ':
+ case '*':
+ case '%':
+ case '_':
+ //FIXME: escaping is allowed only for leading or trailing space char
+ return chars[pos];
+ default:
+ // RFC doesn't explicitly say that escaped hex pair is
+ // interpreted as UTF-8 char. It only contains an example of such DN.
+ return getUTF8();
+ }
+ }
+
+ // decodes UTF-8 char
+ // see http://www.unicode.org for UTF-8 bit distribution table
+ private char getUTF8() {
+ int res = getByte(pos);
+ pos++; //FIXME tmp
+
+ if (res < 128) { // one byte: 0-7F
+ return (char) res;
+ } else if (res >= 192 && res <= 247) {
+
+ int count;
+ if (res <= 223) { // two bytes: C0-DF
+ count = 1;
+ res = res & 0x1F;
+ } else if (res <= 239) { // three bytes: E0-EF
+ count = 2;
+ res = res & 0x0F;
+ } else { // four bytes: F0-F7
+ count = 3;
+ res = res & 0x07;
+ }
+
+ int b;
+ for (int i = 0; i < count; i++) {
+ pos++;
+ if (pos == length || chars[pos] != '\\') {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+ pos++;
+
+ b = getByte(pos);
+ pos++; //FIXME tmp
+ if ((b & 0xC0) != 0x80) {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+
+ res = (res << 6) + (b & 0x3F);
+ }
+ return (char) res;
+ } else {
+ return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
+ }
+ }
+
+ // Returns byte representation of a char pair
+ // The char pair is composed of DN char in
+ // specified 'position' and the next char
+ // According to BNF syntax:
+ // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+ // / "a" / "b" / "c" / "d" / "e" / "f"
+ private int getByte(int position) {
+ if (position + 1 >= length) {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ int b1, b2;
+
+ b1 = chars[position];
+ if (b1 >= '0' && b1 <= '9') {
+ b1 = b1 - '0';
+ } else if (b1 >= 'a' && b1 <= 'f') {
+ b1 = b1 - 87; // 87 = 'a' - 10
+ } else if (b1 >= 'A' && b1 <= 'F') {
+ b1 = b1 - 55; // 55 = 'A' - 10
+ } else {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ b2 = chars[position + 1];
+ if (b2 >= '0' && b2 <= '9') {
+ b2 = b2 - '0';
+ } else if (b2 >= 'a' && b2 <= 'f') {
+ b2 = b2 - 87; // 87 = 'a' - 10
+ } else if (b2 >= 'A' && b2 <= 'F') {
+ b2 = b2 - 55; // 55 = 'A' - 10
+ } else {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ return (b1 << 4) + b2;
+ }
+
+ /**
+ * Parses the DN and returns the most significant attribute value for an attribute type, or null
+ * if none found.
+ *
+ * @param attributeType attribute type to look for (e.g. "ca")
+ */
+ public String findMostSpecific(String attributeType) {
+ // Initialize internal state.
+ pos = 0;
+ beg = 0;
+ end = 0;
+ cur = 0;
+ chars = dn.toCharArray();
+
+ String attType = nextAT();
+ if (attType == null) {
+ return null;
+ }
+ while (true) {
+ String attValue = "";
+
+ if (pos == length) {
+ return null;
+ }
+
+ switch (chars[pos]) {
+ case '"':
+ attValue = quotedAV();
+ break;
+ case '#':
+ attValue = hexAV();
+ break;
+ case '+':
+ case ',':
+ case ';': // compatibility with RFC 1779: semicolon can separate RDNs
+ //empty attribute value
+ break;
+ default:
+ attValue = escapedAV();
+ }
+
+ // Values are ordered from most specific to least specific
+ // due to the RFC2253 formatting. So take the first match
+ // we see.
+ if (attributeType.equalsIgnoreCase(attType)) {
+ return attValue;
+ }
+
+ if (pos >= length) {
+ return null;
+ }
+
+ if (chars[pos] == ',' || chars[pos] == ';') {
+ } else if (chars[pos] != '+') {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+
+ pos++;
+ attType = nextAT();
+ if (attType == null) {
+ throw new IllegalStateException("Malformed DN: " + dn);
+ }
+ }
+ }
+}
diff --git a/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/OkHttpClients.java b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/OkHttpClients.java
index cf505fc5d..cca1d1ec8 100644
--- a/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/OkHttpClients.java
+++ b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/OkHttpClients.java
@@ -163,6 +163,9 @@ private static RemotingOkHttpClient createInternal(
// SSL
client.sslSocketFactory(config.sslSocketFactory(), config.trustManager());
+ if (config.fallbackToCommonNameVerification()) {
+ client.hostnameVerifier(Okhttp39HostnameVerifier.INSTANCE);
+ }
// Intercept calls to augment request meta data
client.addInterceptor(new ConcurrencyLimitingInterceptor());
diff --git a/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/Okhttp39HostnameVerifier.java b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/Okhttp39HostnameVerifier.java
new file mode 100644
index 000000000..704a79e5a
--- /dev/null
+++ b/okhttp-clients/src/main/java/com/palantir/conjure/java/okhttp/Okhttp39HostnameVerifier.java
@@ -0,0 +1,237 @@
+/*
+ * 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 com.palantir.conjure.java.okhttp;
+
+import static okhttp3.internal.Util.verifyAsIpAddress;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A HostnameVerifier consistent with RFC 2818.
+ *
+ * Copied from https://github.com/square/okhttp/blob/parent-3.9.1/okhttp-tests/src/test/java/okhttp3/internal/tls/HostnameVerifierTest.java
+ * The original license is preserved and the following changes were made:
+ *
+ * - Made non-public
+ * - Changed indentation from 2 spaces to 4 spaces
+ * - Renamed from OkHostNameVerifier to Okhttp39HostnameVerifier
+ *
+ */
+final class Okhttp39HostnameVerifier implements HostnameVerifier {
+ public static final Okhttp39HostnameVerifier INSTANCE = new Okhttp39HostnameVerifier();
+
+ private static final int ALT_DNS_NAME = 2;
+ private static final int ALT_IPA_NAME = 7;
+
+ private Okhttp39HostnameVerifier() {
+ }
+
+ @Override
+ public boolean verify(String host, SSLSession session) {
+ try {
+ Certificate[] certificates = session.getPeerCertificates();
+ return verify(host, (X509Certificate) certificates[0]);
+ } catch (SSLException e) {
+ return false;
+ }
+ }
+
+ public boolean verify(String host, X509Certificate certificate) {
+ return verifyAsIpAddress(host)
+ ? verifyIpAddress(host, certificate)
+ : verifyHostname(host, certificate);
+ }
+
+ /** Returns true if {@code certificate} matches {@code ipAddress}. */
+ private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
+ List altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
+ if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if {@code certificate} matches {@code hostname}. */
+ private boolean verifyHostname(String hostname, X509Certificate certificate) {
+ hostname = hostname.toLowerCase(Locale.US);
+ boolean hasDns = false;
+ List altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
+ hasDns = true;
+ if (verifyHostname(hostname, altNames.get(i))) {
+ return true;
+ }
+ }
+
+ if (!hasDns) {
+ X500Principal principal = certificate.getSubjectX500Principal();
+ // RFC 2818 advises using the most specific name for matching.
+ String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
+ if (cn != null) {
+ return verifyHostname(hostname, cn);
+ }
+ }
+
+ return false;
+ }
+
+ public static List allSubjectAltNames(X509Certificate certificate) {
+ List altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ List altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ List result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
+ result.addAll(altIpaNames);
+ result.addAll(altDnsNames);
+ return result;
+ }
+
+ private static List getSubjectAltNames(X509Certificate certificate, int type) {
+ List result = new ArrayList<>();
+ try {
+ Collection> subjectAltNames = certificate.getSubjectAlternativeNames();
+ if (subjectAltNames == null) {
+ return Collections.emptyList();
+ }
+ for (Object subjectAltName : subjectAltNames) {
+ List> entry = (List>) subjectAltName;
+ if (entry == null || entry.size() < 2) {
+ continue;
+ }
+ Integer altNameType = (Integer) entry.get(0);
+ if (altNameType == null) {
+ continue;
+ }
+ if (altNameType == type) {
+ String altName = (String) entry.get(1);
+ if (altName != null) {
+ result.add(altName);
+ }
+ }
+ }
+ return result;
+ } catch (CertificateParsingException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}.
+ *
+ * @param hostname lower-case host name.
+ * @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code
+ * *.android.com}.
+ */
+ public boolean verifyHostname(String hostname, String pattern) {
+ // Basic sanity checks
+ // Check length == 0 instead of .isEmpty() to support Java 5.
+ if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith("."))
+ || (hostname.endsWith(".."))) {
+ // Invalid domain name
+ return false;
+ }
+ if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
+ || (pattern.endsWith(".."))) {
+ // Invalid pattern/domain name
+ return false;
+ }
+
+ // Normalize hostname and pattern by turning them into absolute domain names if they are not
+ // yet absolute. This is needed because server certificates do not normally contain absolute
+ // names or patterns, but they should be treated as absolute. At the same time, any hostname
+ // presented to this method should also be treated as absolute for the purposes of matching
+ // to the server certificate.
+ // www.android.com matches www.android.com
+ // www.android.com matches www.android.com.
+ // www.android.com. matches www.android.com.
+ // www.android.com. matches www.android.com
+ if (!hostname.endsWith(".")) {
+ hostname += '.';
+ }
+ if (!pattern.endsWith(".")) {
+ pattern += '.';
+ }
+ // hostname and pattern are now absolute domain names.
+
+ pattern = pattern.toLowerCase(Locale.US);
+ // hostname and pattern are now in lower case -- domain names are case-insensitive.
+
+ if (!pattern.contains("*")) {
+ // Not a wildcard pattern -- hostname and pattern must match exactly.
+ return hostname.equals(pattern);
+ }
+ // Wildcard pattern
+
+ // WILDCARD PATTERN RULES:
+ // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
+ // only character in that label (i.e., must match the whole left-most label).
+ // For example, *.example.com is permitted, while *a.example.com, a*.example.com,
+ // a*b.example.com, a.*.example.com are not permitted.
+ // 2. Asterisk (*) cannot match across domain name labels.
+ // For example, *.example.com matches test.example.com but does not match
+ // sub.test.example.com.
+ // 3. Wildcard patterns for single-label domain names are not permitted.
+
+ if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
+ // Asterisk (*) is only permitted in the left-most domain name label and must be the only
+ // character in that label
+ return false;
+ }
+
+ // Optimization: check whether hostname is too short to match the pattern. hostName must be at
+ // least as long as the pattern because asterisk must match the whole left-most label and
+ // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters.
+ if (hostname.length() < pattern.length()) {
+ // hostname too short to match the pattern.
+ return false;
+ }
+
+ if ("*.".equals(pattern)) {
+ // Wildcard pattern for single-label domain name -- not permitted.
+ return false;
+ }
+
+ // hostname must end with the region of pattern following the asterisk.
+ String suffix = pattern.substring(1);
+ if (!hostname.endsWith(suffix)) {
+ // hostname does not end with the suffix
+ return false;
+ }
+
+ // Check that asterisk did not match across domain name labels.
+ int suffixStartIndexInHostname = hostname.length() - suffix.length();
+ if ((suffixStartIndexInHostname > 0)
+ && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) {
+ // Asterisk is matching across domain name labels -- not permitted.
+ return false;
+ }
+
+ // hostname matches pattern
+ return true;
+ }
+}