[CB-2431] Update to okhttp to include jwilson's recovery fixes [f38fec5b]

This commit is contained in:
Ian Clelland 2013-04-26 00:27:43 -04:00
parent 553a25cea7
commit cbb0bd5ee7
24 changed files with 1706 additions and 392 deletions

View File

@ -24,11 +24,11 @@ import com.squareup.okhttp.internal.http.RawHeaders;
import com.squareup.okhttp.internal.http.SpdyTransport; import com.squareup.okhttp.internal.http.SpdyTransport;
import com.squareup.okhttp.internal.spdy.SpdyConnection; import com.squareup.okhttp.internal.spdy.SpdyConnection;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
import java.net.Socket; import java.net.Socket;
import java.net.URL; import java.net.URL;
@ -76,10 +76,7 @@ public final class Connection implements Closeable {
'h', 't', 't', 'p', '/', '1', '.', '1' 'h', 't', 't', 'p', '/', '1', '.', '1'
}; };
private final Address address; private final Route route;
private final Proxy proxy;
private final InetSocketAddress inetSocketAddress;
private final boolean modernTls;
private Socket socket; private Socket socket;
private InputStream in; private InputStream in;
@ -89,15 +86,8 @@ public final class Connection implements Closeable {
private int httpMinorVersion = 1; // Assume HTTP/1.1 private int httpMinorVersion = 1; // Assume HTTP/1.1
private long idleStartTimeNs; private long idleStartTimeNs;
public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, public Connection(Route route) {
boolean modernTls) { this.route = route;
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 void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
@ -106,21 +96,20 @@ public final class Connection implements Closeable {
throw new IllegalStateException("already connected"); throw new IllegalStateException("already connected");
} }
connected = true; connected = true;
socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket(); socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
socket.connect(inetSocketAddress, connectTimeout); socket.connect(route.inetSocketAddress, connectTimeout);
socket.setSoTimeout(readTimeout); socket.setSoTimeout(readTimeout);
in = socket.getInputStream(); in = socket.getInputStream();
out = socket.getOutputStream(); out = socket.getOutputStream();
if (address.sslSocketFactory != null) { if (route.address.sslSocketFactory != null) {
upgradeToTls(tunnelRequest); upgradeToTls(tunnelRequest);
} }
// Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes. // Use MTU-sized buffers to send fewer packets.
if (!isSpdy()) { int mtu = Platform.get().getMtu(socket);
int bufferSize = 128; in = new BufferedInputStream(in, mtu);
in = new BufferedInputStream(in, bufferSize); out = new BufferedOutputStream(out, mtu);
}
} }
/** /**
@ -136,16 +125,16 @@ public final class Connection implements Closeable {
} }
// Create the wrapper over connected socket. // Create the wrapper over connected socket.
socket = address.sslSocketFactory socket = route.address.sslSocketFactory
.createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */); .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
SSLSocket sslSocket = (SSLSocket) socket; SSLSocket sslSocket = (SSLSocket) socket;
if (modernTls) { if (route.modernTls) {
platform.enableTlsExtensions(sslSocket, address.uriHost); platform.enableTlsExtensions(sslSocket, route.address.uriHost);
} else { } else {
platform.supportTlsIntolerantServer(sslSocket); platform.supportTlsIntolerantServer(sslSocket);
} }
if (modernTls) { if (route.modernTls) {
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
} }
@ -153,18 +142,20 @@ public final class Connection implements Closeable {
sslSocket.startHandshake(); sslSocket.startHandshake();
// Verify that the socket's certificates are acceptable for the target host. // Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
throw new IOException("Hostname '" + address.uriHost + "' was not verified"); throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
} }
out = sslSocket.getOutputStream(); out = sslSocket.getOutputStream();
in = sslSocket.getInputStream(); in = sslSocket.getInputStream();
byte[] selectedProtocol; byte[] selectedProtocol;
if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { if (route.modernTls
&& (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (Arrays.equals(selectedProtocol, SPDY3)) { if (Arrays.equals(selectedProtocol, SPDY3)) {
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. 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)) { } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
throw new IOException( throw new IOException(
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
@ -181,29 +172,9 @@ public final class Connection implements Closeable {
socket.close(); socket.close();
} }
/** /** Returns the route used by this connection. */
* Returns the proxy that this connection is using. public Route getRoute() {
* return 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;
}
public Address getAddress() {
return address;
}
public InetSocketAddress getSocketAddress() {
return inetSocketAddress;
}
public boolean isModernTls() {
return modernTls;
} }
/** /**
@ -284,7 +255,7 @@ public final class Connection implements Closeable {
* we must avoid buffering bytes intended for the higher-level protocol. * we must avoid buffering bytes intended for the higher-level protocol.
*/ */
public boolean requiresTunnel() { 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;
} }
/** /**
@ -304,9 +275,8 @@ public final class Connection implements Closeable {
case HTTP_PROXY_AUTH: case HTTP_PROXY_AUTH:
requestHeaders = new RawHeaders(requestHeaders); requestHeaders = new RawHeaders(requestHeaders);
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
boolean credentialsFound = boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH,
HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders, responseHeaders, requestHeaders, route.proxy, url);
proxy, url);
if (credentialsFound) { if (credentialsFound) {
continue; continue;
} else { } else {

View File

@ -1,3 +1,19 @@
/*
* 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; package com.squareup.okhttp;
import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Platform;
@ -164,7 +180,7 @@ public class ConnectionPool {
for (ListIterator<Connection> i = connections.listIterator(connections.size()); for (ListIterator<Connection> i = connections.listIterator(connections.size());
i.hasPrevious(); ) { i.hasPrevious(); ) {
Connection connection = i.previous(); Connection connection = i.previous();
if (!connection.getAddress().equals(address) if (!connection.getRoute().getAddress().equals(address)
|| !connection.isAlive() || !connection.isAlive()
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) { || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
continue; continue;

View File

@ -0,0 +1,693 @@
/*
* Copyright (C) 2010 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 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;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.ResponseCache;
import java.net.SecureCacheResponse;
import java.net.URI;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import static com.squareup.okhttp.internal.Util.US_ASCII;
import static com.squareup.okhttp.internal.Util.UTF_8;
/**
* 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 {
private static final char[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
// TODO: add APIs to iterate the cache?
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
private static final int ENTRY_BODY = 1;
private static final int ENTRY_COUNT = 2;
private final DiskLruCache cache;
/* read and write statistics, all guarded by 'this' */
private int writeSuccessCount;
private int writeAbortCount;
private int networkCount;
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);
}
private String uriToKey(URI uri) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
return bytesToHexString(md5bytes);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private static String bytesToHexString(byte[] bytes) {
char[] digits = DIGITS;
char[] buf = new char[bytes.length * 2];
int c = 0;
for (byte b : bytes) {
buf[c++] = digits[(b >> 4) & 0xf];
buf[c++] = digits[b & 0xf];
}
return new String(buf);
}
@Override public CacheResponse get(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) {
String key = uriToKey(uri);
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
if (!entry.matches(uri, requestMethod, requestHeaders)) {
snapshot.close();
return null;
}
return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
: new EntryCacheResponse(entry, snapshot);
}
@Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
if (!(urlConnection instanceof HttpURLConnection)) {
return null;
}
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
String requestMethod = httpConnection.getRequestMethod();
String key = uriToKey(uri);
if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
"DELETE")) {
try {
cache.remove(key);
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
} else if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
HttpEngine httpEngine = getHttpEngine(httpConnection);
if (httpEngine == null) {
// Don't cache unless the HTTP implementation is ours.
return null;
}
ResponseHeaders response = httpEngine.getResponseHeaders();
if (response.hasVaryAll()) {
return null;
}
RawHeaders varyHeaders =
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
Entry entry = new Entry(uri, varyHeaders, httpConnection);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key);
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException {
HttpEngine httpEngine = getHttpEngine(httpConnection);
URI uri = httpEngine.getUri();
ResponseHeaders response = httpEngine.getResponseHeaders();
RawHeaders varyHeaders =
httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
Entry entry = new Entry(uri, varyHeaders, httpConnection);
DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
? ((EntryCacheResponse) conditionalCacheHit).snapshot
: ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
DiskLruCache.Editor editor = null;
try {
editor = snapshot.edit(); // returns null if snapshot is not current
if (editor != null) {
entry.writeTo(editor);
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
private void abortQuietly(DiskLruCache.Editor editor) {
// Give up because the cache cannot be written.
try {
if (editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
}
private HttpEngine getHttpEngine(URLConnection httpConnection) {
if (httpConnection instanceof HttpURLConnectionImpl) {
return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
} else if (httpConnection instanceof HttpsURLConnectionImpl) {
return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
} else {
return null;
}
}
/**
* 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() {
return writeAbortCount;
}
public synchronized int getWriteSuccessCount() {
return writeSuccessCount;
}
private synchronized void trackResponse(ResponseSource source) {
requestCount++;
switch (source) {
case CACHE:
hitCount++;
break;
case CONDITIONAL_CACHE:
case NETWORK:
networkCount++;
break;
}
}
private synchronized void trackConditionalCacheHit() {
hitCount++;
}
public synchronized int getNetworkCount() {
return networkCount;
}
public synchronized int getHitCount() {
return hitCount;
}
public synchronized int getRequestCount() {
return requestCount;
}
private final class CacheRequestImpl extends CacheRequest {
private final DiskLruCache.Editor editor;
private OutputStream cacheOut;
private boolean done;
private OutputStream body;
public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
this.editor = editor;
this.cacheOut = editor.newOutputStream(ENTRY_BODY);
this.body = new FilterOutputStream(cacheOut) {
@Override public void close() throws IOException {
synchronized (HttpResponseCache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
// Since we don't override "write(int oneByte)", we can write directly to "out"
// and avoid the inefficient implementation from the FilterOutputStream.
out.write(buffer, offset, length);
}
};
}
@Override public void abort() {
synchronized (HttpResponseCache.this) {
if (done) {
return;
}
done = true;
writeAbortCount++;
}
Util.closeQuietly(cacheOut);
try {
editor.abort();
} catch (IOException ignored) {
}
}
@Override public OutputStream getBody() throws IOException {
return body;
}
}
private static final class Entry {
private final String uri;
private final RawHeaders varyHeaders;
private final String requestMethod;
private final RawHeaders responseHeaders;
private final String cipherSuite;
private final Certificate[] peerCertificates;
private final Certificate[] localCertificates;
/**
* Reads an entry from an input stream. A typical entry looks like this:
* <pre>{@code
* http://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
* }</pre>
*
* <p>A typical HTTPS file looks like this:
* <pre>{@code
* https://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
*
* AES_256_WITH_MD5
* 2
* base64-encoded peerCertificate[0]
* base64-encoded peerCertificate[1]
* -1
* }</pre>
* The file is newline separated. The first two lines are the URL and
* the request method. Next is the number of HTTP Vary request header
* lines, followed by those lines.
*
* <p>Next is the response status line, followed by the number of HTTP
* response header lines, followed by those lines.
*
* <p>HTTPS responses also contain SSL session information. This begins
* with a blank line, and then a line containing the cipher suite. Next
* is the length of the peer certificate chain. These certificates are
* base64-encoded and appear each on their own line. The next line
* contains the length of the local certificate chain. These
* certificates are also base64-encoded and appear each on their own
* line. A length of -1 is used to encode a null array.
*/
public Entry(InputStream in) throws IOException {
try {
StrictLineReader reader = new StrictLineReader(in, US_ASCII);
uri = reader.readLine();
requestMethod = reader.readLine();
varyHeaders = new RawHeaders();
int varyRequestHeaderLineCount = reader.readInt();
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
varyHeaders.addLine(reader.readLine());
}
responseHeaders = new RawHeaders();
responseHeaders.setStatusLine(reader.readLine());
int responseHeaderLineCount = reader.readInt();
for (int i = 0; i < responseHeaderLineCount; i++) {
responseHeaders.addLine(reader.readLine());
}
if (isHttps()) {
String blank = reader.readLine();
if (blank.length() > 0) {
throw new IOException("expected \"\" but was \"" + blank + "\"");
}
cipherSuite = reader.readLine();
peerCertificates = readCertArray(reader);
localCertificates = readCertArray(reader);
} else {
cipherSuite = null;
peerCertificates = null;
localCertificates = null;
}
} finally {
in.close();
}
}
public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
throws IOException {
this.uri = uri.toString();
this.varyHeaders = varyHeaders;
this.requestMethod = httpConnection.getRequestMethod();
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
if (isHttps()) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
cipherSuite = httpsConnection.getCipherSuite();
Certificate[] peerCertificatesNonFinal = null;
try {
peerCertificatesNonFinal = httpsConnection.getServerCertificates();
} catch (SSLPeerUnverifiedException ignored) {
}
peerCertificates = peerCertificatesNonFinal;
localCertificates = httpsConnection.getLocalCertificates();
} else {
cipherSuite = null;
peerCertificates = null;
localCertificates = null;
}
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
writer.write(uri + '\n');
writer.write(requestMethod + '\n');
writer.write(Integer.toString(varyHeaders.length()) + '\n');
for (int i = 0; i < varyHeaders.length(); i++) {
writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
}
writer.write(responseHeaders.getStatusLine() + '\n');
writer.write(Integer.toString(responseHeaders.length()) + '\n');
for (int i = 0; i < responseHeaders.length(); i++) {
writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
}
if (isHttps()) {
writer.write('\n');
writer.write(cipherSuite + '\n');
writeCertArray(writer, peerCertificates);
writeCertArray(writer, localCertificates);
}
writer.close();
}
private boolean isHttps() {
return uri.startsWith("https://");
}
private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
int length = reader.readInt();
if (length == -1) {
return null;
}
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate[] result = new Certificate[length];
for (int i = 0; i < result.length; i++) {
String line = reader.readLine();
byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
}
return result;
} catch (CertificateException e) {
throw new IOException(e.getMessage());
}
}
private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
if (certificates == null) {
writer.write("-1\n");
return;
}
try {
writer.write(Integer.toString(certificates.length) + '\n');
for (Certificate certificate : certificates) {
byte[] bytes = certificate.getEncoded();
String line = Base64.encode(bytes);
writer.write(line + '\n');
}
} catch (CertificateEncodingException e) {
throw new IOException(e.getMessage());
}
}
public boolean matches(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) {
return this.uri.equals(uri.toString())
&& this.requestMethod.equals(requestMethod)
&& new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
requestHeaders);
}
}
/**
* Returns an input stream that reads the body of a snapshot, closing the
* snapshot when the stream is closed.
*/
private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
};
}
static class EntryCacheResponse extends CacheResponse {
private final Entry entry;
private final DiskLruCache.Snapshot snapshot;
private final InputStream in;
public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
this.entry = entry;
this.snapshot = snapshot;
this.in = newBodyInputStream(snapshot);
}
@Override public Map<String, List<String>> getHeaders() {
return entry.responseHeaders.toMultimap(true);
}
@Override public InputStream getBody() {
return in;
}
}
static class EntrySecureCacheResponse extends SecureCacheResponse {
private final Entry entry;
private final DiskLruCache.Snapshot snapshot;
private final InputStream in;
public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
this.entry = entry;
this.snapshot = snapshot;
this.in = newBodyInputStream(snapshot);
}
@Override public Map<String, List<String>> getHeaders() {
return entry.responseHeaders.toMultimap(true);
}
@Override public InputStream getBody() {
return in;
}
@Override public String getCipherSuite() {
return entry.cipherSuite;
}
@Override public List<Certificate> getServerCertificateChain()
throws SSLPeerUnverifiedException {
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
throw new SSLPeerUnverifiedException(null);
}
return Arrays.asList(entry.peerCertificates.clone());
}
@Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
throw new SSLPeerUnverifiedException(null);
}
return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
}
@Override public List<Certificate> getLocalCertificateChain() {
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
return null;
}
return Arrays.asList(entry.localCertificates.clone());
}
@Override public Principal getLocalPrincipal() {
if (entry.localCertificates == null || entry.localCertificates.length == 0) {
return null;
}
return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
}
}
}

