Update okhttp to commit b48247968a.
Includes mechanism to turn of SPDY on a per request
basis. Leaves SPDY turned off by default.
Change-Id: Ie7a0c7ebfd37d5d2653266e7b6925a2f346323ad
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index 1dc3826..515725a 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -22,8 +22,12 @@
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
+import java.util.Arrays;
+import java.util.List;
public final class HttpsHandler extends URLStreamHandler {
+ private static final List<String> ENABLED_TRANSPORTS = Arrays.asList("http/1.1");
+
@Override protected URLConnection openConnection(URL url) throws IOException {
return new OkHttpClient().open(url);
}
@@ -32,7 +36,7 @@
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
- return new OkHttpClient().setProxy(proxy).open(url);
+ return new OkHttpClient().setProxy(proxy).setTransports(ENABLED_TRANSPORTS).open(url);
}
@Override protected int getDefaultPort() {
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index fe0745c..db633c6 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -18,6 +18,7 @@
import dalvik.system.SocketTagger;
import java.io.OutputStream;
+import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
@@ -34,6 +35,12 @@
public final class Platform {
private static final Platform PLATFORM = new Platform();
+ /*
+ * Default for the maximum transmission unit, used only if
+ * there's an error retrieving it via NetworkInterface.
+ */
+ private static final int DEFAULT_MTU = 1400;
+
public static Platform get() {
return PLATFORM;
}
@@ -94,4 +101,26 @@
OutputStream out, Deflater deflater, boolean syncFlush) {
return new DeflaterOutputStream(out, deflater, syncFlush);
}
+
+ /**
+ * Returns the maximum transmission unit of the network interface used by
+ * {@code socket}, or a reasonable default if there's an error retrieving
+ * it from the socket.
+ *
+ * <p>The returned value should only be used as an optimization; such as to
+ * size buffers efficiently.
+ */
+ public int getMtu(Socket socket) {
+ try {
+ NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+ socket.getLocalAddress());
+ if (networkInterface != null) {
+ return networkInterface.getMTU();
+ }
+
+ return DEFAULT_MTU;
+ } catch (SocketException exception) {
+ return DEFAULT_MTU;
+ }
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/Address.java b/src/main/java/com/squareup/okhttp/Address.java
index cd41ac9..b34bd91 100644
--- a/src/main/java/com/squareup/okhttp/Address.java
+++ b/src/main/java/com/squareup/okhttp/Address.java
@@ -15,8 +15,10 @@
*/
package com.squareup.okhttp;
+import com.squareup.okhttp.internal.Util;
import java.net.Proxy;
import java.net.UnknownHostException;
+import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
@@ -38,16 +40,23 @@
final int uriPort;
final SSLSocketFactory sslSocketFactory;
final HostnameVerifier hostnameVerifier;
+ final OkAuthenticator authenticator;
+ final List<String> transports;
public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
- HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
+ HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy,
+ List<String> transports) throws UnknownHostException {
if (uriHost == null) throw new NullPointerException("uriHost == null");
if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
+ if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
+ if (transports == null) throw new IllegalArgumentException("transports == null");
this.proxy = proxy;
this.uriHost = uriHost;
this.uriPort = uriPort;
this.sslSocketFactory = sslSocketFactory;
this.hostnameVerifier = hostnameVerifier;
+ this.authenticator = authenticator;
+ this.transports = Util.immutableList(transports);
}
/** Returns the hostname of the origin server. */
@@ -79,6 +88,22 @@
return hostnameVerifier;
}
+
+ /**
+ * Returns the client's authenticator. This method never returns null.
+ */
+ public OkAuthenticator getAuthenticator() {
+ return authenticator;
+ }
+
+ /**
+ * Returns the client's transports. This method always returns a non-null list
+ * that contains "http/1.1", possibly among other transports.
+ */
+ public List<String> getTransports() {
+ return transports;
+ }
+
/**
* Returns this address's explicitly-specified HTTP proxy, or null to
* delegate to the HTTP client's proxy selector.
@@ -94,7 +119,9 @@
&& this.uriHost.equals(that.uriHost)
&& this.uriPort == that.uriPort
&& equal(this.sslSocketFactory, that.sslSocketFactory)
- && equal(this.hostnameVerifier, that.hostnameVerifier);
+ && equal(this.hostnameVerifier, that.hostnameVerifier)
+ && equal(this.authenticator, that.authenticator)
+ && equal(this.transports, that.transports);
}
return false;
}
@@ -105,7 +132,9 @@
result = 31 * result + uriPort;
result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+ result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0);
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+ result = 31 * result + transports.hashCode();
return result;
}
}
diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java
index 2394b77..5ec99cc 100644
--- a/src/main/java/com/squareup/okhttp/Connection.java
+++ b/src/main/java/com/squareup/okhttp/Connection.java
@@ -24,11 +24,11 @@
import com.squareup.okhttp.internal.http.SpdyTransport;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.URL;
@@ -76,10 +76,7 @@
'h', 't', 't', 'p', '/', '1', '.', '1'
};
- private final Address address;
- private final Proxy proxy;
- private final InetSocketAddress inetSocketAddress;
- private final boolean modernTls;
+ private final Route route;
private Socket socket;
private InputStream in;
@@ -89,15 +86,8 @@
private int httpMinorVersion = 1; // Assume HTTP/1.1
private long idleStartTimeNs;
- public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
- boolean modernTls) {
- if (address == null) throw new NullPointerException("address == null");
- if (proxy == null) throw new NullPointerException("proxy == null");
- if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
- this.address = address;
- this.proxy = proxy;
- this.inetSocketAddress = inetSocketAddress;
- this.modernTls = modernTls;
+ public Connection(Route route) {
+ this.route = route;
}
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
@@ -106,21 +96,22 @@
throw new IllegalStateException("already connected");
}
connected = true;
- socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket();
- socket.connect(inetSocketAddress, connectTimeout);
+ socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
+ socket.connect(route.inetSocketAddress, connectTimeout);
socket.setSoTimeout(readTimeout);
in = socket.getInputStream();
out = socket.getOutputStream();
- if (address.sslSocketFactory != null) {
+ if (route.address.sslSocketFactory != null) {
upgradeToTls(tunnelRequest);
}
- // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
- if (!isSpdy()) {
- int bufferSize = 128;
- in = new BufferedInputStream(in, bufferSize);
- }
+ // Use MTU-sized buffers to send fewer packets.
+ int mtu = Platform.get().getMtu(socket);
+ if (mtu < 1024) mtu = 1024;
+ if (mtu > 8192) mtu = 8192;
+ in = new BufferedInputStream(in, mtu);
+ out = new BufferedOutputStream(out, mtu);
}
/**
@@ -136,17 +127,17 @@
}
// Create the wrapper over connected socket.
- socket = address.sslSocketFactory
- .createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */);
+ socket = route.address.sslSocketFactory
+ .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
SSLSocket sslSocket = (SSLSocket) socket;
- if (modernTls) {
- platform.enableTlsExtensions(sslSocket, address.uriHost);
+ if (route.modernTls) {
+ platform.enableTlsExtensions(sslSocket, route.address.uriHost);
} else {
platform.supportTlsIntolerantServer(sslSocket);
}
- final boolean spdyEnabled = false;
- if (modernTls && spdyEnabled) {
+ boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
+ if (useNpn) {
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
}
@@ -154,18 +145,19 @@
sslSocket.startHandshake();
// Verify that the socket's certificates are acceptable for the target host.
- if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
- throw new IOException("Hostname '" + address.uriHost + "' was not verified");
+ if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
+ throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
}
out = sslSocket.getOutputStream();
in = sslSocket.getInputStream();
byte[] selectedProtocol;
- if (modernTls && spdyEnabled && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+ if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (Arrays.equals(selectedProtocol, SPDY3)) {
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
- spdyConnection = new SpdyConnection.Builder(address.getUriHost(), true, in, out).build();
+ spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
+ .build();
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
throw new IOException(
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
@@ -182,29 +174,9 @@
socket.close();
}
- /**
- * Returns the proxy that this connection is using.
- *
- * <strong>Warning:</strong> This may be different than the proxy returned
- * by {@link #getAddress}! That is the proxy that the user asked to be
- * connected to; this returns the proxy that they were actually connected
- * to. The two may disagree when a proxy selector selects a different proxy
- * for a connection.
- */
- public Proxy getProxy() {
- return proxy;
- }
-
- public Address getAddress() {
- return address;
- }
-
- public InetSocketAddress getSocketAddress() {
- return inetSocketAddress;
- }
-
- public boolean isModernTls() {
- return modernTls;
+ /** Returns the route used by this connection. */
+ public Route getRoute() {
+ return route;
}
/**
@@ -285,7 +257,7 @@
* we must avoid buffering bytes intended for the higher-level protocol.
*/
public boolean requiresTunnel() {
- return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP;
+ return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
}
/**
@@ -305,9 +277,9 @@
case HTTP_PROXY_AUTH:
requestHeaders = new RawHeaders(requestHeaders);
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
- boolean credentialsFound =
- HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
- proxy, url);
+ boolean credentialsFound = HttpAuthenticator.processAuthHeader(
+ route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
+ route.proxy, url);
if (credentialsFound) {
continue;
} else {
diff --git a/src/main/java/com/squareup/okhttp/ConnectionPool.java b/src/main/java/com/squareup/okhttp/ConnectionPool.java
index 438fe91..009f025 100644
--- a/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -1,10 +1,25 @@
+/*
+ * 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.squareup.okhttp;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import java.net.SocketException;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
@@ -65,8 +80,9 @@
private final LinkedList<Connection> connections = new LinkedList<Connection>();
/** We use a single background thread to cleanup expired connections. */
- private final ExecutorService executorService =
- new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
+ 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+ Util.daemonThreadFactory("OkHttp ConnectionPool"));
private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
@Override public Void call() throws Exception {
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
@@ -162,9 +178,10 @@
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
public synchronized Connection get(Address address) {
Connection foundConnection = null;
- for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
- Connection connection = i.next();
- if (!connection.getAddress().equals(address)
+ for (ListIterator<Connection> i = connections.listIterator(connections.size());
+ i.hasPrevious(); ) {
+ Connection connection = i.previous();
+ if (!connection.getRoute().getAddress().equals(address)
|| !connection.isAlive()
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
continue;
@@ -241,4 +258,17 @@
}
}
}
+
+ /** Close and remove all connections in the pool. */
+ public void evictAll() {
+ List<Connection> connections;
+ synchronized (this) {
+ connections = new ArrayList<Connection>(this.connections);
+ this.connections.clear();
+ }
+
+ for (Connection connection : connections) {
+ Util.closeQuietly(connection);
+ }
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
similarity index 77%
rename from src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
rename to src/main/java/com/squareup/okhttp/HttpResponseCache.java
index 8735166..7622aa3 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
@@ -14,14 +14,18 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp;
-import com.squareup.okhttp.OkResponseCache;
-import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.internal.Base64;
import com.squareup.okhttp.internal.DiskLruCache;
import com.squareup.okhttp.internal.StrictLineReader;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import com.squareup.okhttp.internal.http.ResponseHeaders;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -58,11 +62,63 @@
import static com.squareup.okhttp.internal.Util.UTF_8;
/**
- * Cache responses in a directory on the file system. Most clients should use
- * {@code android.net.HttpResponseCache}, the stable, documented front end for
- * this.
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
+ * saving time and bandwidth.
+ *
+ * <h3>Cache Optimization</h3>
+ * To measure cache effectiveness, this class tracks three statistics:
+ * <ul>
+ * <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
+ * of HTTP requests issued since this cache was created.
+ * <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ * number of those requests that required network use.
+ * <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ * those requests whose responses were served by the cache.
+ * </ul>
+ * Sometimes a request will result in a conditional cache hit. If the cache
+ * contains a stale copy of the response, the client will issue a conditional
+ * {@code GET}. The server will then send either the updated response if it has
+ * changed, or a short 'not modified' response if the client's copy is still
+ * valid. Such responses increment both the network count and hit count.
+ *
+ * <p>The best way to improve the cache hit rate is by configuring the web
+ * server to return cacheable responses. Although this client honors all <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
+ * headers, it doesn't cache partial responses.
+ *
+ * <h3>Force a Network Response</h3>
+ * In some situations, such as after a user clicks a 'refresh' button, it may be
+ * necessary to skip the cache, and fetch data directly from the server. To force
+ * a full refresh, add the {@code no-cache} directive: <pre> {@code
+ * connection.addRequestProperty("Cache-Control", "no-cache");
+ * }</pre>
+ * If it is only necessary to force a cached response to be validated by the
+ * server, use the more efficient {@code max-age=0} instead: <pre> {@code
+ * connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }</pre>
+ *
+ * <h3>Force a Cache Response</h3>
+ * Sometimes you'll want to show resources if they are available immediately,
+ * but not otherwise. This can be used so your application can show
+ * <i>something</i> while waiting for the latest data to be downloaded. To
+ * restrict a request to locally-cached resources, add the {@code
+ * only-if-cached} directive: <pre> {@code
+ * try {
+ * connection.addRequestProperty("Cache-Control", "only-if-cached");
+ * InputStream cached = connection.getInputStream();
+ * // the resource was cached! show it
+ * } catch (FileNotFoundException e) {
+ * // the resource was not cached
+ * }
+ * }</pre>
+ * This technique works even better in situations where a stale response is
+ * better than no response. To permit stale cached responses, use the {@code
+ * max-stale} directive with the maximum staleness in seconds: <pre> {@code
+ * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }</pre>
*/
-public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
+public final class HttpResponseCache extends ResponseCache {
private static final char[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
@@ -81,6 +137,36 @@
private int hitCount;
private int requestCount;
+ /**
+ * Although this class only exposes the limited ResponseCache API, it
+ * implements the full OkResponseCache interface. This field is used as a
+ * package private handle to the complete implementation. It delegates to
+ * public and private members of this type.
+ */
+ final OkResponseCache okResponseCache = new OkResponseCache() {
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ return HttpResponseCache.this.get(uri, requestMethod, requestHeaders);
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ return HttpResponseCache.this.put(uri, connection);
+ }
+
+ @Override public void update(
+ CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
+ HttpResponseCache.this.update(conditionalCacheHit, connection);
+ }
+
+ @Override public void trackConditionalCacheHit() {
+ HttpResponseCache.this.trackConditionalCacheHit();
+ }
+
+ @Override public void trackResponse(ResponseSource source) {
+ HttpResponseCache.this.trackResponse(source);
+ }
+ };
+
public HttpResponseCache(File directory, long maxSize) throws IOException {
cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
}
@@ -185,13 +271,7 @@
}
}
- /**
- * Handles a conditional request hit by updating the stored cache response
- * with the headers from {@code httpConnection}. The cached response body is
- * not updated. If the stored response has changed since {@code
- * conditionalCacheHit} was returned, this does nothing.
- */
- @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+ private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException {
HttpEngine httpEngine = getHttpEngine(httpConnection);
URI uri = httpEngine.getUri();
@@ -234,8 +314,13 @@
}
}
- public DiskLruCache getCache() {
- return cache;
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ cache.delete();
}
public synchronized int getWriteAbortCount() {
@@ -246,7 +331,31 @@
return writeSuccessCount;
}
- public synchronized void trackResponse(ResponseSource source) {
+ public long getSize() {
+ return cache.size();
+ }
+
+ public long getMaxSize() {
+ return cache.getMaxSize();
+ }
+
+ public void flush() throws IOException {
+ cache.flush();
+ }
+
+ public void close() throws IOException {
+ cache.close();
+ }
+
+ public File getDirectory() {
+ return cache.getDirectory();
+ }
+
+ public boolean isClosed() {
+ return cache.isClosed();
+ }
+
+ private synchronized void trackResponse(ResponseSource source) {
requestCount++;
switch (source) {
@@ -260,7 +369,7 @@
}
}
- public synchronized void trackConditionalCacheHit() {
+ private synchronized void trackConditionalCacheHit() {
hitCount++;
}
@@ -405,7 +514,7 @@
if (isHttps()) {
String blank = reader.readLine();
- if (!blank.isEmpty()) {
+ if (blank.length() > 0) {
throw new IOException("expected \"\" but was \"" + blank + "\"");
}
cipherSuite = reader.readLine();
@@ -490,7 +599,7 @@
}
return result;
} catch (CertificateException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
}
@@ -507,7 +616,7 @@
writer.write(line + '\n');
}
} catch (CertificateEncodingException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
}
diff --git a/src/main/java/com/squareup/okhttp/OkAuthenticator.java b/src/main/java/com/squareup/okhttp/OkAuthenticator.java
new file mode 100644
index 0000000..a505419
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/OkAuthenticator.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Base64;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Responds to authentication challenges from the remote web or proxy server by
+ * returning credentials.
+ */
+public interface OkAuthenticator {
+ /**
+ * Returns a credential that satisfies the authentication challenge made by
+ * {@code url}. Returns null if the challenge cannot be satisfied. This method
+ * is called in response to an HTTP 401 unauthorized status code sent by the
+ * origin server.
+ *
+ * @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP
+ * response.
+ */
+ Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
+
+ /**
+ * Returns a credential that satisfies the authentication challenge made by
+ * {@code proxy}. Returns null if the challenge cannot be satisfied. This
+ * method is called in response to an HTTP 401 unauthorized status code sent
+ * by the proxy server.
+ *
+ * @param challenges parsed "Proxy-Authenticate" challenge headers from the
+ * HTTP response.
+ */
+ Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
+
+ /** An RFC 2617 challenge. */
+ public final class Challenge {
+ private final String scheme;
+ private final String realm;
+
+ public Challenge(String scheme, String realm) {
+ this.scheme = scheme;
+ this.realm = realm;
+ }
+
+ /** Returns the authentication scheme, like {@code Basic}. */
+ public String getScheme() {
+ return scheme;
+ }
+
+ /** Returns the protection space. */
+ public String getRealm() {
+ return realm;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof Challenge
+ && ((Challenge) o).scheme.equals(scheme)
+ && ((Challenge) o).realm.equals(realm);
+ }
+
+ @Override public int hashCode() {
+ return scheme.hashCode() + 31 * realm.hashCode();
+ }
+
+ @Override public String toString() {
+ return scheme + " realm=\"" + realm + "\"";
+ }
+ }
+
+ /** An RFC 2617 credential. */
+ public final class Credential {
+ private final String headerValue;
+
+ private Credential(String headerValue) {
+ this.headerValue = headerValue;
+ }
+
+ /** Returns an auth credential for the Basic scheme. */
+ public static Credential basic(String userName, String password) {
+ try {
+ String usernameAndPassword = userName + ":" + password;
+ byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
+ String encoded = Base64.encode(bytes);
+ return new Credential("Basic " + encoded);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ public String getHeaderValue() {
+ return headerValue;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue);
+ }
+
+ @Override public int hashCode() {
+ return headerValue.hashCode();
+ }
+
+ @Override public String toString() {
+ return headerValue;
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java
index d21cdb7..da3e82c 100644
--- a/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -15,29 +15,53 @@
*/
package com.squareup.okhttp;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ResponseCache;
import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
/** Configures and creates HTTP connections. */
public final class OkHttpClient {
+ private static final List<String> DEFAULT_TRANSPORTS
+ = Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
+
private Proxy proxy;
+ private List<String> transports;
+ private final Set<Route> failedRoutes;
private ProxySelector proxySelector;
private CookieHandler cookieHandler;
private ResponseCache responseCache;
private SSLSocketFactory sslSocketFactory;
private HostnameVerifier hostnameVerifier;
+ private OkAuthenticator authenticator;
private ConnectionPool connectionPool;
private boolean followProtocolRedirects = true;
+ public OkHttpClient() {
+ this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
+ }
+
+ private OkHttpClient(OkHttpClient copyFrom) {
+ this.failedRoutes = copyFrom.failedRoutes; // Avoid allocating an unnecessary LinkedHashSet.
+ }
+
/**
* Sets the HTTP proxy that will be used by connections created by this
* client. This takes precedence over {@link #setProxySelector}, which is
@@ -102,13 +126,23 @@
return responseCache;
}
+ private OkResponseCache okResponseCache() {
+ if (responseCache instanceof HttpResponseCache) {
+ return ((HttpResponseCache) responseCache).okResponseCache;
+ } else if (responseCache != null) {
+ return new OkResponseCacheAdapter(responseCache);
+ } else {
+ return null;
+ }
+ }
+
/**
* Sets the socket factory used to secure HTTPS connections.
*
* <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
* system-wide default} SSL socket factory will be used.
*/
- public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+ public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
return this;
}
@@ -134,6 +168,22 @@
}
/**
+ * Sets the authenticator used to respond to challenges from the remote web
+ * server or proxy server.
+ *
+ * <p>If unset, the {@link java.net.Authenticator#setDefault system-wide default}
+ * authenticator will be used.
+ */
+ public OkHttpClient setAuthenticator(OkAuthenticator authenticator) {
+ this.authenticator = authenticator;
+ return this;
+ }
+
+ public OkAuthenticator getAuthenticator() {
+ return authenticator;
+ }
+
+ /**
* Sets the connection pool used to recycle HTTP and HTTPS connections.
*
* <p>If unset, the {@link ConnectionPool#getDefault() system-wide
@@ -164,23 +214,67 @@
return followProtocolRedirects;
}
+ /**
+ * Configure the transports used by this client to communicate with remote
+ * servers. By default this client will prefer the most efficient transport
+ * available, falling back to more ubiquitous transports. Applications should
+ * only call this method to avoid specific compatibility problems, such as web
+ * servers that behave incorrectly when SPDY is enabled.
+ *
+ * <p>The following transports are currently supported:
+ * <ul>
+ * <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
+ * <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3">spdy/3</a>
+ * </ul>
+ *
+ * <p><strong>This is an evolving set.</strong> Future releases may drop
+ * support for transitional transports (like spdy/3), in favor of their
+ * successors (spdy/4 or http/2.0). The http/1.1 transport will never be
+ * dropped.
+ *
+ * <p>If multiple protocols are specified, <a
+ * href="https://technotes.googlecode.com/git/nextprotoneg.html">NPN</a> will
+ * be used to negotiate a transport. Future releases may use another mechanism
+ * (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
+ * to negotiate a transport.
+ *
+ * @param transports the transports to use, in order of preference. The list
+ * must contain "http/1.1". It must not contain null.
+ */
+ public OkHttpClient setTransports(List<String> transports) {
+ transports = Util.immutableList(transports);
+ if (!transports.contains("http/1.1")) {
+ throw new IllegalArgumentException("transports doesn't contain http/1.1: " + transports);
+ }
+ if (transports.contains(null)) {
+ throw new IllegalArgumentException("transports must not contain null");
+ }
+ this.transports = transports;
+ return this;
+ }
+
+ public List<String> getTransports() {
+ return transports;
+ }
+
public HttpURLConnection open(URL url) {
String protocol = url.getProtocol();
+ OkHttpClient copy = copyWithDefaults();
if (protocol.equals("http")) {
- return new HttpURLConnectionImpl(url, copyWithDefaults());
+ return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else if (protocol.equals("https")) {
- return new HttpsURLConnectionImpl(url, copyWithDefaults());
+ return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else {
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
}
/**
- * Returns a copy of this OkHttpClient that uses the system-wide default for
+ * Returns a shallow copy of this OkHttpClient that uses the system-wide default for
* each field that hasn't been explicitly configured.
*/
private OkHttpClient copyWithDefaults() {
- OkHttpClient result = new OkHttpClient();
+ OkHttpClient result = new OkHttpClient(this);
result.proxy = proxy;
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
@@ -190,9 +284,13 @@
: HttpsURLConnection.getDefaultSSLSocketFactory();
result.hostnameVerifier = hostnameVerifier != null
? hostnameVerifier
- : HttpsURLConnection.getDefaultHostnameVerifier();
+ : new OkHostnameVerifier();
+ result.authenticator = authenticator != null
+ ? authenticator
+ : HttpAuthenticator.SYSTEM_DEFAULT;
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
result.followProtocolRedirects = followProtocolRedirects;
+ result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
return result;
}
}
diff --git a/src/main/java/com/squareup/okhttp/OkResponseCache.java b/src/main/java/com/squareup/okhttp/OkResponseCache.java
deleted file mode 100644
index b7e3801..0000000
--- a/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed 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.squareup.okhttp;
-
-import java.io.IOException;
-import java.net.CacheResponse;
-import java.net.HttpURLConnection;
-
-/**
- * A response cache that supports statistics tracking and updating stored
- * responses. Implementations of {@link java.net.ResponseCache} should implement
- * this interface to receive additional support from the HTTP engine.
- */
-public interface OkResponseCache {
-
- /** Track an HTTP response being satisfied by {@code source}. */
- void trackResponse(ResponseSource source);
-
- /** Track an conditional GET that was satisfied by this cache. */
- void trackConditionalCacheHit();
-
- /** Updates stored HTTP headers using a hit on a conditional GET. */
- void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
- throws IOException;
-}
diff --git a/src/main/java/com/squareup/okhttp/Route.java b/src/main/java/com/squareup/okhttp/Route.java
new file mode 100644
index 0000000..6968c60
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/Route.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/** Represents the route used by a connection to reach an endpoint. */
+public class Route {
+ final Address address;
+ final Proxy proxy;
+ final InetSocketAddress inetSocketAddress;
+ final boolean modernTls;
+
+ public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+ boolean modernTls) {
+ if (address == null) throw new NullPointerException("address == null");
+ if (proxy == null) throw new NullPointerException("proxy == null");
+ if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+ this.address = address;
+ this.proxy = proxy;
+ this.inetSocketAddress = inetSocketAddress;
+ this.modernTls = modernTls;
+ }
+
+ /** Returns the {@link Address} of this route. */
+ public Address getAddress() {
+ return address;
+ }
+
+ /**
+ * Returns the {@link Proxy} of this route.
+ *
+ * <strong>Warning:</strong> This may be different than the proxy returned
+ * by {@link #getAddress}! That is the proxy that the user asked to be
+ * connected to; this returns the proxy that they were actually connected
+ * to. The two may disagree when a proxy selector selects a different proxy
+ * for a connection.
+ */
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /** Returns the {@link InetSocketAddress} of this route. */
+ public InetSocketAddress getSocketAddress() {
+ return inetSocketAddress;
+ }
+
+ /** Returns true if this route uses modern tls. */
+ public boolean isModernTls() {
+ return modernTls;
+ }
+
+ /** Returns a copy of this route with flipped tls mode. */
+ public Route flipTlsMode() {
+ return new Route(address, proxy, inetSocketAddress, !modernTls);
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof Route) {
+ Route other = (Route) obj;
+ return (address.equals(other.address)
+ && proxy.equals(other.proxy)
+ && inetSocketAddress.equals(other.inetSocketAddress)
+ && modernTls == other.modernTls);
+ }
+ return false;
+ }
+
+ @Override public int hashCode() {
+ int result = 17;
+ result = 31 * result + address.hashCode();
+ result = 31 * result + proxy.hashCode();
+ result = 31 * result + inetSocketAddress.hashCode();
+ result = result + (modernTls ? (31 * result) : 0);
+ return result;
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
similarity index 80%
rename from src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
rename to src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
index 90675b0..78c9691 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
@@ -14,18 +14,18 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal;
import java.io.IOException;
import java.io.OutputStream;
/**
- * An output stream for the body of an HTTP request.
+ * An output stream for an HTTP request body.
*
* <p>Since a single socket's output stream may be used to write multiple HTTP
* requests to the same server, subclasses should not close the socket stream.
*/
-abstract class AbstractHttpOutputStream extends OutputStream {
+public abstract class AbstractOutputStream extends OutputStream {
protected boolean closed;
@Override public final void write(int data) throws IOException {
@@ -37,4 +37,9 @@
throw new IOException("stream closed");
}
}
+
+ /** Returns true if this stream was closed locally. */
+ public boolean isClosed() {
+ return closed;
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index 00fe2f1..f7fcb1e 100644
--- a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -23,7 +23,6 @@
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
-import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -32,23 +31,22 @@
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
- * entry has a string key and a fixed number of values. Values are byte
- * sequences, accessible as streams or files. Each value must be between {@code
- * 0} and {@code Integer.MAX_VALUE} bytes in length.
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
*
* <p>The cache stores its data in a directory on the filesystem. This
* directory must be exclusive to the cache; the cache may delete or overwrite
@@ -66,12 +64,12 @@
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
- * <li>When an entry is being <strong>created</strong> it is necessary to
- * supply a full set of values; the empty value should be used as a
- * placeholder if necessary.
- * <li>When an entry is being <strong>edited</strong>, it is not necessary
- * to supply data for every value; values default to their previous
- * value.
+ * <li>When an entry is being <strong>created</strong> it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
* </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
@@ -89,58 +87,63 @@
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
- static final String JOURNAL_FILE_TMP = "journal.tmp";
+ static final String JOURNAL_FILE_TEMP = "journal.tmp";
+ static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
+ static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
- // This cache uses a journal file named "journal". A typical journal file
- // looks like this:
- // libcore.io.DiskLruCache
- // 1
- // 100
- // 2
- //
- // CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
- // DIRTY 335c4c6028171cfddfbaae1a9c313c52
- // CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
- // REMOVE 335c4c6028171cfddfbaae1a9c313c52
- // DIRTY 1ab96a171faeeee38496d8b330771a7a
- // CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
- // READ 335c4c6028171cfddfbaae1a9c313c52
- // READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
- //
- // The first five lines of the journal form its header. They are the
- // constant string "libcore.io.DiskLruCache", the disk cache's version,
- // the application's version, the value count, and a blank line.
- //
- // Each of the subsequent lines in the file is a record of the state of a
- // cache entry. Each line contains space-separated values: a state, a key,
- // and optional state-specific values.
- // o DIRTY lines track that an entry is actively being created or updated.
- // Every successful DIRTY action should be followed by a CLEAN or REMOVE
- // action. DIRTY lines without a matching CLEAN or REMOVE indicate that
- // temporary files may need to be deleted.
- // o CLEAN lines track a cache entry that has been successfully published
- // and may be read. A publish line is followed by the lengths of each of
- // its values.
- // o READ lines track accesses for LRU.
- // o REMOVE lines track entries that have been deleted.
- //
- // The journal file is appended to as cache operations occur. The journal may
- // occasionally be compacted by dropping redundant lines. A temporary file named
- // "journal.tmp" will be used during compaction; that file should be deleted if
- // it exists when the cache is opened.
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this:
+ * libcore.io.DiskLruCache
+ * 1
+ * 100
+ * 2
+ *
+ * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ * DIRTY 335c4c6028171cfddfbaae1a9c313c52
+ * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ * REMOVE 335c4c6028171cfddfbaae1a9c313c52
+ * DIRTY 1ab96a171faeeee38496d8b330771a7a
+ * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ * READ 335c4c6028171cfddfbaae1a9c313c52
+ * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ *
+ * The first five lines of the journal form its header. They are the
+ * constant string "libcore.io.DiskLruCache", the disk cache's version,
+ * the application's version, the value count, and a blank line.
+ *
+ * Each of the subsequent lines in the file is a record of the state of a
+ * cache entry. Each line contains space-separated values: a state, a key,
+ * and optional state-specific values.
+ * o DIRTY lines track that an entry is actively being created or updated.
+ * Every successful DIRTY action should be followed by a CLEAN or REMOVE
+ * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+ * temporary files may need to be deleted.
+ * o CLEAN lines track a cache entry that has been successfully published
+ * and may be read. A publish line is followed by the lengths of each of
+ * its values.
+ * o READ lines track accesses for LRU.
+ * o REMOVE lines track entries that have been deleted.
+ *
+ * The journal file is appended to as cache operations occur. The journal may
+ * occasionally be compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted if
+ * it exists when the cache is opened.
+ */
private final File directory;
private final File journalFile;
private final File journalFileTmp;
+ private final File journalFileBackup;
private final int appVersion;
- private final long maxSize;
+ private long maxSize;
private final int valueCount;
private long size = 0;
private Writer journalWriter;
@@ -156,13 +159,13 @@
private long nextSequenceNumber = 0;
/** This cache uses a single background thread to evict entries. */
- private final ExecutorService executorService =
+ final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() {
- @Override public Void call() throws Exception {
+ public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
- return null; // closed
+ return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
@@ -178,7 +181,8 @@
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
- this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+ this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+ this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
@@ -201,26 +205,35 @@
throw new IllegalArgumentException("valueCount <= 0");
}
- // prefer to pick up where we left off
+ // If a bkp file exists, use it instead.
+ File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+ if (backupFile.exists()) {
+ File journalFile = new File(directory, JOURNAL_FILE);
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ backupFile.delete();
+ } else {
+ renameTo(backupFile, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
- cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
+ cache.journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
return cache;
} catch (IOException journalIsCorrupt) {
- Platform.get()
- .logW("DiskLruCache "
- + directory
- + " is corrupt: "
- + journalIsCorrupt.getMessage()
- + ", removing");
+ Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
+ + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
- // create a new empty cache
+ // Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
@@ -235,42 +248,47 @@
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
- if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
- .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
- .equals(blank)) {
- throw new IOException("unexpected journal header: ["
- + magic
- + ", "
- + version
- + ", "
- + valueCountString
- + ", "
- + blank
- + "]");
+ if (!MAGIC.equals(magic)
+ || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString)
+ || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ + valueCountString + ", " + blank + "]");
}
+ int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
+ lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
+ redundantOpCount = lineCount - lruEntries.size();
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
- String[] parts = line.split(" ");
- if (parts.length < 2) {
+ int firstSpace = line.indexOf(' ');
+ if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
- String key = parts[1];
- if (parts[0].equals(REMOVE) && parts.length == 2) {
- lruEntries.remove(key);
- return;
+ int keyBegin = firstSpace + 1;
+ int secondSpace = line.indexOf(' ', keyBegin);
+ final String key;
+ if (secondSpace == -1) {
+ key = line.substring(keyBegin);
+ if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+ lruEntries.remove(key);
+ return;
+ }
+ } else {
+ key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
@@ -279,14 +297,15 @@
lruEntries.put(key, entry);
}
- if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+ if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+ String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
- entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
- } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+ entry.setLengths(parts);
+ } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
- } else if (parts[0].equals(READ) && parts.length == 2) {
- // this work was already done by calling lruEntries.get()
+ } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+ // This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
@@ -324,32 +343,53 @@
journalWriter.close();
}
- Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
- writer.write(MAGIC);
- writer.write("\n");
- writer.write(VERSION_1);
- writer.write("\n");
- writer.write(Integer.toString(appVersion));
- writer.write("\n");
- writer.write(Integer.toString(valueCount));
- writer.write("\n");
- writer.write("\n");
+ Writer writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+ try {
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
- for (Entry entry : lruEntries.values()) {
- if (entry.currentEditor != null) {
- writer.write(DIRTY + ' ' + entry.key + '\n');
- } else {
- writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ for (Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
}
+ } finally {
+ writer.close();
}
- writer.close();
- journalFileTmp.renameTo(journalFile);
- journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+ if (journalFile.exists()) {
+ renameTo(journalFile, journalFileBackup, true);
+ }
+ renameTo(journalFileTmp, journalFile, false);
+ journalFileBackup.delete();
+
+ journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
private static void deleteIfExists(File file) throws IOException {
- file.delete();
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+ if (deleteDestination) {
+ deleteIfExists(to);
+ }
+ if (!from.renameTo(to)) {
+ throw new IOException();
+ }
}
/**
@@ -378,7 +418,14 @@
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
- // a file must have been deleted manually!
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (ins[i] != null) {
+ Util.closeQuietly(ins[i]);
+ } else {
+ break;
+ }
+ }
return null;
}
@@ -388,7 +435,7 @@
executorService.submit(cleanupCallable);
}
- return new Snapshot(key, entry.sequenceNumber, ins);
+ return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
/**
@@ -405,19 +452,19 @@
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
- return null; // snapshot is stale
+ return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
- return null; // another edit is in progress
+ return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
- // flush the journal before creating files to prevent file leaks
+ // Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
@@ -432,11 +479,20 @@
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
- public long maxSize() {
+ public long getMaxSize() {
return maxSize;
}
/**
+ * Changes the maximum number of bytes the cache can store and queues a job
+ * to trim the existing store, if necessary.
+ */
+ public synchronized void setMaxSize(long maxSize) {
+ this.maxSize = maxSize;
+ executorService.submit(cleanupCallable);
+ }
+
+ /**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
@@ -451,7 +507,7 @@
throw new IllegalStateException();
}
- // if this edit is creating the entry for the first time, every index must have a value
+ // If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
@@ -460,7 +516,6 @@
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
- Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
return;
}
}
@@ -494,6 +549,7 @@
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
+ journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
@@ -506,7 +562,8 @@
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
- return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
+ return redundantOpCount >= redundantOpCompactThreshold //
+ && redundantOpCount >= lruEntries.size();
}
/**
@@ -564,7 +621,7 @@
/** Closes this cache. Stored values will remain on the filesystem. */
public synchronized void close() throws IOException {
if (journalWriter == null) {
- return; // already closed
+ return; // Already closed.
}
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
if (entry.currentEditor != null) {
@@ -594,14 +651,14 @@
}
private void validateKey(String key) {
- if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
- throw new IllegalArgumentException(
- "keys must not contain spaces or newlines: \"" + key + "\"");
+ Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
}
}
private static String inputStreamToString(InputStream in) throws IOException {
- return Util.readFully(new InputStreamReader(in, UTF_8));
+ return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
/** A snapshot of the values for an entry. */
@@ -609,11 +666,13 @@
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
+ private final long[] lengths;
- private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+ private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
+ this.lengths = lengths;
}
/**
@@ -635,18 +694,31 @@
return inputStreamToString(getInputStream(index));
}
- @Override public void close() {
+ /** Returns the byte length of the value for {@code index}. */
+ public long getLength(int index) {
+ return lengths[index];
+ }
+
+ public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
+ private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // Eat all writes silently. Nom nom.
+ }
+ };
+
/** Edits the values for an entry. */
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
+ private boolean committed;
private Editor(Entry entry) {
this.entry = entry;
@@ -665,7 +737,11 @@
if (!entry.readable) {
return null;
}
- return new FileInputStream(entry.getCleanFile(index));
+ try {
+ return new FileInputStream(entry.getCleanFile(index));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
}
}
@@ -693,7 +769,21 @@
if (!entry.readable) {
written[index] = true;
}
- return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+ File dirtyFile = entry.getDirtyFile(index);
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e) {
+ // Attempt to recreate the cache directory.
+ directory.mkdirs();
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e2) {
+ // We are unable to recover. Silently eat the writes.
+ return NULL_OUTPUT_STREAM;
+ }
+ }
+ return new FaultHidingOutputStream(outputStream);
}
}
@@ -701,7 +791,7 @@
public void set(int index, String value) throws IOException {
Writer writer = null;
try {
- writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+ writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value);
} finally {
Util.closeQuietly(writer);
@@ -715,10 +805,11 @@
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
- remove(entry.key); // the previous entry is stale
+ remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
+ committed = true;
}
/**
@@ -729,7 +820,16 @@
completeEdit(this, false);
}
- private final class FaultHidingOutputStream extends FilterOutputStream {
+ public void abortUnlessCommitted() {
+ if (!committed) {
+ try {
+ abort();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@@ -812,7 +912,7 @@
}
private IOException invalidLengths(String[] strings) throws IOException {
- throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+ throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
}
public File getCleanFile(int i) {
diff --git a/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
new file mode 100644
index 0000000..c32b27a
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An output stream wrapper that recovers from failures in the underlying stream
+ * by replacing it with another stream. This class buffers a fixed amount of
+ * data under the assumption that failures occur early in a stream's life.
+ * If a failure occurs after the buffer has been exhausted, no recovery is
+ * attempted.
+ *
+ * <p>Subclasses must override {@link #replacementStream} which will request a
+ * replacement stream each time an {@link IOException} is encountered on the
+ * current stream.
+ */
+public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
+ private final int maxReplayBufferLength;
+
+ /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
+ private ByteArrayOutputStream replayBuffer;
+ private OutputStream out;
+
+ /**
+ * @param maxReplayBufferLength the maximum number of successfully written
+ * bytes to buffer so they can be replayed in the event of an error.
+ * Failure recoveries are not possible once this limit has been exceeded.
+ */
+ public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
+ if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
+ this.maxReplayBufferLength = maxReplayBufferLength;
+ this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
+ this.out = out;
+ }
+
+ @Override public final void write(byte[] buffer, int offset, int count) throws IOException {
+ if (closed) throw new IOException("stream closed");
+ checkOffsetAndCount(buffer.length, offset, count);
+
+ while (true) {
+ try {
+ out.write(buffer, offset, count);
+
+ if (replayBuffer != null) {
+ if (count + replayBuffer.size() > maxReplayBufferLength) {
+ // Failure recovery is no longer possible once we overflow the replay buffer.
+ replayBuffer = null;
+ } else {
+ // Remember the written bytes to the replay buffer.
+ replayBuffer.write(buffer, offset, count);
+ }
+ }
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ @Override public final void flush() throws IOException {
+ if (closed) {
+ return; // don't throw; this stream might have been closed on the caller's behalf
+ }
+ while (true) {
+ try {
+ out.flush();
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ @Override public final void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ while (true) {
+ try {
+ out.close();
+ closed = true;
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ /**
+ * Attempt to replace {@code out} with another equivalent stream. Returns true
+ * if a suitable replacement stream was found.
+ */
+ private boolean recover(IOException e) {
+ if (replayBuffer == null) {
+ return false; // Can't recover because we've dropped data that we would need to replay.
+ }
+
+ while (true) {
+ OutputStream replacementStream = null;
+ try {
+ replacementStream = replacementStream(e);
+ if (replacementStream == null) {
+ return false;
+ }
+ replaceStream(replacementStream);
+ return true;
+ } catch (IOException replacementStreamFailure) {
+ // The replacement was also broken. Loop to ask for another replacement.
+ Util.closeQuietly(replacementStream);
+ e = replacementStreamFailure;
+ }
+ }
+ }
+
+ /**
+ * Returns true if errors in the underlying stream can currently be recovered.
+ */
+ public boolean isRecoverable() {
+ return replayBuffer != null;
+ }
+
+ /**
+ * Replaces the current output stream with {@code replacementStream}, writing
+ * any replay bytes to it if they exist. The current output stream is closed.
+ */
+ public final void replaceStream(OutputStream replacementStream) throws IOException {
+ if (!isRecoverable()) {
+ throw new IllegalStateException();
+ }
+ if (this.out == replacementStream) {
+ return; // Don't replace a stream with itself.
+ }
+ replayBuffer.writeTo(replacementStream);
+ Util.closeQuietly(out);
+ out = replacementStream;
+ }
+
+ /**
+ * Returns a replacement output stream to recover from {@code e} thrown by the
+ * previous stream. Returns a new OutputStream if recovery was successful, in
+ * which case all previously-written data will be replayed. Returns null if
+ * the failure cannot be recovered.
+ */
+ protected abstract OutputStream replacementStream(IOException e) throws IOException;
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java b/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
index ce430b2..992b2ae 100644
--- a/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
+++ b/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
@@ -20,10 +20,10 @@
* Runnable implementation which always sets its thread name.
*/
public abstract class NamedRunnable implements Runnable {
- private String name;
+ private final String name;
- public NamedRunnable(String name) {
- this.name = name;
+ public NamedRunnable(String format, Object... args) {
+ this.name = String.format(format, args);
}
@Override public final void run() {
diff --git a/src/main/java/com/squareup/okhttp/internal/Platform.java b/src/main/java/com/squareup/okhttp/internal/Platform.java
index 21761d3..c06f480 100644
--- a/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal;
import com.squareup.okhttp.OkHttpClient;
+import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
@@ -24,6 +25,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
+import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
@@ -123,8 +125,27 @@
}
}
+ /**
+ * Returns the maximum transmission unit of the network interface used by
+ * {@code socket}, or a reasonable default if this platform doesn't expose the
+ * MTU to the application layer.
+ *
+ * <p>The returned value should only be used as an optimization; such as to
+ * size buffers efficiently.
+ */
+ public int getMtu(Socket socket) throws IOException {
+ return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
+ }
+
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
+ Method getMtu;
+ try {
+ getMtu = NetworkInterface.class.getMethod("getMTU");
+ } catch (NoSuchMethodException e) {
+ return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
+ }
+
// Attempt to find Android 2.3+ APIs.
Class<?> openSslSocketClass;
Method setUseSessionTickets;
@@ -145,10 +166,10 @@
try {
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
- return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
- getNpnSelectedProtocol);
+ return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
+ setNpnProtocols, getNpnSelectedProtocol);
} catch (NoSuchMethodException ignored) {
- return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
+ return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
}
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
@@ -165,26 +186,53 @@
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
- return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
+ return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
serverProviderClass);
} catch (ClassNotFoundException ignored) {
- return new Platform(); // NPN isn't on the classpath.
+ // NPN isn't on the classpath.
} catch (NoSuchMethodException ignored) {
- return new Platform(); // The NPN version isn't what we expect.
+ // The NPN version isn't what we expect.
+ }
+
+ return new Java6(getMtu);
+ }
+
+ private static class Java6 extends Platform {
+ private final Method getMtu;
+
+ private Java6(Method getMtu) {
+ this.getMtu = getMtu;
+ }
+
+ @Override public int getMtu(Socket socket) throws IOException {
+ try {
+ NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+ socket.getLocalAddress());
+ if (networkInterface == null) {
+ return super.getMtu(socket); // There's no longer an interface with this local address.
+ }
+ return (Integer) getMtu.invoke(networkInterface);
+ } catch (SocketException e) {
+ // Certain Motorola devices always throw on getByInetAddress. Return the default for those.
+ return super.getMtu(socket);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError(e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
+ throw new RuntimeException(e.getCause());
+ }
}
}
- /**
- * Android version 2.3 and newer support TLS session tickets and server name
- * indication (SNI).
- */
- private static class Android23 extends Platform {
+ /** Android version 2.3 and newer support TLS session tickets and server name indication (SNI). */
+ private static class Android23 extends Java6 {
protected final Class<?> openSslSocketClass;
private final Method setUseSessionTickets;
private final Method setHostname;
- private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
+ private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
Method setHostname) {
+ super(getMtu);
this.openSslSocketClass = openSslSocketClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
@@ -211,9 +259,9 @@
private final Method setNpnProtocols;
private final Method getNpnSelectedProtocol;
- private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
- Method setNpnProtocols, Method getNpnSelectedProtocol) {
- super(openSslSocketClass, setUseSessionTickets, setHostname);
+ private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
+ Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
+ super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
this.setNpnProtocols = setNpnProtocols;
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
}
@@ -245,18 +293,16 @@
}
}
- /**
- * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
- * path.
- */
- private static class JdkWithJettyNpnPlatform extends Platform {
+ /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
+ private static class JdkWithJettyNpnPlatform extends Java6 {
private final Method getMethod;
private final Method putMethod;
private final Class<?> clientProviderClass;
private final Class<?> serverProviderClass;
- public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
- Class<?> serverProviderClass) {
+ public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
+ Class<?> clientProviderClass, Class<?> serverProviderClass) {
+ super(getMtu);
this.putMethod = putMethod;
this.getMethod = getMethod;
this.clientProviderClass = clientProviderClass;
diff --git a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
index 93f1754..3ddc693 100644
--- a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
@@ -21,26 +21,23 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
-import static com.squareup.okhttp.internal.Util.ISO_8859_1;
-import static com.squareup.okhttp.internal.Util.US_ASCII;
-import static com.squareup.okhttp.internal.Util.UTF_8;
-
/**
* Buffers input from an {@link InputStream} for reading lines.
*
- * This class is used for buffered reading of lines. For purposes of this class, a line ends with
+ * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends with
* "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
* end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
* to detect it after catching the {@code EOFException}.
*
- * This class is intended for reading input that strictly consists of lines, such as line-based
- * cache entries or cache journal. Unlike the {@link BufferedReader} which in conjunction with
- * {@link InputStreamReader} provides similar functionality, this class uses different
+ * <p>This class is intended for reading input that strictly consists of lines, such as line-based
+ * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
+ * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
- * This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
+ * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
@@ -52,42 +49,22 @@
private final InputStream in;
private final Charset charset;
- // Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
- // and the data in the range [pos, end) is buffered for reading. At end of input, if there is
- // an unterminated line, we set end == -1, otherwise end == pos. If the underlying
- // {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ /*
+ * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+ * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+ * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+ * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ */
private byte[] buf;
private int pos;
private int end;
/**
- * Constructs a new {@code StrictLineReader} with the default capacity and charset.
- *
- * @param in the {@code InputStream} to read data from.
- * @throws NullPointerException if {@code in} is null.
- */
- public StrictLineReader(InputStream in) {
- this(in, 8192);
- }
-
- /**
- * Constructs a new {@code LineReader} with the specified capacity and the default charset.
- *
- * @param in the {@code InputStream} to read data from.
- * @param capacity the capacity of the buffer.
- * @throws NullPointerException if {@code in} is null.
- * @throws IllegalArgumentException for negative or zero {@code capacity}.
- */
- public StrictLineReader(InputStream in, int capacity) {
- this(in, capacity, US_ASCII);
- }
-
- /**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
- * @param charset the charset used to decode data.
- * Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
@@ -100,11 +77,11 @@
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
- * @param charset the charset used to decode data.
- * Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
- * or the specified charset is not supported.
+ * or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
@@ -113,7 +90,7 @@
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
- if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) {
+ if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
@@ -128,7 +105,6 @@
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
- @Override
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
@@ -162,7 +138,7 @@
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
- String res = new String(buf, pos, lineEnd - pos, charset);
+ String res = new String(buf, pos, lineEnd - pos, charset.name());
pos = i + 1;
return res;
}
@@ -173,7 +149,11 @@
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
- return new String(buf, 0, length, charset);
+ try {
+ return new String(buf, 0, length, charset.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Since we control the charset this will never happen.
+ }
}
};
@@ -215,9 +195,6 @@
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
- *
- * @throws IOException for underlying {@code InputStream} errors.
- * @throws EOFException for the end of source stream.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
diff --git a/src/main/java/com/squareup/okhttp/internal/Util.java b/src/main/java/com/squareup/okhttp/internal/Util.java
index dc914cc..0ce7f8a 100644
--- a/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -29,6 +29,10 @@
import java.net.URL;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
/** Junk drawer of utility methods. */
@@ -149,12 +153,14 @@
throw new AssertionError(thrown);
}
- /** Recursively delete everything in {@code dir}. */
- // TODO: this should specify paths as Strings rather than as Files
+ /**
+ * Deletes the contents of {@code dir}. Throws an IOException if any file
+ * could not be deleted, or if {@code dir} is not a readable directory.
+ */
public static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
- throw new IllegalArgumentException("not a directory: " + dir);
+ throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
@@ -322,4 +328,19 @@
}
return result.toString();
}
+
+ /** Returns an immutable copy of {@code list}. */
+ public static <T> List<T> immutableList(List<T> list) {
+ return Collections.unmodifiableList(new ArrayList<T>(list));
+ }
+
+ public static ThreadFactory daemonThreadFactory(final String name) {
+ return new ThreadFactory() {
+ @Override public Thread newThread(Runnable runnable) {
+ Thread result = new Thread(runnable, name);
+ result.setDaemon(true);
+ return result;
+ }
+ };
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
index 4ccd12a..566431e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
@@ -16,7 +16,8 @@
*/
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.OkAuthenticator.Challenge;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetAddress;
@@ -27,11 +28,49 @@
import java.util.ArrayList;
import java.util.List;
+import static com.squareup.okhttp.OkAuthenticator.Credential;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
/** Handles HTTP authentication headers from origin and proxy servers. */
public final class HttpAuthenticator {
+ /** Uses the global authenticator to get the password. */
+ public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
+ @Override public Credential authenticate(
+ Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
+ for (Challenge challenge : challenges) {
+ PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(url.getHost(),
+ getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
+ challenge.getRealm(), challenge.getScheme(), url, Authenticator.RequestorType.SERVER);
+ if (auth != null) {
+ return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
+ }
+ }
+ return null;
+ }
+
+ @Override public Credential authenticateProxy(
+ Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
+ for (Challenge challenge : challenges) {
+ InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+ PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
+ proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
+ url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
+ Authenticator.RequestorType.PROXY);
+ if (auth != null) {
+ return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
+ }
+ }
+ return null;
+ }
+
+ private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+ return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
+ ? ((InetSocketAddress) proxy.address()).getAddress()
+ : InetAddress.getByName(url.getHost());
+ }
+ };
+
private HttpAuthenticator() {
}
@@ -41,68 +80,33 @@
* @return true if credentials have been added to successorRequestHeaders
* and another request should be attempted.
*/
- public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
- RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
- if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
- throw new IllegalArgumentException();
+ public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
+ RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
+ throws IOException {
+ String responseField;
+ String requestField;
+ if (responseCode == HTTP_UNAUTHORIZED) {
+ responseField = "WWW-Authenticate";
+ requestField = "Authorization";
+ } else if (responseCode == HTTP_PROXY_AUTH) {
+ responseField = "Proxy-Authenticate";
+ requestField = "Proxy-Authorization";
+ } else {
+ throw new IllegalArgumentException(); // TODO: ProtocolException?
}
-
- // Keep asking for username/password until authorized.
- String challengeHeader =
- responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
- String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
- if (credentials == null) {
- return false; // Could not find credentials so end the request cycle.
- }
-
- // Add authorization credentials, bypassing the already-connected check.
- String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
- successorRequestHeaders.set(fieldName, credentials);
- return true;
- }
-
- /**
- * Returns the authorization credentials that may satisfy the challenge.
- * Returns null if a challenge header was not provided or if credentials
- * were not available.
- */
- private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
- Proxy proxy, URL url) throws IOException {
- List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
+ List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
if (challenges.isEmpty()) {
- return null;
+ return false; // Could not find a challenge so end the request cycle.
}
-
- for (Challenge challenge : challenges) {
- // Use the global authenticator to get the password.
- PasswordAuthentication auth;
- if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
- InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
- auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
- getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
- challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
- } else {
- auth = Authenticator.requestPasswordAuthentication(url.getHost(),
- getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
- challenge.scheme, url, Authenticator.RequestorType.SERVER);
- }
- if (auth == null) {
- continue;
- }
-
- // Use base64 to encode the username and password.
- String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
- byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
- String encoded = Base64.encode(bytes);
- return challenge.scheme + " " + encoded;
+ Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
+ ? authenticator.authenticateProxy(proxy, url, challenges)
+ : authenticator.authenticate(proxy, url, challenges);
+ if (credential == null) {
+ return false; // Could not satisfy the challenge so end the request cycle.
}
-
- return null;
- }
-
- private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
- return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
- ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
+ // Add authorization credentials, bypassing the already-connected check.
+ successorRequestHeaders.set(requestField, credential.getHeaderValue());
+ return true;
}
/**
@@ -151,25 +155,4 @@
}
return result;
}
-
- /** An RFC 2617 challenge. */
- private static final class Challenge {
- final String scheme;
- final String realm;
-
- Challenge(String scheme, String realm) {
- this.scheme = scheme;
- this.realm = realm;
- }
-
- @Override public boolean equals(Object o) {
- return o instanceof Challenge
- && ((Challenge) o).scheme.equals(scheme)
- && ((Challenge) o).realm.equals(realm);
- }
-
- @Override public int hashCode() {
- return scheme.hashCode() + 31 * realm.hashCode();
- }
- }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 8816bc4..22d4395 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -19,7 +19,6 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns;
@@ -154,7 +153,7 @@
try {
uri = Platform.get().toUriLenient(policy.getURL());
} catch (URISyntaxException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
@@ -176,8 +175,8 @@
prepareRawRequestHeaders();
initResponseSource();
- if (policy.responseCache instanceof OkResponseCache) {
- ((OkResponseCache) policy.responseCache).trackResponse(responseSource);
+ if (policy.responseCache != null) {
+ policy.responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
@@ -198,6 +197,7 @@
sendSocketRequest();
} else if (connection != null) {
policy.connectionPool.recycle(connection);
+ policy.getFailedRoutes().remove(connection.getRoute());
connection = null;
}
}
@@ -278,17 +278,18 @@
hostnameVerifier = policy.hostnameVerifier;
}
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
- hostnameVerifier, policy.requestedProxy);
- routeSelector =
- new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
+ hostnameVerifier, policy.authenticator, policy.requestedProxy, policy.getTransports());
+ routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
+ Dns.DEFAULT, policy.getFailedRoutes());
}
connection = routeSelector.next();
if (!connection.isConnected()) {
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
policy.connectionPool.maybeShare(connection);
+ policy.getFailedRoutes().remove(connection.getRoute());
}
connected(connection);
- if (connection.getProxy() != policy.requestedProxy) {
+ if (connection.getRoute().getProxy() != policy.requestedProxy) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
}
@@ -574,7 +575,7 @@
protected boolean includeAuthorityInRequestLine() {
return connection == null
? policy.usingProxy() // A proxy was requested.
- : connection.getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
+ : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
}
public static String getDefaultUserAgent() {
@@ -597,6 +598,7 @@
*/
public final void readResponse() throws IOException {
if (hasResponse()) {
+ responseHeaders.setResponseSource(responseSource);
return;
}
@@ -627,17 +629,15 @@
responseHeaders = transport.readResponseHeaders();
responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
+ responseHeaders.setResponseSource(responseSource);
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
if (cachedResponseHeaders.validate(responseHeaders)) {
release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
setResponse(combinedHeaders, cachedResponseBody);
- if (policy.responseCache instanceof OkResponseCache) {
- OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache;
- httpResponseCache.trackConditionalCacheHit();
- httpResponseCache.update(cacheResponse, policy.getHttpConnectionToCache());
- }
+ policy.responseCache.trackConditionalCacheHit();
+ policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
return;
} else {
Util.closeQuietly(cachedResponseBody);
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index dd7a38d..f04b317 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -17,8 +17,8 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.internal.AbstractOutputStream;
import com.squareup.okhttp.internal.Util;
-import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -31,14 +31,6 @@
public final class HttpTransport implements Transport {
/**
- * The maximum number of bytes to buffer when sending headers and a request
- * body. When the headers and body can be sent in a single write, the
- * request completes sooner. In one WiFi benchmark, using a large enough
- * buffer sped up some uploads by half.
- */
- private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
-
- /**
* The timeout to use while discarding a stream of input data. Since this is
* used for connection reuse, this timeout should be significantly less than
* the time it takes to establish a new connection.
@@ -129,22 +121,19 @@
*/
public void writeRequestHeaders() throws IOException {
httpEngine.writingRequestHeaders();
- int contentLength = httpEngine.requestHeaders.getContentLength();
RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
byte[] bytes = headersToSend.toBytes();
-
- if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
- requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
- }
-
requestOut.write(bytes);
}
@Override public ResponseHeaders readResponseHeaders() throws IOException {
- RawHeaders headers = RawHeaders.fromBytes(socketIn);
- httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
- httpEngine.receiveHeaders(headers);
- return new ResponseHeaders(httpEngine.uri, headers);
+ RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
+ httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
+ httpEngine.receiveHeaders(rawHeaders);
+
+ ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
+ headers.setTransport("http/1.1");
+ return headers;
}
public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
@@ -154,7 +143,7 @@
}
// We cannot reuse sockets that have incomplete output.
- if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+ if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
return false;
}
@@ -224,7 +213,7 @@
}
/** An HTTP body with a fixed length known in advance. */
- private static final class FixedLengthOutputStream extends AbstractHttpOutputStream {
+ private static final class FixedLengthOutputStream extends AbstractOutputStream {
private final OutputStream socketOut;
private int bytesRemaining;
@@ -266,7 +255,7 @@
* buffered until {@code maxChunkLength} bytes are ready, at which point the
* chunk is written and the buffer is cleared.
*/
- private static final class ChunkedOutputStream extends AbstractHttpOutputStream {
+ private static final class ChunkedOutputStream extends AbstractOutputStream {
private static final byte[] CRLF = { '\r', '\n' };
private static final byte[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index ff0a2c4..c7d75a9 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -19,7 +19,11 @@
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkAuthenticator;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.AbstractOutputStream;
+import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
import com.squareup.okhttp.internal.Util;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -32,13 +36,14 @@
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.ProxySelector;
-import java.net.ResponseCache;
import java.net.SocketPermission;
import java.net.URL;
import java.security.Permission;
import java.security.cert.CertificateException;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
@@ -60,12 +65,23 @@
* is currently connected to a server.
*/
public class HttpURLConnectionImpl extends HttpURLConnection {
+
+ /** Numeric status code, 307: Temporary Redirect. */
+ static final int HTTP_TEMP_REDIRECT = 307;
+
/**
* How many redirects should we follow? Chrome follows 21; Firefox, curl,
* and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_REDIRECTS = 20;
+ /**
+ * The minimum number of request body bytes to transmit before we're willing
+ * to let a routine {@link IOException} bubble up to the user. This is used to
+ * size a buffer for data that will be replayed upon error.
+ */
+ private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
+
private final boolean followProtocolRedirects;
/** The proxy requested by the client, or null for a proxy to be selected automatically. */
@@ -73,29 +89,45 @@
final ProxySelector proxySelector;
final CookieHandler cookieHandler;
- final ResponseCache responseCache;
+ final OkResponseCache responseCache;
final ConnectionPool connectionPool;
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
SSLSocketFactory sslSocketFactory;
HostnameVerifier hostnameVerifier;
+ private List<String> transports;
+ OkAuthenticator authenticator;
+ final Set<Route> failedRoutes;
private final RawHeaders rawRequestHeaders = new RawHeaders();
private int redirectionCount;
+ private FaultRecoveringOutputStream faultRecoveringRequestBody;
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
- public HttpURLConnectionImpl(URL url, OkHttpClient client) {
+ public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set<Route> failedRoutes) {
super(url);
this.followProtocolRedirects = client.getFollowProtocolRedirects();
+ this.failedRoutes = failedRoutes;
this.requestedProxy = client.getProxy();
this.proxySelector = client.getProxySelector();
this.cookieHandler = client.getCookieHandler();
- this.responseCache = client.getResponseCache();
this.connectionPool = client.getConnectionPool();
this.sslSocketFactory = client.getSslSocketFactory();
this.hostnameVerifier = client.getHostnameVerifier();
+ this.transports = client.getTransports();
+ this.authenticator = client.getAuthenticator();
+ this.responseCache = responseCache;
+ }
+
+ Set<Route> getFailedRoutes() {
+ return failedRoutes;
+ }
+
+ List<String> getTransports() {
+ return transports;
}
@Override public final void connect() throws IOException {
@@ -212,14 +244,29 @@
@Override public final OutputStream getOutputStream() throws IOException {
connect();
- OutputStream result = httpEngine.getRequestBody();
- if (result == null) {
+ OutputStream out = httpEngine.getRequestBody();
+ if (out == null) {
throw new ProtocolException("method does not support a request body: " + method);
} else if (httpEngine.hasResponse()) {
throw new ProtocolException("cannot write request body after response has been read");
}
- return result;
+ if (faultRecoveringRequestBody == null) {
+ faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
+ @Override protected OutputStream replacementStream(IOException e) throws IOException {
+ if (httpEngine.getRequestBody() instanceof AbstractOutputStream
+ && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
+ return null; // Don't recover once the underlying stream has been closed.
+ }
+ if (handleFailure(e)) {
+ return httpEngine.getRequestBody();
+ }
+ return null; // This is a permanent failure.
+ }
+ };
+ }
+
+ return faultRecoveringRequestBody;
}
@Override public final Permission getPermission() throws IOException {
@@ -349,29 +396,50 @@
}
return true;
} catch (IOException e) {
- RouteSelector routeSelector = httpEngine.routeSelector;
- if (routeSelector != null && httpEngine.connection != null) {
- routeSelector.connectFailed(httpEngine.connection, e);
- }
- if (routeSelector == null && httpEngine.connection == null) {
- throw e; // If we failed before finding a route or a connection, give up.
- }
-
- // The connection failure isn't fatal if there's another route to attempt.
- OutputStream requestBody = httpEngine.getRequestBody();
- if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody
- == null || requestBody instanceof RetryableOutputStream)) {
- httpEngine.release(true);
- httpEngine =
- newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody);
- httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+ if (handleFailure(e)) {
return false;
+ } else {
+ throw e;
}
- httpEngineFailure = e;
- throw e;
}
}
+ /**
+ * Report and attempt to recover from {@code e}. Returns true if the HTTP
+ * engine was replaced and the request should be retried. Otherwise the
+ * failure is permanent.
+ */
+ private boolean handleFailure(IOException e) throws IOException {
+ RouteSelector routeSelector = httpEngine.routeSelector;
+ if (routeSelector != null && httpEngine.connection != null) {
+ routeSelector.connectFailed(httpEngine.connection, e);
+ }
+
+ OutputStream requestBody = httpEngine.getRequestBody();
+ boolean canRetryRequestBody = requestBody == null
+ || requestBody instanceof RetryableOutputStream
+ || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
+ if (routeSelector == null && httpEngine.connection == null // No connection.
+ || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
+ || !isRecoverable(e)
+ || !canRetryRequestBody) {
+ httpEngineFailure = e;
+ return false;
+ }
+
+ httpEngine.release(true);
+ RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
+ ? (RetryableOutputStream) requestBody
+ : null;
+ httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
+ httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+ if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
+ httpEngine.sendRequest();
+ faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
+ }
+ return true;
+ }
+
private boolean isRecoverable(IOException e) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry, we didn't have an abrupt server initiated exception.
@@ -381,7 +449,7 @@
return !sslFailure && !protocolFailure;
}
- HttpEngine getHttpEngine() {
+ public HttpEngine getHttpEngine() {
return httpEngine;
}
@@ -398,29 +466,37 @@
*/
private Retry processResponseHeaders() throws IOException {
Proxy selectedProxy = httpEngine.connection != null
- ? httpEngine.connection.getProxy()
+ ? httpEngine.connection.getRoute().getProxy()
: requestedProxy;
- switch (getResponseCode()) {
+ final int responseCode = getResponseCode();
+ switch (responseCode) {
case HTTP_PROXY_AUTH:
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
// fall-through
case HTTP_UNAUTHORIZED:
- boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
- httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
+ boolean credentialsFound = HttpAuthenticator.processAuthHeader(authenticator,
+ getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
+ selectedProxy, url);
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
+ case HTTP_TEMP_REDIRECT:
if (!getInstanceFollowRedirects()) {
return Retry.NONE;
}
if (++redirectionCount > MAX_REDIRECTS) {
throw new ProtocolException("Too many redirects: " + redirectionCount);
}
+ if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
+ // "If the 307 status code is received in response to a request other than GET or HEAD,
+ // the user agent MUST NOT automatically redirect the request"
+ return Retry.NONE;
+ }
String location = getHeaderField("Location");
if (location == null) {
return Retry.NONE;
@@ -476,7 +552,11 @@
if (field == null) {
throw new NullPointerException("field == null");
}
- rawRequestHeaders.set(field, newValue);
+ if ("X-Android-Transports".equals(field)) {
+ setTransports(newValue, false /* append */);
+ } else {
+ rawRequestHeaders.set(field, newValue);
+ }
}
@Override public final void addRequestProperty(String field, String value) {
@@ -486,6 +566,54 @@
if (field == null) {
throw new NullPointerException("field == null");
}
- rawRequestHeaders.add(field, value);
+
+ if ("X-Android-Transports".equals(field)) {
+ setTransports(value, true /* append */);
+ } else {
+ rawRequestHeaders.add(field, value);
+ }
+ }
+
+ /*
+ * Splits and validates a comma-separated string of transports.
+ * When append == false, we require that the transport list contains "http/1.1".
+ */
+ private void setTransports(String transportsString, boolean append) {
+ if (transportsString == null) {
+ throw new NullPointerException("transportsString == null");
+ }
+
+ String[] transports = transportsString.split(",", -1);
+ ArrayList<String> transportsList = new ArrayList<String>();
+ if (!append) {
+ // If we're not appending to the list, we need to make sure
+ // the list contains "http/1.1". We do this in a separate loop
+ // to avoid modifying any state before we validate the input.
+ boolean containsHttp = false;
+ for (int i = 0; i < transports.length; ++i) {
+ if ("http/1.1".equals(transports[i])) {
+ containsHttp = true;
+ break;
+ }
+ }
+
+ if (!containsHttp) {
+ throw new IllegalArgumentException("Transport list doesn't contain http/1.1");
+ }
+ } else {
+ transportsList.addAll(this.transports);
+ }
+
+ for (int i = 0; i < transports.length; ++i) {
+ if (transports[i].length() == 0) {
+ throw new IllegalArgumentException("Transport list contains an empty transport");
+ }
+
+ if (!transportsList.contains(transports[i])) {
+ transportsList.add(transports[i]);
+ }
+ }
+
+ this.transports = Util.immutableList(transportsList);
}
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index c224270..235f862 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -18,6 +18,7 @@
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Route;
import com.squareup.okhttp.TunnelRequest;
import java.io.IOException;
import java.io.InputStream;
@@ -32,6 +33,7 @@
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -45,9 +47,10 @@
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
private final HttpUrlConnectionDelegate delegate;
- public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
+ public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set<Route> failedRoutes) {
super(url);
- delegate = new HttpUrlConnectionDelegate(url, client);
+ delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
}
@Override public String getCipherSuite() {
@@ -112,7 +115,7 @@
return null;
}
- HttpEngine getHttpEngine() {
+ public HttpEngine getHttpEngine() {
return delegate.getHttpEngine();
}
@@ -399,8 +402,9 @@
}
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
- private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
- super(url, client);
+ private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set<Route> failedRoutes) {
+ super(url, client, responseCache, failedRoutes);
}
@Override protected HttpURLConnection getHttpConnectionToCache() {
@@ -425,8 +429,7 @@
* @param policy the HttpURLConnectionImpl with connection configuration
*/
public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
- Connection connection, RetryableOutputStream requestBody)
- throws IOException {
+ Connection connection, RetryableOutputStream requestBody) throws IOException {
super(policy, method, requestHeaders, connection, requestBody);
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java
new file mode 100644
index 0000000..5829f02
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An extended response cache API. Unlike {@link java.net.ResponseCache}, this
+ * interface supports conditional caching and statistics.
+ *
+ * <p>Along with the rest of the {@code internal} package, this is not a public
+ * API. Applications wishing to supply their own caches must use the more
+ * limited {@link java.net.ResponseCache} interface.
+ */
+public interface OkResponseCache {
+ CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
+ throws IOException;
+
+ CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
+
+ /**
+ * Handles a conditional request hit by updating the stored cache response
+ * with the headers from {@code httpConnection}. The cached response body is
+ * not updated. If the stored response has changed since {@code
+ * conditionalCacheHit} was returned, this does nothing.
+ */
+ void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
+
+ /** Track an conditional GET that was satisfied by this cache. */
+ void trackConditionalCacheHit();
+
+ /** Track an HTTP response being satisfied by {@code source}. */
+ void trackResponse(ResponseSource source);
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
new file mode 100644
index 0000000..2ac915a
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+public final class OkResponseCacheAdapter implements OkResponseCache {
+ private final ResponseCache responseCache;
+ public OkResponseCacheAdapter(ResponseCache responseCache) {
+ this.responseCache = responseCache;
+ }
+
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ return responseCache.get(uri, requestMethod, requestHeaders);
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+ return responseCache.put(uri, urlConnection);
+ }
+
+ @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
+ throws IOException {
+ }
+
+ @Override public void trackConditionalCacheHit() {
+ }
+
+ @Override public void trackResponse(ResponseSource source) {
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
index ba83796..eba887e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -17,7 +17,6 @@
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.io.InputStream;
@@ -186,25 +185,27 @@
public void addLine(String line) {
int index = line.indexOf(":");
if (index == -1) {
- add("", line);
+ addLenient("", line);
} else {
- add(line.substring(0, index), line.substring(index + 1));
+ addLenient(line.substring(0, index), line.substring(index + 1));
}
}
/** Add a field with the specified value. */
public void add(String fieldName, String value) {
- if (fieldName == null) {
- throw new IllegalArgumentException("fieldName == null");
+ if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
+ if (value == null) throw new IllegalArgumentException("value == null");
+ if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+ throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
}
- if (value == null) {
- // Given null values, the RI sends a malformed field line like
- // "Accept\r\n". For platform compatibility and HTTP compliance, we
- // print a warning and ignore null values.
- Platform.get()
- .logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
- return;
- }
+ addLenient(fieldName, value);
+ }
+
+ /**
+ * Add a field with the specified value without any validation. Only
+ * appropriate for headers from the remote peer.
+ */
+ private void addLenient(String fieldName, String value) {
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
}
@@ -351,7 +352,9 @@
String fieldName = entry.getKey();
List<String> values = entry.getValue();
if (fieldName != null) {
- result.addAll(fieldName, values);
+ for (String value : values) {
+ result.addLenient(fieldName, value);
+ }
} else if (!values.isEmpty()) {
result.setStatusLine(values.get(values.size() - 1));
}
@@ -371,13 +374,6 @@
String name = namesAndValues.get(i).toLowerCase(Locale.US);
String value = namesAndValues.get(i + 1);
- // TODO: promote this check to where names and values are created
- if (name.length() == 0
- || name.indexOf('\0') != -1
- || value.indexOf('\0') != -1) {
- throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
- }
-
// Drop headers that are forbidden when layering HTTP over SPDY.
if (name.equals("connection")
|| name.equals("host")
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
index 2544cee..5ec4fcc 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -22,7 +22,7 @@
import java.util.Map;
/** Parsed HTTP request headers. */
-final class RequestHeaders {
+public final class RequestHeaders {
private final URI uri;
private final RawHeaders headers;
diff --git a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
index 4e520db..97925c2 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -31,7 +31,7 @@
import static com.squareup.okhttp.internal.Util.equal;
/** Parsed HTTP response headers. */
-final class ResponseHeaders {
+public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
@@ -39,6 +39,12 @@
/** HTTP header name for the local time when the response was received. */
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+ /** HTTP synthetic header with the response source. */
+ static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+
+ /** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc.) */
+ static final String SELECTED_TRANSPORT = "X-Android-Selected-Transport";
+
private final URI uri;
private final RawHeaders headers;
@@ -271,6 +277,14 @@
headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
}
+ public void setResponseSource(ResponseSource responseSource) {
+ headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
+ }
+
+ public void setTransport(String transport) {
+ headers.set(SELECTED_TRANSPORT, transport);
+ }
+
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.
@@ -403,7 +417,8 @@
if (ageMillis + minFreshMillis >= freshMillis) {
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
}
- if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
+ long oneDayMillis = 24 * 60 * 60 * 1000L;
+ if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return ResponseSource.CACHE;
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
index 325327d..5eb6b76 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
@@ -16,6 +16,7 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.internal.AbstractOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -28,7 +29,7 @@
* the post body to be transparently re-sent if the HTTP request must be
* sent multiple times.
*/
-final class RetryableOutputStream extends AbstractHttpOutputStream {
+final class RetryableOutputStream extends AbstractOutputStream {
private final int limit;
private final ByteArrayOutputStream content;
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index 798cff3..ce0a71d 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -18,6 +18,7 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
@@ -28,8 +29,11 @@
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@@ -51,6 +55,7 @@
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
+ private final Set<Route> failedRoutes;
/* The most recently attempted route. */
private Proxy lastProxy;
@@ -64,19 +69,23 @@
/* State for negotiating the next InetSocketAddress to use. */
private InetAddress[] socketAddresses;
private int nextSocketAddressIndex;
- private String socketHost;
private int socketPort;
/* State for negotiating the next TLS configuration */
private int nextTlsMode = TLS_MODE_NULL;
+ /* State for negotiating failed routes */
+ private final List<Route> postponedRoutes;
+
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
- Dns dns) {
+ Dns dns, Set<Route> failedRoutes) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
+ this.failedRoutes = failedRoutes;
+ this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy());
}
@@ -86,7 +95,7 @@
* least one route.
*/
public boolean hasNext() {
- return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
+ return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
}
/**
@@ -105,7 +114,10 @@
if (!hasNextTlsMode()) {
if (!hasNextInetSocketAddress()) {
if (!hasNextProxy()) {
- throw new NoSuchElementException();
+ if (!hasNextPostponed()) {
+ throw new NoSuchElementException();
+ }
+ return new Connection(nextPostponed());
}
lastProxy = nextProxy();
resetNextInetSocketAddress(lastProxy);
@@ -113,9 +125,17 @@
lastInetSocketAddress = nextInetSocketAddress();
resetNextTlsMode();
}
- boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
- return new Connection(address, lastProxy, lastInetSocketAddress, modernTls);
+ boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
+ Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
+ if (failedRoutes.contains(route)) {
+ postponedRoutes.add(route);
+ // We will only recurse in order to skip previously failed routes. They will be
+ // tried last.
+ return next();
+ }
+
+ return new Connection(route);
}
/**
@@ -123,9 +143,17 @@
* failure on a connection returned by this route selector.
*/
public void connectFailed(Connection connection, IOException failure) {
- if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
+ Route failedRoute = connection.getRoute();
+ if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
- proxySelector.connectFailed(uri, connection.getProxy().address(), failure);
+ proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
+ }
+
+ failedRoutes.add(failedRoute);
+ if (!(failure instanceof SSLHandshakeException)) {
+ // If the problem was not related to SSL then it will also fail with
+ // a different Tls mode therefore we can be proactive about it.
+ failedRoutes.add(failedRoute.flipTlsMode());
}
}
@@ -175,6 +203,7 @@
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
+ String socketHost;
if (proxy.type() == Proxy.Type.DIRECT) {
socketHost = uri.getHost();
socketPort = getEffectivePort(uri);
@@ -233,4 +262,14 @@
throw new AssertionError();
}
}
+
+ /** Returns true if there is another postponed route to try. */
+ private boolean hasNextPostponed() {
+ return !postponedRoutes.isEmpty();
+ }
+
+ /** Returns the next postponed route to try. */
+ private Route nextPostponed() {
+ return postponedRoutes.remove(0);
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index 18ab566..73709b5 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -71,7 +71,10 @@
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
rawHeaders.computeResponseStatusLineFromSpdyHeaders();
httpEngine.receiveHeaders(rawHeaders);
- return new ResponseHeaders(httpEngine.uri, rawHeaders);
+
+ ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
+ headers.setTransport("spdy/3");
+ return headers;
}
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index b3e248c..299da8e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -32,8 +32,6 @@
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-import static java.util.concurrent.Executors.defaultThreadFactory;
-
/**
* A socket connection to a remote peer. A connection hosts streams which can
* send and receive data.
@@ -77,9 +75,9 @@
static final int GOAWAY_PROTOCOL_ERROR = 1;
static final int GOAWAY_INTERNAL_ERROR = 2;
- private static final ExecutorService executor =
- new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>(), defaultThreadFactory());
+ private static final ExecutorService executor = new ThreadPoolExecutor(0,
+ Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
+ Util.daemonThreadFactory("OkHttp SpdyConnection"));
/** True if this peer initiated the connection. */
final boolean client;
@@ -139,17 +137,17 @@
return stream;
}
- private void setIdle(boolean value) {
+ private synchronized void setIdle(boolean value) {
idleStartTimeNs = value ? System.nanoTime() : 0L;
}
/** Returns true if this connection is idle. */
- public boolean isIdle() {
+ public synchronized boolean isIdle() {
return idleStartTimeNs != 0L;
}
/** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
- public long getIdleStartTimeNs() {
+ public synchronized long getIdleStartTimeNs() {
return idleStartTimeNs;
}
@@ -202,15 +200,14 @@
}
void writeSynResetLater(final int streamId, final int statusCode) {
- executor.submit(
- new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
- @Override public void execute() {
- try {
- writeSynReset(streamId, statusCode);
- } catch (IOException ignored) {
- }
- }
- });
+ executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
+ @Override public void execute() {
+ try {
+ writeSynReset(streamId, statusCode);
+ } catch (IOException ignored) {
+ }
+ }
+ });
}
void writeSynReset(int streamId, int statusCode) throws IOException {
@@ -218,15 +215,14 @@
}
void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
- executor.submit(
- new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
- @Override public void execute() {
- try {
- writeWindowUpdate(streamId, deltaWindowSize);
- } catch (IOException ignored) {
- }
- }
- });
+ executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
+ @Override public void execute() {
+ try {
+ writeWindowUpdate(streamId, deltaWindowSize);
+ } catch (IOException ignored) {
+ }
+ }
+ });
}
void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
@@ -254,7 +250,7 @@
}
private void writePingLater(final int streamId, final Ping ping) {
- executor.submit(new NamedRunnable(String.format("Spdy Writer %s ping %d", hostName, streamId)) {
+ executor.submit(new NamedRunnable("OkHttp SPDY Writer %s ping %d", hostName, streamId) {
@Override public void execute() {
try {
writePing(streamId, ping);
@@ -471,8 +467,7 @@
return;
}
- executor.submit(
- new NamedRunnable(String.format("Callback %s stream %d", hostName, streamId)) {
+ executor.submit(new NamedRunnable("OkHttp SPDY Callback %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
handler.receive(synStream);
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
index db3b50c..7d3f2bd 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -21,6 +21,7 @@
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
@@ -31,39 +32,46 @@
/** Read spdy/3 frames. */
final class SpdyReader implements Closeable {
- static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
- + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
- + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
- + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
- + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
- + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
- + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
- + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
- + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
- + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
- + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
- + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
- + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
- + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
- + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
- + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
- + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
- + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
- + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
- + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
- + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
- + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
- + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
- + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
- + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
- + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
- + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
- + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
- + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
- + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
- + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
- + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
- + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8);
+ static final byte[] DICTIONARY;
+ static {
+ try {
+ DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+ + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+ + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
private final DataInputStream in;
private final DataInputStream nameValueBlockIn;
@@ -232,6 +240,10 @@
this.compressedLimit += length;
try {
int numberOfPairs = nameValueBlockIn.readInt();
+ if (numberOfPairs < 0) {
+ Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs);
+ throw ioException("numberOfPairs < 0");
+ }
List<String> entries = new ArrayList<String>(numberOfPairs * 2);
for (int i = 0; i < numberOfPairs; i++) {
String name = readString();
@@ -248,7 +260,7 @@
return entries;
} catch (DataFormatException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
}
diff --git a/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java b/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java
new file mode 100644
index 0000000..e0aef14
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java
@@ -0,0 +1,407 @@
+/*
+ * 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.squareup.okhttp.internal.tls;
+
+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.
+ */
+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;
+
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
new file mode 100644
index 0000000..fbe8c68
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
@@ -0,0 +1,189 @@
+/*
+ * 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.squareup.okhttp.internal.tls;
+
+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 java.util.regex.Pattern;
+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 <a
+ * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
+ */
+public final class OkHostnameVerifier implements HostnameVerifier {
+ /**
+ * Quick and dirty pattern to differentiate IP addresses from hostnames. This
+ * is an approximation of Android's private InetAddress#isNumeric API.
+ *
+ * <p>This matches IPv6 addresses as a hex string containing at least one
+ * colon, and possibly including dots after the first colon. It matches IPv4
+ * addresses as strings containing only decimal digits and dots. This pattern
+ * matches strings like "a:.23" and "54" that are neither IP addresses nor
+ * hostnames; they will be verified as IP addresses (which is a more strict
+ * verification).
+ */
+ private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
+ "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
+
+ private static final int ALT_DNS_NAME = 2;
+ private static final int ALT_IPA_NAME = 7;
+
+ 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);
+ }
+
+ static boolean verifyAsIpAddress(String host) {
+ return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
+ }
+
+ /**
+ * Returns true if {@code certificate} matches {@code ipAddress}.
+ */
+ private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
+ for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
+ if (ipAddress.equalsIgnoreCase(altName)) {
+ 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;
+ for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
+ hasDns = true;
+ if (verifyHostName(hostName, altName)) {
+ 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;
+ }
+
+ private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
+ List<String> result = new ArrayList<String>();
+ 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 true if {@code hostName} matches the name or pattern {@code cn}.
+ *
+ * @param hostName lowercase host name.
+ * @param cn certificate host name. May include wildcards like
+ * {@code *.android.com}.
+ */
+ public boolean verifyHostName(String hostName, String cn) {
+ // Check length == 0 instead of .isEmpty() to support Java 5.
+ if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) {
+ return false;
+ }
+
+ cn = cn.toLowerCase(Locale.US);
+
+ if (!cn.contains("*")) {
+ return hostName.equals(cn);
+ }
+
+ if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
+ return true; // "*.foo.com" matches "foo.com"
+ }
+
+ int asterisk = cn.indexOf('*');
+ int dot = cn.indexOf('.');
+ if (asterisk > dot) {
+ return false; // malformed; wildcard must be in the first part of the cn
+ }
+
+ if (!hostName.regionMatches(0, cn, 0, asterisk)) {
+ return false; // prefix before '*' doesn't match
+ }
+
+ int suffixLength = cn.length() - (asterisk + 1);
+ int suffixStart = hostName.length() - suffixLength;
+ if (hostName.indexOf('.', asterisk) < suffixStart) {
+ // TODO: remove workaround for *.clients.google.com http://b/5426333
+ if (!hostName.endsWith(".clients.google.com")) {
+ return false; // wildcard '*' can't match a '.'
+ }
+ }
+
+ if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
+ return false; // suffix after '*' doesn't match
+ }
+
+ return true;
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index dca9625..a3a9ea8 100644
--- a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -19,6 +19,7 @@
import com.squareup.okhttp.internal.RecordingHostnameVerifier;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
import java.io.IOException;
import java.net.InetAddress;
@@ -70,30 +71,33 @@
@Before public void setUp() throws Exception {
httpServer.play();
- httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null, null);
+ httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null,
+ HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
httpServer.getPort());
spdyServer.play();
- spdyAddress =
- new Address(spdyServer.getHostName(), spdyServer.getPort(), sslContext.getSocketFactory(),
- new RecordingHostnameVerifier(), null);
+ spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(),
+ sslContext.getSocketFactory(), new RecordingHostnameVerifier(),
+ HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
spdyServer.getPort());
- httpA = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ httpA = new Connection(httpRoute);
httpA.connect(100, 100, null);
- httpB = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpB = new Connection(httpRoute);
httpB.connect(100, 100, null);
- httpC = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpC = new Connection(httpRoute);
httpC.connect(100, 100, null);
- httpD = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpD = new Connection(httpRoute);
httpD.connect(100, 100, null);
- httpE = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpE = new Connection(httpRoute);
httpE.connect(100, 100, null);
- spdyA = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ spdyA = new Connection(spdyRoute);
spdyA.connect(100, 100, null);
- spdyB = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ spdyB = new Connection(spdyRoute);
spdyB.connect(100, 100, null);
}
@@ -115,7 +119,7 @@
Connection connection = pool.get(httpAddress);
assertNull(connection);
- connection = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ connection = new Connection(new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
connection.connect(100, 100, null);
assertEquals(0, pool.getConnectionCount());
pool.recycle(connection);
@@ -385,6 +389,18 @@
assertEquals(0, pool.getSpdyConnectionCount());
}
+ @Test public void evictAllConnections() {
+ ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ Util.closeQuietly(httpA); // Include a closed connection in the pool.
+ pool.recycle(httpB);
+ pool.maybeShare(spdyA);
+ assertEquals(3, pool.getConnectionCount());
+
+ pool.evictAll();
+ assertEquals(0, pool.getConnectionCount());
+ }
+
private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
assertEquals(Arrays.asList(connections), pool.getConnections());
}
diff --git a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
deleted file mode 100644
index 72cc70d..0000000
--- a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
+++ /dev/null
@@ -1,802 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed 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.squareup.okhttp.internal;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.InputStream;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
-import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
-import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class DiskLruCacheTest {
- private final int appVersion = 100;
- private String javaTmpDir;
- private File cacheDir;
- private File journalFile;
- private DiskLruCache cache;
-
- @Before public void setUp() throws Exception {
- javaTmpDir = System.getProperty("java.io.tmpdir");
- cacheDir = new File(javaTmpDir, "DiskLruCacheTest");
- cacheDir.mkdir();
- journalFile = new File(cacheDir, JOURNAL_FILE);
- for (File file : cacheDir.listFiles()) {
- file.delete();
- }
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- }
-
- @After public void tearDown() throws Exception {
- cache.close();
- }
-
- @Test public void emptyCache() throws Exception {
- cache.close();
- assertJournalEquals();
- }
-
- @Test public void writeAndReadEntry() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(0, "ABC");
- creator.set(1, "DE");
- assertNull(creator.getString(0));
- assertNull(creator.newInputStream(0));
- assertNull(creator.getString(1));
- assertNull(creator.newInputStream(1));
- creator.commit();
-
- DiskLruCache.Snapshot snapshot = cache.get("k1");
- assertEquals("ABC", snapshot.getString(0));
- assertEquals("DE", snapshot.getString(1));
- }
-
- @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(0, "A");
- creator.set(1, "B");
- creator.commit();
- cache.close();
-
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- DiskLruCache.Snapshot snapshot = cache.get("k1");
- assertEquals("A", snapshot.getString(0));
- assertEquals("B", snapshot.getString(1));
- snapshot.close();
- }
-
- @Test public void journalWithEditAndPublish() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
- creator.set(0, "AB");
- creator.set(1, "C");
- creator.commit();
- cache.close();
- assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
- }
-
- @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
- creator.set(0, "AB");
- creator.set(1, "C");
- creator.abort();
- cache.close();
- assertJournalEquals("DIRTY k1", "REMOVE k1");
- }
-
- @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
- cache.edit("k1");
- cache.close();
- assertJournalEquals("DIRTY k1", "REMOVE k1");
- }
-
- @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- assertNull(cache.get("k1"));
- creator.set(0, "A");
- creator.set(1, "BC");
- creator.commit();
- cache.close();
- assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
- }
-
- @Test public void journalWithEditAndPublishAndRead() throws Exception {
- DiskLruCache.Editor k1Creator = cache.edit("k1");
- k1Creator.set(0, "AB");
- k1Creator.set(1, "C");
- k1Creator.commit();
- DiskLruCache.Editor k2Creator = cache.edit("k2");
- k2Creator.set(0, "DEF");
- k2Creator.set(1, "G");
- k2Creator.commit();
- DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
- k1Snapshot.close();
- cache.close();
- assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
- }
-
- @Test public void cannotOperateOnEditAfterPublish() throws Exception {
- DiskLruCache.Editor editor = cache.edit("k1");
- editor.set(0, "A");
- editor.set(1, "B");
- editor.commit();
- assertInoperable(editor);
- }
-
- @Test public void cannotOperateOnEditAfterRevert() throws Exception {
- DiskLruCache.Editor editor = cache.edit("k1");
- editor.set(0, "A");
- editor.set(1, "B");
- editor.abort();
- assertInoperable(editor);
- }
-
- @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
- DiskLruCache.Editor editor = cache.edit("k1");
- editor.set(0, "ABC");
- editor.set(1, "B");
- editor.commit();
- File k1 = getCleanFile("k1", 0);
- assertEquals("ABC", readFile(k1));
- cache.remove("k1");
- assertFalse(k1.exists());
- }
-
- /**
- * Each read sees a snapshot of the file at the time read was called.
- * This means that two reads of the same key can see different data.
- */
- @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
- DiskLruCache.Editor v1Creator = cache.edit("k1");
- v1Creator.set(0, "AAaa");
- v1Creator.set(1, "BBbb");
- v1Creator.commit();
-
- DiskLruCache.Snapshot snapshot1 = cache.get("k1");
- InputStream inV1 = snapshot1.getInputStream(0);
- assertEquals('A', inV1.read());
- assertEquals('A', inV1.read());
-
- DiskLruCache.Editor v1Updater = cache.edit("k1");
- v1Updater.set(0, "CCcc");
- v1Updater.set(1, "DDdd");
- v1Updater.commit();
-
- DiskLruCache.Snapshot snapshot2 = cache.get("k1");
- assertEquals("CCcc", snapshot2.getString(0));
- assertEquals("DDdd", snapshot2.getString(1));
- snapshot2.close();
-
- assertEquals('a', inV1.read());
- assertEquals('a', inV1.read());
- assertEquals("BBbb", snapshot1.getString(1));
- snapshot1.close();
- }
-
- @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
- cache.close();
- File cleanFile0 = getCleanFile("k1", 0);
- File cleanFile1 = getCleanFile("k1", 1);
- File dirtyFile0 = getDirtyFile("k1", 0);
- File dirtyFile1 = getDirtyFile("k1", 1);
- writeFile(cleanFile0, "A");
- writeFile(cleanFile1, "B");
- writeFile(dirtyFile0, "C");
- writeFile(dirtyFile1, "D");
- createJournal("CLEAN k1 1 1", "DIRTY k1");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertFalse(cleanFile0.exists());
- assertFalse(cleanFile1.exists());
- assertFalse(dirtyFile0.exists());
- assertFalse(dirtyFile1.exists());
- assertNull(cache.get("k1"));
- }
-
- @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournalWithHeader(MAGIC, "0", "100", "2", "");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- }
-
- @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournalWithHeader(MAGIC, "1", "101", "2", "");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- }
-
- @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournalWithHeader(MAGIC, "1", "100", "1", "");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- }
-
- @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournalWithHeader(MAGIC, "1", "100", "2", "x");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- }
-
- @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournal("CLEAN k1 1 1", "BOGUS");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- assertNull(cache.get("k1"));
- }
-
- @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournal("CLEAN k1 0000x001 1");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- assertNull(cache.get("k1"));
- }
-
- @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
- cache.close();
- writeFile(getCleanFile("k1", 0), "A");
- writeFile(getCleanFile("k1", 1), "B");
- Writer writer = new FileWriter(journalFile);
- writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
- writer.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertNull(cache.get("k1"));
- }
-
- @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
- cache.close();
- generateSomeGarbageFiles();
- createJournal("CLEAN k1 1 1 1");
- cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
- assertGarbageFilesAllDeleted();
- assertNull(cache.get("k1"));
- }
-
- @Test public void keyWithSpaceNotPermitted() throws Exception {
- try {
- cache.edit("my key");
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test public void keyWithNewlineNotPermitted() throws Exception {
- try {
- cache.edit("my\nkey");
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
- try {
- cache.edit("my\rkey");
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test public void nullKeyThrows() throws Exception {
- try {
- cache.edit(null);
- fail();
- } catch (NullPointerException expected) {
- }
- }
-
- @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(1, "A");
- try {
- creator.commit();
- fail();
- } catch (IllegalStateException expected) {
- }
-
- assertFalse(getCleanFile("k1", 0).exists());
- assertFalse(getCleanFile("k1", 1).exists());
- assertFalse(getDirtyFile("k1", 0).exists());
- assertFalse(getDirtyFile("k1", 1).exists());
- assertNull(cache.get("k1"));
-
- DiskLruCache.Editor creator2 = cache.edit("k1");
- creator2.set(0, "B");
- creator2.set(1, "C");
- creator2.commit();
- }
-
- @Test public void createNewEntryWithMissingFileAborts() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(0, "A");
- creator.set(1, "A");
- assertTrue(getDirtyFile("k1", 0).exists());
- assertTrue(getDirtyFile("k1", 1).exists());
- assertTrue(getDirtyFile("k1", 0).delete());
- assertFalse(getDirtyFile("k1", 0).exists());
- creator.commit(); // silently abort if file does not exist due to I/O issue
-
- assertFalse(getCleanFile("k1", 0).exists());
- assertFalse(getCleanFile("k1", 1).exists());
- assertFalse(getDirtyFile("k1", 0).exists());
- assertFalse(getDirtyFile("k1", 1).exists());
- assertNull(cache.get("k1"));
-
- DiskLruCache.Editor creator2 = cache.edit("k1");
- creator2.set(0, "B");
- creator2.set(1, "C");
- creator2.commit();
- }
-
- @Test public void revertWithTooFewValues() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(1, "A");
- creator.abort();
- assertFalse(getCleanFile("k1", 0).exists());
- assertFalse(getCleanFile("k1", 1).exists());
- assertFalse(getDirtyFile("k1", 0).exists());
- assertFalse(getDirtyFile("k1", 1).exists());
- assertNull(cache.get("k1"));
- }
-
- @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
- DiskLruCache.Editor creator = cache.edit("k1");
- creator.set(0, "A");
- creator.set(1, "B");
- creator.commit();
-
- DiskLruCache.Editor updater = cache.edit("k1");
- updater.set(0, "C");
- updater.commit();
-
- DiskLruCache.Snapshot snapshot = cache.get("k1");
- assertEquals("C", snapshot.getString(0));
- assertEquals("B", snapshot.getString(1));
- snapshot.close();
- }
-
- @Test public void evictOnInsert() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
- set("A", "a", "aaa"); // size 4
- set("B", "bb", "bbbb"); // size 6
- assertEquals(10, cache.size());
-
- // cause the size to grow to 12 should evict 'A'
- set("C", "c", "c");
- cache.flush();
- assertEquals(8, cache.size());
- assertAbsent("A");
- assertValue("B", "bb", "bbbb");
- assertValue("C", "c", "c");
-
- // causing the size to grow to 10 should evict nothing
- set("D", "d", "d");
- cache.flush();
- assertEquals(10, cache.size());
- assertAbsent("A");
- assertValue("B", "bb", "bbbb");
- assertValue("C", "c", "c");
- assertValue("D", "d", "d");
-
- // causing the size to grow to 18 should evict 'B' and 'C'
- set("E", "eeee", "eeee");
- cache.flush();
- assertEquals(10, cache.size());
- assertAbsent("A");
- assertAbsent("B");
- assertAbsent("C");
- assertValue("D", "d", "d");
- assertValue("E", "eeee", "eeee");
- }
-
- @Test public void evictOnUpdate() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
- set("A", "a", "aa"); // size 3
- set("B", "b", "bb"); // size 3
- set("C", "c", "cc"); // size 3
- assertEquals(9, cache.size());
-
- // causing the size to grow to 11 should evict 'A'
- set("B", "b", "bbbb");
- cache.flush();
- assertEquals(8, cache.size());
- assertAbsent("A");
- assertValue("B", "b", "bbbb");
- assertValue("C", "c", "cc");
- }
-
- @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
- set("A", "a", "a");
- set("B", "b", "b");
- set("C", "c", "c");
- set("D", "d", "d");
- set("E", "e", "e");
- cache.get("B").close(); // 'B' is now least recently used
-
- // causing the size to grow to 12 should evict 'A'
- set("F", "f", "f");
- // causing the size to grow to 12 should evict 'C'
- set("G", "g", "g");
- cache.flush();
- assertEquals(10, cache.size());
- assertAbsent("A");
- assertValue("B", "b", "b");
- assertAbsent("C");
- assertValue("D", "d", "d");
- assertValue("E", "e", "e");
- assertValue("F", "f", "f");
- }
-
- @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
- set("A", "a", "a");
- set("B", "b", "b");
- set("C", "c", "c");
- set("D", "d", "d");
- set("E", "e", "e");
- set("F", "f", "f");
- cache.get("B").close(); // 'B' is now least recently used
- assertEquals(12, cache.size());
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
- set("G", "g", "g");
- cache.flush();
- assertEquals(10, cache.size());
- assertAbsent("A");
- assertValue("B", "b", "b");
- assertAbsent("C");
- assertValue("D", "d", "d");
- assertValue("E", "e", "e");
- assertValue("F", "f", "f");
- assertValue("G", "g", "g");
- }
-
- @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
- set("A", "aaaaa", "aaaaaa"); // size=11
- cache.flush();
- assertAbsent("A");
- }
-
- @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
- set("A", "aaaaaaaaaaa", "a"); // size=12
- cache.flush();
- assertAbsent("A");
- }
-
- @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
- try {
- DiskLruCache.open(cacheDir, appVersion, 2, 0);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
- try {
- DiskLruCache.open(cacheDir, appVersion, 0, 10);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test public void removeAbsentElement() throws Exception {
- cache.remove("A");
- }
-
- @Test public void readingTheSameStreamMultipleTimes() throws Exception {
- set("A", "a", "b");
- DiskLruCache.Snapshot snapshot = cache.get("A");
- assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0));
- snapshot.close();
- }
-
- @Test public void rebuildJournalOnRepeatedReads() throws Exception {
- set("A", "a", "a");
- set("B", "b", "b");
- long lastJournalLength = 0;
- while (true) {
- long journalLength = journalFile.length();
- assertValue("A", "a", "a");
- assertValue("B", "b", "b");
- if (journalLength < lastJournalLength) {
- System.out
- .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
- journalLength);
- break; // test passed!
- }
- lastJournalLength = journalLength;
- }
- }
-
- @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
- long lastJournalLength = 0;
- while (true) {
- long journalLength = journalFile.length();
- set("A", "a", "a");
- set("B", "b", "b");
- if (journalLength < lastJournalLength) {
- System.out
- .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
- journalLength);
- break;
- }
- lastJournalLength = journalLength;
- }
-
- // sanity check that a rebuilt journal behaves normally
- assertValue("A", "a", "a");
- assertValue("B", "b", "b");
- }
-
- @Test public void openCreatesDirectoryIfNecessary() throws Exception {
- cache.close();
- File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary");
- cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
- set("A", "a", "a");
- assertTrue(new File(dir, "A.0").exists());
- assertTrue(new File(dir, "A.1").exists());
- assertTrue(new File(dir, "journal").exists());
- }
-
- @Test public void fileDeletedExternally() throws Exception {
- set("A", "a", "a");
- getCleanFile("A", 1).delete();
- assertNull(cache.get("A"));
- }
-
- @Test public void editSameVersion() throws Exception {
- set("A", "a", "a");
- DiskLruCache.Snapshot snapshot = cache.get("A");
- DiskLruCache.Editor editor = snapshot.edit();
- editor.set(1, "a2");
- editor.commit();
- assertValue("A", "a", "a2");
- }
-
- @Test public void editSnapshotAfterChangeAborted() throws Exception {
- set("A", "a", "a");
- DiskLruCache.Snapshot snapshot = cache.get("A");
- DiskLruCache.Editor toAbort = snapshot.edit();
- toAbort.set(0, "b");
- toAbort.abort();
- DiskLruCache.Editor editor = snapshot.edit();
- editor.set(1, "a2");
- editor.commit();
- assertValue("A", "a", "a2");
- }
-
- @Test public void editSnapshotAfterChangeCommitted() throws Exception {
- set("A", "a", "a");
- DiskLruCache.Snapshot snapshot = cache.get("A");
- DiskLruCache.Editor toAbort = snapshot.edit();
- toAbort.set(0, "b");
- toAbort.commit();
- assertNull(snapshot.edit());
- }
-
- @Test public void editSinceEvicted() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
- set("A", "aa", "aaa"); // size 5
- DiskLruCache.Snapshot snapshot = cache.get("A");
- set("B", "bb", "bbb"); // size 5
- set("C", "cc", "ccc"); // size 5; will evict 'A'
- cache.flush();
- assertNull(snapshot.edit());
- }
-
- @Test public void editSinceEvictedAndRecreated() throws Exception {
- cache.close();
- cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
- set("A", "aa", "aaa"); // size 5
- DiskLruCache.Snapshot snapshot = cache.get("A");
- set("B", "bb", "bbb"); // size 5
- set("C", "cc", "ccc"); // size 5; will evict 'A'
- set("A", "a", "aaaa"); // size 5; will evict 'B'
- cache.flush();
- assertNull(snapshot.edit());
- }
-
- private void assertJournalEquals(String... expectedBodyLines) throws Exception {
- List<String> expectedLines = new ArrayList<String>();
- expectedLines.add(MAGIC);
- expectedLines.add(VERSION_1);
- expectedLines.add("100");
- expectedLines.add("2");
- expectedLines.add("");
- expectedLines.addAll(Arrays.asList(expectedBodyLines));
- assertEquals(expectedLines, readJournalLines());
- }
-
- private void createJournal(String... bodyLines) throws Exception {
- createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
- }
-
- private void createJournalWithHeader(String magic, String version, String appVersion,
- String valueCount, String blank, String... bodyLines) throws Exception {
- Writer writer = new FileWriter(journalFile);
- writer.write(magic + "\n");
- writer.write(version + "\n");
- writer.write(appVersion + "\n");
- writer.write(valueCount + "\n");
- writer.write(blank + "\n");
- for (String line : bodyLines) {
- writer.write(line);
- writer.write('\n');
- }
- writer.close();
- }
-
- private List<String> readJournalLines() throws Exception {
- List<String> result = new ArrayList<String>();
- BufferedReader reader = new BufferedReader(new FileReader(journalFile));
- String line;
- while ((line = reader.readLine()) != null) {
- result.add(line);
- }
- reader.close();
- return result;
- }
-
- private File getCleanFile(String key, int index) {
- return new File(cacheDir, key + "." + index);
- }
-
- private File getDirtyFile(String key, int index) {
- return new File(cacheDir, key + "." + index + ".tmp");
- }
-
- private String readFile(File file) throws Exception {
- Reader reader = new FileReader(file);
- StringWriter writer = new StringWriter();
- char[] buffer = new char[1024];
- int count;
- while ((count = reader.read(buffer)) != -1) {
- writer.write(buffer, 0, count);
- }
- reader.close();
- return writer.toString();
- }
-
- public void writeFile(File file, String content) throws Exception {
- FileWriter writer = new FileWriter(file);
- writer.write(content);
- writer.close();
- }
-
- private void assertInoperable(DiskLruCache.Editor editor) throws Exception {
- try {
- editor.getString(0);
- fail();
- } catch (IllegalStateException expected) {
- }
- try {
- editor.set(0, "A");
- fail();
- } catch (IllegalStateException expected) {
- }
- try {
- editor.newInputStream(0);
- fail();
- } catch (IllegalStateException expected) {
- }
- try {
- editor.newOutputStream(0);
- fail();
- } catch (IllegalStateException expected) {
- }
- try {
- editor.commit();
- fail();
- } catch (IllegalStateException expected) {
- }
- try {
- editor.abort();
- fail();
- } catch (IllegalStateException expected) {
- }
- }
-
- private void generateSomeGarbageFiles() throws Exception {
- File dir1 = new File(cacheDir, "dir1");
- File dir2 = new File(dir1, "dir2");
- writeFile(getCleanFile("g1", 0), "A");
- writeFile(getCleanFile("g1", 1), "B");
- writeFile(getCleanFile("g2", 0), "C");
- writeFile(getCleanFile("g2", 1), "D");
- writeFile(getCleanFile("g2", 1), "D");
- writeFile(new File(cacheDir, "otherFile0"), "E");
- dir1.mkdir();
- dir2.mkdir();
- writeFile(new File(dir2, "otherFile1"), "F");
- }
-
- private void assertGarbageFilesAllDeleted() throws Exception {
- assertFalse(getCleanFile("g1", 0).exists());
- assertFalse(getCleanFile("g1", 1).exists());
- assertFalse(getCleanFile("g2", 0).exists());
- assertFalse(getCleanFile("g2", 1).exists());
- assertFalse(new File(cacheDir, "otherFile0").exists());
- assertFalse(new File(cacheDir, "dir1").exists());
- }
-
- private void set(String key, String value0, String value1) throws Exception {
- DiskLruCache.Editor editor = cache.edit(key);
- editor.set(0, value0);
- editor.set(1, value1);
- editor.commit();
- }
-
- private void assertAbsent(String key) throws Exception {
- DiskLruCache.Snapshot snapshot = cache.get(key);
- if (snapshot != null) {
- snapshot.close();
- fail();
- }
- assertFalse(getCleanFile(key, 0).exists());
- assertFalse(getCleanFile(key, 1).exists());
- assertFalse(getDirtyFile(key, 0).exists());
- assertFalse(getDirtyFile(key, 1).exists());
- }
-
- private void assertValue(String key, String value0, String value1) throws Exception {
- DiskLruCache.Snapshot snapshot = cache.get(key);
- assertEquals(value0, snapshot.getString(0));
- assertEquals(value1, snapshot.getString(1));
- assertTrue(getCleanFile(key, 0).exists());
- assertTrue(getCleanFile(key, 1).exists());
- snapshot.close();
- }
-}
diff --git a/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java b/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
new file mode 100644
index 0000000..e933c17
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp.internal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class FaultRecoveringOutputStreamTest {
+ @Test public void noRecoveryWithoutReplacement() throws Exception {
+ FaultingOutputStream faulting = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting);
+
+ recovering.write('a');
+ faulting.nextFault = "system on fire";
+ try {
+ recovering.write('b');
+ fail();
+ } catch (IOException e) {
+ assertEquals(Arrays.asList("system on fire"), recovering.exceptionMessages);
+ assertEquals("ab", faulting.receivedUtf8);
+ assertFalse(faulting.closed);
+ }
+ }
+
+ @Test public void successfulRecoveryOnWriteFault() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+
+ recovering.write('a');
+ assertEquals("a", faulting1.receivedUtf8);
+ assertEquals("", faulting2.receivedUtf8);
+ faulting1.nextFault = "system under water";
+ recovering.write('b');
+ assertEquals(Arrays.asList("system under water"), recovering.exceptionMessages);
+ assertEquals("ab", faulting1.receivedUtf8);
+ assertEquals("ab", faulting2.receivedUtf8);
+ assertTrue(faulting1.closed);
+ assertFalse(faulting2.closed);
+
+ // Confirm that new data goes to the new stream.
+ recovering.write('c');
+ assertEquals("ab", faulting1.receivedUtf8);
+ assertEquals("abc", faulting2.receivedUtf8);
+ }
+
+ @Test public void successfulRecoveryOnFlushFault() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+
+ recovering.write('a');
+ faulting1.nextFault = "bad weather";
+ recovering.flush();
+ assertEquals(Arrays.asList("bad weather"), recovering.exceptionMessages);
+ assertEquals("a", faulting1.receivedUtf8);
+ assertEquals("a", faulting2.receivedUtf8);
+ assertTrue(faulting1.closed);
+ assertFalse(faulting2.closed);
+ assertEquals("a", faulting2.flushedUtf8);
+
+ // Confirm that new data goes to the new stream.
+ recovering.write('b');
+ assertEquals("a", faulting1.receivedUtf8);
+ assertEquals("ab", faulting2.receivedUtf8);
+ assertEquals("a", faulting2.flushedUtf8);
+ }
+
+ @Test public void successfulRecoveryOnCloseFault() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+
+ recovering.write('a');
+ faulting1.nextFault = "termites";
+ recovering.close();
+ assertEquals(Arrays.asList("termites"), recovering.exceptionMessages);
+ assertEquals("a", faulting1.receivedUtf8);
+ assertEquals("a", faulting2.receivedUtf8);
+ assertTrue(faulting1.closed);
+ assertTrue(faulting2.closed);
+ }
+
+ @Test public void replacementStreamFaultsImmediately() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ FaultingOutputStream faulting3 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+ recovering.replacements.addLast(faulting3);
+
+ recovering.write('a');
+ assertEquals("a", faulting1.receivedUtf8);
+ assertEquals("", faulting2.receivedUtf8);
+ assertEquals("", faulting3.receivedUtf8);
+ faulting1.nextFault = "offline";
+ faulting2.nextFault = "slow";
+ recovering.write('b');
+ assertEquals(Arrays.asList("offline", "slow"), recovering.exceptionMessages);
+ assertEquals("ab", faulting1.receivedUtf8);
+ assertEquals("a", faulting2.receivedUtf8);
+ assertEquals("ab", faulting3.receivedUtf8);
+ assertTrue(faulting1.closed);
+ assertTrue(faulting2.closed);
+ assertFalse(faulting3.closed);
+
+ // Confirm that new data goes to the new stream.
+ recovering.write('c');
+ assertEquals("ab", faulting1.receivedUtf8);
+ assertEquals("a", faulting2.receivedUtf8);
+ assertEquals("abc", faulting3.receivedUtf8);
+ }
+
+ @Test public void recoverWithFullBuffer() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+
+ recovering.write("abcdefghij".getBytes(UTF_8)); // 10 bytes.
+ faulting1.nextFault = "unlucky";
+ recovering.write('k');
+ assertEquals("abcdefghijk", faulting1.receivedUtf8);
+ assertEquals("abcdefghijk", faulting2.receivedUtf8);
+ assertEquals(Arrays.asList("unlucky"), recovering.exceptionMessages);
+ assertTrue(faulting1.closed);
+ assertFalse(faulting2.closed);
+
+ // Confirm that new data goes to the new stream.
+ recovering.write('l');
+ assertEquals("abcdefghijk", faulting1.receivedUtf8);
+ assertEquals("abcdefghijkl", faulting2.receivedUtf8);
+ }
+
+ @Test public void noRecoveryWithOverfullBuffer() throws Exception {
+ FaultingOutputStream faulting1 = new FaultingOutputStream();
+ FaultingOutputStream faulting2 = new FaultingOutputStream();
+ TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
+ recovering.replacements.addLast(faulting2);
+
+ recovering.write("abcdefghijk".getBytes(UTF_8)); // 11 bytes.
+ faulting1.nextFault = "out to lunch";
+ try {
+ recovering.write('l');
+ fail();
+ } catch (IOException expected) {
+ assertEquals("out to lunch", expected.getMessage());
+ }
+
+ assertEquals(Arrays.<String>asList(), recovering.exceptionMessages);
+ assertEquals("abcdefghijkl", faulting1.receivedUtf8);
+ assertEquals("", faulting2.receivedUtf8);
+ assertFalse(faulting1.closed);
+ assertFalse(faulting2.closed);
+ }
+
+ static class FaultingOutputStream extends OutputStream {
+ String receivedUtf8 = "";
+ String flushedUtf8 = null;
+ String nextFault;
+ boolean closed;
+
+ @Override public final void write(int data) throws IOException {
+ write(new byte[] { (byte) data });
+ }
+
+ @Override public void write(byte[] buffer, int offset, int count) throws IOException {
+ receivedUtf8 += new String(buffer, offset, count, UTF_8);
+ if (nextFault != null) throw new IOException(nextFault);
+ }
+
+ @Override public void flush() throws IOException {
+ flushedUtf8 = receivedUtf8;
+ if (nextFault != null) throw new IOException(nextFault);
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ if (nextFault != null) throw new IOException(nextFault);
+ }
+ }
+
+ static class TestFaultRecoveringOutputStream extends FaultRecoveringOutputStream {
+ final List<String> exceptionMessages = new ArrayList<String>();
+ final Deque<OutputStream> replacements = new ArrayDeque<OutputStream>();
+
+ TestFaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream first) {
+ super(maxReplayBufferLength, first);
+ }
+
+ @Override protected OutputStream replacementStream(IOException e) {
+ exceptionMessages.add(e.getMessage());
+ return replacements.poll();
+ }
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java b/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
new file mode 100644
index 0000000..636acbd
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed 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.squareup.okhttp.internal;
+
+import com.squareup.okhttp.OkAuthenticator;
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class RecordingOkAuthenticator implements OkAuthenticator {
+ public final List<String> calls = new ArrayList<String>();
+ public final Credential credential;
+
+ public RecordingOkAuthenticator(Credential credential) {
+ this.credential = credential;
+ }
+
+ @Override public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges)
+ throws IOException {
+ calls.add("authenticate"
+ + " proxy=" + proxy.type()
+ + " url=" + url
+ + " challenges=" + challenges);
+ return credential;
+ }
+
+ @Override public Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges)
+ throws IOException {
+ calls.add("authenticateProxy"
+ + " proxy=" + proxy.type()
+ + " url=" + url
+ + " challenges=" + challenges);
+ return credential;
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index e54ed21..64a4738 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -19,7 +19,9 @@
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.HttpResponseCache;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
@@ -112,7 +114,7 @@
@After public void tearDown() throws Exception {
server.shutdown();
ResponseCache.setDefault(null);
- cache.getCache().delete();
+ cache.delete();
CookieHandler.setDefault(null);
}
@@ -970,7 +972,7 @@
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(connection));
}
@Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
@@ -1584,6 +1586,74 @@
assertEquals(2, server.getRequestCount());
}
+ @Test public void responseSourceHeaderCached() throws IOException {
+ server.enqueue(new MockResponse().setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+ server.play();
+
+ assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ URLConnection connection = openConnection(server.getUrl("/"));
+ connection.addRequestProperty("Cache-Control", "only-if-cached");
+ assertEquals("A", readAscii(connection));
+
+ String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+ assertEquals(ResponseSource.CACHE.toString() + " 200", source);
+ }
+
+ @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
+ server.enqueue(new MockResponse().setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse().setBody("B")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+ server.play();
+
+ assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("B", readAscii(connection));
+
+ String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+ assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 200", source);
+ }
+
+ @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
+ server.enqueue(new MockResponse().setBody("A")
+ .addHeader("Cache-Control: max-age=0")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse().setResponseCode(304));
+ server.play();
+
+ assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(connection));
+
+ String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+ assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 304", source);
+ }
+
+ @Test public void responseSourceHeaderFetched() throws IOException {
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
+
+ URLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(connection));
+
+ String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
+ assertEquals(ResponseSource.NETWORK.toString() + " 200", source);
+ }
+
+ @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=120")
+ .addHeader(": A")
+ .setBody("body"));
+ server.play();
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ assertEquals("A", connection.getHeaderField(""));
+ }
+
/**
* @param delta the offset from the current date to use. Negative
* values yield dates in the past; positive values yield dates in the
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
index e5629f0..474e507 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
@@ -21,7 +21,6 @@
import org.junit.Test;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
public final class RawHeadersTest {
@Test public void parseNameValueBlock() {
@@ -80,7 +79,7 @@
final int version = 1;
final int code = 503;
rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " ");
- assertTrue(rawHeaders.getResponseMessage().isEmpty());
+ assertEquals("", rawHeaders.getResponseMessage());
assertEquals(version, rawHeaders.getHttpMinorVersion());
assertEquals(code, rawHeaders.getResponseCode());
}
@@ -95,7 +94,7 @@
final int version = 1;
final int code = 503;
rawHeaders.setStatusLine("HTTP/1." + version + " " + code);
- assertTrue(rawHeaders.getResponseMessage().isEmpty());
+ assertEquals("", rawHeaders.getResponseMessage());
assertEquals(version, rawHeaders.getHttpMinorVersion());
assertEquals(code, rawHeaders.getResponseCode());
}
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index 4dc2e32..687a397 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -18,6 +18,8 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Dns;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.IOException;
@@ -30,10 +32,14 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Test;
@@ -60,6 +66,7 @@
private static final SSLSocketFactory socketFactory;
private static final HostnameVerifier hostnameVerifier;
private static final ConnectionPool pool;
+
static {
try {
uri = new URI("http://" + uriHost + ":" + uriPort + "/path");
@@ -72,12 +79,15 @@
}
}
+ private final OkAuthenticator authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
+ private final List<String> transports = Arrays.asList("http/1.1");
private final FakeDns dns = new FakeDns();
private final FakeProxySelector proxySelector = new FakeProxySelector();
@Test public void singleRoute() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
@@ -92,9 +102,31 @@
}
}
+ @Test public void singleRouteReturnsFailedRoute() throws Exception {
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
+
+ assertTrue(routeSelector.hasNext());
+ dns.inetAddresses = makeFakeAddresses(255, 1);
+ Connection connection = routeSelector.next();
+ Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ failedRoutes.add(connection.getRoute());
+ routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
+ assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+ assertFalse(routeSelector.hasNext());
+ try {
+ routeSelector.next();
+ fail();
+ } catch (NoSuchElementException expected) {
+ }
+ }
+
@Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, proxyA);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -109,8 +141,10 @@
}
@Test public void explicitDirectProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, NO_PROXY);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY,
+ transports);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -123,10 +157,11 @@
}
@Test public void proxySelectorReturnsNull() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
proxySelector.proxies = null;
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
proxySelector.assertRequests(uri);
assertTrue(routeSelector.hasNext());
@@ -138,8 +173,9 @@
}
@Test public void proxySelectorReturnsNoProxies() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -152,11 +188,12 @@
}
@Test public void proxySelectorReturnsMultipleProxies() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
proxySelector.assertRequests(uri);
// First try the IP addresses of the first proxy, in sequence.
@@ -185,10 +222,11 @@
}
@Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
proxySelector.proxies.add(NO_PROXY);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
proxySelector.assertRequests(uri);
// Only the origin server will be attempted.
@@ -201,12 +239,13 @@
}
@Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, null, null, null);
+ Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
proxySelector.proxies.add(proxyA);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
proxySelector.assertRequests(uri);
assertTrue(routeSelector.hasNext());
@@ -238,28 +277,39 @@
assertFalse(routeSelector.hasNext());
}
- @Test public void multipleTlsModes() throws Exception {
- Address address =
- new Address(uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
+ Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+ Proxy.NO_PROXY, transports);
+ Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ failedRoutes);
- assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
- dns.assertRequests(uriHost);
+ Connection connection = routeSelector.next();
+ routeSelector.connectFailed(connection, new IOException("Non SSL exception"));
+ assertTrue(failedRoutes.size() == 2);
+ }
- assertTrue(routeSelector.hasNext());
- assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
- dns.assertRequests(); // No more DNS requests since the previous!
+ @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception {
+ Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+ Proxy.NO_PROXY, transports);
+ Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ failedRoutes);
- assertFalse(routeSelector.hasNext());
+ dns.inetAddresses = makeFakeAddresses(255, 1);
+ Connection connection = routeSelector.next();
+ routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception"));
+ assertTrue(failedRoutes.size() == 1);
}
@Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, null);
+ Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+ null, transports);
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ Collections.EMPTY_SET);
// Proxy A
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -292,13 +342,45 @@
assertFalse(routeSelector.hasNext());
}
+ @Test public void failedRoutesAreLast() throws Exception {
+ Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
+ Proxy.NO_PROXY, transports);
+
+ Set<Route> failedRoutes = new LinkedHashSet<Route>(1);
+ RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
+ failedRoutes);
+ dns.inetAddresses = makeFakeAddresses(255, 1);
+
+ // Extract the regular sequence of routes from selector.
+ List<Connection> regularRoutes = new ArrayList<Connection>();
+ while (routeSelector.hasNext()) {
+ regularRoutes.add(routeSelector.next());
+ }
+
+ // Check that we do indeed have more than one route.
+ assertTrue(regularRoutes.size() > 1);
+ // Add first regular route as failed.
+ failedRoutes.add(regularRoutes.get(0).getRoute());
+ // Reset selector
+ routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes);
+
+ List<Connection> routesWithFailedRoute = new ArrayList<Connection>();
+ while (routeSelector.hasNext()) {
+ routesWithFailedRoute.add(routeSelector.next());
+ }
+
+ assertEquals(regularRoutes.get(0).getRoute(),
+ routesWithFailedRoute.get(routesWithFailedRoute.size() - 1).getRoute());
+ assertEquals(regularRoutes.size(), routesWithFailedRoute.size());
+ }
+
private void assertConnection(Connection connection, Address address, Proxy proxy,
InetAddress socketAddress, int socketPort, boolean modernTls) {
- assertEquals(address, connection.getAddress());
- assertEquals(proxy, connection.getProxy());
- assertEquals(socketAddress, connection.getSocketAddress().getAddress());
- assertEquals(socketPort, connection.getSocketAddress().getPort());
- assertEquals(modernTls, connection.isModernTls());
+ assertEquals(address, connection.getRoute().getAddress());
+ assertEquals(proxy, connection.getRoute().getProxy());
+ assertEquals(socketAddress, connection.getRoute().getSocketAddress().getAddress());
+ assertEquals(socketPort, connection.getRoute().getSocketAddress().getPort());
+ assertEquals(modernTls, connection.getRoute().isModernTls());
}
private static InetAddress[] makeFakeAddresses(int prefix, int count) {
diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 9a3572a..7f66519 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -20,9 +20,11 @@
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.HttpResponseCache;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -56,6 +58,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +80,7 @@
import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
+import static com.squareup.okhttp.OkAuthenticator.Credential;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -117,7 +121,7 @@
server.shutdown();
server2.shutdown();
if (cache != null) {
- cache.getCache().delete();
+ cache.delete();
}
}
@@ -153,9 +157,17 @@
fail();
} catch (NullPointerException expected) {
}
- urlConnection.setRequestProperty("NullValue", null); // should fail silently!
+ try {
+ urlConnection.setRequestProperty("NullValue", null);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
assertNull(urlConnection.getRequestProperty("NullValue"));
- urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently!
+ try {
+ urlConnection.addRequestProperty("AnotherNullValue", null);
+ fail();
+ } catch (Exception expected) {
+ }
assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
urlConnection.getResponseCode();
@@ -507,7 +519,7 @@
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(server.getUrl("/foo"));
@@ -527,7 +539,7 @@
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- client.setSSLSocketFactory(clientSocketFactory);
+ client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(hostnameVerifier);
HttpURLConnection connection = client.open(server.getUrl("/"));
assertContent("this response comes via HTTPS", connection);
@@ -547,12 +559,12 @@
server.play();
// install a custom SSL socket factory so the server can be authorized
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection1 = client.open(server.getUrl("/"));
assertContent("this response comes via HTTPS", connection1);
- client.setSSLSocketFactory(null);
+ client.setSslSocketFactory(null);
HttpURLConnection connection2 = client.open(server.getUrl("/"));
try {
readAscii(connection2.getInputStream(), Integer.MAX_VALUE);
@@ -567,7 +579,7 @@
server.enqueue(new MockResponse().setBody("this response comes via SSL"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(server.getUrl("/foo"));
@@ -663,7 +675,7 @@
server.play();
URL url = server.getUrl("/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = proxyConfig.connect(server, client, url);
@@ -703,7 +715,7 @@
server.play();
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(hostnameVerifier);
HttpURLConnection connection = proxyConfig.connect(server, client, url);
@@ -740,7 +752,7 @@
client.setProxy(server.toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
HttpURLConnection connection = client.open(url);
try {
@@ -778,7 +790,7 @@
client.setProxy(server.toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(hostnameVerifier);
HttpURLConnection connection = client.open(url);
connection.addRequestProperty("Private", "Secret");
@@ -810,7 +822,7 @@
client.setProxy(server.toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(url);
assertContent("A", connection);
@@ -840,7 +852,7 @@
client.setProxy(server.toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(url);
connection.setRequestProperty("Connection", "close");
@@ -861,7 +873,7 @@
client.setProxy(server.toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(socketFactory);
+ client.setSslSocketFactory(socketFactory);
client.setHostnameVerifier(hostnameVerifier);
assertContent("response 1", client.open(url));
assertContent("response 2", client.open(url));
@@ -1076,7 +1088,7 @@
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
server.useHttps(socketFactory, false);
- client.setSSLSocketFactory(socketFactory);
+ client.setSslSocketFactory(socketFactory);
client.setHostnameVerifier(hostnameVerifier);
}
@@ -1387,7 +1399,7 @@
server.enqueue(new MockResponse().setBody("Success!"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
@@ -1521,7 +1533,7 @@
server.enqueue(new MockResponse().setBody("This is the new location!"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("This is the new location!",
@@ -1542,7 +1554,7 @@
server.play();
client.setFollowProtocolRedirects(false);
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
@@ -1570,7 +1582,7 @@
.setBody("This page has moved!"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
client.setFollowProtocolRedirects(true);
HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
@@ -1593,7 +1605,7 @@
.setBody("This page has moved!"));
server.play();
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
client.setFollowProtocolRedirects(true);
HttpURLConnection connection = client.open(server.getUrl("/"));
@@ -1614,7 +1626,7 @@
if (https) {
server.useHttps(sslContext.getSocketFactory(), false);
server2.useHttps(sslContext.getSocketFactory(), false);
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
}
@@ -1731,6 +1743,70 @@
assertEquals(1, server.getRequestCount());
}
+ @Test public void response307WithGet() throws Exception {
+ test307Redirect("GET");
+ }
+
+ @Test public void response307WithHead() throws Exception {
+ test307Redirect("HEAD");
+ }
+
+ @Test public void response307WithOptions() throws Exception {
+ test307Redirect("OPTIONS");
+ }
+
+ @Test public void response307WithPost() throws Exception {
+ test307Redirect("POST");
+ }
+
+ private void test307Redirect(String method) throws Exception {
+ MockResponse response1 = new MockResponse()
+ .setResponseCode(HttpURLConnectionImpl.HTTP_TEMP_REDIRECT)
+ .addHeader("Location: /page2");
+ if (!method.equals("HEAD")) {
+ response1.setBody("This page has moved!");
+ }
+ server.enqueue(response1);
+ server.enqueue(new MockResponse().setBody("Page 2"));
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/page1"));
+ connection.setRequestMethod(method);
+ byte[] requestBody = { 'A', 'B', 'C', 'D' };
+ if (method.equals("POST")) {
+ connection.setDoOutput(true);
+ OutputStream outputStream = connection.getOutputStream();
+ outputStream.write(requestBody);
+ outputStream.close();
+ }
+
+ String response = readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+
+ RecordedRequest page1 = server.takeRequest();
+ assertEquals(method + " /page1 HTTP/1.1", page1.getRequestLine());
+
+ if (method.equals("GET")) {
+ assertEquals("Page 2", response);
+ } else if (method.equals("HEAD")) {
+ assertEquals("", response);
+ } else {
+ // Methods other than GET/HEAD shouldn't follow the redirect
+ if (method.equals("POST")) {
+ assertTrue(connection.getDoOutput());
+ assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+ }
+ assertEquals(1, server.getRequestCount());
+ assertEquals("This page has moved!", response);
+ return;
+ }
+
+ // GET/HEAD requests should have followed the redirect with the same method
+ assertFalse(connection.getDoOutput());
+ assertEquals(2, server.getRequestCount());
+ RecordedRequest page2 = server.takeRequest();
+ assertEquals(method + " /page2 HTTP/1.1", page2.getRequestLine());
+ }
+
@Test public void follow20Redirects() throws Exception {
for (int i = 0; i < 20; i++) {
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
@@ -1772,7 +1848,7 @@
sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
client.setHostnameVerifier(hostnameVerifier);
- client.setSSLSocketFactory(sc.getSocketFactory());
+ client.setSslSocketFactory(sc.getSocketFactory());
server.useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
@@ -2213,6 +2289,97 @@
assertEquals(-1, in.read());
}
+ @Test public void postFailsWithBufferedRequestForSmallRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 1024);
+ }
+
+ // This test is ignored because we don't (yet) reliably recover for large request bodies.
+ @Test @Ignore public void postFailsWithBufferedRequestForLargeRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 16384);
+ }
+
+ @Test public void postFailsWithChunkedRequestForSmallRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.CHUNKED, 1024);
+ }
+
+ // This test is ignored because we don't (yet) reliably recover for large request bodies.
+ @Test @Ignore public void postFailsWithChunkedRequestForLargeRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.CHUNKED, 16384);
+ }
+
+ @Test public void postFailsWithFixedLengthRequestForSmallRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 1024);
+ }
+
+ // This test is ignored because we don't (yet) reliably recover for large request bodies.
+ @Test @Ignore public void postFailsWithFixedLengthRequestForLargeRequest() throws Exception {
+ reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 16384);
+ }
+
+ private void reusedConnectionFailsWithPost(TransferKind transferKind, int requestSize)
+ throws Exception {
+ server.enqueue(new MockResponse().setBody("A").setSocketPolicy(SHUTDOWN_INPUT_AT_END));
+ server.enqueue(new MockResponse().setBody("B"));
+ server.play();
+
+ assertContent("A", client.open(server.getUrl("/a")));
+
+ // If the request body is larger than OkHttp's replay buffer, the failure may still occur.
+ byte[] requestBody = new byte[requestSize];
+ new Random(0).nextBytes(requestBody);
+
+ HttpURLConnection connection = client.open(server.getUrl("/b"));
+ connection.setRequestMethod("POST");
+ transferKind.setForRequest(connection, requestBody.length);
+ for (int i = 0; i < requestBody.length; i += 1024) {
+ connection.getOutputStream().write(requestBody, i, 1024);
+ }
+ connection.getOutputStream().close();
+ assertContent("B", connection);
+
+ RecordedRequest requestA = server.takeRequest();
+ assertEquals("/a", requestA.getPath());
+ RecordedRequest requestB = server.takeRequest();
+ assertEquals("/b", requestB.getPath());
+ assertEquals(Arrays.toString(requestBody), Arrays.toString(requestB.getBody()));
+ }
+
+ @Test public void fullyBufferedPostIsTooShort() throws Exception {
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/b"));
+ connection.setRequestProperty("Content-Length", "4");
+ connection.setRequestMethod("POST");
+ OutputStream out = connection.getOutputStream();
+ out.write('a');
+ out.write('b');
+ out.write('c');
+ try {
+ out.close();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ @Test public void fullyBufferedPostIsTooLong() throws Exception {
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/b"));
+ connection.setRequestProperty("Content-Length", "3");
+ connection.setRequestMethod("POST");
+ OutputStream out = connection.getOutputStream();
+ out.write('a');
+ out.write('b');
+ out.write('c');
+ try {
+ out.write('d');
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
@Test @Ignore public void testPooledConnectionsDetectHttp10() {
// TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1)
fail("TODO");
@@ -2237,14 +2404,40 @@
fail("TODO");
}
- @Test @Ignore public void emptyHeaderName() {
- // This is relevant for SPDY
- fail("TODO");
+ @Test public void emptyRequestHeaderValueIsAllowed() throws Exception {
+ server.enqueue(new MockResponse().setBody("body"));
+ server.play();
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ urlConnection.addRequestProperty("B", "");
+ assertContent("body", urlConnection);
+ assertEquals("", urlConnection.getRequestProperty("B"));
}
- @Test @Ignore public void emptyHeaderValue() {
- // This is relevant for SPDY
- fail("TODO");
+ @Test public void emptyResponseHeaderValueIsAllowed() throws Exception {
+ server.enqueue(new MockResponse().addHeader("A:").setBody("body"));
+ server.play();
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ assertContent("body", urlConnection);
+ assertEquals("", urlConnection.getHeaderField("A"));
+ }
+
+ @Test public void emptyRequestHeaderNameIsStrict() throws Exception {
+ server.enqueue(new MockResponse().setBody("body"));
+ server.play();
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ try {
+ urlConnection.setRequestProperty("", "A");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void emptyResponseHeaderNameIsLenient() throws Exception {
+ server.enqueue(new MockResponse().addHeader(":A").setBody("body"));
+ server.play();
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ urlConnection.getResponseCode();
+ assertEquals("A", urlConnection.getHeaderField(""));
}
@Test @Ignore public void deflateCompression() {
@@ -2259,6 +2452,53 @@
fail("TODO");
}
+ @Test public void customAuthenticator() throws Exception {
+ MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
+ .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+ .setBody("Please authenticate.");
+ server.enqueue(pleaseAuthenticate);
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
+
+ Credential credential = Credential.basic("jesse", "peanutbutter");
+ RecordingOkAuthenticator authenticator = new RecordingOkAuthenticator(credential);
+ client.setAuthenticator(authenticator);
+ assertContent("A", client.open(server.getUrl("/private")));
+
+ assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*");
+ assertContains(server.takeRequest().getHeaders(),
+ "Authorization: " + credential.getHeaderValue());
+
+ assertEquals(1, authenticator.calls.size());
+ String call = authenticator.calls.get(0);
+ assertTrue(call, call.contains("proxy=DIRECT"));
+ assertTrue(call, call.contains("url=" + server.getUrl("/private")));
+ assertTrue(call, call.contains("challenges=[Basic realm=\"protected area\"]"));
+ }
+
+ @Test public void setTransports() throws Exception {
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
+ client.setTransports(Arrays.asList("http/1.1"));
+ assertContent("A", client.open(server.getUrl("/")));
+ }
+
+ @Test public void setTransportsWithoutHttp11() throws Exception {
+ try {
+ client.setTransports(Arrays.asList("spdy/3"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void setTransportsWithNull() throws Exception {
+ try {
+ client.setTransports(Arrays.asList("http/1.1", null));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
/** Returns a gzipped copy of {@code bytes}. */
public byte[] gzip(byte[] bytes) throws IOException {
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
@@ -2314,7 +2554,7 @@
response.setBody(content);
}
@Override void setForRequest(HttpURLConnection connection, int contentLength) {
- connection.setChunkedStreamingMode(contentLength);
+ connection.setFixedLengthStreamingMode(contentLength);
}
},
END_OF_STREAM() {
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
index b8afeb2..5970088 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
@@ -17,11 +17,11 @@
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.HttpResponseCache;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpResponseCache;
import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -78,7 +78,7 @@
private HttpResponseCache cache;
@Before public void setUp() throws Exception {
- client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
String systemTmpDir = System.getProperty("java.io.tmpdir");
File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID());
diff --git a/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java b/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
new file mode 100644
index 0000000..215e968
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
@@ -0,0 +1,120 @@
+/*
+ * 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.squareup.okhttp.internal.tls;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionContext;
+import javax.security.cert.X509Certificate;
+
+final class FakeSSLSession implements SSLSession {
+ private final Certificate[] certificates;
+
+ public FakeSSLSession(Certificate... certificates) throws Exception {
+ this.certificates = certificates;
+ }
+
+ public int getApplicationBufferSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getCipherSuite() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long getCreationTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ public byte[] getId() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long getLastAccessedTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Certificate[] getLocalCertificates() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Principal getLocalPrincipal() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getPacketBufferSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+ if (certificates.length == 0) {
+ throw new SSLPeerUnverifiedException("peer not authenticated");
+ } else {
+ return certificates;
+ }
+ }
+
+ public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getPeerHost() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getPeerPort() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getProtocol() {
+ throw new UnsupportedOperationException();
+ }
+
+ public SSLSessionContext getSessionContext() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void putValue(String s, Object obj) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void removeValue(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object getValue(String s) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String[] getValueNames() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void invalidate() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isValid() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
new file mode 100644
index 0000000..f908fb8
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
@@ -0,0 +1,553 @@
+/*
+ * 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.squareup.okhttp.internal.tls;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for our hostname verifier. Most of these tests are from AOSP, which
+ * itself includes tests from the Apache HTTP Client test suite.
+ */
+public final class HostnameVerifierTest {
+ private HostnameVerifier verifier = new OkHostnameVerifier();
+
+ @Test public void verify() throws Exception {
+ FakeSSLSession session = new FakeSSLSession();
+ assertFalse(verifier.verify("localhost", session));
+ }
+
+ @Test public void verifyCn() throws Exception {
+ // CN=foo.com
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aQMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzE0MVoXDTI4MTEwNTE1MzE0MVowgaQx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+ + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+ + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+ + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+ + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+ + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+ + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+ + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB\n"
+ + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE\n"
+ + "FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS\n"
+ + "yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQC3jRmEya6sQCkmieULcvx8zz1euCk9\n"
+ + "fSez7BEtki8+dmfMXe3K7sH0lI8f4jJR0rbSCjpmCQLYmzC3NxBKeJOW0RcjNBpO\n"
+ + "c2JlGO9auXv2GDP4IYiXElLJ6VSqc8WvDikv0JmCCWm0Zga+bZbR/EWN5DeEtFdF\n"
+ + "815CLpJZNcYwiYwGy/CVQ7w2TnXlG+mraZOz+owr+cL6J/ZesbdEWfjoS1+cUEhE\n"
+ + "HwlNrAu8jlZ2UqSgskSWlhYdMTAP9CPHiUv9N7FcT58Itv/I4fKREINQYjDpvQcx\n"
+ + "SaTYb9dr5sB4WLNglk7zxDtM80H518VvihTcP7FHL+Gn6g4j5fkI98+S\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertFalse(verifier.verify("bar.com", session));
+ }
+
+ @Test public void verifyNonAsciiCn() throws Exception {
+ // CN=花子.co.jp
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIESzCCAzOgAwIBAgIJAIz+EYMBU6aTMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1NDIxNVoXDTI4MTEwNTE1NDIxNVowgakx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEVMBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkB\n"
+ + "FhZqdWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n"
+ + "MIIBCgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjU\n"
+ + "g4pNjYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQc\n"
+ + "wHf0ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t\n"
+ + "7iu1JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAn\n"
+ + "AxK6q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArD\n"
+ + "qUYxqJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG\n"
+ + "CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV\n"
+ + "HQ4EFgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLS\n"
+ + "rNuzA1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBALJ27i3okV/KvlDp6KMID3gd\n"
+ + "ITl68PyItzzx+SquF8gahMh016NX73z/oVZoVUNdftla8wPUB1GwIkAnGkhQ9LHK\n"
+ + "spBdbRiCj0gMmLCsX8SrjFvr7cYb2cK6J/fJe92l1tg/7Y4o7V/s4JBe/cy9U9w8\n"
+ + "a0ctuDmEBCgC784JMDtT67klRfr/2LlqWhlOEq7pUFxRLbhpquaAHSOjmIcWnVpw\n"
+ + "9BsO7qe46hidgn39hKh1WjKK2VcL/3YRsC4wUi0PBtFW6ScMCuMhgIRXSPU55Rae\n"
+ + "UIlOdPjjr1SUNWGId1rD7W16Scpwnknn310FNxFMHVI0GTGFkNdkilNCFJcIoRA=\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+ assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+ }
+
+ @Test public void verifySubjectAlt() throws Exception {
+ // CN=foo.com, subjectAlt=bar.com
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIEXDCCA0SgAwIBAgIJAIz+EYMBU6aRMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzYyOVoXDTI4MTEwNTE1MzYyOVowgaQx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+ + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+ + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+ + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+ + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+ + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+ + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+ + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCG\n"
+ + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+ + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+ + "A1LKh6YNPg0wEgYDVR0RBAswCYIHYmFyLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEA\n"
+ + "dQyprNZBmVnvuVWjV42sey/PTfkYShJwy1j0/jcFZR/ypZUovpiHGDO1DgL3Y3IP\n"
+ + "zVQ26uhUsSw6G0gGRiaBDe/0LUclXZoJzXX1qpS55OadxW73brziS0sxRgGrZE/d\n"
+ + "3g5kkio6IED47OP6wYnlmZ7EKP9cqjWwlnvHnnUcZ2SscoLNYs9rN9ccp8tuq2by\n"
+ + "88OyhKwGjJfhOudqfTNZcDzRHx4Fzm7UsVaycVw4uDmhEHJrAsmMPpj/+XRK9/42\n"
+ + "2xq+8bc6HojdtbCyug/fvBZvZqQXSmU8m8IVcMmWMz0ZQO8ee3QkBHMZfCy7P/kr\n"
+ + "VbWx/uETImUu+NZg22ewEw==\n"
+ + "-----END CERTIFICATE-----\n");
+ assertFalse(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertTrue(verifier.verify("bar.com", session));
+ assertFalse(verifier.verify("a.bar.com", session));
+ }
+
+ /**
+ * Ignored due to incompatibilities between Android and Java on how non-ASCII
+ * subject alt names are parsed. Android fails to parse these, which means we
+ * fall back to the CN. The RI does parse them, so the CN is unused.
+ */
+ @Test @Ignore public void verifyNonAsciiSubjectAlt() throws Exception {
+ // CN=foo.com, subjectAlt=bar.com, subjectAlt=花子.co.jp
+ // (hanako.co.jp in kanji)
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIEajCCA1KgAwIBAgIJAIz+EYMBU6aSMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzgxM1oXDTI4MTEwNTE1MzgxM1owgaQx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs\n"
+ + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+ + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+ + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+ + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+ + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+ + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+ + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBnjCBmzAJBgNVHRMEAjAAMCwGCWCG\n"
+ + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+ + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+ + "A1LKh6YNPg0wIAYDVR0RBBkwF4IHYmFyLmNvbYIM6Iqx5a2QLmNvLmpwMA0GCSqG\n"
+ + "SIb3DQEBBQUAA4IBAQBeZs7ZIYyKtdnVxVvdLgwySEPOE4pBSXii7XYv0Q9QUvG/\n"
+ + "++gFGQh89HhABzA1mVUjH5dJTQqSLFvRfqTHqLpxSxSWqMHnvRM4cPBkIRp/XlMK\n"
+ + "PlXadYtJLPTgpbgvulA1ickC9EwlNYWnowZ4uxnfsMghW4HskBqaV+PnQ8Zvy3L0\n"
+ + "12c7Cg4mKKS5pb1HdRuiD2opZ+Hc77gRQLvtWNS8jQvd/iTbh6fuvTKfAOFoXw22\n"
+ + "sWIKHYrmhCIRshUNohGXv50m2o+1w9oWmQ6Dkq7lCjfXfUB4wIbggJjpyEtbNqBt\n"
+ + "j4MC2x5rfsLKKqToKmNE7pFEgqwe8//Aar1b+Qj+\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ // these checks test alternative subjects. The test data contains an
+ // alternative subject starting with a japanese kanji character. This is
+ // not supported by Android because the underlying implementation from
+ // harmony follows the definition from rfc 1034 page 10 for alternative
+ // subject names. This causes the code to drop all alternative subjects.
+ // assertTrue(verifier.verify("bar.com", session));
+ // assertFalse(verifier.verify("a.bar.com", session));
+ // assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+ }
+
+ @Test public void verifySubjectAltOnly() throws Exception {
+ // subjectAlt=foo.com
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIESjCCAzKgAwIBAgIJAIz+EYMBU6aYMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MjYxMFoXDTI4MTEwNTE2MjYxMFowgZIx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczElMCMGCSqGSIb3DQEJARYWanVsaXVzZGF2aWVzQGdtYWlsLmNv\n"
+ + "bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhjr5aCPoyp0R1iroWA\n"
+ + "fnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2BlYho4O84X244QrZTRl8kQbYt\n"
+ + "xnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRyzerA/ZtrlUqf+lKo0uWcocxe\n"
+ + "Rc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY07hNKXAb2odnVqgzcYiDkLV8\n"
+ + "ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8BqnGd87xQU3FVZI4tbtkB+Kz\n"
+ + "jD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiVJTxpTKqym93whYk93l3ocEe5\n"
+ + "5c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM\n"
+ + "IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86tso4gkJIFiza\n"
+ + "0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0wEgYDVR0RBAsw\n"
+ + "CYIHZm9vLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAjl78oMjzFdsMy6F1sGg/IkO8\n"
+ + "tF5yUgPgFYrs41yzAca7IQu6G9qtFDJz/7ehh/9HoG+oqCCIHPuIOmS7Sd0wnkyJ\n"
+ + "Y7Y04jVXIb3a6f6AgBkEFP1nOT0z6kjT7vkA5LJ2y3MiDcXuRNMSta5PYVnrX8aZ\n"
+ + "yiqVUNi40peuZ2R8mAUSBvWgD7z2qWhF8YgDb7wWaFjg53I36vWKn90ZEti3wNCw\n"
+ + "qAVqixM+J0qJmQStgAc53i2aTMvAQu3A3snvH/PHTBo+5UL72n9S1kZyNCsVf1Qo\n"
+ + "n8jKTiRriEM+fMFlcgQP284EBFzYHyCXFb9O/hMjK2+6mY9euMB1U1aFFzM/Bg==\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertTrue(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ }
+
+ @Test public void verifyMultipleCn() throws Exception {
+ // CN=foo.com, CN=bar.com, CN=花子.co.jp
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIEbzCCA1egAwIBAgIJAIz+EYMBU6aXMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTk0NVoXDTI4MTEwNTE2MTk0NVowgc0x\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEQMA4GA1UEAwwHZm9vLmNvbTEQMA4GA1UEAwwHYmFyLmNvbTEV\n"
+ + "MBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGOv\n"
+ + "loI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pNjYGViGjg7zhf\n"
+ + "bjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0ZHLN6sD9m2uV\n"
+ + "Sp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1JVjTuE0pcBva\n"
+ + "h2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6q/wGqcZ3zvFB\n"
+ + "TcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYxqJUlPGlMqrKb\n"
+ + "3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQf\n"
+ + "Fh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86\n"
+ + "tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0w\n"
+ + "DQYJKoZIhvcNAQEFBQADggEBAGuZb8ai1NO2j4v3y9TLZvd5s0vh5/TE7n7RX+8U\n"
+ + "y37OL5k7x9nt0mM1TyAKxlCcY+9h6frue8MemZIILSIvMrtzccqNz0V1WKgA+Orf\n"
+ + "uUrabmn+CxHF5gpy6g1Qs2IjVYWA5f7FROn/J+Ad8gJYc1azOWCLQqSyfpNRLSvY\n"
+ + "EriQFEV63XvkJ8JrG62b+2OT2lqT4OO07gSPetppdlSa8NBSKP6Aro9RIX1ZjUZQ\n"
+ + "SpQFCfo02NO0uNRDPUdJx2huycdNb+AXHaO7eXevDLJ+QnqImIzxWiY6zLOdzjjI\n"
+ + "VBMkLHmnP7SjGSQ3XA4ByrQOxfOUTyLyE7NuemhHppuQPxE=\n"
+ + "-----END CERTIFICATE-----\n");
+ assertFalse(verifier.verify("foo.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertFalse(verifier.verify("bar.com", session));
+ assertFalse(verifier.verify("a.bar.com", session));
+ assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+ assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
+ }
+
+ @Test public void verifyWilcardCn() throws Exception {
+ // CN=*.foo.com
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIESDCCAzCgAwIBAgIJAIz+EYMBU6aUMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTU1NVoXDTI4MTEwNTE2MTU1NVowgaYx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq\n"
+ + "dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+ + "CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN\n"
+ + "jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0\n"
+ + "ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1\n"
+ + "JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6\n"
+ + "q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx\n"
+ + "qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCG\n"
+ + "SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E\n"
+ + "FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz\n"
+ + "A1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBAH0ipG6J561UKUfgkeW7GvYwW98B\n"
+ + "N1ZooWX+JEEZK7+Pf/96d3Ij0rw9ACfN4bpfnCq0VUNZVSYB+GthQ2zYuz7tf/UY\n"
+ + "A6nxVgR/IjG69BmsBl92uFO7JTNtHztuiPqBn59pt+vNx4yPvno7zmxsfI7jv0ww\n"
+ + "yfs+0FNm7FwdsC1k47GBSOaGw38kuIVWqXSAbL4EX9GkryGGOKGNh0qvAENCdRSB\n"
+ + "G9Z6tyMbmfRY+dLSh3a9JwoEcBUso6EWYBakLbq4nG/nvYdYvG9ehrnLVwZFL82e\n"
+ + "l3Q/RK95bnA6cuRClGusLad0e6bjkBzx/VQ3VarDEpAkTLUGVAa0CLXtnyc=\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("foo.com", session));
+ assertTrue(verifier.verify("www.foo.com", session));
+ assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
+ assertFalse(verifier.verify("a.b.foo.com", session));
+ }
+
+ @Test public void verifyWilcardCnOnTld() throws Exception {
+ // It's the CA's responsibility to not issue broad-matching certificates!
+ // CN=*.co.jp
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aVMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTYzMFoXDTI4MTEwNTE2MTYzMFowgaQx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczEQMA4GA1UEAxQHKi5jby5qcDElMCMGCSqGSIb3DQEJARYWanVs\n"
+ + "aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"
+ + "ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B\n"
+ + "lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy\n"
+ + "zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY\n"
+ + "07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8\n"
+ + "BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV\n"
+ + "JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB\n"
+ + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE\n"
+ + "FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS\n"
+ + "yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQA0sWglVlMx2zNGvUqFC73XtREwii53\n"
+ + "CfMM6mtf2+f3k/d8KXhLNySrg8RRlN11zgmpPaLtbdTLrmG4UdAHHYr8O4y2BBmE\n"
+ + "1cxNfGxxechgF8HX10QV4dkyzp6Z1cfwvCeMrT5G/V1pejago0ayXx+GPLbWlNeZ\n"
+ + "S+Kl0m3p+QplXujtwG5fYcIpaGpiYraBLx3Tadih39QN65CnAh/zRDhLCUzKyt9l\n"
+ + "UGPLEUDzRHMPHLnSqT1n5UU5UDRytbjJPXzF+l/+WZIsanefWLsxnkgAuZe/oMMF\n"
+ + "EJMryEzOjg4Tfuc5qM0EXoPcQ/JlheaxZ40p2IyHqbsWV4MRYuFH4bkM\n"
+ + "-----END CERTIFICATE-----\n");
+ assertTrue(verifier.verify("foo.co.jp", session));
+ assertTrue(verifier.verify("\u82b1\u5b50.co.jp", session));
+ }
+
+ /**
+ * Ignored due to incompatibilities between Android and Java on how non-ASCII
+ * subject alt names are parsed. Android fails to parse these, which means we
+ * fall back to the CN. The RI does parse them, so the CN is unused.
+ */
+ @Test @Ignore public void testWilcardNonAsciiSubjectAlt() throws Exception {
+ // CN=*.foo.com, subjectAlt=*.bar.com, subjectAlt=*.花子.co.jp
+ // (*.hanako.co.jp in kanji)
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIEcDCCA1igAwIBAgIJAIz+EYMBU6aWMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD\n"
+ + "VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE\n"
+ + "ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU\n"
+ + "FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp\n"
+ + "ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTczMVoXDTI4MTEwNTE2MTczMVowgaYx\n"
+ + "CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0\n"
+ + "IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl\n"
+ + "cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq\n"
+ + "dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+ + "CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN\n"
+ + "jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0\n"
+ + "ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1\n"
+ + "JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6\n"
+ + "q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx\n"
+ + "qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo4GiMIGfMAkGA1UdEwQCMAAwLAYJ\n"
+ + "YIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1Ud\n"
+ + "DgQWBBSfFHe/Pzq2yjiCQkgWLNrQy16H2DAfBgNVHSMEGDAWgBR7mtqPkJlOUtKs\n"
+ + "27MDUsqHpg0+DTAkBgNVHREEHTAbggkqLmJhci5jb22CDiou6Iqx5a2QLmNvLmpw\n"
+ + "MA0GCSqGSIb3DQEBBQUAA4IBAQBobWC+D5/lx6YhX64CwZ26XLjxaE0S415ajbBq\n"
+ + "DK7lz+Rg7zOE3GsTAMi+ldUYnhyz0wDiXB8UwKXl0SDToB2Z4GOgqQjAqoMmrP0u\n"
+ + "WB6Y6dpkfd1qDRUzI120zPYgSdsXjHW9q2H77iV238hqIU7qCvEz+lfqqWEY504z\n"
+ + "hYNlknbUnR525ItosEVwXFBJTkZ3Yw8gg02c19yi8TAh5Li3Ad8XQmmSJMWBV4XK\n"
+ + "qFr0AIZKBlg6NZZFf/0dP9zcKhzSriW27bY0XfzA6GSiRDXrDjgXq6baRT6YwgIg\n"
+ + "pgJsDbJtZfHnV1nd3M6zOtQPm1TIQpNmMMMd/DPrGcUQerD3\n"
+ + "-----END CERTIFICATE-----\n");
+ // try the foo.com variations
+ assertTrue(verifier.verify("foo.com", session));
+ assertTrue(verifier.verify("www.foo.com", session));
+ assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
+ assertFalse(verifier.verify("a.b.foo.com", session));
+ // these checks test alternative subjects. The test data contains an
+ // alternative subject starting with a japanese kanji character. This is
+ // not supported by Android because the underlying implementation from
+ // harmony follows the definition from rfc 1034 page 10 for alternative
+ // subject names. This causes the code to drop all alternative subjects.
+ // assertFalse(verifier.verify("bar.com", session));
+ // assertTrue(verifier.verify("www.bar.com", session));
+ // assertTrue(verifier.verify("\u82b1\u5b50.bar.com", session));
+ // assertTrue(verifier.verify("a.b.bar.com", session));
+ }
+
+ @Test public void subjectAltUsesLocalDomainAndIp() throws Exception {
+ // cat cert.cnf
+ // [req]
+ // distinguished_name=distinguished_name
+ // req_extensions=req_extensions
+ // x509_extensions=x509_extensions
+ // [distinguished_name]
+ // [req_extensions]
+ // [x509_extensions]
+ // subjectAltName=DNS:localhost.localdomain,DNS:localhost,IP:127.0.0.1
+ //
+ // $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \
+ // -newkey rsa:512 -out cert.pem
+ X509Certificate certificate = certificate(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIBWDCCAQKgAwIBAgIJANS1EtICX2AZMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\n"
+ + "BAMTCWxvY2FsaG9zdDAgFw0xMjAxMDIxOTA4NThaGA8yMTExMTIwOTE5MDg1OFow\n"
+ + "FDESMBAGA1UEAxMJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPpt\n"
+ + "atK8r4/hf4hSIs0os/BSlQLbRBaK9AfBReM4QdAklcQqe6CHsStKfI8pp0zs7Ptg\n"
+ + "PmMdpbttL0O7mUboBC8CAwEAAaM1MDMwMQYDVR0RBCowKIIVbG9jYWxob3N0Lmxv\n"
+ + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEFBQADQQD0ntfL\n"
+ + "DCzOCv9Ma6Lv5o5jcYWVxvBSTsnt22hsJpWD1K7iY9lbkLwl0ivn73pG2evsAn9G\n"
+ + "X8YKH52fnHsCrhSD\n"
+ + "-----END CERTIFICATE-----");
+ assertEquals(new X500Principal("CN=localhost"), certificate.getSubjectX500Principal());
+
+ FakeSSLSession session = new FakeSSLSession(certificate);
+ assertTrue(verifier.verify("localhost", session));
+ assertTrue(verifier.verify("localhost.localdomain", session));
+ assertFalse(verifier.verify("local.host", session));
+
+ assertTrue(verifier.verify("127.0.0.1", session));
+ assertFalse(verifier.verify("127.0.0.2", session));
+ }
+
+ @Test public void wildcardsCannotMatchIpAddresses() throws Exception {
+ // openssl req -x509 -nodes -days 36500 -subj '/CN=*.0.0.1' -newkey rsa:512 -out cert.pem
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIBkjCCATygAwIBAgIJAMdemqOwd/BEMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNV\n"
+ + "BAMUByouMC4wLjEwIBcNMTAxMjIwMTY0NDI1WhgPMjExMDExMjYxNjQ0MjVaMBIx\n"
+ + "EDAOBgNVBAMUByouMC4wLjEwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAqY8c9Qrt\n"
+ + "YPWCvb7lclI+aDHM6fgbJcHsS9Zg8nUOh5dWrS7AgeA25wyaokFl4plBbbHQe2j+\n"
+ + "cCjsRiJIcQo9HwIDAQABo3MwcTAdBgNVHQ4EFgQUJ436TZPJvwCBKklZZqIvt1Yt\n"
+ + "JjEwQgYDVR0jBDswOYAUJ436TZPJvwCBKklZZqIvt1YtJjGhFqQUMBIxEDAOBgNV\n"
+ + "BAMUByouMC4wLjGCCQDHXpqjsHfwRDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB\n"
+ + "BQUAA0EAk9i88xdjWoewqvE+iMC9tD2obMchgFDaHH0ogxxiRaIKeEly3g0uGxIt\n"
+ + "fl2WRY8hb4x+zRrwsFaLEpdEvqcjOQ==\n"
+ + "-----END CERTIFICATE-----");
+ assertFalse(verifier.verify("127.0.0.1", session));
+ }
+
+ /**
+ * Earlier implementations of Android's hostname verifier required that
+ * wildcard names wouldn't match "*.com" or similar. This was a nonstandard
+ * check that we've since dropped. It is the CA's responsibility to not hand
+ * out certificates that match so broadly.
+ */
+ @Test public void wildcardsDoesNotNeedTwoDots() throws Exception {
+ // openssl req -x509 -nodes -days 36500 -subj '/CN=*.com' -newkey rsa:512 -out cert.pem
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIBjDCCATagAwIBAgIJAOVulXCSu6HuMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV\n"
+ + "BAMUBSouY29tMCAXDTEwMTIyMDE2NDkzOFoYDzIxMTAxMTI2MTY0OTM4WjAQMQ4w\n"
+ + "DAYDVQQDFAUqLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDJd8xqni+h7Iaz\n"
+ + "ypItivs9kPuiJUqVz+SuJ1C05SFc3PmlRCvwSIfhyD67fHcbMdl+A/LrIjhhKZJe\n"
+ + "1joO0+pFAgMBAAGjcTBvMB0GA1UdDgQWBBS4Iuzf5w8JdCp+EtBfdFNudf6+YzBA\n"
+ + "BgNVHSMEOTA3gBS4Iuzf5w8JdCp+EtBfdFNudf6+Y6EUpBIwEDEOMAwGA1UEAxQF\n"
+ + "Ki5jb22CCQDlbpVwkruh7jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA0EA\n"
+ + "U6LFxmZr31lFyis2/T68PpjAppc0DpNQuA2m/Y7oTHBDi55Fw6HVHCw3lucuWZ5d\n"
+ + "qUYo4ES548JdpQtcLrW2sA==\n"
+ + "-----END CERTIFICATE-----");
+ assertTrue(verifier.verify("google.com", session));
+ }
+
+ @Test public void subjectAltName() throws Exception {
+ // $ cat ./cert.cnf
+ // [req]
+ // distinguished_name=distinguished_name
+ // req_extensions=req_extensions
+ // x509_extensions=x509_extensions
+ // [distinguished_name]
+ // [req_extensions]
+ // [x509_extensions]
+ // subjectAltName=DNS:bar.com,DNS:baz.com
+ //
+ // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \
+ // -newkey rsa:512 -out cert.pem
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIBPTCB6KADAgECAgkA7zoHaaqNGHQwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE\n"
+ + "AxMHZm9vLmNvbTAgFw0xMDEyMjAxODM5MzZaGA8yMTEwMTEyNjE4MzkzNlowEjEQ\n"
+ + "MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC+gmoSxF+8\n"
+ + "hbV+rgRQqHIJd50216OWQJbU3BvdlPbca779NYO4+UZWTFdBM8BdQqs3H4B5Agvp\n"
+ + "y7HeSff1F7XRAgMBAAGjHzAdMBsGA1UdEQQUMBKCB2Jhci5jb22CB2Jhei5jb20w\n"
+ + "DQYJKoZIhvcNAQEFBQADQQBXpZZPOY2Dy1lGG81JTr8L4or9jpKacD7n51eS8iqI\n"
+ + "oTznPNuXHU5bFN0AAGX2ij47f/EahqTpo5RdS95P4sVm\n"
+ + "-----END CERTIFICATE-----");
+ assertFalse(verifier.verify("foo.com", session));
+ assertTrue(verifier.verify("bar.com", session));
+ assertTrue(verifier.verify("baz.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertFalse(verifier.verify("quux.com", session));
+ }
+
+ @Test public void subjectAltNameWithWildcard() throws Exception {
+ // $ cat ./cert.cnf
+ // [req]
+ // distinguished_name=distinguished_name
+ // req_extensions=req_extensions
+ // x509_extensions=x509_extensions
+ // [distinguished_name]
+ // [req_extensions]
+ // [x509_extensions]
+ // subjectAltName=DNS:bar.com,DNS:*.baz.com
+ //
+ // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \
+ // -newkey rsa:512 -out cert.pem
+ SSLSession session = session(""
+ + "-----BEGIN CERTIFICATE-----\n"
+ + "MIIBPzCB6qADAgECAgkAnv/7Jv5r7pMwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE\n"
+ + "AxMHZm9vLmNvbTAgFw0xMDEyMjAxODQ2MDFaGA8yMTEwMTEyNjE4NDYwMVowEjEQ\n"
+ + "MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDAz2YXnyog\n"
+ + "YdYLSFr/OEgSumtwqtZKJTB4wqTW/eKbBCEzxnyUMxWZIqUGu353PzwfOuWp2re3\n"
+ + "nvVV+QDYQlh9AgMBAAGjITAfMB0GA1UdEQQWMBSCB2Jhci5jb22CCSouYmF6LmNv\n"
+ + "bTANBgkqhkiG9w0BAQUFAANBAB8yrSl8zqy07i0SNYx2B/FnvQY734pxioaqFWfO\n"
+ + "Bqo1ZZl/9aPHEWIwBrxYNVB0SGu/kkbt/vxqOjzzrkXukmI=\n"
+ + "-----END CERTIFICATE-----");
+ assertFalse(verifier.verify("foo.com", session));
+ assertTrue(verifier.verify("bar.com", session));
+ assertTrue(verifier.verify("a.baz.com", session));
+ assertTrue(verifier.verify("baz.com", session));
+ assertFalse(verifier.verify("a.foo.com", session));
+ assertFalse(verifier.verify("a.bar.com", session));
+ assertFalse(verifier.verify("quux.com", session));
+ }
+
+ @Test public void verifyAsIpAddress() {
+ // IPv4
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("127.0.0.1"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("1.2.3.4"));
+
+ // IPv6
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("::1"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("2001:db8::1"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("::192.168.0.1"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("::ffff:192.168.0.1"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080:0:0:0:8:800:200C:417A"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080::8:800:200C:417A"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("FF01::101"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:0:13.1.68.3"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:FFFF:129.144.52.38"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("::13.1.68.3"));
+ assertTrue(OkHostnameVerifier.verifyAsIpAddress("::FFFF:129.144.52.38"));
+
+ // Hostnames
+ assertFalse(OkHostnameVerifier.verifyAsIpAddress("go"));
+ assertFalse(OkHostnameVerifier.verifyAsIpAddress("localhost"));
+ assertFalse(OkHostnameVerifier.verifyAsIpAddress("squareup.com"));
+ assertFalse(OkHostnameVerifier.verifyAsIpAddress("www.nintendo.co.jp"));
+ }
+
+ private X509Certificate certificate(String certificate) throws Exception {
+ return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(
+ new ByteArrayInputStream(certificate.getBytes(Util.UTF_8)));
+ }
+
+ private SSLSession session(String certificate) throws Exception {
+ return new FakeSSLSession(certificate(certificate));
+ }
+}