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;