View File

@ -17,12 +17,17 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
import com.squareup.okhttp.internal.http.OkResponseCache;
import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.Proxy; import java.net.Proxy;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.ResponseCache; import java.net.ResponseCache;
import java.net.URL; import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
@ -30,6 +35,7 @@ import javax.net.ssl.SSLSocketFactory;
/** Configures and creates HTTP connections. */ /** Configures and creates HTTP connections. */
public final class OkHttpClient { public final class OkHttpClient {
private Proxy proxy; private Proxy proxy;
private Set<Route> failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
private ProxySelector proxySelector; private ProxySelector proxySelector;
private CookieHandler cookieHandler; private CookieHandler cookieHandler;
private ResponseCache responseCache; private ResponseCache responseCache;
@ -102,6 +108,16 @@ public final class OkHttpClient {
return responseCache; 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. * Sets the socket factory used to secure HTTPS connections.
* *
@ -166,22 +182,24 @@ public final class OkHttpClient {
public HttpURLConnection open(URL url) { public HttpURLConnection open(URL url) {
String protocol = url.getProtocol(); String protocol = url.getProtocol();
OkHttpClient copy = copyWithDefaults();
if (protocol.equals("http")) { if (protocol.equals("http")) {
return new HttpURLConnectionImpl(url, copyWithDefaults()); return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else if (protocol.equals("https")) { } else if (protocol.equals("https")) {
return new HttpsURLConnectionImpl(url, copyWithDefaults()); return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else { } else {
throw new IllegalArgumentException("Unexpected protocol: " + protocol); 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. * each field that hasn't been explicitly configured.
*/ */
private OkHttpClient copyWithDefaults() { private OkHttpClient copyWithDefaults() {
OkHttpClient result = new OkHttpClient(); OkHttpClient result = new OkHttpClient();
result.proxy = proxy; result.proxy = proxy;
result.failedRoutes = failedRoutes;
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault(); result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault(); result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault(); result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();

View File

@ -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;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2010 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.IOException;
import java.io.OutputStream;
/**
* 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.
*/
public abstract class AbstractOutputStream extends OutputStream {
protected boolean closed;
@Override public final void write(int data) throws IOException {
write(new byte[] { (byte) data });
}
protected final void checkNotClosed() throws IOException {
if (closed) {
throw new IOException("stream closed");
}
}
/** Returns true if this stream was closed locally. */
public boolean isClosed() {
return closed;
}
}

View File

@ -23,7 +23,6 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream; import java.io.FilterOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -32,23 +31,22 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import static com.squareup.okhttp.internal.Util.UTF_8; import java.util.regex.Pattern;
/** /**
* A cache that uses a bounded amount of space on a filesystem. Each cache * 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 * entry has a string key and a fixed number of values. Each key must match
* sequences, accessible as streams or files. Each value must be between {@code * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
* 0} and {@code Integer.MAX_VALUE} bytes in length. * 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 * <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 * directory must be exclusive to the cache; the cache may delete or overwrite
@ -66,12 +64,12 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
* entry may have only one editor at one time; if a value is not available to be * entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null. * edited then {@link #edit} will return null.
* <ul> * <ul>
* <li>When an entry is being <strong>created</strong> it is necessary to * <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 * supply a full set of values; the empty value should be used as a
* placeholder if necessary. * placeholder if necessary.
* <li>When an entry is being <strong>edited</strong>, it is not 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 * to supply data for every value; values default to their previous
* value. * value.
* </ul> * </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit} * 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 * or {@link Editor#abort}. Committing is atomic: a read observes the full set
@ -89,58 +87,63 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
*/ */
public final class DiskLruCache implements Closeable { public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal"; 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 MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1"; static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -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 CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY"; private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE"; private static final String REMOVE = "REMOVE";
private static final String READ = "READ"; private static final String READ = "READ";
// This cache uses a journal file named "journal". A typical journal file /*
// looks like this: * This cache uses a journal file named "journal". A typical journal file
// libcore.io.DiskLruCache * looks like this:
// 1 * libcore.io.DiskLruCache
// 100 * 1
// 2 * 100
// * 2
// CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 *
// DIRTY 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
// CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 * DIRTY 335c4c6028171cfddfbaae1a9c313c52
// REMOVE 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
// DIRTY 1ab96a171faeeee38496d8b330771a7a * REMOVE 335c4c6028171cfddfbaae1a9c313c52
// CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 * DIRTY 1ab96a171faeeee38496d8b330771a7a
// READ 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
// READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 * 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 first five lines of the journal form its header. They are the
// the application's version, the value count, and a blank line. * 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, * Each of the subsequent lines in the file is a record of the state of a
// and optional state-specific values. * cache entry. Each line contains space-separated values: a state, a key,
// o DIRTY lines track that an entry is actively being created or updated. * and optional state-specific values.
// Every successful DIRTY action should be followed by a CLEAN or REMOVE * o DIRTY lines track that an entry is actively being created or updated.
// action. DIRTY lines without a matching CLEAN or REMOVE indicate that * Every successful DIRTY action should be followed by a CLEAN or REMOVE
// temporary files may need to be deleted. * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
// o CLEAN lines track a cache entry that has been successfully published * temporary files may need to be deleted.
// and may be read. A publish line is followed by the lengths of each of * o CLEAN lines track a cache entry that has been successfully published
// its values. * and may be read. A publish line is followed by the lengths of each of
// o READ lines track accesses for LRU. * its values.
// o REMOVE lines track entries that have been deleted. * 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 * The journal file is appended to as cache operations occur. The journal may
// "journal.tmp" will be used during compaction; that file should be deleted if * occasionally be compacted by dropping redundant lines. A temporary file named
// it exists when the cache is opened. * "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 directory;
private final File journalFile; private final File journalFile;
private final File journalFileTmp; private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion; private final int appVersion;
private final long maxSize; private long maxSize;
private final int valueCount; private final int valueCount;
private long size = 0; private long size = 0;
private Writer journalWriter; private Writer journalWriter;
@ -156,13 +159,13 @@ public final class DiskLruCache implements Closeable {
private long nextSequenceNumber = 0; private long nextSequenceNumber = 0;
/** This cache uses a single background thread to evict entries. */ /** 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>()); new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() { private final Callable<Void> cleanupCallable = new Callable<Void>() {
@Override public Void call() throws Exception { public Void call() throws Exception {
synchronized (DiskLruCache.this) { synchronized (DiskLruCache.this) {
if (journalWriter == null) { if (journalWriter == null) {
return null; // closed return null; // Closed.
} }
trimToSize(); trimToSize();
if (journalRebuildRequired()) { if (journalRebuildRequired()) {
@ -178,7 +181,8 @@ public final class DiskLruCache implements Closeable {
this.directory = directory; this.directory = directory;
this.appVersion = appVersion; this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE); 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.valueCount = valueCount;
this.maxSize = maxSize; this.maxSize = maxSize;
} }
@ -201,26 +205,35 @@ public final class DiskLruCache implements Closeable {
throw new IllegalArgumentException("valueCount <= 0"); 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); DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) { if (cache.journalFile.exists()) {
try { try {
cache.readJournal(); cache.readJournal();
cache.processJournal(); 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; return cache;
} catch (IOException journalIsCorrupt) { } catch (IOException journalIsCorrupt) {
Platform.get() Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
.logW("DiskLruCache " + journalIsCorrupt.getMessage() + ", removing");
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete(); cache.delete();
} }
} }
// create a new empty cache // Create a new empty cache.
directory.mkdirs(); directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal(); cache.rebuildJournal();
@ -235,42 +248,47 @@ public final class DiskLruCache implements Closeable {
String appVersionString = reader.readLine(); String appVersionString = reader.readLine();
String valueCountString = reader.readLine(); String valueCountString = reader.readLine();
String blank = reader.readLine(); String blank = reader.readLine();
if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion) if (!MAGIC.equals(magic)
.equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !"" || !VERSION_1.equals(version)
.equals(blank)) { || !Integer.toString(appVersion).equals(appVersionString)
throw new IOException("unexpected journal header: [" || !Integer.toString(valueCount).equals(valueCountString)
+ magic || !"".equals(blank)) {
+ ", " throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ version + valueCountString + ", " + blank + "]");
+ ", "
+ valueCountString
+ ", "
+ blank
+ "]");
} }
int lineCount = 0;
while (true) { while (true) {
try { try {
readJournalLine(reader.readLine()); readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) { } catch (EOFException endOfJournal) {
break; break;
} }
} }
redundantOpCount = lineCount - lruEntries.size();
} finally { } finally {
Util.closeQuietly(reader); Util.closeQuietly(reader);
} }
} }
private void readJournalLine(String line) throws IOException { private void readJournalLine(String line) throws IOException {
String[] parts = line.split(" "); int firstSpace = line.indexOf(' ');
if (parts.length < 2) { if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line); throw new IOException("unexpected journal line: " + line);
} }
String key = parts[1]; int keyBegin = firstSpace + 1;
if (parts[0].equals(REMOVE) && parts.length == 2) { int secondSpace = line.indexOf(' ', keyBegin);
lruEntries.remove(key); final String key;
return; 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); Entry entry = lruEntries.get(key);
@ -279,14 +297,15 @@ public final class DiskLruCache implements Closeable {
lruEntries.put(key, entry); 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.readable = true;
entry.currentEditor = null; entry.currentEditor = null;
entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); entry.setLengths(parts);
} else if (parts[0].equals(DIRTY) && parts.length == 2) { } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry); entry.currentEditor = new Editor(entry);
} else if (parts[0].equals(READ) && parts.length == 2) { } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// this work was already done by calling lruEntries.get() // This work was already done by calling lruEntries.get().
} else { } else {
throw new IOException("unexpected journal line: " + line); throw new IOException("unexpected journal line: " + line);
} }
@ -324,32 +343,53 @@ public final class DiskLruCache implements Closeable {
journalWriter.close(); journalWriter.close();
} }
Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); Writer writer = new BufferedWriter(
writer.write(MAGIC); new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
writer.write("\n"); try {
writer.write(VERSION_1); writer.write(MAGIC);
writer.write("\n"); writer.write("\n");
writer.write(Integer.toString(appVersion)); writer.write(VERSION_1);
writer.write("\n"); writer.write("\n");
writer.write(Integer.toString(valueCount)); writer.write(Integer.toString(appVersion));
writer.write("\n"); writer.write("\n");
writer.write("\n"); writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) { for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) { if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n'); writer.write(DIRTY + ' ' + entry.key + '\n');
} else { } else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
} }
} finally {
writer.close();
} }
writer.close(); if (journalFile.exists()) {
journalFileTmp.renameTo(journalFile); renameTo(journalFile, journalFileBackup, true);
journalWriter = new BufferedWriter(new FileWriter(journalFile, 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 { 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 @@ public final class DiskLruCache implements Closeable {
ins[i] = new FileInputStream(entry.getCleanFile(i)); ins[i] = new FileInputStream(entry.getCleanFile(i));
} }
} catch (FileNotFoundException e) { } 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; return null;
} }
@ -388,7 +435,7 @@ public final class DiskLruCache implements Closeable {
executorService.submit(cleanupCallable); executorService.submit(cleanupCallable);
} }
return new Snapshot(key, entry.sequenceNumber, ins); return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
} }
/** /**
@ -405,19 +452,19 @@ public final class DiskLruCache implements Closeable {
Entry entry = lruEntries.get(key); Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) { || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // snapshot is stale return null; // Snapshot is stale.
} }
if (entry == null) { if (entry == null) {
entry = new Entry(key); entry = new Entry(key);
lruEntries.put(key, entry); lruEntries.put(key, entry);
} else if (entry.currentEditor != null) { } else if (entry.currentEditor != null) {
return null; // another edit is in progress return null; // Another edit is in progress.
} }
Editor editor = new Editor(entry); Editor editor = new Editor(entry);
entry.currentEditor = editor; 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.write(DIRTY + ' ' + key + '\n');
journalWriter.flush(); journalWriter.flush();
return editor; return editor;
@ -432,10 +479,19 @@ public final class DiskLruCache implements Closeable {
* Returns the maximum number of bytes that this cache should use to store * Returns the maximum number of bytes that this cache should use to store
* its data. * its data.
*/ */
public long maxSize() { public long getMaxSize() {
return maxSize; 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 * 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 * this cache. This may be greater than the max size if a background
@ -451,7 +507,7 @@ public final class DiskLruCache implements Closeable {
throw new IllegalStateException(); 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) { if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) { for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) { if (!editor.written[i]) {
@ -460,7 +516,6 @@ public final class DiskLruCache implements Closeable {
} }
if (!entry.getDirtyFile(i).exists()) { if (!entry.getDirtyFile(i).exists()) {
editor.abort(); editor.abort();
Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
return; return;
} }
} }
@ -494,6 +549,7 @@ public final class DiskLruCache implements Closeable {
lruEntries.remove(entry.key); lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n'); journalWriter.write(REMOVE + ' ' + entry.key + '\n');
} }
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) { if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable); executorService.submit(cleanupCallable);
@ -506,7 +562,8 @@ public final class DiskLruCache implements Closeable {
*/ */
private boolean journalRebuildRequired() { private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000; final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size(); return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
} }
/** /**
@ -564,7 +621,7 @@ public final class DiskLruCache implements Closeable {
/** Closes this cache. Stored values will remain on the filesystem. */ /** Closes this cache. Stored values will remain on the filesystem. */
public synchronized void close() throws IOException { public synchronized void close() throws IOException {
if (journalWriter == null) { if (journalWriter == null) {
return; // already closed return; // Already closed.
} }
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
if (entry.currentEditor != null) { if (entry.currentEditor != null) {
@ -594,14 +651,14 @@ public final class DiskLruCache implements Closeable {
} }
private void validateKey(String key) { private void validateKey(String key) {
if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
throw new IllegalArgumentException( if (!matcher.matches()) {
"keys must not contain spaces or newlines: \"" + key + "\""); throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
} }
} }
private static String inputStreamToString(InputStream in) throws IOException { 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. */ /** A snapshot of the values for an entry. */
@ -609,11 +666,13 @@ public final class DiskLruCache implements Closeable {
private final String key; private final String key;
private final long sequenceNumber; private final long sequenceNumber;
private final InputStream[] ins; 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.key = key;
this.sequenceNumber = sequenceNumber; this.sequenceNumber = sequenceNumber;
this.ins = ins; this.ins = ins;
this.lengths = lengths;
} }
/** /**
@ -635,18 +694,31 @@ public final class DiskLruCache implements Closeable {
return inputStreamToString(getInputStream(index)); 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) { for (InputStream in : ins) {
Util.closeQuietly(in); 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. */ /** Edits the values for an entry. */
public final class Editor { public final class Editor {
private final Entry entry; private final Entry entry;
private final boolean[] written; private final boolean[] written;
private boolean hasErrors; private boolean hasErrors;
private boolean committed;
private Editor(Entry entry) { private Editor(Entry entry) {
this.entry = entry; this.entry = entry;
@ -665,7 +737,11 @@ public final class DiskLruCache implements Closeable {
if (!entry.readable) { if (!entry.readable) {
return null; return null;
} }
return new FileInputStream(entry.getCleanFile(index)); try {
return new FileInputStream(entry.getCleanFile(index));
} catch (FileNotFoundException e) {
return null;
}
} }
} }
@ -693,7 +769,21 @@ public final class DiskLruCache implements Closeable {
if (!entry.readable) { if (!entry.readable) {
written[index] = true; 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 final class DiskLruCache implements Closeable {
public void set(int index, String value) throws IOException { public void set(int index, String value) throws IOException {
Writer writer = null; Writer writer = null;
try { try {
writer = new OutputStreamWriter(newOutputStream(index), UTF_8); writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value); writer.write(value);
} finally { } finally {
Util.closeQuietly(writer); Util.closeQuietly(writer);
@ -715,10 +805,11 @@ public final class DiskLruCache implements Closeable {
public void commit() throws IOException { public void commit() throws IOException {
if (hasErrors) { if (hasErrors) {
completeEdit(this, false); completeEdit(this, false);
remove(entry.key); // the previous entry is stale remove(entry.key); // The previous entry is stale.
} else { } else {
completeEdit(this, true); completeEdit(this, true);
} }
committed = true;
} }
/** /**
@ -729,7 +820,16 @@ public final class DiskLruCache implements Closeable {
completeEdit(this, false); 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) { private FaultHidingOutputStream(OutputStream out) {
super(out); super(out);
} }
@ -812,7 +912,7 @@ public final class DiskLruCache implements Closeable {
} }
private IOException invalidLengths(String[] strings) throws IOException { 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) { public File getCleanFile(int i) {

View File

@ -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;
}

View File

@ -17,6 +17,7 @@
package com.squareup.okhttp.internal; package com.squareup.okhttp.internal;
import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkHttpClient;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
@ -24,6 +25,7 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.net.NetworkInterface;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.net.URI; import java.net.URI;
@ -123,8 +125,27 @@ public class Platform {
} }
} }
/**
* 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. */ /** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() { 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. // Attempt to find Android 2.3+ APIs.
Class<?> openSslSocketClass; Class<?> openSslSocketClass;
Method setUseSessionTickets; Method setUseSessionTickets;
@ -138,10 +159,10 @@ public class Platform {
try { try {
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols, return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
getNpnSelectedProtocol); setNpnProtocols, getNpnSelectedProtocol);
} catch (NoSuchMethodException ignored) { } catch (NoSuchMethodException ignored) {
return new Android23(openSslSocketClass, setUseSessionTickets, setHostname); return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
} }
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
// This isn't an Android runtime. // This isn't an Android runtime.
@ -158,12 +179,35 @@ public class Platform {
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider"); Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass); Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class); Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass, return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
serverProviderClass); serverProviderClass);
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
return new Platform(); // NPN isn't on the classpath. // NPN isn't on the classpath.
} catch (NoSuchMethodException ignored) { } catch (NoSuchMethodException ignored) {
return new Platform(); // The NPN version isn't what we expect. // The NPN version isn't what we expect.
}
return getMtu != null ? new Java5(getMtu) : new Platform();
}
private static class Java5 extends Platform {
private final Method getMtu;
private Java5(Method getMtu) {
this.getMtu = getMtu;
}
@Override public int getMtu(Socket socket) throws IOException {
try {
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
socket.getLocalAddress());
return (Integer) getMtu.invoke(networkInterface);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
throw new RuntimeException(e.getCause());
}
} }
} }
@ -171,13 +215,14 @@ public class Platform {
* Android version 2.3 and newer support TLS session tickets and server name * Android version 2.3 and newer support TLS session tickets and server name
* indication (SNI). * indication (SNI).
*/ */
private static class Android23 extends Platform { private static class Android23 extends Java5 {
protected final Class<?> openSslSocketClass; protected final Class<?> openSslSocketClass;
private final Method setUseSessionTickets; private final Method setUseSessionTickets;
private final Method setHostname; private final Method setHostname;
private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets, private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
Method setHostname) { Method setHostname) {
super(getMtu);
this.openSslSocketClass = openSslSocketClass; this.openSslSocketClass = openSslSocketClass;
this.setUseSessionTickets = setUseSessionTickets; this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname; this.setHostname = setHostname;
@ -204,9 +249,9 @@ public class Platform {
private final Method setNpnProtocols; private final Method setNpnProtocols;
private final Method getNpnSelectedProtocol; private final Method getNpnSelectedProtocol;
private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname, private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
Method setNpnProtocols, Method getNpnSelectedProtocol) { Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
super(openSslSocketClass, setUseSessionTickets, setHostname); super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
this.setNpnProtocols = setNpnProtocols; this.setNpnProtocols = setNpnProtocols;
this.getNpnSelectedProtocol = getNpnSelectedProtocol; this.getNpnSelectedProtocol = getNpnSelectedProtocol;
} }
@ -242,14 +287,15 @@ public class Platform {
* OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
* path. * path.
*/ */
private static class JdkWithJettyNpnPlatform extends Platform { private static class JdkWithJettyNpnPlatform extends Java5 {
private final Method getMethod; private final Method getMethod;
private final Method putMethod; private final Method putMethod;
private final Class<?> clientProviderClass; private final Class<?> clientProviderClass;
private final Class<?> serverProviderClass; private final Class<?> serverProviderClass;
public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass, public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
Class<?> serverProviderClass) { Class<?> clientProviderClass, Class<?> serverProviderClass) {
super(getMtu);
this.putMethod = putMethod; this.putMethod = putMethod;
this.getMethod = getMethod; this.getMethod = getMethod;
this.clientProviderClass = clientProviderClass; this.clientProviderClass = clientProviderClass;

View File

@ -21,26 +21,23 @@ import java.io.Closeable;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset; 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. * 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 * "\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()} * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
* to detect it after catching the {@code EOFException}. * to detect it after catching the {@code EOFException}.
* *
* This class is intended for reading input that strictly consists of lines, such as line-based * <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 BufferedReader} which in conjunction with * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
* {@link InputStreamReader} provides similar functionality, this class uses different * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line. * 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. * 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. * 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. * The default charset is US_ASCII.
@ -52,42 +49,22 @@ public class StrictLineReader implements Closeable {
private final InputStream in; private final InputStream in;
private final Charset charset; 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 * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
// an unterminated line, we set end == -1, otherwise end == pos. If the underlying * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
// {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. * 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 byte[] buf;
private int pos; private int pos;
private int end; 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. * Constructs a new {@code LineReader} with the specified charset and the default capacity.
* *
* @param in the {@code InputStream} to read data from. * @param in the {@code InputStream} to read data from.
* @param charset the charset used to decode data. * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null. * @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported. * @throws IllegalArgumentException if the specified charset is not supported.
*/ */
@ -100,11 +77,11 @@ public class StrictLineReader implements Closeable {
* *
* @param in the {@code InputStream} to read data from. * @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer. * @param capacity the capacity of the buffer.
* @param charset the charset used to decode data. * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* Only US-ASCII, UTF-8 and ISO-8859-1 is supported. * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null. * @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero * @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) { public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) { if (in == null || charset == null) {
@ -113,7 +90,7 @@ public class StrictLineReader implements Closeable {
if (capacity < 0) { if (capacity < 0) {
throw new IllegalArgumentException("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"); throw new IllegalArgumentException("Unsupported encoding");
} }
@ -128,7 +105,6 @@ public class StrictLineReader implements Closeable {
* *
* @throws IOException for errors when closing the underlying {@code InputStream}. * @throws IOException for errors when closing the underlying {@code InputStream}.
*/ */
@Override
public void close() throws IOException { public void close() throws IOException {
synchronized (in) { synchronized (in) {
if (buf != null) { if (buf != null) {
@ -162,7 +138,7 @@ public class StrictLineReader implements Closeable {
for (int i = pos; i != end; ++i) { for (int i = pos; i != end; ++i) {
if (buf[i] == LF) { if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; 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; pos = i + 1;
return res; return res;
} }
@ -173,7 +149,11 @@ public class StrictLineReader implements Closeable {
@Override @Override
public String toString() { public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; 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 @@ public class StrictLineReader implements Closeable {
/** /**
* Reads new input data into the buffer. Call only with pos == end or end == -1, * Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws. * 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 { private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length); int result = in.read(buf, 0, buf.length);

View File

@ -149,12 +149,14 @@ public final class Util {
throw new AssertionError(thrown); 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 { public static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles(); File[] files = dir.listFiles();
if (files == null) { if (files == null) {
throw new IllegalArgumentException("not a directory: " + dir); throw new IOException("not a readable directory: " + dir);
} }
for (File file : files) { for (File file : files) {
if (file.isDirectory()) { if (file.isDirectory()) {

View File

@ -19,7 +19,6 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address; import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection; import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource; import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns; import com.squareup.okhttp.internal.Dns;
@ -154,7 +153,7 @@ public class HttpEngine {
try { try {
uri = Platform.get().toUriLenient(policy.getURL()); uri = Platform.get().toUriLenient(policy.getURL());
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
throw new IOException(e); throw new IOException(e.getMessage());
} }
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
@ -176,8 +175,8 @@ public class HttpEngine {
prepareRawRequestHeaders(); prepareRawRequestHeaders();
initResponseSource(); initResponseSource();
if (policy.responseCache instanceof OkResponseCache) { if (policy.responseCache != null) {
((OkResponseCache) policy.responseCache).trackResponse(responseSource); policy.responseCache.trackResponse(responseSource);
} }
// The raw response source may require the network, but the request // The raw response source may require the network, but the request
@ -198,6 +197,7 @@ public class HttpEngine {
sendSocketRequest(); sendSocketRequest();
} else if (connection != null) { } else if (connection != null) {
policy.connectionPool.recycle(connection); policy.connectionPool.recycle(connection);
policy.getFailedRoutes().remove(connection.getRoute());
connection = null; connection = null;
} }
} }
@ -279,16 +279,17 @@ public class HttpEngine {
} }
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory, Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
hostnameVerifier, policy.requestedProxy); hostnameVerifier, policy.requestedProxy);
routeSelector = routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT); Dns.DEFAULT, policy.getFailedRoutes());
} }
connection = routeSelector.next(); connection = routeSelector.next();
if (!connection.isConnected()) { if (!connection.isConnected()) {
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig()); connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
policy.connectionPool.maybeShare(connection); policy.connectionPool.maybeShare(connection);
policy.getFailedRoutes().remove(connection.getRoute());
} }
connected(connection); 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. // Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine()); requestHeaders.getHeaders().setRequestLine(getRequestLine());
} }
@ -574,7 +575,7 @@ public class HttpEngine {
protected boolean includeAuthorityInRequestLine() { protected boolean includeAuthorityInRequestLine() {
return connection == null return connection == null
? policy.usingProxy() // A proxy was requested. ? 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() { public static String getDefaultUserAgent() {
@ -635,11 +636,8 @@ public class HttpEngine {
release(false); release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
setResponse(combinedHeaders, cachedResponseBody); setResponse(combinedHeaders, cachedResponseBody);
if (policy.responseCache instanceof OkResponseCache) { policy.responseCache.trackConditionalCacheHit();
OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache; policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
httpResponseCache.trackConditionalCacheHit();
httpResponseCache.update(cacheResponse, policy.getHttpConnectionToCache());
}
return; return;
} else { } else {
Util.closeQuietly(cachedResponseBody); Util.closeQuietly(cachedResponseBody);

View File

@ -17,8 +17,8 @@
package com.squareup.okhttp.internal.http; package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection; import com.squareup.okhttp.Connection;
import com.squareup.okhttp.internal.AbstractOutputStream;
import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.Util;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -30,14 +30,6 @@ import java.net.Socket;
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
public final class HttpTransport implements Transport { 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 * 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 * used for connection reuse, this timeout should be significantly less than
@ -129,14 +121,8 @@ public final class HttpTransport implements Transport {
*/ */
public void writeRequestHeaders() throws IOException { public void writeRequestHeaders() throws IOException {
httpEngine.writingRequestHeaders(); httpEngine.writingRequestHeaders();
int contentLength = httpEngine.requestHeaders.getContentLength();
RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders(); RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
byte[] bytes = headersToSend.toBytes(); byte[] bytes = headersToSend.toBytes();
if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
}
requestOut.write(bytes); requestOut.write(bytes);
} }
@ -154,7 +140,7 @@ public final class HttpTransport implements Transport {
} }
// We cannot reuse sockets that have incomplete output. // We cannot reuse sockets that have incomplete output.
if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) { if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
return false; return false;
} }
@ -224,7 +210,7 @@ public final class HttpTransport implements Transport {
} }
/** An HTTP body with a fixed length known in advance. */ /** 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 final OutputStream socketOut;
private int bytesRemaining; private int bytesRemaining;
@ -266,7 +252,7 @@ public final class HttpTransport implements Transport {
* buffered until {@code maxChunkLength} bytes are ready, at which point the * buffered until {@code maxChunkLength} bytes are ready, at which point the
* chunk is written and the buffer is cleared. * 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[] CRLF = { '\r', '\n' };
private static final byte[] HEX_DIGITS = { private static final byte[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'

View File

@ -20,6 +20,9 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection; import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkHttpClient; 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 com.squareup.okhttp.internal.Util;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -32,13 +35,13 @@ import java.net.InetSocketAddress;
import java.net.ProtocolException; import java.net.ProtocolException;
import java.net.Proxy; import java.net.Proxy;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.ResponseCache;
import java.net.SocketPermission; import java.net.SocketPermission;
import java.net.URL; import java.net.URL;
import java.security.Permission; import java.security.Permission;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
@ -70,6 +73,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/ */
private static final int MAX_REDIRECTS = 20; 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; private final boolean followProtocolRedirects;
/** The proxy requested by the client, or null for a proxy to be selected automatically. */ /** The proxy requested by the client, or null for a proxy to be selected automatically. */
@ -77,29 +87,37 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
final ProxySelector proxySelector; final ProxySelector proxySelector;
final CookieHandler cookieHandler; final CookieHandler cookieHandler;
final ResponseCache responseCache; final OkResponseCache responseCache;
final ConnectionPool connectionPool; final ConnectionPool connectionPool;
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */ /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
SSLSocketFactory sslSocketFactory; SSLSocketFactory sslSocketFactory;
HostnameVerifier hostnameVerifier; HostnameVerifier hostnameVerifier;
final Set<Route> failedRoutes;
private final RawHeaders rawRequestHeaders = new RawHeaders(); private final RawHeaders rawRequestHeaders = new RawHeaders();
private int redirectionCount; private int redirectionCount;
private FaultRecoveringOutputStream faultRecoveringRequestBody;
protected IOException httpEngineFailure; protected IOException httpEngineFailure;
protected HttpEngine httpEngine; protected HttpEngine httpEngine;
public HttpURLConnectionImpl(URL url, OkHttpClient client) { public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url); super(url);
this.followProtocolRedirects = client.getFollowProtocolRedirects(); this.followProtocolRedirects = client.getFollowProtocolRedirects();
this.failedRoutes = failedRoutes;
this.requestedProxy = client.getProxy(); this.requestedProxy = client.getProxy();
this.proxySelector = client.getProxySelector(); this.proxySelector = client.getProxySelector();
this.cookieHandler = client.getCookieHandler(); this.cookieHandler = client.getCookieHandler();
this.responseCache = client.getResponseCache();
this.connectionPool = client.getConnectionPool(); this.connectionPool = client.getConnectionPool();
this.sslSocketFactory = client.getSslSocketFactory(); this.sslSocketFactory = client.getSslSocketFactory();
this.hostnameVerifier = client.getHostnameVerifier(); this.hostnameVerifier = client.getHostnameVerifier();
this.responseCache = responseCache;
}
Set<Route> getFailedRoutes() {
return failedRoutes;
} }
@Override public final void connect() throws IOException { @Override public final void connect() throws IOException {
@ -216,14 +234,29 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
@Override public final OutputStream getOutputStream() throws IOException { @Override public final OutputStream getOutputStream() throws IOException {
connect(); connect();
OutputStream result = httpEngine.getRequestBody(); OutputStream out = httpEngine.getRequestBody();
if (result == null) { if (out == null) {
throw new ProtocolException("method does not support a request body: " + method); throw new ProtocolException("method does not support a request body: " + method);
} else if (httpEngine.hasResponse()) { } else if (httpEngine.hasResponse()) {
throw new ProtocolException("cannot write request body after response has been read"); 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 { @Override public final Permission getPermission() throws IOException {
@ -353,29 +386,50 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
} }
return true; return true;
} catch (IOException e) { } catch (IOException e) {
RouteSelector routeSelector = httpEngine.routeSelector; if (handleFailure(e)) {
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.
return false; 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) { private boolean isRecoverable(IOException e) {
// If the problem was a CertificateException from the X509TrustManager, // If the problem was a CertificateException from the X509TrustManager,
// do not retry, we didn't have an abrupt server initiated exception. // do not retry, we didn't have an abrupt server initiated exception.
@ -385,7 +439,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return !sslFailure && !protocolFailure; return !sslFailure && !protocolFailure;
} }
HttpEngine getHttpEngine() { public HttpEngine getHttpEngine() {
return httpEngine; return httpEngine;
} }
@ -402,7 +456,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/ */
private Retry processResponseHeaders() throws IOException { private Retry processResponseHeaders() throws IOException {
Proxy selectedProxy = httpEngine.connection != null Proxy selectedProxy = httpEngine.connection != null
? httpEngine.connection.getProxy() ? httpEngine.connection.getRoute().getProxy()
: requestedProxy; : requestedProxy;
final int responseCode = getResponseCode(); final int responseCode = getResponseCode();
switch (responseCode) { switch (responseCode) {

View File

@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection; import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.TunnelRequest;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -32,6 +33,7 @@ import java.security.Principal;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLPeerUnverifiedException;
@ -45,9 +47,10 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */ /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
private final HttpUrlConnectionDelegate delegate; private final HttpUrlConnectionDelegate delegate;
public HttpsURLConnectionImpl(URL url, OkHttpClient client) { public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url); super(url);
delegate = new HttpUrlConnectionDelegate(url, client); delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
} }
@Override public String getCipherSuite() { @Override public String getCipherSuite() {
@ -112,7 +115,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
return null; return null;
} }
HttpEngine getHttpEngine() { public HttpEngine getHttpEngine() {
return delegate.getHttpEngine(); return delegate.getHttpEngine();
} }
@ -399,8 +402,9 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
} }
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
private HttpUrlConnectionDelegate(URL url, OkHttpClient client) { private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
super(url, client); Set<Route> failedRoutes) {
super(url, client, responseCache, failedRoutes);
} }
@Override protected HttpURLConnection getHttpConnectionToCache() { @Override protected HttpURLConnection getHttpConnectionToCache() {
@ -425,8 +429,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
* @param policy the HttpURLConnectionImpl with connection configuration * @param policy the HttpURLConnectionImpl with connection configuration
*/ */
public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBody) Connection connection, RetryableOutputStream requestBody) throws IOException {
throws IOException {
super(policy, method, requestHeaders, connection, requestBody); super(policy, method, requestHeaders, connection, requestBody);
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null; this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
} }

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -17,7 +17,6 @@
package com.squareup.okhttp.internal.http; package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -186,25 +185,27 @@ public final class RawHeaders {
public void addLine(String line) { public void addLine(String line) {
int index = line.indexOf(":"); int index = line.indexOf(":");
if (index == -1) { if (index == -1) {
add("", line); addLenient("", line);
} else { } 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. */ /** Add a field with the specified value. */
public void add(String fieldName, String value) { public void add(String fieldName, String value) {
if (fieldName == null) { if (fieldName == null) throw new IllegalArgumentException("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) {
if (value == null) { throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
// 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(fieldName);
namesAndValues.add(value.trim()); namesAndValues.add(value.trim());
} }
@ -351,7 +352,9 @@ public final class RawHeaders {
String fieldName = entry.getKey(); String fieldName = entry.getKey();
List<String> values = entry.getValue(); List<String> values = entry.getValue();
if (fieldName != null) { if (fieldName != null) {
result.addAll(fieldName, values); for (String value : values) {
result.addLenient(fieldName, value);
}
} else if (!values.isEmpty()) { } else if (!values.isEmpty()) {
result.setStatusLine(values.get(values.size() - 1)); result.setStatusLine(values.get(values.size() - 1));
} }
@ -371,14 +374,6 @@ public final class RawHeaders {
String name = namesAndValues.get(i).toLowerCase(Locale.US); String name = namesAndValues.get(i).toLowerCase(Locale.US);
String value = namesAndValues.get(i + 1); String value = namesAndValues.get(i + 1);
// TODO: promote this check to where names and values are created
if (name.length() == 0
|| value.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. // Drop headers that are forbidden when layering HTTP over SPDY.
if (name.equals("connection") if (name.equals("connection")
|| name.equals("host") || name.equals("host")

View File

@ -22,7 +22,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** Parsed HTTP request headers. */ /** Parsed HTTP request headers. */
final class RequestHeaders { public final class RequestHeaders {
private final URI uri; private final URI uri;
private final RawHeaders headers; private final RawHeaders headers;

View File

@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
import static com.squareup.okhttp.internal.Util.equal; import static com.squareup.okhttp.internal.Util.equal;
/** Parsed HTTP response headers. */ /** Parsed HTTP response headers. */
final class ResponseHeaders { public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */ /** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = "X-Android-Sent-Millis"; private static final String SENT_MILLIS = "X-Android-Sent-Millis";
@ -410,7 +410,8 @@ final class ResponseHeaders {
if (ageMillis + minFreshMillis >= freshMillis) { if (ageMillis + minFreshMillis >= freshMillis) {
headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); 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\""); headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
} }
return ResponseSource.CACHE; return ResponseSource.CACHE;

View File

@ -16,6 +16,7 @@
package com.squareup.okhttp.internal.http; package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.AbstractOutputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -28,7 +29,7 @@ import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
* the post body to be transparently re-sent if the HTTP request must be * the post body to be transparently re-sent if the HTTP request must be
* sent multiple times. * sent multiple times.
*/ */
final class RetryableOutputStream extends AbstractHttpOutputStream { final class RetryableOutputStream extends AbstractOutputStream {
private final int limit; private final int limit;
private final ByteArrayOutputStream content; private final ByteArrayOutputStream content;

View File

@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address; import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection; import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Dns; import com.squareup.okhttp.internal.Dns;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
@ -28,8 +29,11 @@ import java.net.SocketAddress;
import java.net.URI; import java.net.URI;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort; import static com.squareup.okhttp.internal.Util.getEffectivePort;
@ -51,6 +55,7 @@ public final class RouteSelector {
private final ProxySelector proxySelector; private final ProxySelector proxySelector;
private final ConnectionPool pool; private final ConnectionPool pool;
private final Dns dns; private final Dns dns;
private final Set<Route> failedRoutes;
/* The most recently attempted route. */ /* The most recently attempted route. */
private Proxy lastProxy; private Proxy lastProxy;
@ -64,19 +69,23 @@ public final class RouteSelector {
/* State for negotiating the next InetSocketAddress to use. */ /* State for negotiating the next InetSocketAddress to use. */
private InetAddress[] socketAddresses; private InetAddress[] socketAddresses;
private int nextSocketAddressIndex; private int nextSocketAddressIndex;
private String socketHost;
private int socketPort; private int socketPort;
/* State for negotiating the next TLS configuration */ /* State for negotiating the next TLS configuration */
private int nextTlsMode = TLS_MODE_NULL; 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, public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
Dns dns) { Dns dns, Set<Route> failedRoutes) {
this.address = address; this.address = address;
this.uri = uri; this.uri = uri;
this.proxySelector = proxySelector; this.proxySelector = proxySelector;
this.pool = pool; this.pool = pool;
this.dns = dns; this.dns = dns;
this.failedRoutes = failedRoutes;
this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy()); resetNextProxy(uri, address.getProxy());
} }
@ -86,7 +95,7 @@ public final class RouteSelector {
* least one route. * least one route.
*/ */
public boolean hasNext() { public boolean hasNext() {
return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy(); return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
} }
/** /**
@ -105,7 +114,10 @@ public final class RouteSelector {
if (!hasNextTlsMode()) { if (!hasNextTlsMode()) {
if (!hasNextInetSocketAddress()) { if (!hasNextInetSocketAddress()) {
if (!hasNextProxy()) { if (!hasNextProxy()) {
throw new NoSuchElementException(); if (!hasNextPostponed()) {
throw new NoSuchElementException();
}
return new Connection(nextPostponed());
} }
lastProxy = nextProxy(); lastProxy = nextProxy();
resetNextInetSocketAddress(lastProxy); resetNextInetSocketAddress(lastProxy);
@ -113,9 +125,17 @@ public final class RouteSelector {
lastInetSocketAddress = nextInetSocketAddress(); lastInetSocketAddress = nextInetSocketAddress();
resetNextTlsMode(); 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 @@ public final class RouteSelector {
* failure on a connection returned by this route selector. * failure on a connection returned by this route selector.
*/ */
public void connectFailed(Connection connection, IOException failure) { 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. // 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 @@ public final class RouteSelector {
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException { private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws! socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
String socketHost;
if (proxy.type() == Proxy.Type.DIRECT) { if (proxy.type() == Proxy.Type.DIRECT) {
socketHost = uri.getHost(); socketHost = uri.getHost();
socketPort = getEffectivePort(uri); socketPort = getEffectivePort(uri);
@ -233,4 +262,14 @@ public final class RouteSelector {
throw new AssertionError(); 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);
}
} }

View File

@ -139,17 +139,17 @@ public final class SpdyConnection implements Closeable {
return stream; return stream;
} }
private void setIdle(boolean value) { private synchronized void setIdle(boolean value) {
idleStartTimeNs = value ? System.nanoTime() : 0L; idleStartTimeNs = value ? System.nanoTime() : 0L;
} }
/** Returns true if this connection is idle. */ /** Returns true if this connection is idle. */
public boolean isIdle() { public synchronized boolean isIdle() {
return idleStartTimeNs != 0L; return idleStartTimeNs != 0L;
} }
/** Returns the time in ns when this connection became idle or 0L if connection is not idle. */ /** 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; return idleStartTimeNs;
} }

View File

@ -21,6 +21,7 @@ import java.io.Closeable;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException; import java.net.ProtocolException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -31,39 +32,46 @@ import java.util.zip.InflaterInputStream;
/** Read spdy/3 frames. */ /** Read spdy/3 frames. */
final class SpdyReader implements Closeable { final class SpdyReader implements Closeable {
static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" static final byte[] DICTIONARY;
+ "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" static {
+ "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" try {
+ "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8); + "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 in;
private final DataInputStream nameValueBlockIn; private final DataInputStream nameValueBlockIn;
@ -252,7 +260,7 @@ final class SpdyReader implements Closeable {
return entries; return entries;
} catch (DataFormatException e) { } catch (DataFormatException e) {
throw new IOException(e); throw new IOException(e.getMessage());
} }
} }