From 06a9e3ec7846f17684ea41017ad8cb9aa6440755 Mon Sep 17 00:00:00 2001 From: Marek Sebera Date: Sat, 19 Sep 2015 18:06:28 +0200 Subject: [PATCH] Version 4.4.1 release and documentation --- README.md | 6 +- build.sh | 15 +- gradle.properties | 4 +- patches/AbstractVerifier.java.patch.4.4.1 | 29 ++ .../DefaultHostnameVerifier.java.patch.4.4.1 | 48 ++ patches/DistinguishedNameParser.java | 417 ++++++++++++++++++ 6 files changed, 509 insertions(+), 10 deletions(-) create mode 100644 patches/AbstractVerifier.java.patch.4.4.1 create mode 100644 patches/DefaultHostnameVerifier.java.patch.4.4.1 create mode 100644 patches/DistinguishedNameParser.java diff --git a/README.md b/README.md index 93755e1..89a9345 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Build script and dependencies to create repackaged upstream version of HttpClient and depdendencies (HttpMime, HttpCore, HttpClient-Cache) and get it working on Android API version from 3 to 23 -Current version: **4.3.6** (originating from upstream HttpClient 4.3.6 version) +Current version: **4.4.1** (originating from upstream HttpClient 4.4.1 version) ## Usage @@ -27,7 +27,7 @@ Build params (ENV variables) you can use: ## Maven Central -This repository version will publish the library under namespace `cz.msebera.android:httpclient:4.3.6` +This repository version will publish the library under namespace `cz.msebera.android:httpclient:4.4.1` ## Maven Local @@ -39,7 +39,7 @@ Gradle dependency string, once you have it installed ```gradle dependencies { - compile "cz.msebera.android:httpclient:4.3.6" + compile "cz.msebera.android:httpclient:4.4.1" } ``` diff --git a/build.sh b/build.sh index a781403..ed8aafb 100755 --- a/build.sh +++ b/build.sh @@ -8,10 +8,10 @@ ANDROIDPROJECTPATH=${ROOTDIR}/${PROJECTNAME} EXTRAPACKAGENAME=extras KERBEROS_LIB_NAME="kerberos" -CORE_VER=4.3.3 -CLIENT_VER=4.3.6 -CACHE_VER=4.3.6 -MIME_VER=4.3.6 +CORE_VER=4.4.1 +CLIENT_VER=4.4.1 +CACHE_VER=4.4.1 +MIME_VER=4.4.1 : ${GRADLEW_VERSION:=2.6} : ${GRADLE_COMMAND:="gradle"} @@ -166,6 +166,7 @@ if [ ${INCLUDE_JGSS_API} -ne 1 ]; then rm impl/auth/NegotiateSchemeFactory.java rm impl/auth/GGSSchemeBase.java rm impl/auth/KerberosScheme.java + rm auth/KerberosCredentials.java rm impl/auth/KerberosSchemeFactory.java rm impl/auth/SPNegoScheme.java rm impl/auth/SPNegoSchemeFactory.java @@ -193,6 +194,7 @@ else cd ${PACKAGEDIR} find . -name "GssUtil.java" -exec ${SED_CMD} "s/new Boolean(second)\.toString();/Boolean.toString(second);/" {} + find . -name "GssUtil.java" -exec ${SED_CMD} "s/new Integer(second)\.toString();/Integer.toString(second);/" {} + + find . -name "GGSSchemeBase.java" -exec ${SED_CMD} "/final Base64 base64codec = new Base64(0);/c \/\* Base64 instance removed by HttpClient for Android script. \*\/" {} + find . -name "GGSSchemeBase.java" -exec ${SED_CMD} "/private final Base64 base64codec;/c \/\* Base64 instance removed by HttpClient for Android script. \*\/" {} + find . -name "GGSSchemeBase.java" -exec ${SED_CMD} "/this\.base64codec = new Base64(0);/c \/\* Base64 instance removed by HttpClient for Android script. \*\/" {} + find . -name "GGSSchemeBase.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/base64codec.encode(\([^;]*\)));/Base64.encode(\1, Base64.NO_WRAP));/g;p;}' {} + @@ -208,6 +210,7 @@ find . -name "BasicScheme.java" -exec ${SED_CMD} "/this\.base64codec = new Base6 find . -name "BasicScheme.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/Base64.encodeBase64(\([^;]*\));/Base64.encode(\1, Base64.NO_WRAP);/g;p;}' {} + find . -name "BasicScheme.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/base64codec.encode(\([^;]*\));/Base64.encode(\1, Base64.NO_WRAP);/g;p;}' {} + find . -name "BasicScheme.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/EncodingUtils\.getBytes(tmp\.toString(), charset), false/EncodingUtils.getBytes(tmp.toString(), charset)/g;p;}' {} + +find . -name "BasicScheme.java" -exec ${SED_CMD} "/final Base64 base64codec = new Base64(0);/c \/\* Base64 instance removed by HttpClient for Android script. \*\/" {} + find . -name "NTLMEngineImpl.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/Base64.encodeBase64(resp)/Base64.encode(resp, Base64.NO_WRAP)/g;p;}' {} + find . -name "*.java" -exec ${SED_CMD} -n '1h;1!H;${;g;s/Base64.decodeBase64(\([^;]*\));/Base64.decode(\1, Base64.NO_WRAP);/g;p;}' {} + @@ -242,7 +245,9 @@ cp ../AndroidManifest.xml src/main/ ${SED_CMD} "s/sedpackage/cz\.msebera\.httpclient\.android/g" src/main/AndroidManifest.xml cd ${ANDROIDPROJECTPATH} -patch ${PACKAGEDIR}/conn/ssl/AbstractVerifier.java ../patches/AbstractVerifier.java.patch.4.3.5 +patch ${PACKAGEDIR}/conn/ssl/DefaultHostnameVerifier.java ../patches/DefaultHostnameVerifier.java.patch.4.4.1 +patch ${PACKAGEDIR}/conn/ssl/AbstractVerifier.java ../patches/AbstractVerifier.java.patch.4.4.1 +cp ../patches/DistinguishedNameParser.java ${PACKAGEDIR}/conn/ssl/ echo ">> Gradle build proceed" if [ ${INCLUDE_JGSS_API} -eq 1 ]; then diff --git a/gradle.properties b/gradle.properties index e9751e5..0c65ed5 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=4.3.6 -VERSION_CODE=436 +VERSION_NAME=4.4.1 +VERSION_CODE=441 GROUP=cz.msebera.android POM_ARTIFACT_ID=httpclient diff --git a/patches/AbstractVerifier.java.patch.4.4.1 b/patches/AbstractVerifier.java.patch.4.4.1 new file mode 100644 index 0000000..89e06c4 --- /dev/null +++ b/patches/AbstractVerifier.java.patch.4.4.1 @@ -0,0 +1,29 @@ +diff --git a/AbstractVerifier.java b/AbstractVerifier2.java +index d7f7e8a..a1e772b 100644 +--- a/AbstractVerifier.java ++++ b/AbstractVerifier2.java +@@ -136,7 +136,7 @@ public abstract class AbstractVerifier implements X509HostnameVerifier { + final int subjectType = ipv4 || ipv6 ? DefaultHostnameVerifier.IP_ADDRESS_TYPE : DefaultHostnameVerifier.DNS_NAME_TYPE; + final List subjectAlts = DefaultHostnameVerifier.extractSubjectAlts(cert, subjectType); + final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); +- final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); ++ final String cn = new DistinguishedNameParser(subjectPrincipal).findMostSpecific("cn"); + verify(host, + cn != null ? new String[] {cn} : null, + subjectAlts != null && !subjectAlts.isEmpty() ? subjectAlts.toArray(new String[subjectAlts.size()]) : null); +@@ -218,13 +218,8 @@ public abstract class AbstractVerifier implements X509HostnameVerifier { + } + + public static String[] getCNs(final X509Certificate cert) { +- final String subjectPrincipal = cert.getSubjectX500Principal().toString(); +- try { +- final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal); +- return cn != null ? new String[] { cn } : null; +- } catch (SSLException ex) { +- return null; +- } ++ final String cn = new DistinguishedNameParser(cert.getSubjectX500Principal()).findMostSpecific("cn"); ++ return cn != null ? new String[] { cn } : null; + } + + /** diff --git a/patches/DefaultHostnameVerifier.java.patch.4.4.1 b/patches/DefaultHostnameVerifier.java.patch.4.4.1 new file mode 100644 index 0000000..ff7e53c --- /dev/null +++ b/patches/DefaultHostnameVerifier.java.patch.4.4.1 @@ -0,0 +1,48 @@ +diff --git a/DefaultHostnameVerifier.java b/DefaultHostnameVerifier2.java +index 7fe7cb4..b4d0382 100644 +--- a/DefaultHostnameVerifier.java ++++ b/DefaultHostnameVerifier2.java +@@ -111,7 +111,7 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { + // CN matching has been deprecated by rfc2818 and can be used + // as fallback only when no subjectAlts are available + final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); +- final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); ++ final String cn = new DistinguishedNameParser(subjectPrincipal).findMostSpecific("cn"); + if (cn == null) { + throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + + "a common name and does not have alternative names"); +@@ -229,34 +229,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { + return matchIdentity(host, identity, null, true); + } + +- static String extractCN(final String subjectPrincipal) throws SSLException { +- if (subjectPrincipal == null) { +- return null; +- } +- try { +- final LdapName subjectDN = new LdapName(subjectPrincipal); +- final List rdns = subjectDN.getRdns(); +- for (int i = rdns.size() - 1; i >= 0; i--) { +- final Rdn rds = rdns.get(i); +- final Attributes attributes = rds.toAttributes(); +- final Attribute cn = attributes.get("cn"); +- if (cn != null) { +- try { +- final Object value = cn.get(); +- if (value != null) { +- return value.toString(); +- } +- } catch (NoSuchElementException ignore) { +- } catch (NamingException ignore) { +- } +- } +- } +- return null; +- } catch (InvalidNameException e) { +- throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); +- } +- } +- + static List extractSubjectAlts(final X509Certificate cert, final int subjectType) { + Collection> c = null; + try { diff --git a/patches/DistinguishedNameParser.java b/patches/DistinguishedNameParser.java new file mode 100644 index 0000000..3e413ab --- /dev/null +++ b/patches/DistinguishedNameParser.java @@ -0,0 +1,417 @@ +/* + * 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 cz.msebera.android.httpclient.conn.ssl; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +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. + * + * @hide + */ +public final class DistinguishedNameParser { + private final String dn; + private final int length; + private int pos; + private int beg; + private int end; + /** tmp vars to store positions of the currently parsed item */ + private int cur; + /** distinguished name chars */ + private char[] chars; + public 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); + } + } + } + /** + * Parses the DN and returns all values for an attribute type, in + * the order of decreasing significance (most significant first). + * + * @param attributeType attribute type to look for (e.g. "ca") + */ + public List getAllMostSpecificFirst(String attributeType) { + // Initialize internal state. + pos = 0; + beg = 0; + end = 0; + cur = 0; + chars = dn.toCharArray(); + List result = Collections.emptyList(); + String attType = nextAT(); + if (attType == null) { + return result; + } + while (pos < length) { + String attValue = ""; + 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)) { + if (result.isEmpty()) { + result = new ArrayList(); + } + result.add(attValue); + } + if (pos >= length) { + break; + } + 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); + } + } + return result; + } +} \ No newline at end of file