diff --git a/okhttp3/src/main/java/okhttp3/Call.java b/okhttp3/src/main/java/okhttp3/Call.java index 3baaf647d..58506d137 100644 --- a/okhttp3/src/main/java/okhttp3/Call.java +++ b/okhttp3/src/main/java/okhttp3/Call.java @@ -51,8 +51,7 @@ public interface Call { * immediately unless there are several other requests currently being executed. * *

This client will later call back {@code responseCallback} with either an HTTP response or a - * failure exception. If you {@link #cancel} a request before it completes the callback will not - * be invoked. + * failure exception. * * @throws IllegalStateException when the call has already been executed. */ diff --git a/okhttp3/src/main/java/okhttp3/CertificatePinner.java b/okhttp3/src/main/java/okhttp3/CertificatePinner.java index ec5b98c2e..f7a136764 100644 --- a/okhttp3/src/main/java/okhttp3/CertificatePinner.java +++ b/okhttp3/src/main/java/okhttp3/CertificatePinner.java @@ -17,30 +17,25 @@ package okhttp3; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Set; import javax.net.ssl.SSLPeerUnverifiedException; import okhttp3.internal.Util; import okhttp3.internal.tls.CertificateChainCleaner; import okhttp3.internal.tls.TrustRootIndex; import okio.ByteString; -import static java.util.Collections.unmodifiableSet; - /** * Constrains which certificates are trusted. Pinning certificates defends against attacks on * certificate authorities. It also prevents connections through man-in-the-middle certificate * authorities either known or unknown to the application's user. * *

This class currently pins a certificate's Subject Public Key Info as described on Adam Langley's Weblog. Pins are base-64 SHA-1 hashes, consistent - * with the format Chromium uses for static certificates. See - * Chromium's pinsets for hostnames that are pinned in that - * browser. + * href="http://goo.gl/AIx3e5">Adam Langley's Weblog. Pins are either base64 SHA-256 hashes as + * in HTTP Public Key Pinning (HPKP) or SHA-1 + * base64 hashes as in Chromium's static certificates. * *

Setting up Certificate Pinning

* @@ -54,7 +49,7 @@ import static java.util.Collections.unmodifiableSet; * * String hostname = "publicobject.com"; * CertificatePinner certificatePinner = new CertificatePinner.Builder() - * .add(hostname, "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=") + * .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") * .build(); * OkHttpClient client = new OkHttpClient(); * client.setCertificatePinner(certificatePinner); @@ -69,12 +64,12 @@ import static java.util.Collections.unmodifiableSet; * * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure! * Peer certificate chain: - * sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL - * sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA - * sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority - * sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root + * sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL + * sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA + * sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority + * sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root * Pinned certificates for publicobject.com: - * sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA= + * sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= * at okhttp3.CertificatePinner.check(CertificatePinner.java) * at okhttp3.Connection.upgradeToTls(Connection.java) * at okhttp3.Connection.connect(Connection.java) @@ -85,10 +80,10 @@ import static java.util.Collections.unmodifiableSet; * certificate pinner's configuration:
   {@code
  *
  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
- *       .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
- *       .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
- *       .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
- *       .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
+ *       .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
+ *       .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
+ *       .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
+ *       .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
  *       .build();
  * }
* @@ -129,11 +124,11 @@ import static java.util.Collections.unmodifiableSet; public final class CertificatePinner { public static final CertificatePinner DEFAULT = new Builder().build(); - private final Map> hostnameToPins; + private final List pins; private final TrustRootIndex trustRootIndex; private CertificatePinner(Builder builder) { - this.hostnameToPins = Util.immutableMap(builder.hostnameToPins); + this.pins = Util.immutableList(builder.pins); this.trustRootIndex = builder.trustRootIndex; } @@ -147,31 +142,47 @@ public final class CertificatePinner { */ public void check(String hostname, List peerCertificates) throws SSLPeerUnverifiedException { + List pins = findMatchingPins(hostname); + if (pins.isEmpty()) return; + if (trustRootIndex != null) { peerCertificates = new CertificateChainCleaner(trustRootIndex).clean(peerCertificates); } - Set pins = findMatchingPins(hostname); + for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) { + X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c); - if (pins == null) return; + // Lazily compute the hashes for each certificate. + ByteString sha1 = null; + ByteString sha256 = null; - for (int i = 0, size = peerCertificates.size(); i < size; i++) { - X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i); - if (pins.contains(sha1(x509Certificate))) return; // Success! + for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) { + Pin pin = pins.get(p); + if (pin.hashAlgorithm.equals("sha256/")) { + if (sha256 == null) sha256 = sha256(x509Certificate); + if (pin.hash.equals(sha256)) return; // Success! + } else if (pin.hashAlgorithm.equals("sha1/")) { + if (sha1 == null) sha1 = sha1(x509Certificate); + if (pin.hash.equals(sha1)) return; // Success! + } else { + throw new AssertionError(); + } + } } // If we couldn't find a matching pin, format a nice exception. StringBuilder message = new StringBuilder() .append("Certificate pinning failure!") .append("\n Peer certificate chain:"); - for (int i = 0, size = peerCertificates.size(); i < size; i++) { - X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i); + for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) { + X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c); message.append("\n ").append(pin(x509Certificate)) .append(": ").append(x509Certificate.getSubjectDN().getName()); } message.append("\n Pinned certificates for ").append(hostname).append(":"); - for (ByteString pin : pins) { - message.append("\n sha1/").append(pin.base64()); + for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) { + Pin pin = pins.get(p); + message.append("\n ").append(pin); } throw new SSLPeerUnverifiedException(message.toString()); } @@ -183,36 +194,18 @@ public final class CertificatePinner { } /** - * Returns list of matching certificates' pins for the hostname or {@code null} if hostname does - * not have pinned certificates. + * Returns list of matching certificates' pins for the hostname. Returns an empty list if the + * hostname does not have pinned certificates. */ - Set findMatchingPins(String hostname) { - Set directPins = hostnameToPins.get(hostname); - Set wildcardPins = null; - - int indexOfFirstDot = hostname.indexOf('.'); - int indexOfLastDot = hostname.lastIndexOf('.'); - - // Skip hostnames with one dot symbol for wildcard pattern search - // example.com will be skipped - // a.example.com won't be skipped - if (indexOfFirstDot != indexOfLastDot) { - // a.example.com -> search for wildcard pattern *.example.com - wildcardPins = hostnameToPins.get("*." + hostname.substring(indexOfFirstDot + 1)); + List findMatchingPins(String hostname) { + List result = Collections.emptyList(); + for (Pin pin : pins) { + if (pin.matches(hostname)) { + if (result.isEmpty()) result = new ArrayList<>(); + result.add(pin); + } } - - if (directPins == null && wildcardPins == null) return null; - - if (directPins != null && wildcardPins != null) { - Set pins = new LinkedHashSet<>(); - pins.addAll(directPins); - pins.addAll(wildcardPins); - return pins; - } - - if (directPins != null) return directPins; - - return wildcardPins; + return result; } Builder newBuilder() { @@ -220,31 +213,89 @@ public final class CertificatePinner { } /** - * Returns the SHA-1 of {@code certificate}'s public key. This uses the mechanism Moxie - * Marlinspike describes in Android - * Pinning. + * Returns the SHA-256 of {@code certificate}'s public key. + * + *

In OkHttp 3.1.2 and earlier, this returned a SHA-1 hash of the public key. Both types are + * supported, but SHA-256 is preferred. */ public static String pin(Certificate certificate) { if (!(certificate instanceof X509Certificate)) { throw new IllegalArgumentException("Certificate pinning requires X509 certificates"); } - return "sha1/" + sha1((X509Certificate) certificate).base64(); + return "sha256/" + sha256((X509Certificate) certificate).base64(); } - private static ByteString sha1(X509Certificate x509Certificate) { + static ByteString sha1(X509Certificate x509Certificate) { return Util.sha1(ByteString.of(x509Certificate.getPublicKey().getEncoded())); } + static ByteString sha256(X509Certificate x509Certificate) { + return Util.sha256(ByteString.of(x509Certificate.getPublicKey().getEncoded())); + } + + static final class Pin { + /** A hostname like {@code example.com} or a pattern like {@code *.example.com}. */ + final String pattern; + /** Either {@code sha1/} or {@code sha256/}. */ + final String hashAlgorithm; + /** The hash of the pinned certificate using {@link #hashAlgorithm}. */ + final ByteString hash; + + Pin(String pattern, String pin) { + this.pattern = pattern; + if (pin.startsWith("sha1/")) { + this.hashAlgorithm = "sha1/"; + this.hash = ByteString.decodeBase64(pin.substring("sha1/".length())); + } else if (pin.startsWith("sha256/")) { + this.hashAlgorithm = "sha256/"; + this.hash = ByteString.decodeBase64(pin.substring("sha256/".length())); + } else { + throw new IllegalArgumentException("pins must start with 'sha256/' or 'sha1/': " + pin); + } + + if (this.hash == null) { + throw new IllegalArgumentException("pins must be base64: " + pin); + } + } + + boolean matches(String hostname) { + if (pattern.equals(hostname)) return true; + + int firstDot = hostname.indexOf('.'); + return pattern.startsWith("*.") + && hostname.regionMatches(false, firstDot + 1, pattern, 2, pattern.length() - 2); + } + + @Override public boolean equals(Object other) { + return other instanceof Pin + && pattern.equals(((Pin) other).pattern) + && hashAlgorithm.equals(((Pin) other).hashAlgorithm) + && hash.equals(((Pin) other).hash); + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + pattern.hashCode(); + result = 31 * result + hashAlgorithm.hashCode(); + result = 31 * result + hash.hashCode(); + return result; + } + + @Override public String toString() { + return hashAlgorithm + hash.base64(); + } + } + /** Builds a configured certificate pinner. */ public static final class Builder { - private final Map> hostnameToPins = new LinkedHashMap<>(); + private final List pins = new ArrayList<>(); private TrustRootIndex trustRootIndex; public Builder() { } Builder(CertificatePinner certificatePinner) { - this.hostnameToPins.putAll(certificatePinner.hostnameToPins); + this.pins.addAll(certificatePinner.pins); this.trustRootIndex = certificatePinner.trustRootIndex; } @@ -254,30 +305,17 @@ public final class CertificatePinner { } /** - * Pins certificates for {@code hostname}. + * Pins certificates for {@code pattern}. * - * @param hostname lower-case host name or wildcard pattern such as {@code *.example.com}. - * @param pins SHA-1 hashes. Each pin is a SHA-1 hash of a certificate's Subject Public Key - * Info, base64-encoded and prefixed with {@code sha1/}. + * @param pattern lower-case host name or wildcard pattern such as {@code *.example.com}. + * @param pins SHA-256 or SHA-1 hashes. Each pin is a hash of a certificate's Subject Public Key + * Info, base64-encoded and prefixed with either {@code sha256/} or {@code sha1/}. */ - public Builder add(String hostname, String... pins) { - if (hostname == null) throw new IllegalArgumentException("hostname == null"); - - Set hostPins = new LinkedHashSet<>(); - Set previousPins = hostnameToPins.put(hostname, unmodifiableSet(hostPins)); - if (previousPins != null) { - hostPins.addAll(previousPins); - } + public Builder add(String pattern, String... pins) { + if (pattern == null) throw new IllegalArgumentException("pattern == null"); for (String pin : pins) { - if (!pin.startsWith("sha1/")) { - throw new IllegalArgumentException("pins must start with 'sha1/': " + pin); - } - ByteString decodedPin = ByteString.decodeBase64(pin.substring("sha1/".length())); - if (decodedPin == null) { - throw new IllegalArgumentException("pins must be base64: " + pin); - } - hostPins.add(decodedPin); + this.pins.add(new Pin(pattern, pin)); } return this; diff --git a/okhttp3/src/main/java/okhttp3/HttpUrl.java b/okhttp3/src/main/java/okhttp3/HttpUrl.java index 59cbdd435..d7c0c35c3 100644 --- a/okhttp3/src/main/java/okhttp3/HttpUrl.java +++ b/okhttp3/src/main/java/okhttp3/HttpUrl.java @@ -772,6 +772,15 @@ public final class HttpUrl { return this; } + /** + * Adds a set of path segments separated by a slash (either {@code \} or {@code /}). If + * {@code pathSegments} starts with a slash, the resulting URL will have empty path segment. + */ + public Builder addPathSegments(String pathSegments) { + if (pathSegments == null) throw new IllegalArgumentException("pathSegments == null"); + return addPathSegments(pathSegments, false); + } + public Builder addEncodedPathSegment(String encodedPathSegment) { if (encodedPathSegment == null) { throw new IllegalArgumentException("encodedPathSegment == null"); @@ -780,6 +789,29 @@ public final class HttpUrl { return this; } + /** + * Adds a set of encoded path segments separated by a slash (either {@code \} or {@code /}). If + * {@code encodedPathSegments} starts with a slash, the resulting URL will have empty path + * segment. + */ + public Builder addEncodedPathSegments(String encodedPathSegments) { + if (encodedPathSegments == null) { + throw new IllegalArgumentException("encodedPathSegments == null"); + } + return addPathSegments(encodedPathSegments, true); + } + + private Builder addPathSegments(String pathSegments, boolean alreadyEncoded) { + int offset = 0; + do { + int segmentEnd = delimiterOffset(pathSegments, offset, pathSegments.length(), "/\\"); + boolean addTrailingSlash = segmentEnd < pathSegments.length(); + push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded); + offset = segmentEnd + 1; + } while (offset <= pathSegments.length()); + return this; + } + public Builder setPathSegment(int index, String pathSegment) { if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); String canonicalPathSegment = canonicalize( @@ -1270,9 +1302,12 @@ public final class HttpUrl { // checked for IPv6 square braces. But Chrome does it first, and that's more lenient. String percentDecoded = percentDecode(input, pos, limit, false); - // If the input is encased in square braces "[...]", drop 'em. We have an IPv6 address. - if (percentDecoded.startsWith("[") && percentDecoded.endsWith("]")) { - InetAddress inetAddress = decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1); + // If the input contains a :, it’s an IPv6 address. + if (percentDecoded.contains(":")) { + // If the input is encased in square braces "[...]", drop 'em. + InetAddress inetAddress = percentDecoded.startsWith("[") && percentDecoded.endsWith("]") + ? decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1) + : decodeIpv6(percentDecoded, 0, percentDecoded.length()); if (inetAddress == null) return null; byte[] address = inetAddress.getAddress(); if (address.length == 16) return inet6AddressToAscii(address); diff --git a/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java b/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java index 25e12eeb9..1be6a93b4 100644 --- a/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java +++ b/okhttp3/src/main/java/okhttp3/internal/DiskLruCache.java @@ -151,6 +151,7 @@ public final class DiskLruCache implements Closeable, Flushable { // Must be read and written when synchronized on 'this'. private boolean initialized; private boolean closed; + private boolean mostRecentTrimFailed; /** * To differentiate between old and current snapshots, each entry is given a sequence number each @@ -167,8 +168,14 @@ public final class DiskLruCache implements Closeable, Flushable { if (!initialized | closed) { return; // Nothing to do } + try { trimToSize(); + } catch (IOException ignored) { + mostRecentTrimFailed = true; + } + + try { if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0; @@ -453,6 +460,11 @@ public final class DiskLruCache implements Closeable, Flushable { if (entry != null && entry.currentEditor != null) { return null; // Another edit is in progress. } + if (mostRecentTrimFailed) { + // Prevent new writes so the cache doesn't grow any further and retry the clean up operation. + executor.execute(cleanupRunnable); + return null; + } // Flush the journal before creating files to prevent file leaks. journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n'); @@ -586,7 +598,9 @@ public final class DiskLruCache implements Closeable, Flushable { validateKey(key); Entry entry = lruEntries.get(key); if (entry == null) return false; - return removeEntry(entry); + boolean removed = removeEntry(entry); + if (removed && size <= maxSize) mostRecentTrimFailed = false; + return removed; } private boolean removeEntry(Entry entry) throws IOException { @@ -654,6 +668,7 @@ public final class DiskLruCache implements Closeable, Flushable { Entry toEvict = lruEntries.values().iterator().next(); removeEntry(toEvict); } + mostRecentTrimFailed = false; } /** @@ -675,6 +690,7 @@ public final class DiskLruCache implements Closeable, Flushable { for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) { removeEntry(entry); } + mostRecentTrimFailed = false; } private void validateKey(String key) { diff --git a/okhttp3/src/main/java/okhttp3/internal/Platform.java b/okhttp3/src/main/java/okhttp3/internal/Platform.java index 3b370c8b5..91568ab4f 100644 --- a/okhttp3/src/main/java/okhttp3/internal/Platform.java +++ b/okhttp3/src/main/java/okhttp3/internal/Platform.java @@ -25,7 +25,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -83,14 +82,18 @@ public class Platform { System.out.println(warning); } - public void tagSocket(Socket socket) throws SocketException { - } - - public void untagSocket(Socket socket) throws SocketException { - } - public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { - return null; + // Attempt to get the trust manager from an OpenJDK socket factory. We attempt this on all + // platforms in order to support Robolectric, which mixes classes from both Android and the + // Oracle JDK. Note that we don't support HTTP/2 or other nice features on Robolectric. + try { + Class sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl"); + Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context"); + if (context == null) return null; + return readFieldOrNull(context, X509TrustManager.class, "trustManager"); + } catch (ClassNotFoundException e) { + return null; + } } public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { @@ -140,61 +143,43 @@ public class Platform { "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl"); } - OptionalMethod setUseSessionTickets - = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class); - OptionalMethod setHostname - = new OptionalMethod<>(null, "setHostname", String.class); - Method trafficStatsTagSocket = null; - Method trafficStatsUntagSocket = null; + OptionalMethod setUseSessionTickets = new OptionalMethod<>( + null, "setUseSessionTickets", boolean.class); + OptionalMethod setHostname = new OptionalMethod<>( + null, "setHostname", String.class); OptionalMethod getAlpnSelectedProtocol = null; OptionalMethod setAlpnProtocols = null; - // Attempt to find Android 4.0+ APIs. + // Attempt to find Android 5.0+ APIs. try { - Class trafficStats = Class.forName("android.net.TrafficStats"); - trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class); - trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class); - - // Attempt to find Android 5.0+ APIs. - try { - Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0. - getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol"); - setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class); - } catch (ClassNotFoundException ignored) { - } - } catch (ClassNotFoundException | NoSuchMethodException ignored) { + Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0. + getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol"); + setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class); + } catch (ClassNotFoundException ignored) { } return new Android(sslParametersClass, setUseSessionTickets, setHostname, - trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol, - setAlpnProtocols); + getAlpnSelectedProtocol, setAlpnProtocols); } catch (ClassNotFoundException ignored) { // This isn't an Android runtime. } - // Find an Oracle JDK. + // Find Jetty's ALPN extension for OpenJDK. try { - Class sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl"); - - // Find Jetty's ALPN extension for OpenJDK. - try { - String negoClassName = "org.eclipse.jetty.alpn.ALPN"; - Class negoClass = Class.forName(negoClassName); - Class providerClass = Class.forName(negoClassName + "$Provider"); - Class clientProviderClass = Class.forName(negoClassName + "$ClientProvider"); - Class serverProviderClass = Class.forName(negoClassName + "$ServerProvider"); - Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass); - Method getMethod = negoClass.getMethod("get", SSLSocket.class); - Method removeMethod = negoClass.getMethod("remove", SSLSocket.class); - return new JdkWithJettyBootPlatform(sslContextClass, - putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass); - } catch (ClassNotFoundException | NoSuchMethodException ignored) { - } - - return new JdkPlatform(sslContextClass); - } catch (ClassNotFoundException ignored) { + String negoClassName = "org.eclipse.jetty.alpn.ALPN"; + Class negoClass = Class.forName(negoClassName); + Class providerClass = Class.forName(negoClassName + "$Provider"); + Class clientProviderClass = Class.forName(negoClassName + "$ClientProvider"); + Class serverProviderClass = Class.forName(negoClassName + "$ServerProvider"); + Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass); + Method getMethod = negoClass.getMethod("get", SSLSocket.class); + Method removeMethod = negoClass.getMethod("remove", SSLSocket.class); + return new JdkWithJettyBootPlatform( + putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass); + } catch (ClassNotFoundException | NoSuchMethodException ignored) { } + // Probably an Oracle JDK like OpenJDK. return new Platform(); } @@ -206,23 +191,16 @@ public class Platform { private final OptionalMethod setUseSessionTickets; private final OptionalMethod setHostname; - // Non-null on Android 4.0+. - private final Method trafficStatsTagSocket; - private final Method trafficStatsUntagSocket; - // Non-null on Android 5.0+. private final OptionalMethod getAlpnSelectedProtocol; private final OptionalMethod setAlpnProtocols; public Android(Class sslParametersClass, OptionalMethod setUseSessionTickets, - OptionalMethod setHostname, Method trafficStatsTagSocket, - Method trafficStatsUntagSocket, OptionalMethod getAlpnSelectedProtocol, + OptionalMethod setHostname, OptionalMethod getAlpnSelectedProtocol, OptionalMethod setAlpnProtocols) { this.sslParametersClass = sslParametersClass; this.setUseSessionTickets = setUseSessionTickets; this.setHostname = setHostname; - this.trafficStatsTagSocket = trafficStatsTagSocket; - this.trafficStatsUntagSocket = trafficStatsUntagSocket; this.getAlpnSelectedProtocol = getAlpnSelectedProtocol; this.setAlpnProtocols = setAlpnProtocols; } @@ -254,7 +232,7 @@ public class Platform { sslSocketFactory.getClass().getClassLoader()); context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters"); } catch (ClassNotFoundException e) { - return null; + return super.trustManager(sslSocketFactory); } } @@ -294,30 +272,6 @@ public class Platform { return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null; } - @Override public void tagSocket(Socket socket) throws SocketException { - if (trafficStatsTagSocket == null) return; - - try { - trafficStatsTagSocket.invoke(null, socket); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } - - @Override public void untagSocket(Socket socket) throws SocketException { - if (trafficStatsUntagSocket == null) return; - - try { - trafficStatsUntagSocket.invoke(null, socket); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } - @Override public void log(String message) { // Split by line, then ensure each line can fit into Log's maximum length. for (int i = 0, length = message.length(); i < length; i++) { @@ -332,34 +286,18 @@ public class Platform { } } - /** JDK 1.7 or better. */ - private static class JdkPlatform extends Platform { - private final Class sslContextClass; - - public JdkPlatform(Class sslContextClass) { - this.sslContextClass = sslContextClass; - } - - @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { - Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context"); - if (context == null) return null; - return readFieldOrNull(context, X509TrustManager.class, "trustManager"); - } - } - /** * OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path. */ - private static class JdkWithJettyBootPlatform extends JdkPlatform { + private static class JdkWithJettyBootPlatform extends Platform { private final Method putMethod; private final Method getMethod; private final Method removeMethod; private final Class clientProviderClass; private final Class serverProviderClass; - public JdkWithJettyBootPlatform(Class sslContextClass, Method putMethod, Method getMethod, - Method removeMethod, Class clientProviderClass, Class serverProviderClass) { - super(sslContextClass); + public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod, + Class clientProviderClass, Class serverProviderClass) { this.putMethod = putMethod; this.getMethod = getMethod; this.removeMethod = removeMethod; diff --git a/okhttp3/src/main/java/okhttp3/internal/Util.java b/okhttp3/src/main/java/okhttp3/internal/Util.java index e9efad04c..4dc139f4f 100644 --- a/okhttp3/src/main/java/okhttp3/internal/Util.java +++ b/okhttp3/src/main/java/okhttp3/internal/Util.java @@ -223,6 +223,17 @@ public final class Util { } } + /** Returns a SHA-256 hash of {@code s}. */ + public static ByteString sha256(ByteString s) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + byte[] sha1Bytes = messageDigest.digest(s.toByteArray()); + return ByteString.of(sha1Bytes); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + /** Returns an immutable copy of {@code list}. */ public static List immutableList(List list) { return Collections.unmodifiableList(new ArrayList<>(list)); @@ -275,11 +286,13 @@ public final class Util { return result; } - public static String hostHeader(HttpUrl url) { - // TODO: square braces for IPv6 ? - return url.port() != HttpUrl.defaultPort(url.scheme()) - ? url.host() + ":" + url.port() + public static String hostHeader(HttpUrl url, boolean includeDefaultPort) { + String host = url.host().contains(":") + ? "[" + url.host() + "]" : url.host(); + return includeDefaultPort || url.port() != HttpUrl.defaultPort(url.scheme()) + ? host + ":" + url.port() + : host; } /** Returns {@code s} with control characters and non-ASCII characters replaced with '?'. */ diff --git a/okhttp3/src/main/java/okhttp3/internal/Version.java b/okhttp3/src/main/java/okhttp3/internal/Version.java index 3b86d3a29..d80d1265d 100644 --- a/okhttp3/src/main/java/okhttp3/internal/Version.java +++ b/okhttp3/src/main/java/okhttp3/internal/Version.java @@ -17,7 +17,7 @@ package okhttp3.internal; public final class Version { public static String userAgent() { - return "okhttp/3.1.1"; + return "okhttp/3.2.0"; } private Version() { diff --git a/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java b/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java index 83cddc3e5..99151f837 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/Http2xStream.java @@ -161,7 +161,7 @@ public final class Http2xStream implements HttpStream { result.add(new Header(TARGET_METHOD, request.method())); result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url()))); result.add(new Header(VERSION, "HTTP/1.1")); - result.add(new Header(TARGET_HOST, Util.hostHeader(request.url()))); + result.add(new Header(TARGET_HOST, Util.hostHeader(request.url(), false))); result.add(new Header(TARGET_SCHEME, request.url().scheme())); Set names = new LinkedHashSet<>(); @@ -200,7 +200,7 @@ public final class Http2xStream implements HttpStream { List

result = new ArrayList<>(headers.size() + 4); result.add(new Header(TARGET_METHOD, request.method())); result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url()))); - result.add(new Header(TARGET_AUTHORITY, Util.hostHeader(request.url()))); // Optional. + result.add(new Header(TARGET_AUTHORITY, Util.hostHeader(request.url(), false))); // Optional. result.add(new Header(TARGET_SCHEME, request.url().scheme())); for (int i = 0, size = headers.size(); i < size; i++) { diff --git a/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java b/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java index 287bb369e..7bd65d983 100644 --- a/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java +++ b/okhttp3/src/main/java/okhttp3/internal/http/HttpEngine.java @@ -502,7 +502,7 @@ public final class HttpEngine { Request.Builder result = request.newBuilder(); if (request.header("Host") == null) { - result.header("Host", hostHeader(request.url())); + result.header("Host", hostHeader(request.url(), false)); } if (request.header("Connection") == null) { diff --git a/okhttp3/src/main/java/okhttp3/internal/io/FileSystem.java b/okhttp3/src/main/java/okhttp3/internal/io/FileSystem.java index 6a786954b..0b1b1cfbc 100644 --- a/okhttp3/src/main/java/okhttp3/internal/io/FileSystem.java +++ b/okhttp3/src/main/java/okhttp3/internal/io/FileSystem.java @@ -71,7 +71,7 @@ public interface FileSystem { } } - @Override public boolean exists(File file) throws IOException { + @Override public boolean exists(File file) { return file.exists(); } @@ -121,7 +121,7 @@ public interface FileSystem { void delete(File file) throws IOException; /** Returns true if {@code file} exists on the file system. */ - boolean exists(File file) throws IOException; + boolean exists(File file); /** Returns the number of bytes stored in {@code file}, or 0 if it does not exist. */ long size(File file); diff --git a/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java b/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java index e0fe6ccc7..ed8019e2f 100644 --- a/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java +++ b/okhttp3/src/main/java/okhttp3/internal/io/RealConnection.java @@ -241,7 +241,7 @@ public final class RealConnection extends FramedConnection.Listener implements C // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. Request tunnelRequest = createTunnelRequest(); HttpUrl url = tunnelRequest.url(); - String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1"; + String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1"; while (true) { Http1xStream tunnelConnection = new Http1xStream(null, source, sink); source.timeout().timeout(readTimeout, MILLISECONDS); @@ -291,7 +291,7 @@ public final class RealConnection extends FramedConnection.Listener implements C private Request createTunnelRequest() throws IOException { return new Request.Builder() .url(route.address().url()) - .header("Host", Util.hostHeader(route.address().url())) + .header("Host", Util.hostHeader(route.address().url(), true)) .header("Proxy-Connection", "Keep-Alive") .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid. .build(); diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java b/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java index 3805939c1..3074a1e98 100644 --- a/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java +++ b/okhttp3/src/main/java/okhttp3/internal/tls/AndroidTrustRootIndex.java @@ -40,7 +40,9 @@ public final class AndroidTrustRootIndex implements TrustRootIndex { try { TrustAnchor trustAnchor = (TrustAnchor) findByIssuerAndSignatureMethod.invoke( trustManager, cert); - return trustAnchor.getTrustedCert(); + return trustAnchor != null + ? trustAnchor.getTrustedCert() + : null; } catch (IllegalAccessException e) { throw new AssertionError(); } catch (InvocationTargetException e) { diff --git a/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java b/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java index 0121aba30..d887df615 100644 --- a/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java +++ b/okhttp3/src/main/java/okhttp3/internal/tls/CertificateChainCleaner.java @@ -40,6 +40,9 @@ import javax.net.ssl.SSLPeerUnverifiedException; * TrustManagerImpl} and {@code TrustedCertificateIndex}. */ public final class CertificateChainCleaner { + /** The maximum number of signers in a chain. We use 9 for consistency with OpenSSL. */ + private static final int MAX_SIGNERS = 9; + private final TrustRootIndex trustRootIndex; public CertificateChainCleaner(TrustRootIndex trustRootIndex) { @@ -57,40 +60,53 @@ public final class CertificateChainCleaner { Deque queue = new ArrayDeque<>(chain); List result = new ArrayList<>(); result.add(queue.removeFirst()); + boolean foundTrustedCertificate = false; followIssuerChain: - while (true) { + for (int c = 0; c < MAX_SIGNERS; c++) { X509Certificate toVerify = (X509Certificate) result.get(result.size() - 1); - // If this cert has been signed by a trusted CA cert, we're done. Add the trusted CA - // certificate to the end of the chain, unless it's already present. (That would happen if the - // first certificate in the chain is itself a self-signed and trusted CA certificate.) - X509Certificate caCert = trustRootIndex.findByIssuerAndSignature(toVerify); - if (caCert != null) { - if (result.size() > 1 || !toVerify.equals(caCert)) { - result.add(caCert); + // If this cert has been signed by a trusted cert, use that. Add the trusted certificate to + // the end of the chain unless it's already present. (That would happen if the first + // certificate in the chain is itself a self-signed and trusted CA certificate.) + X509Certificate trustedCert = trustRootIndex.findByIssuerAndSignature(toVerify); + if (trustedCert != null) { + if (result.size() > 1 || !toVerify.equals(trustedCert)) { + result.add(trustedCert); } - return result; + if (verifySignature(trustedCert, trustedCert)) { + return result; // The self-signed cert is a root CA. We're done. + } + foundTrustedCertificate = true; + continue; } // Search for the certificate in the chain that signed this certificate. This is typically the // next element in the chain, but it could be any element. for (Iterator i = queue.iterator(); i.hasNext(); ) { X509Certificate signingCert = (X509Certificate) i.next(); - if (toVerify.getIssuerDN().equals(signingCert.getSubjectDN()) - && verifySignature(toVerify, signingCert)) { + if (verifySignature(toVerify, signingCert)) { i.remove(); result.add(signingCert); continue followIssuerChain; } } - throw new SSLPeerUnverifiedException("Failed to find a cert that signed " + toVerify); + // We've reached the end of the chain. If any cert in the chain is trusted, we're done. + if (foundTrustedCertificate) { + return result; + } + + // The last link isn't trusted. Fail. + throw new SSLPeerUnverifiedException("Failed to find a trusted cert that signed " + toVerify); } + + throw new SSLPeerUnverifiedException("Certificate chain too long: " + result); } /** Returns true if {@code toVerify} was signed by {@code signingCert}'s public key. */ private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) { + if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false; try { toVerify.verify(signingCert.getPublicKey()); return true;