mirror of
https://github.com/apache/cordova-android.git
synced 2025-02-20 23:56:20 +08:00
[CB-2431] Switch to OkHttp for FileTransfer connections
This commit is contained in:
parent
53982272d6
commit
553a25cea7
111
framework/src/com/squareup/okhttp/Address.java
Normal file
111
framework/src/com/squareup/okhttp/Address.java
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.net.UnknownHostException;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.equal;
|
||||
|
||||
/**
|
||||
* A specification for a connection to an origin server. For simple connections,
|
||||
* this is the server's hostname and port. If an explicit proxy is requested (or
|
||||
* {@link Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
|
||||
* that proxy information. For secure connections the address also includes the
|
||||
* SSL socket factory and hostname verifier.
|
||||
*
|
||||
* <p>HTTP requests that share the same {@code Address} may also share the same
|
||||
* {@link Connection}.
|
||||
*/
|
||||
public final class Address {
|
||||
final Proxy proxy;
|
||||
final String uriHost;
|
||||
final int uriPort;
|
||||
final SSLSocketFactory sslSocketFactory;
|
||||
final HostnameVerifier hostnameVerifier;
|
||||
|
||||
public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
|
||||
HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
|
||||
if (uriHost == null) throw new NullPointerException("uriHost == null");
|
||||
if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
|
||||
this.proxy = proxy;
|
||||
this.uriHost = uriHost;
|
||||
this.uriPort = uriPort;
|
||||
this.sslSocketFactory = sslSocketFactory;
|
||||
this.hostnameVerifier = hostnameVerifier;
|
||||
}
|
||||
|
||||
/** Returns the hostname of the origin server. */
|
||||
public String getUriHost() {
|
||||
return uriHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the port of the origin server; typically 80 or 443. Unlike
|
||||
* may {@code getPort()} accessors, this method never returns -1.
|
||||
*/
|
||||
public int getUriPort() {
|
||||
return uriPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SSL socket factory, or null if this is not an HTTPS
|
||||
* address.
|
||||
*/
|
||||
public SSLSocketFactory getSslSocketFactory() {
|
||||
return sslSocketFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hostname verifier, or null if this is not an HTTPS
|
||||
* address.
|
||||
*/
|
||||
public HostnameVerifier getHostnameVerifier() {
|
||||
return hostnameVerifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this address's explicitly-specified HTTP proxy, or null to
|
||||
* delegate to the HTTP client's proxy selector.
|
||||
*/
|
||||
public Proxy getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object other) {
|
||||
if (other instanceof Address) {
|
||||
Address that = (Address) other;
|
||||
return equal(this.proxy, that.proxy)
|
||||
&& this.uriHost.equals(that.uriHost)
|
||||
&& this.uriPort == that.uriPort
|
||||
&& equal(this.sslSocketFactory, that.sslSocketFactory)
|
||||
&& equal(this.hostnameVerifier, that.hostnameVerifier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + uriHost.hashCode();
|
||||
result = 31 * result + uriPort;
|
||||
result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
|
||||
result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
|
||||
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
321
framework/src/com/squareup/okhttp/Connection.java
Normal file
321
framework/src/com/squareup/okhttp/Connection.java
Normal file
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.Platform;
|
||||
import com.squareup.okhttp.internal.http.HttpAuthenticator;
|
||||
import com.squareup.okhttp.internal.http.HttpEngine;
|
||||
import com.squareup.okhttp.internal.http.HttpTransport;
|
||||
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||
import com.squareup.okhttp.internal.http.SpdyTransport;
|
||||
import com.squareup.okhttp.internal.spdy.SpdyConnection;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_OK;
|
||||
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
|
||||
|
||||
/**
|
||||
* Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
|
||||
* which may be used for multiple HTTP request/response exchanges. Connections
|
||||
* may be direct to the origin server or via a proxy.
|
||||
*
|
||||
* <p>Typically instances of this class are created, connected and exercised
|
||||
* automatically by the HTTP client. Applications may use this class to monitor
|
||||
* HTTP connections as members of a {@link ConnectionPool connection pool}.
|
||||
*
|
||||
* <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
|
||||
* which isn't so much a connection as a single request/response exchange.
|
||||
*
|
||||
* <h3>Modern TLS</h3>
|
||||
* There are tradeoffs when selecting which options to include when negotiating
|
||||
* a secure connection to a remote host. Newer TLS options are quite useful:
|
||||
* <ul>
|
||||
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
|
||||
* connections for multiple domain names.
|
||||
* <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
|
||||
* for both HTTP and SPDY transports.
|
||||
* </ul>
|
||||
* Unfortunately, older HTTPS servers refuse to connect when such options are
|
||||
* presented. Rather than avoiding these options entirely, this class allows a
|
||||
* connection to be attempted with modern options and then retried without them
|
||||
* should the attempt fail.
|
||||
*/
|
||||
public final class Connection implements Closeable {
|
||||
private static final byte[] NPN_PROTOCOLS = new byte[] {
|
||||
6, 's', 'p', 'd', 'y', '/', '3',
|
||||
8, 'h', 't', 't', 'p', '/', '1', '.', '1'
|
||||
};
|
||||
private static final byte[] SPDY3 = new byte[] {
|
||||
's', 'p', 'd', 'y', '/', '3'
|
||||
};
|
||||
private static final byte[] HTTP_11 = new byte[] {
|
||||
'h', 't', 't', 'p', '/', '1', '.', '1'
|
||||
};
|
||||
|
||||
private final Address address;
|
||||
private final Proxy proxy;
|
||||
private final InetSocketAddress inetSocketAddress;
|
||||
private final boolean modernTls;
|
||||
|
||||
private Socket socket;
|
||||
private InputStream in;
|
||||
private OutputStream out;
|
||||
private boolean connected = false;
|
||||
private SpdyConnection spdyConnection;
|
||||
private int httpMinorVersion = 1; // Assume HTTP/1.1
|
||||
private long idleStartTimeNs;
|
||||
|
||||
public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
|
||||
boolean modernTls) {
|
||||
if (address == null) throw new NullPointerException("address == null");
|
||||
if (proxy == null) throw new NullPointerException("proxy == null");
|
||||
if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
|
||||
this.address = address;
|
||||
this.proxy = proxy;
|
||||
this.inetSocketAddress = inetSocketAddress;
|
||||
this.modernTls = modernTls;
|
||||
}
|
||||
|
||||
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
|
||||
throws IOException {
|
||||
if (connected) {
|
||||
throw new IllegalStateException("already connected");
|
||||
}
|
||||
connected = true;
|
||||
socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket();
|
||||
socket.connect(inetSocketAddress, connectTimeout);
|
||||
socket.setSoTimeout(readTimeout);
|
||||
in = socket.getInputStream();
|
||||
out = socket.getOutputStream();
|
||||
|
||||
if (address.sslSocketFactory != null) {
|
||||
upgradeToTls(tunnelRequest);
|
||||
}
|
||||
|
||||
// Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
|
||||
if (!isSpdy()) {
|
||||
int bufferSize = 128;
|
||||
in = new BufferedInputStream(in, bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@code SSLSocket} and perform the TLS handshake and certificate
|
||||
* validation.
|
||||
*/
|
||||
private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
|
||||
Platform platform = Platform.get();
|
||||
|
||||
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
|
||||
if (requiresTunnel()) {
|
||||
makeTunnel(tunnelRequest);
|
||||
}
|
||||
|
||||
// Create the wrapper over connected socket.
|
||||
socket = address.sslSocketFactory
|
||||
.createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */);
|
||||
SSLSocket sslSocket = (SSLSocket) socket;
|
||||
if (modernTls) {
|
||||
platform.enableTlsExtensions(sslSocket, address.uriHost);
|
||||
} else {
|
||||
platform.supportTlsIntolerantServer(sslSocket);
|
||||
}
|
||||
|
||||
if (modernTls) {
|
||||
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
|
||||
}
|
||||
|
||||
// Force handshake. This can throw!
|
||||
sslSocket.startHandshake();
|
||||
|
||||
// Verify that the socket's certificates are acceptable for the target host.
|
||||
if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
|
||||
throw new IOException("Hostname '" + address.uriHost + "' was not verified");
|
||||
}
|
||||
|
||||
out = sslSocket.getOutputStream();
|
||||
in = sslSocket.getInputStream();
|
||||
|
||||
byte[] selectedProtocol;
|
||||
if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
|
||||
if (Arrays.equals(selectedProtocol, SPDY3)) {
|
||||
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
|
||||
spdyConnection = new SpdyConnection.Builder(address.getUriHost(), true, in, out).build();
|
||||
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
|
||||
throw new IOException(
|
||||
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if {@link #connect} has been attempted on this connection. */
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proxy that this connection is using.
|
||||
*
|
||||
* <strong>Warning:</strong> This may be different than the proxy returned
|
||||
* by {@link #getAddress}! That is the proxy that the user asked to be
|
||||
* connected to; this returns the proxy that they were actually connected
|
||||
* to. The two may disagree when a proxy selector selects a different proxy
|
||||
* for a connection.
|
||||
*/
|
||||
public Proxy getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public InetSocketAddress getSocketAddress() {
|
||||
return inetSocketAddress;
|
||||
}
|
||||
|
||||
public boolean isModernTls() {
|
||||
return modernTls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the socket that this connection uses, or null if the connection
|
||||
* is not currently connected.
|
||||
*/
|
||||
public Socket getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
/** Returns true if this connection is alive. */
|
||||
public boolean isAlive() {
|
||||
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
|
||||
}
|
||||
|
||||
public void resetIdleStartTime() {
|
||||
if (spdyConnection != null) {
|
||||
throw new IllegalStateException("spdyConnection != null");
|
||||
}
|
||||
this.idleStartTimeNs = System.nanoTime();
|
||||
}
|
||||
|
||||
/** Returns true if this connection is idle. */
|
||||
public boolean isIdle() {
|
||||
return spdyConnection == null || spdyConnection.isIdle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this connection has been idle for longer than
|
||||
* {@code keepAliveDurationNs}.
|
||||
*/
|
||||
public boolean isExpired(long keepAliveDurationNs) {
|
||||
return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time in ns when this connection became idle. Undefined if
|
||||
* this connection is not idle.
|
||||
*/
|
||||
public long getIdleStartTimeNs() {
|
||||
return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
|
||||
}
|
||||
|
||||
/** Returns the transport appropriate for this connection. */
|
||||
public Object newTransport(HttpEngine httpEngine) throws IOException {
|
||||
return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
|
||||
: new HttpTransport(httpEngine, out, in);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a SPDY connection. Such connections can be used
|
||||
* in multiple HTTP requests simultaneously.
|
||||
*/
|
||||
public boolean isSpdy() {
|
||||
return spdyConnection != null;
|
||||
}
|
||||
|
||||
public SpdyConnection getSpdyConnection() {
|
||||
return spdyConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minor HTTP version that should be used for future requests on
|
||||
* this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
|
||||
* value is 1 for new connections.
|
||||
*/
|
||||
public int getHttpMinorVersion() {
|
||||
return httpMinorVersion;
|
||||
}
|
||||
|
||||
public void setHttpMinorVersion(int httpMinorVersion) {
|
||||
this.httpMinorVersion = httpMinorVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the HTTP connection needs to tunnel one protocol over
|
||||
* another, such as when using HTTPS through an HTTP proxy. When doing so,
|
||||
* we must avoid buffering bytes intended for the higher-level protocol.
|
||||
*/
|
||||
public boolean requiresTunnel() {
|
||||
return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* To make an HTTPS connection over an HTTP proxy, send an unencrypted
|
||||
* CONNECT request to create the proxy connection. This may need to be
|
||||
* retried if the proxy requires authorization.
|
||||
*/
|
||||
private void makeTunnel(TunnelRequest tunnelRequest) throws IOException {
|
||||
RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
|
||||
while (true) {
|
||||
out.write(requestHeaders.toBytes());
|
||||
RawHeaders responseHeaders = RawHeaders.fromBytes(in);
|
||||
|
||||
switch (responseHeaders.getResponseCode()) {
|
||||
case HTTP_OK:
|
||||
return;
|
||||
case HTTP_PROXY_AUTH:
|
||||
requestHeaders = new RawHeaders(requestHeaders);
|
||||
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
|
||||
boolean credentialsFound =
|
||||
HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
|
||||
proxy, url);
|
||||
if (credentialsFound) {
|
||||
continue;
|
||||
} else {
|
||||
throw new IOException("Failed to authenticate with proxy");
|
||||
}
|
||||
default:
|
||||
throw new IOException(
|
||||
"Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
257
framework/src/com/squareup/okhttp/ConnectionPool.java
Normal file
257
framework/src/com/squareup/okhttp/ConnectionPool.java
Normal file
@ -0,0 +1,257 @@
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.Platform;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
|
||||
* requests that share the same {@link com.squareup.okhttp.Address} may share a
|
||||
* {@link com.squareup.okhttp.Connection}. This class implements the policy of
|
||||
* which connections to keep open for future use.
|
||||
*
|
||||
* <p>The {@link #getDefault() system-wide default} uses system properties for
|
||||
* tuning parameters:
|
||||
* <ul>
|
||||
* <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
|
||||
* pooled at all. Default is true.
|
||||
* <li>{@code http.maxConnections} maximum number of idle connections to
|
||||
* each to keep in the pool. Default is 5.
|
||||
* <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
|
||||
* connection alive in the pool before closing it. Default is 5 minutes.
|
||||
* This property isn't used by {@code HttpURLConnection}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>The default instance <i>doesn't</i> adjust its configuration as system
|
||||
* properties are changed. This assumes that the applications that set these
|
||||
* parameters do so before making HTTP connections, and that this class is
|
||||
* initialized lazily.
|
||||
*/
|
||||
public class ConnectionPool {
|
||||
private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
|
||||
private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
|
||||
|
||||
private static final ConnectionPool systemDefault;
|
||||
|
||||
static {
|
||||
String keepAlive = System.getProperty("http.keepAlive");
|
||||
String keepAliveDuration = System.getProperty("http.keepAliveDuration");
|
||||
String maxIdleConnections = System.getProperty("http.maxConnections");
|
||||
long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
|
||||
: DEFAULT_KEEP_ALIVE_DURATION_MS;
|
||||
if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
|
||||
systemDefault = new ConnectionPool(0, keepAliveDurationMs);
|
||||
} else if (maxIdleConnections != null) {
|
||||
systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
|
||||
} else {
|
||||
systemDefault = new ConnectionPool(5, keepAliveDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
/** The maximum number of idle connections for each address. */
|
||||
private final int maxIdleConnections;
|
||||
private final long keepAliveDurationNs;
|
||||
|
||||
private final LinkedList<Connection> connections = new LinkedList<Connection>();
|
||||
|
||||
/** We use a single background thread to cleanup expired connections. */
|
||||
private final ExecutorService executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||
private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
|
||||
@Override public Void call() throws Exception {
|
||||
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
|
||||
int idleConnectionCount = 0;
|
||||
synchronized (ConnectionPool.this) {
|
||||
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||
i.hasPrevious(); ) {
|
||||
Connection connection = i.previous();
|
||||
if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
|
||||
i.remove();
|
||||
expiredConnections.add(connection);
|
||||
if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
|
||||
} else if (connection.isIdle()) {
|
||||
idleConnectionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||
i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
|
||||
Connection connection = i.previous();
|
||||
if (connection.isIdle()) {
|
||||
expiredConnections.add(connection);
|
||||
i.remove();
|
||||
--idleConnectionCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Connection expiredConnection : expiredConnections) {
|
||||
Util.closeQuietly(expiredConnection);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
|
||||
this.maxIdleConnections = maxIdleConnections;
|
||||
this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the connections in this pool, ordered from newest to
|
||||
* oldest. Waits for the cleanup callable to run if it is currently scheduled.
|
||||
*/
|
||||
List<Connection> getConnections() {
|
||||
waitForCleanupCallableToRun();
|
||||
synchronized (this) {
|
||||
return new ArrayList<Connection>(connections);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until the executor service has processed all currently enqueued
|
||||
* jobs.
|
||||
*/
|
||||
private void waitForCleanupCallableToRun() {
|
||||
try {
|
||||
executorService.submit(new Runnable() {
|
||||
@Override public void run() {
|
||||
}
|
||||
}).get();
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static ConnectionPool getDefault() {
|
||||
return systemDefault;
|
||||
}
|
||||
|
||||
/** Returns total number of connections in the pool. */
|
||||
public synchronized int getConnectionCount() {
|
||||
return connections.size();
|
||||
}
|
||||
|
||||
/** Returns total number of spdy connections in the pool. */
|
||||
public synchronized int getSpdyConnectionCount() {
|
||||
int total = 0;
|
||||
for (Connection connection : connections) {
|
||||
if (connection.isSpdy()) total++;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Returns total number of http connections in the pool. */
|
||||
public synchronized int getHttpConnectionCount() {
|
||||
int total = 0;
|
||||
for (Connection connection : connections) {
|
||||
if (!connection.isSpdy()) total++;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
|
||||
public synchronized Connection get(Address address) {
|
||||
Connection foundConnection = null;
|
||||
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
||||
i.hasPrevious(); ) {
|
||||
Connection connection = i.previous();
|
||||
if (!connection.getAddress().equals(address)
|
||||
|| !connection.isAlive()
|
||||
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
|
||||
continue;
|
||||
}
|
||||
i.remove();
|
||||
if (!connection.isSpdy()) {
|
||||
try {
|
||||
Platform.get().tagSocket(connection.getSocket());
|
||||
} catch (SocketException e) {
|
||||
Util.closeQuietly(connection);
|
||||
// When unable to tag, skip recycling and close
|
||||
Platform.get().logW("Unable to tagSocket(): " + e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foundConnection = connection;
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundConnection != null && foundConnection.isSpdy()) {
|
||||
connections.addFirst(foundConnection); // Add it back after iteration.
|
||||
}
|
||||
|
||||
executorService.submit(connectionsCleanupCallable);
|
||||
return foundConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives {@code connection} to the pool. The pool may store the connection,
|
||||
* or close it, as its policy describes.
|
||||
*
|
||||
* <p>It is an error to use {@code connection} after calling this method.
|
||||
*/
|
||||
public void recycle(Connection connection) {
|
||||
executorService.submit(connectionsCleanupCallable);
|
||||
|
||||
if (connection.isSpdy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.isAlive()) {
|
||||
Util.closeQuietly(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Platform.get().untagSocket(connection.getSocket());
|
||||
} catch (SocketException e) {
|
||||
// When unable to remove tagging, skip recycling and close.
|
||||
Platform.get().logW("Unable to untagSocket(): " + e);
|
||||
Util.closeQuietly(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
connections.addFirst(connection);
|
||||
connection.resetIdleStartTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the SPDY connection with the pool. Callers to this method may
|
||||
* continue to use {@code connection}.
|
||||
*/
|
||||
public void maybeShare(Connection connection) {
|
||||
executorService.submit(connectionsCleanupCallable);
|
||||
if (!connection.isSpdy()) {
|
||||
// Only SPDY connections are sharable.
|
||||
return;
|
||||
}
|
||||
if (connection.isAlive()) {
|
||||
synchronized (this) {
|
||||
connections.addFirst(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Close and remove all connections in the pool. */
|
||||
public void evictAll() {
|
||||
List<Connection> connections;
|
||||
synchronized (this) {
|
||||
connections = new ArrayList<Connection>(this.connections);
|
||||
this.connections.clear();
|
||||
}
|
||||
|
||||
for (Connection connection : connections) {
|
||||
Util.closeQuietly(connection);
|
||||
}
|
||||
}
|
||||
}
|
198
framework/src/com/squareup/okhttp/OkHttpClient.java
Normal file
198
framework/src/com/squareup/okhttp/OkHttpClient.java
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
|
||||
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.ResponseCache;
|
||||
import java.net.URL;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/** Configures and creates HTTP connections. */
|
||||
public final class OkHttpClient {
|
||||
private Proxy proxy;
|
||||
private ProxySelector proxySelector;
|
||||
private CookieHandler cookieHandler;
|
||||
private ResponseCache responseCache;
|
||||
private SSLSocketFactory sslSocketFactory;
|
||||
private HostnameVerifier hostnameVerifier;
|
||||
private ConnectionPool connectionPool;
|
||||
private boolean followProtocolRedirects = true;
|
||||
|
||||
/**
|
||||
* Sets the HTTP proxy that will be used by connections created by this
|
||||
* client. This takes precedence over {@link #setProxySelector}, which is
|
||||
* only honored when this proxy is null (which it is by default). To disable
|
||||
* proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
|
||||
*/
|
||||
public OkHttpClient setProxy(Proxy proxy) {
|
||||
this.proxy = proxy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Proxy getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the proxy selection policy to be used if no {@link #setProxy proxy}
|
||||
* is specified explicitly. The proxy selector may return multiple proxies;
|
||||
* in that case they will be tried in sequence until a successful connection
|
||||
* is established.
|
||||
*
|
||||
* <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
|
||||
* proxy selector will be used.
|
||||
*/
|
||||
public OkHttpClient setProxySelector(ProxySelector proxySelector) {
|
||||
this.proxySelector = proxySelector;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProxySelector getProxySelector() {
|
||||
return proxySelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cookie handler to be used to read outgoing cookies and write
|
||||
* incoming cookies.
|
||||
*
|
||||
* <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
|
||||
* cookie handler will be used.
|
||||
*/
|
||||
public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
|
||||
this.cookieHandler = cookieHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CookieHandler getCookieHandler() {
|
||||
return cookieHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response cache to be used to read and write cached responses.
|
||||
*
|
||||
* <p>If unset, the {@link ResponseCache#getDefault() system-wide default}
|
||||
* response cache will be used.
|
||||
*/
|
||||
public OkHttpClient setResponseCache(ResponseCache responseCache) {
|
||||
this.responseCache = responseCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseCache getResponseCache() {
|
||||
return responseCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the socket factory used to secure HTTPS connections.
|
||||
*
|
||||
* <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
|
||||
* system-wide default} SSL socket factory will be used.
|
||||
*/
|
||||
public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
|
||||
this.sslSocketFactory = sslSocketFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SSLSocketFactory getSslSocketFactory() {
|
||||
return sslSocketFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the verifier used to confirm that response certificates apply to
|
||||
* requested hostnames for HTTPS connections.
|
||||
*
|
||||
* <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
|
||||
* system-wide default} hostname verifier will be used.
|
||||
*/
|
||||
public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
|
||||
this.hostnameVerifier = hostnameVerifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HostnameVerifier getHostnameVerifier() {
|
||||
return hostnameVerifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection pool used to recycle HTTP and HTTPS connections.
|
||||
*
|
||||
* <p>If unset, the {@link ConnectionPool#getDefault() system-wide
|
||||
* default} connection pool will be used.
|
||||
*/
|
||||
public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
|
||||
this.connectionPool = connectionPool;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConnectionPool getConnectionPool() {
|
||||
return connectionPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure this client to follow redirects from HTTPS to HTTP and from HTTP
|
||||
* to HTTPS.
|
||||
*
|
||||
* <p>If unset, protocol redirects will be followed. This is different than
|
||||
* the built-in {@code HttpURLConnection}'s default.
|
||||
*/
|
||||
public OkHttpClient setFollowProtocolRedirects(boolean followProtocolRedirects) {
|
||||
this.followProtocolRedirects = followProtocolRedirects;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean getFollowProtocolRedirects() {
|
||||
return followProtocolRedirects;
|
||||
}
|
||||
|
||||
public HttpURLConnection open(URL url) {
|
||||
String protocol = url.getProtocol();
|
||||
if (protocol.equals("http")) {
|
||||
return new HttpURLConnectionImpl(url, copyWithDefaults());
|
||||
} else if (protocol.equals("https")) {
|
||||
return new HttpsURLConnectionImpl(url, copyWithDefaults());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this OkHttpClient that uses the system-wide default for
|
||||
* each field that hasn't been explicitly configured.
|
||||
*/
|
||||
private OkHttpClient copyWithDefaults() {
|
||||
OkHttpClient result = new OkHttpClient();
|
||||
result.proxy = proxy;
|
||||
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
|
||||
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
|
||||
result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
|
||||
result.sslSocketFactory = sslSocketFactory != null
|
||||
? sslSocketFactory
|
||||
: HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||
result.hostnameVerifier = hostnameVerifier != null
|
||||
? hostnameVerifier
|
||||
: HttpsURLConnection.getDefaultHostnameVerifier();
|
||||
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
|
||||
result.followProtocolRedirects = followProtocolRedirects;
|
||||
return result;
|
||||
}
|
||||
}
|
38
framework/src/com/squareup/okhttp/OkResponseCache.java
Normal file
38
framework/src/com/squareup/okhttp/OkResponseCache.java
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.CacheResponse;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
/**
|
||||
* A response cache that supports statistics tracking and updating stored
|
||||
* responses. Implementations of {@link java.net.ResponseCache} should implement
|
||||
* this interface to receive additional support from the HTTP engine.
|
||||
*/
|
||||
public interface OkResponseCache {
|
||||
|
||||
/** Track an HTTP response being satisfied by {@code source}. */
|
||||
void trackResponse(ResponseSource source);
|
||||
|
||||
/** Track an conditional GET that was satisfied by this cache. */
|
||||
void trackConditionalCacheHit();
|
||||
|
||||
/** Updates stored HTTP headers using a hit on a conditional GET. */
|
||||
void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
|
||||
throws IOException;
|
||||
}
|
37
framework/src/com/squareup/okhttp/ResponseSource.java
Normal file
37
framework/src/com/squareup/okhttp/ResponseSource.java
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
/** The source of an HTTP response. */
|
||||
public enum ResponseSource {
|
||||
|
||||
/** The response was returned from the local cache. */
|
||||
CACHE,
|
||||
|
||||
/**
|
||||
* The response is available in the cache but must be validated with the
|
||||
* network. The cache result will be used if it is still valid; otherwise
|
||||
* the network's response will be used.
|
||||
*/
|
||||
CONDITIONAL_CACHE,
|
||||
|
||||
/** The response was returned from the network. */
|
||||
NETWORK;
|
||||
|
||||
public boolean requiresConnection() {
|
||||
return this == CONDITIONAL_CACHE || this == NETWORK;
|
||||
}
|
||||
}
|
75
framework/src/com/squareup/okhttp/TunnelRequest.java
Normal file
75
framework/src/com/squareup/okhttp/TunnelRequest.java
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp;
|
||||
|
||||
import com.squareup.okhttp.internal.http.RawHeaders;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.getDefaultPort;
|
||||
|
||||
/**
|
||||
* Routing and authentication information sent to an HTTP proxy to create a
|
||||
* HTTPS to an origin server. Everything in the tunnel request is sent
|
||||
* unencrypted to the proxy server.
|
||||
*
|
||||
* <p>See <a href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section
|
||||
* 5.2</a>.
|
||||
*/
|
||||
public final class TunnelRequest {
|
||||
final String host;
|
||||
final int port;
|
||||
final String userAgent;
|
||||
final String proxyAuthorization;
|
||||
|
||||
/**
|
||||
* @param host the origin server's hostname. Not null.
|
||||
* @param port the origin server's port, like 80 or 443.
|
||||
* @param userAgent the client's user-agent. Not null.
|
||||
* @param proxyAuthorization proxy authorization, or null if the proxy is
|
||||
* used without an authorization header.
|
||||
*/
|
||||
public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
|
||||
if (host == null) throw new NullPointerException("host == null");
|
||||
if (userAgent == null) throw new NullPointerException("userAgent == null");
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.userAgent = userAgent;
|
||||
this.proxyAuthorization = proxyAuthorization;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're creating a TLS tunnel, send only the minimum set of headers.
|
||||
* This avoids sending potentially sensitive data like HTTP cookies to
|
||||
* the proxy unencrypted.
|
||||
*/
|
||||
RawHeaders getRequestHeaders() {
|
||||
RawHeaders result = new RawHeaders();
|
||||
result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
|
||||
|
||||
// Always set Host and User-Agent.
|
||||
result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
|
||||
result.set("User-Agent", userAgent);
|
||||
|
||||
// Copy over the Proxy-Authorization header if it exists.
|
||||
if (proxyAuthorization != null) {
|
||||
result.set("Proxy-Authorization", proxyAuthorization);
|
||||
}
|
||||
|
||||
// Always set the Proxy-Connection to Keep-Alive for the benefit of
|
||||
// HTTP/1.0 proxies like Squid.
|
||||
result.set("Proxy-Connection", "Keep-Alive");
|
||||
return result;
|
||||
}
|
||||
}
|
164
framework/src/com/squareup/okhttp/internal/Base64.java
Normal file
164
framework/src/com/squareup/okhttp/internal/Base64.java
Normal file
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @author Alexander Y. Kleymenov
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
|
||||
|
||||
/**
|
||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> encoder/decoder.
|
||||
* In violation of the RFC, this encoder doesn't wrap lines at 76 columns.
|
||||
*/
|
||||
public final class Base64 {
|
||||
private Base64() {
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] in) {
|
||||
return decode(in, in.length);
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] in, int len) {
|
||||
// approximate output length
|
||||
int length = len / 4 * 3;
|
||||
// return an empty array on empty or short input without padding
|
||||
if (length == 0) {
|
||||
return EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
// temporary array
|
||||
byte[] out = new byte[length];
|
||||
// number of padding characters ('=')
|
||||
int pad = 0;
|
||||
byte chr;
|
||||
// compute the number of the padding characters
|
||||
// and adjust the length of the input
|
||||
for (; ; len--) {
|
||||
chr = in[len - 1];
|
||||
// skip the neutral characters
|
||||
if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
|
||||
continue;
|
||||
}
|
||||
if (chr == '=') {
|
||||
pad++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// index in the output array
|
||||
int outIndex = 0;
|
||||
// index in the input array
|
||||
int inIndex = 0;
|
||||
// holds the value of the input character
|
||||
int bits = 0;
|
||||
// holds the value of the input quantum
|
||||
int quantum = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
chr = in[i];
|
||||
// skip the neutral characters
|
||||
if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
|
||||
continue;
|
||||
}
|
||||
if ((chr >= 'A') && (chr <= 'Z')) {
|
||||
// char ASCII value
|
||||
// A 65 0
|
||||
// Z 90 25 (ASCII - 65)
|
||||
bits = chr - 65;
|
||||
} else if ((chr >= 'a') && (chr <= 'z')) {
|
||||
// char ASCII value
|
||||
// a 97 26
|
||||
// z 122 51 (ASCII - 71)
|
||||
bits = chr - 71;
|
||||
} else if ((chr >= '0') && (chr <= '9')) {
|
||||
// char ASCII value
|
||||
// 0 48 52
|
||||
// 9 57 61 (ASCII + 4)
|
||||
bits = chr + 4;
|
||||
} else if (chr == '+') {
|
||||
bits = 62;
|
||||
} else if (chr == '/') {
|
||||
bits = 63;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
// append the value to the quantum
|
||||
quantum = (quantum << 6) | (byte) bits;
|
||||
if (inIndex % 4 == 3) {
|
||||
// 4 characters were read, so make the output:
|
||||
out[outIndex++] = (byte) (quantum >> 16);
|
||||
out[outIndex++] = (byte) (quantum >> 8);
|
||||
out[outIndex++] = (byte) quantum;
|
||||
}
|
||||
inIndex++;
|
||||
}
|
||||
if (pad > 0) {
|
||||
// adjust the quantum value according to the padding
|
||||
quantum = quantum << (6 * pad);
|
||||
// make output
|
||||
out[outIndex++] = (byte) (quantum >> 16);
|
||||
if (pad == 1) {
|
||||
out[outIndex++] = (byte) (quantum >> 8);
|
||||
}
|
||||
}
|
||||
// create the resulting array
|
||||
byte[] result = new byte[outIndex];
|
||||
System.arraycopy(out, 0, result, 0, outIndex);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final byte[] MAP = new byte[] {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
|
||||
'5', '6', '7', '8', '9', '+', '/'
|
||||
};
|
||||
|
||||
public static String encode(byte[] in) {
|
||||
int length = (in.length + 2) * 4 / 3;
|
||||
byte[] out = new byte[length];
|
||||
int index = 0, end = in.length - in.length % 3;
|
||||
for (int i = 0; i < end; i += 3) {
|
||||
out[index++] = MAP[(in[i] & 0xff) >> 2];
|
||||
out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
|
||||
out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
|
||||
out[index++] = MAP[(in[i + 2] & 0x3f)];
|
||||
}
|
||||
switch (in.length % 3) {
|
||||
case 1:
|
||||
out[index++] = MAP[(in[end] & 0xff) >> 2];
|
||||
out[index++] = MAP[(in[end] & 0x03) << 4];
|
||||
out[index++] = '=';
|
||||
out[index++] = '=';
|
||||
break;
|
||||
case 2:
|
||||
out[index++] = MAP[(in[end] & 0xff) >> 2];
|
||||
out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
|
||||
out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
|
||||
out[index++] = '=';
|
||||
break;
|
||||
}
|
||||
try {
|
||||
return new String(out, 0, index, "US-ASCII");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
826
framework/src/com/squareup/okhttp/internal/DiskLruCache.java
Normal file
826
framework/src/com/squareup/okhttp/internal/DiskLruCache.java
Normal file
@ -0,0 +1,826 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.UTF_8;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Values are byte
|
||||
* sequences, accessible as streams or files. Each value must be between {@code
|
||||
* 0} and {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <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
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TMP = "journal.tmp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
// This cache uses a journal file named "journal". A typical journal file
|
||||
// looks like this:
|
||||
// libcore.io.DiskLruCache
|
||||
// 1
|
||||
// 100
|
||||
// 2
|
||||
//
|
||||
// CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
// DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
// CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
// REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
// DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
// CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
// READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
// READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
//
|
||||
// The first five lines of the journal form its header. They are the
|
||||
// constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
// the application's version, the value count, and a blank line.
|
||||
//
|
||||
// Each of the subsequent lines in the file is a record of the state of a
|
||||
// cache entry. Each line contains space-separated values: a state, a key,
|
||||
// and optional state-specific values.
|
||||
// o DIRTY lines track that an entry is actively being created or updated.
|
||||
// Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
// action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
// temporary files may need to be deleted.
|
||||
// o CLEAN lines track a cache entry that has been successfully published
|
||||
// and may be read. A publish line is followed by the lengths of each of
|
||||
// its values.
|
||||
// o READ lines track accesses for LRU.
|
||||
// o REMOVE lines track entries that have been deleted.
|
||||
//
|
||||
// The journal file is appended to as cache operations occur. The journal may
|
||||
// occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
// "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
// it exists when the cache is opened.
|
||||
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final int appVersion;
|
||||
private final long maxSize;
|
||||
private final int valueCount;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<String, Entry>(0, 0.75f, true);
|
||||
private int redundantOpCount;
|
||||
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0;
|
||||
|
||||
/** This cache uses a single background thread to evict entries. */
|
||||
private final ExecutorService executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||
private final Callable<Void> cleanupCallable = new Callable<Void>() {
|
||||
@Override public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // closed
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// prefer to pick up where we left off
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
Platform.get()
|
||||
.logW("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// create a new empty cache
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
|
||||
.equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
|
||||
.equals(blank)) {
|
||||
throw new IOException("unexpected journal header: ["
|
||||
+ magic
|
||||
+ ", "
|
||||
+ version
|
||||
+ ", "
|
||||
+ valueCountString
|
||||
+ ", "
|
||||
+ blank
|
||||
+ "]");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
String[] parts = line.split(" ");
|
||||
if (parts.length < 2) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
String key = parts[1];
|
||||
if (parts[0].equals(REMOVE) && parts.length == 2) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
|
||||
} else if (parts[0].equals(DIRTY) && parts.length == 2) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (parts[0].equals(READ) && parts.length == 2) {
|
||||
// this work was already done by calling lruEntries.get()
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
writer.close();
|
||||
journalFileTmp.renameTo(journalFile);
|
||||
journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// a file must have been deleted manually!
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ' + key + '\n');
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // snapshot is stale
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // another edit is in progress
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// flush the journal before creating files to prevent file leaks
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/** Returns the directory where this cache stores its data. */
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public long maxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// if this edit is creating the entry for the first time, every index must have a value
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ' + key + '\n');
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns true if this cache has been closed. */
|
||||
public boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/** Force buffered operations to the filesystem. */
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/** Closes this cache. Stored values will remain on the filesystem. */
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // already closed
|
||||
}
|
||||
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
|
||||
throw new IllegalArgumentException(
|
||||
"keys must not contain spaces or newlines: \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, UTF_8));
|
||||
}
|
||||
|
||||
/** A snapshot of the values for an entry. */
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/** Returns the unbuffered stream with the value for {@code index}. */
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/** Returns the string value for {@code index}. */
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Edits the values for an entry. */
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) throws IOException {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the value at {@code index} to {@code value}. */
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // the previous entry is stale
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
private final class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/** Lengths of this entry's files. */
|
||||
private final long[] lengths;
|
||||
|
||||
/** True if this entry has ever been published. */
|
||||
private boolean readable;
|
||||
|
||||
/** The ongoing edit or null if this entry is not being edited. */
|
||||
private Editor currentEditor;
|
||||
|
||||
/** The sequence number of the most recently committed edit to this entry. */
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/** Set lengths using decimal numbers like "10123". */
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
}
|
33
framework/src/com/squareup/okhttp/internal/Dns.java
Normal file
33
framework/src/com/squareup/okhttp/internal/Dns.java
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Domain name service. Prefer this over {@link InetAddress#getAllByName} to
|
||||
* make code more testable.
|
||||
*/
|
||||
public interface Dns {
|
||||
Dns DEFAULT = new Dns() {
|
||||
@Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
|
||||
return InetAddress.getAllByName(host);
|
||||
}
|
||||
};
|
||||
|
||||
InetAddress[] getAllByName(String host) throws UnknownHostException;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Runnable implementation which always sets its thread name.
|
||||
*/
|
||||
public abstract class NamedRunnable implements Runnable {
|
||||
private String name;
|
||||
|
||||
public NamedRunnable(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override public final void run() {
|
||||
String oldName = Thread.currentThread().getName();
|
||||
Thread.currentThread().setName(name);
|
||||
try {
|
||||
execute();
|
||||
} finally {
|
||||
Thread.currentThread().setName(oldName);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void execute();
|
||||
}
|
343
framework/src/com/squareup/okhttp/internal/Platform.java
Normal file
343
framework/src/com/squareup/okhttp/internal/Platform.java
Normal file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
/**
|
||||
* Access to Platform-specific features necessary for SPDY and advanced TLS.
|
||||
*
|
||||
* <h3>SPDY</h3>
|
||||
* SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
|
||||
* available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
|
||||
* also requires a recent version of {@code DeflaterOutputStream} that is
|
||||
* public API in Java 7 and callable via reflection in Android 4.1+.
|
||||
*/
|
||||
public class Platform {
|
||||
private static final Platform PLATFORM = findPlatform();
|
||||
|
||||
private Constructor<DeflaterOutputStream> deflaterConstructor;
|
||||
|
||||
public static Platform get() {
|
||||
return PLATFORM;
|
||||
}
|
||||
|
||||
public void logW(String warning) {
|
||||
System.out.println(warning);
|
||||
}
|
||||
|
||||
public void tagSocket(Socket socket) throws SocketException {
|
||||
}
|
||||
|
||||
public void untagSocket(Socket socket) throws SocketException {
|
||||
}
|
||||
|
||||
public URI toUriLenient(URL url) throws URISyntaxException {
|
||||
return url.toURI(); // this isn't as good as the built-in toUriLenient
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt a TLS connection with useful extensions enabled. This mode
|
||||
* supports more features, but is less likely to be compatible with older
|
||||
* HTTPS servers.
|
||||
*/
|
||||
public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt a secure connection with basic functionality to maximize
|
||||
* compatibility. Currently this uses SSL 3.0.
|
||||
*/
|
||||
public void supportTlsIntolerantServer(SSLSocket socket) {
|
||||
socket.setEnabledProtocols(new String[] {"SSLv3"});
|
||||
}
|
||||
|
||||
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
|
||||
public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets client-supported protocols on a socket to send to a server. The
|
||||
* protocols are only sent if the socket implementation supports NPN.
|
||||
*/
|
||||
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
|
||||
* value blocks. This throws an {@link UnsupportedOperationException} on
|
||||
* Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
|
||||
*/
|
||||
public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
|
||||
boolean syncFlush) {
|
||||
try {
|
||||
Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
|
||||
if (constructor == null) {
|
||||
constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
|
||||
OutputStream.class, Deflater.class, boolean.class);
|
||||
}
|
||||
return constructor.newInstance(out, deflater, syncFlush);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
|
||||
: new RuntimeException(e.getCause());
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt to match the host runtime to a capable Platform implementation. */
|
||||
private static Platform findPlatform() {
|
||||
// Attempt to find Android 2.3+ APIs.
|
||||
Class<?> openSslSocketClass;
|
||||
Method setUseSessionTickets;
|
||||
Method setHostname;
|
||||
try {
|
||||
openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
|
||||
setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
|
||||
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
|
||||
|
||||
// Attempt to find Android 4.1+ APIs.
|
||||
try {
|
||||
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
|
||||
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
|
||||
return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
|
||||
getNpnSelectedProtocol);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
|
||||
}
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// This isn't an Android runtime.
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// This isn't Android 2.3 or better.
|
||||
}
|
||||
|
||||
// Attempt to find the Jetty's NPN extension for OpenJDK.
|
||||
try {
|
||||
String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
|
||||
Class<?> nextProtoNegoClass = Class.forName(npnClassName);
|
||||
Class<?> providerClass = Class.forName(npnClassName + "$Provider");
|
||||
Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
|
||||
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
|
||||
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
|
||||
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
|
||||
return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
|
||||
serverProviderClass);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
return new Platform(); // NPN isn't on the classpath.
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return new Platform(); // The NPN version isn't what we expect.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android version 2.3 and newer support TLS session tickets and server name
|
||||
* indication (SNI).
|
||||
*/
|
||||
private static class Android23 extends Platform {
|
||||
protected final Class<?> openSslSocketClass;
|
||||
private final Method setUseSessionTickets;
|
||||
private final Method setHostname;
|
||||
|
||||
private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
|
||||
Method setHostname) {
|
||||
this.openSslSocketClass = openSslSocketClass;
|
||||
this.setUseSessionTickets = setUseSessionTickets;
|
||||
this.setHostname = setHostname;
|
||||
}
|
||||
|
||||
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
||||
super.enableTlsExtensions(socket, uriHost);
|
||||
if (openSslSocketClass.isInstance(socket)) {
|
||||
// This is Android: use reflection on OpenSslSocketImpl.
|
||||
try {
|
||||
setUseSessionTickets.invoke(socket, true);
|
||||
setHostname.invoke(socket, uriHost);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Android version 4.1 and newer support NPN. */
|
||||
private static class Android41 extends Android23 {
|
||||
private final Method setNpnProtocols;
|
||||
private final Method getNpnSelectedProtocol;
|
||||
|
||||
private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
|
||||
Method setNpnProtocols, Method getNpnSelectedProtocol) {
|
||||
super(openSslSocketClass, setUseSessionTickets, setHostname);
|
||||
this.setNpnProtocols = setNpnProtocols;
|
||||
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
|
||||
}
|
||||
|
||||
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||
if (!openSslSocketClass.isInstance(socket)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||
if (!openSslSocketClass.isInstance(socket)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (byte[]) getNpnSelectedProtocol.invoke(socket);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
|
||||
* path.
|
||||
*/
|
||||
private static class JdkWithJettyNpnPlatform extends Platform {
|
||||
private final Method getMethod;
|
||||
private final Method putMethod;
|
||||
private final Class<?> clientProviderClass;
|
||||
private final Class<?> serverProviderClass;
|
||||
|
||||
public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
|
||||
Class<?> serverProviderClass) {
|
||||
this.putMethod = putMethod;
|
||||
this.getMethod = getMethod;
|
||||
this.clientProviderClass = clientProviderClass;
|
||||
this.serverProviderClass = serverProviderClass;
|
||||
}
|
||||
|
||||
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
||||
try {
|
||||
List<String> strings = new ArrayList<String>();
|
||||
for (int i = 0; i < npnProtocols.length; ) {
|
||||
int length = npnProtocols[i++];
|
||||
strings.add(new String(npnProtocols, i, length, "US-ASCII"));
|
||||
i += length;
|
||||
}
|
||||
Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
|
||||
new Class[] {clientProviderClass, serverProviderClass},
|
||||
new JettyNpnProvider(strings));
|
||||
putMethod.invoke(null, socket, provider);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
||||
try {
|
||||
JettyNpnProvider provider =
|
||||
(JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
|
||||
if (!provider.unsupported && provider.selected == null) {
|
||||
Logger logger = Logger.getLogger(OkHttpClient.class.getName());
|
||||
logger.log(Level.INFO,
|
||||
"NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
|
||||
return null;
|
||||
}
|
||||
return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new AssertionError();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the methods of NextProtoNego's ClientProvider and ServerProvider
|
||||
* without a compile-time dependency on those interfaces.
|
||||
*/
|
||||
private static class JettyNpnProvider implements InvocationHandler {
|
||||
private final List<String> protocols;
|
||||
private boolean unsupported;
|
||||
private String selected;
|
||||
|
||||
public JettyNpnProvider(List<String> protocols) {
|
||||
this.protocols = protocols;
|
||||
}
|
||||
|
||||
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
String methodName = method.getName();
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (args == null) {
|
||||
args = Util.EMPTY_STRING_ARRAY;
|
||||
}
|
||||
if (methodName.equals("supports") && boolean.class == returnType) {
|
||||
return true;
|
||||
} else if (methodName.equals("unsupported") && void.class == returnType) {
|
||||
this.unsupported = true;
|
||||
return null;
|
||||
} else if (methodName.equals("protocols") && args.length == 0) {
|
||||
return protocols;
|
||||
} else if (methodName.equals("selectProtocol")
|
||||
&& String.class == returnType
|
||||
&& args.length == 1
|
||||
&& (args[0] == null || args[0] instanceof List)) {
|
||||
// TODO: use OpenSSL's algorithm which uses both lists
|
||||
List<?> serverProtocols = (List) args[0];
|
||||
this.selected = protocols.get(0);
|
||||
return selected;
|
||||
} else if (methodName.equals("protocolSelected") && args.length == 1) {
|
||||
this.selected = (String) args[0];
|
||||
return null;
|
||||
} else {
|
||||
return method.invoke(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
231
framework/src/com/squareup/okhttp/internal/StrictLineReader.java
Normal file
231
framework/src/com/squareup/okhttp/internal/StrictLineReader.java
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.ISO_8859_1;
|
||||
import static com.squareup.okhttp.internal.Util.US_ASCII;
|
||||
import static com.squareup.okhttp.internal.Util.UTF_8;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* This class is used for buffered reading of lines. For purposes of this class, a line ends with
|
||||
* "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
|
||||
* end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
|
||||
* to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link BufferedReader} which in conjunction with
|
||||
* {@link InputStreamReader} provides similar functionality, this class uses different
|
||||
* 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
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
public class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
// Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
// and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
// an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
// {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code StrictLineReader} with the default capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @throws NullPointerException if {@code in} is null.
|
||||
*/
|
||||
public StrictLineReader(InputStream in) {
|
||||
this(in, 8192);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and the default charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @throws NullPointerException if {@code in} is null.
|
||||
* @throws IllegalArgumentException for negative or zero {@code capacity}.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity) {
|
||||
this(in, capacity, US_ASCII);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data.
|
||||
* Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data.
|
||||
* Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset);
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
return new String(buf, 0, length, charset);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an {@code int} from a line containing its decimal representation.
|
||||
*
|
||||
* @return the value of the {@code int} from the next line.
|
||||
* @throws IOException for underlying {@code InputStream} errors or conversion error.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public int readInt() throws IOException {
|
||||
String intString = readLine();
|
||||
try {
|
||||
return Integer.parseInt(intString);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("expected an int but was \"" + intString + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
|
325
framework/src/com/squareup/okhttp/internal/Util.java
Normal file
325
framework/src/com/squareup/okhttp/internal/Util.java
Normal file
@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/** Junk drawer of utility methods. */
|
||||
public final class Util {
|
||||
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
public static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
|
||||
/** A cheap and type-safe constant for the ISO-8859-1 Charset. */
|
||||
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
|
||||
|
||||
/** A cheap and type-safe constant for the US-ASCII Charset. */
|
||||
public static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||
|
||||
/** A cheap and type-safe constant for the UTF-8 Charset. */
|
||||
public static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
public static int getEffectivePort(URI uri) {
|
||||
return getEffectivePort(uri.getScheme(), uri.getPort());
|
||||
}
|
||||
|
||||
public static int getEffectivePort(URL url) {
|
||||
return getEffectivePort(url.getProtocol(), url.getPort());
|
||||
}
|
||||
|
||||
private static int getEffectivePort(String scheme, int specifiedPort) {
|
||||
return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
|
||||
}
|
||||
|
||||
public static int getDefaultPort(String scheme) {
|
||||
if ("http".equalsIgnoreCase(scheme)) {
|
||||
return 80;
|
||||
} else if ("https".equalsIgnoreCase(scheme)) {
|
||||
return 443;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkOffsetAndCount(int arrayLength, int offset, int count) {
|
||||
if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
|
||||
throw new ArrayIndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
|
||||
if (order == ByteOrder.BIG_ENDIAN) {
|
||||
dst[offset++] = (byte) ((value >> 24) & 0xff);
|
||||
dst[offset++] = (byte) ((value >> 16) & 0xff);
|
||||
dst[offset++] = (byte) ((value >> 8) & 0xff);
|
||||
dst[offset] = (byte) ((value >> 0) & 0xff);
|
||||
} else {
|
||||
dst[offset++] = (byte) ((value >> 0) & 0xff);
|
||||
dst[offset++] = (byte) ((value >> 8) & 0xff);
|
||||
dst[offset++] = (byte) ((value >> 16) & 0xff);
|
||||
dst[offset] = (byte) ((value >> 24) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if two possibly-null objects are equal. */
|
||||
public static boolean equal(Object a, Object b) {
|
||||
return a == b || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes {@code closeable}, ignoring any checked exceptions. Does nothing
|
||||
* if {@code closeable} is null.
|
||||
*/
|
||||
public static void closeQuietly(Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes {@code socket}, ignoring any checked exceptions. Does nothing if
|
||||
* {@code socket} is null.
|
||||
*/
|
||||
public static void closeQuietly(Socket socket) {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes {@code a} and {@code b}. If either close fails, this completes
|
||||
* the other close and rethrows the first encountered exception.
|
||||
*/
|
||||
public static void closeAll(Closeable a, Closeable b) throws IOException {
|
||||
Throwable thrown = null;
|
||||
try {
|
||||
a.close();
|
||||
} catch (Throwable e) {
|
||||
thrown = e;
|
||||
}
|
||||
try {
|
||||
b.close();
|
||||
} catch (Throwable e) {
|
||||
if (thrown == null) thrown = e;
|
||||
}
|
||||
if (thrown == null) return;
|
||||
if (thrown instanceof IOException) throw (IOException) thrown;
|
||||
if (thrown instanceof RuntimeException) throw (RuntimeException) thrown;
|
||||
if (thrown instanceof Error) throw (Error) thrown;
|
||||
throw new AssertionError(thrown);
|
||||
}
|
||||
|
||||
/** Recursively delete everything in {@code dir}. */
|
||||
// TODO: this should specify paths as Strings rather than as Files
|
||||
public static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IllegalArgumentException("not a directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int).
|
||||
* InputStream assumes that you implement InputStream.read(int) and provides default
|
||||
* implementations of the others, but often the opposite is more efficient.
|
||||
*/
|
||||
public static int readSingleByte(InputStream in) throws IOException {
|
||||
byte[] buffer = new byte[1];
|
||||
int result = in.read(buffer, 0, 1);
|
||||
return (result != -1) ? buffer[0] & 0xff : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int).
|
||||
* OutputStream assumes that you implement OutputStream.write(int) and provides default
|
||||
* implementations of the others, but often the opposite is more efficient.
|
||||
*/
|
||||
public static void writeSingleByte(OutputStream out, int b) throws IOException {
|
||||
byte[] buffer = new byte[1];
|
||||
buffer[0] = (byte) (b & 0xff);
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
|
||||
*/
|
||||
public static void readFully(InputStream in, byte[] dst) throws IOException {
|
||||
readFully(in, dst, 0, dst.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
|
||||
* EOFException if insufficient bytes are available.
|
||||
*
|
||||
* Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
|
||||
*/
|
||||
public static void readFully(InputStream in, byte[] dst, int offset, int byteCount)
|
||||
throws IOException {
|
||||
if (byteCount == 0) {
|
||||
return;
|
||||
}
|
||||
if (in == null) {
|
||||
throw new NullPointerException("in == null");
|
||||
}
|
||||
if (dst == null) {
|
||||
throw new NullPointerException("dst == null");
|
||||
}
|
||||
checkOffsetAndCount(dst.length, offset, byteCount);
|
||||
while (byteCount > 0) {
|
||||
int bytesRead = in.read(dst, offset, byteCount);
|
||||
if (bytesRead < 0) {
|
||||
throw new EOFException();
|
||||
}
|
||||
offset += bytesRead;
|
||||
byteCount -= bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the remainder of 'reader' as a string, closing it when done. */
|
||||
public static String readFully(Reader reader) throws IOException {
|
||||
try {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void skipAll(InputStream in) throws IOException {
|
||||
do {
|
||||
in.skip(Long.MAX_VALUE);
|
||||
} while (in.read() != -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@code in.read()} repeatedly until either the stream is exhausted or
|
||||
* {@code byteCount} bytes have been read.
|
||||
*
|
||||
* <p>This method reuses the skip buffer but is careful to never use it at
|
||||
* the same time that another stream is using it. Otherwise streams that use
|
||||
* the caller's buffer for consistency checks like CRC could be clobbered by
|
||||
* other threads. A thread-local buffer is also insufficient because some
|
||||
* streams may call other streams in their skip() method, also clobbering the
|
||||
* buffer.
|
||||
*/
|
||||
public static long skipByReading(InputStream in, long byteCount) throws IOException {
|
||||
// acquire the shared skip buffer.
|
||||
byte[] buffer = skipBuffer.getAndSet(null);
|
||||
if (buffer == null) {
|
||||
buffer = new byte[4096];
|
||||
}
|
||||
|
||||
long skipped = 0;
|
||||
while (skipped < byteCount) {
|
||||
int toRead = (int) Math.min(byteCount - skipped, buffer.length);
|
||||
int read = in.read(buffer, 0, toRead);
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
skipped += read;
|
||||
if (read < toRead) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// release the shared skip buffer.
|
||||
skipBuffer.set(buffer);
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
|
||||
* Returns the total number of bytes transferred.
|
||||
*/
|
||||
public static int copy(InputStream in, OutputStream out) throws IOException {
|
||||
int total = 0;
|
||||
byte[] buffer = new byte[8192];
|
||||
int c;
|
||||
while ((c = in.read(buffer)) != -1) {
|
||||
total += c;
|
||||
out.write(buffer, 0, c);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ASCII characters up to but not including the next "\r\n", or
|
||||
* "\n".
|
||||
*
|
||||
* @throws java.io.EOFException if the stream is exhausted before the next newline
|
||||
* character.
|
||||
*/
|
||||
public static String readAsciiLine(InputStream in) throws IOException {
|
||||
// TODO: support UTF-8 here instead
|
||||
StringBuilder result = new StringBuilder(80);
|
||||
while (true) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
throw new EOFException();
|
||||
} else if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
|
||||
result.append((char) c);
|
||||
}
|
||||
int length = result.length();
|
||||
if (length > 0 && result.charAt(length - 1) == '\r') {
|
||||
result.setLength(length - 1);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheRequest;
|
||||
|
||||
/**
|
||||
* An input stream for the body of an HTTP response.
|
||||
*
|
||||
* <p>Since a single socket's input stream may be used to read multiple HTTP
|
||||
* responses from the same server, subclasses shouldn't close the socket stream.
|
||||
*
|
||||
* <p>A side effect of reading an HTTP response is that the response cache
|
||||
* is populated. If the stream is closed early, that cache entry will be
|
||||
* invalidated.
|
||||
*/
|
||||
abstract class AbstractHttpInputStream extends InputStream {
|
||||
protected final InputStream in;
|
||||
protected final HttpEngine httpEngine;
|
||||
private final CacheRequest cacheRequest;
|
||||
private final OutputStream cacheBody;
|
||||
protected boolean closed;
|
||||
|
||||
AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, CacheRequest cacheRequest)
|
||||
throws IOException {
|
||||
this.in = in;
|
||||
this.httpEngine = httpEngine;
|
||||
|
||||
OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
|
||||
|
||||
// some apps return a null body; for compatibility we treat that like a null cache request
|
||||
if (cacheBody == null) {
|
||||
cacheRequest = null;
|
||||
}
|
||||
|
||||
this.cacheBody = cacheBody;
|
||||
this.cacheRequest = cacheRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* read() is implemented using read(byte[], int, int) so subclasses only
|
||||
* need to override the latter.
|
||||
*/
|
||||
@Override public final int read() throws IOException {
|
||||
return Util.readSingleByte(this);
|
||||
}
|
||||
|
||||
protected final void checkNotClosed() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("stream closed");
|
||||
}
|
||||
}
|
||||
|
||||
protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
if (cacheBody != null) {
|
||||
cacheBody.write(buffer, offset, count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache entry and makes the socket available for reuse. This
|
||||
* should be invoked when the end of the body has been reached.
|
||||
*/
|
||||
protected final void endOfInput(boolean streamCancelled) throws IOException {
|
||||
if (cacheRequest != null) {
|
||||
cacheBody.close();
|
||||
}
|
||||
httpEngine.release(streamCancelled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls abort on the cache entry and disconnects the socket. This
|
||||
* should be invoked when the connection is closed unexpectedly to
|
||||
* invalidate the cache entry and to prevent the HTTP connection from
|
||||
* being reused. HTTP messages are sent in serial so whenever a message
|
||||
* cannot be read to completion, subsequent messages cannot be read
|
||||
* either and the connection must be discarded.
|
||||
*
|
||||
* <p>An earlier implementation skipped the remaining bytes, but this
|
||||
* requires that the entire transfer be completed. If the intention was
|
||||
* to cancel the transfer, closing the connection is the only solution.
|
||||
*/
|
||||
protected final void unexpectedEndOfInput() {
|
||||
if (cacheRequest != null) {
|
||||
cacheRequest.abort();
|
||||
}
|
||||
httpEngine.release(true);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* An output stream for the body of an HTTP request.
|
||||
*
|
||||
* <p>Since a single socket's output stream may be used to write multiple HTTP
|
||||
* requests to the same server, subclasses should not close the socket stream.
|
||||
*/
|
||||
abstract class AbstractHttpOutputStream extends OutputStream {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
final class HeaderParser {
|
||||
|
||||
public interface CacheControlHandler {
|
||||
void handle(String directive, String parameter);
|
||||
}
|
||||
|
||||
/** Parse a comma-separated list of cache control header values. */
|
||||
public static void parseCacheControl(String value, CacheControlHandler handler) {
|
||||
int pos = 0;
|
||||
while (pos < value.length()) {
|
||||
int tokenStart = pos;
|
||||
pos = skipUntil(value, pos, "=,");
|
||||
String directive = value.substring(tokenStart, pos).trim();
|
||||
|
||||
if (pos == value.length() || value.charAt(pos) == ',') {
|
||||
pos++; // consume ',' (if necessary)
|
||||
handler.handle(directive, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
pos++; // consume '='
|
||||
pos = skipWhitespace(value, pos);
|
||||
|
||||
String parameter;
|
||||
|
||||
// quoted string
|
||||
if (pos < value.length() && value.charAt(pos) == '\"') {
|
||||
pos++; // consume '"' open quote
|
||||
int parameterStart = pos;
|
||||
pos = skipUntil(value, pos, "\"");
|
||||
parameter = value.substring(parameterStart, pos);
|
||||
pos++; // consume '"' close quote (if necessary)
|
||||
|
||||
// unquoted string
|
||||
} else {
|
||||
int parameterStart = pos;
|
||||
pos = skipUntil(value, pos, ",");
|
||||
parameter = value.substring(parameterStart, pos).trim();
|
||||
}
|
||||
|
||||
handler.handle(directive, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next index in {@code input} at or after {@code pos} that
|
||||
* contains a character from {@code characters}. Returns the input length if
|
||||
* none of the requested characters can be found.
|
||||
*/
|
||||
public static int skipUntil(String input, int pos, String characters) {
|
||||
for (; pos < input.length(); pos++) {
|
||||
if (characters.indexOf(input.charAt(pos)) != -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next non-whitespace character in {@code input} that is white
|
||||
* space. Result is undefined if input contains newline characters.
|
||||
*/
|
||||
public static int skipWhitespace(String input, int pos) {
|
||||
for (; pos < input.length(); pos++) {
|
||||
char c = input.charAt(pos);
|
||||
if (c != ' ' && c != '\t') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code value} as a positive integer, or 0 if it is negative, or
|
||||
* -1 if it cannot be parsed.
|
||||
*/
|
||||
public static int parseSeconds(String value) {
|
||||
try {
|
||||
long seconds = Long.parseLong(value);
|
||||
if (seconds > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
} else if (seconds < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return (int) seconds;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private HeaderParser() {
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.internal.Base64;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
|
||||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||
|
||||
/** Handles HTTP authentication headers from origin and proxy servers. */
|
||||
public final class HttpAuthenticator {
|
||||
private HttpAuthenticator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a failed authorization response by looking up new credentials.
|
||||
*
|
||||
* @return true if credentials have been added to successorRequestHeaders
|
||||
* and another request should be attempted.
|
||||
*/
|
||||
public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
|
||||
RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
|
||||
if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
// Keep asking for username/password until authorized.
|
||||
String challengeHeader =
|
||||
responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
|
||||
String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
|
||||
if (credentials == null) {
|
||||
return false; // Could not find credentials so end the request cycle.
|
||||
}
|
||||
|
||||
// Add authorization credentials, bypassing the already-connected check.
|
||||
String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
|
||||
successorRequestHeaders.set(fieldName, credentials);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization credentials that may satisfy the challenge.
|
||||
* Returns null if a challenge header was not provided or if credentials
|
||||
* were not available.
|
||||
*/
|
||||
private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
|
||||
Proxy proxy, URL url) throws IOException {
|
||||
List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
|
||||
if (challenges.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Challenge challenge : challenges) {
|
||||
// Use the global authenticator to get the password.
|
||||
PasswordAuthentication auth;
|
||||
if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
|
||||
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
|
||||
auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
|
||||
getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
|
||||
challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
|
||||
} else {
|
||||
auth = Authenticator.requestPasswordAuthentication(url.getHost(),
|
||||
getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
|
||||
challenge.scheme, url, Authenticator.RequestorType.SERVER);
|
||||
}
|
||||
if (auth == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use base64 to encode the username and password.
|
||||
String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
|
||||
byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
|
||||
String encoded = Base64.encode(bytes);
|
||||
return challenge.scheme + " " + encoded;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
|
||||
return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
|
||||
? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RFC 2617 challenges. This API is only interested in the scheme
|
||||
* name and realm.
|
||||
*/
|
||||
private static List<Challenge> parseChallenges(RawHeaders responseHeaders,
|
||||
String challengeHeader) {
|
||||
// auth-scheme = token
|
||||
// auth-param = token "=" ( token | quoted-string )
|
||||
// challenge = auth-scheme 1*SP 1#auth-param
|
||||
// realm = "realm" "=" realm-value
|
||||
// realm-value = quoted-string
|
||||
List<Challenge> result = new ArrayList<Challenge>();
|
||||
for (int h = 0; h < responseHeaders.length(); h++) {
|
||||
if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) {
|
||||
continue;
|
||||
}
|
||||
String value = responseHeaders.getValue(h);
|
||||
int pos = 0;
|
||||
while (pos < value.length()) {
|
||||
int tokenStart = pos;
|
||||
pos = HeaderParser.skipUntil(value, pos, " ");
|
||||
|
||||
String scheme = value.substring(tokenStart, pos).trim();
|
||||
pos = HeaderParser.skipWhitespace(value, pos);
|
||||
|
||||
// TODO: This currently only handles schemes with a 'realm' parameter;
|
||||
// It needs to be fixed to handle any scheme and any parameters
|
||||
// http://code.google.com/p/android/issues/detail?id=11140
|
||||
|
||||
if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
|
||||
break; // Unexpected challenge parameter; give up!
|
||||
}
|
||||
|
||||
pos += "realm=\"".length();
|
||||
int realmStart = pos;
|
||||
pos = HeaderParser.skipUntil(value, pos, "\"");
|
||||
String realm = value.substring(realmStart, pos);
|
||||
pos++; // Consume '"' close quote.
|
||||
pos = HeaderParser.skipUntil(value, pos, ",");
|
||||
pos++; // Consume ',' comma.
|
||||
pos = HeaderParser.skipWhitespace(value, pos);
|
||||
result.add(new Challenge(scheme, realm));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** An RFC 2617 challenge. */
|
||||
private static final class Challenge {
|
||||
final String scheme;
|
||||
final String realm;
|
||||
|
||||
Challenge(String scheme, String realm) {
|
||||
this.scheme = scheme;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof Challenge
|
||||
&& ((Challenge) o).scheme.equals(scheme)
|
||||
&& ((Challenge) o).realm.equals(realm);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return scheme.hashCode() + 31 * realm.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Best-effort parser for HTTP dates.
|
||||
*/
|
||||
final class HttpDate {
|
||||
|
||||
/**
|
||||
* Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
|
||||
* cookies are on the fast path.
|
||||
*/
|
||||
private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT =
|
||||
new ThreadLocal<DateFormat>() {
|
||||
@Override protected DateFormat initialValue() {
|
||||
DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
|
||||
rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return rfc1123;
|
||||
}
|
||||
};
|
||||
|
||||
/** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */
|
||||
private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
|
||||
/* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
|
||||
"EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
|
||||
"EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
|
||||
"EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z",
|
||||
"EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z",
|
||||
"EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z",
|
||||
"EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z",
|
||||
|
||||
/* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
|
||||
"EEE MMM d yyyy HH:mm:ss z", };
|
||||
|
||||
/**
|
||||
* Returns the date for {@code value}. Returns null if the value couldn't be
|
||||
* parsed.
|
||||
*/
|
||||
public static Date parse(String value) {
|
||||
try {
|
||||
return STANDARD_DATE_FORMAT.get().parse(value);
|
||||
} catch (ParseException ignore) {
|
||||
}
|
||||
for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
|
||||
try {
|
||||
return new SimpleDateFormat(formatString, Locale.US).parse(value);
|
||||
} catch (ParseException ignore) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the string for {@code value}. */
|
||||
public static String format(Date value) {
|
||||
return STANDARD_DATE_FORMAT.get().format(value);
|
||||
}
|
||||
|
||||
private HttpDate() {
|
||||
}
|
||||
}
|
666
framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
Normal file
666
framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
Normal file
@ -0,0 +1,666 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.Address;
|
||||
import com.squareup.okhttp.Connection;
|
||||
import com.squareup.okhttp.OkResponseCache;
|
||||
import com.squareup.okhttp.ResponseSource;
|
||||
import com.squareup.okhttp.TunnelRequest;
|
||||
import com.squareup.okhttp.internal.Dns;
|
||||
import com.squareup.okhttp.internal.Platform;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheRequest;
|
||||
import java.net.CacheResponse;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.Proxy;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
|
||||
import static com.squareup.okhttp.internal.Util.getDefaultPort;
|
||||
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||
|
||||
/**
|
||||
* Handles a single HTTP request/response pair. Each HTTP engine follows this
|
||||
* lifecycle:
|
||||
* <ol>
|
||||
* <li>It is created.
|
||||
* <li>The HTTP request message is sent with sendRequest(). Once the request
|
||||
* is sent it is an error to modify the request headers. After
|
||||
* sendRequest() has been called the request body can be written to if
|
||||
* it exists.
|
||||
* <li>The HTTP response message is read with readResponse(). After the
|
||||
* response has been read the response headers and body can be read.
|
||||
* All responses have a response body input stream, though in some
|
||||
* instances this stream is empty.
|
||||
* </ol>
|
||||
*
|
||||
* <p>The request and response may be served by the HTTP response cache, by the
|
||||
* network, or by both in the event of a conditional GET.
|
||||
*
|
||||
* <p>This class may hold a socket connection that needs to be released or
|
||||
* recycled. By default, this socket connection is held when the last byte of
|
||||
* the response is consumed. To release the connection when it is no longer
|
||||
* required, use {@link #automaticallyReleaseConnectionToPool()}.
|
||||
*/
|
||||
public class HttpEngine {
|
||||
private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() {
|
||||
@Override public Map<String, List<String>> getHeaders() throws IOException {
|
||||
Map<String, List<String>> result = new HashMap<String, List<String>>();
|
||||
result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout"));
|
||||
return result;
|
||||
}
|
||||
@Override public InputStream getBody() throws IOException {
|
||||
return new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
|
||||
}
|
||||
};
|
||||
public static final int HTTP_CONTINUE = 100;
|
||||
|
||||
protected final HttpURLConnectionImpl policy;
|
||||
|
||||
protected final String method;
|
||||
|
||||
private ResponseSource responseSource;
|
||||
|
||||
protected Connection connection;
|
||||
protected RouteSelector routeSelector;
|
||||
private OutputStream requestBodyOut;
|
||||
|
||||
private Transport transport;
|
||||
|
||||
private InputStream responseTransferIn;
|
||||
private InputStream responseBodyIn;
|
||||
|
||||
private CacheResponse cacheResponse;
|
||||
private CacheRequest cacheRequest;
|
||||
|
||||
/** The time when the request headers were written, or -1 if they haven't been written yet. */
|
||||
long sentRequestMillis = -1;
|
||||
|
||||
/**
|
||||
* True if this client added an "Accept-Encoding: gzip" header field and is
|
||||
* therefore responsible for also decompressing the transfer stream.
|
||||
*/
|
||||
private boolean transparentGzip;
|
||||
|
||||
final URI uri;
|
||||
|
||||
final RequestHeaders requestHeaders;
|
||||
|
||||
/** Null until a response is received from the network or the cache. */
|
||||
ResponseHeaders responseHeaders;
|
||||
|
||||
// The cache response currently being validated on a conditional get. Null
|
||||
// if the cached response doesn't exist or doesn't need validation. If the
|
||||
// conditional get succeeds, these will be used for the response headers and
|
||||
// body. If it fails, these be closed and set to null.
|
||||
private ResponseHeaders cachedResponseHeaders;
|
||||
private InputStream cachedResponseBody;
|
||||
|
||||
/**
|
||||
* True if the socket connection should be released to the connection pool
|
||||
* when the response has been fully read.
|
||||
*/
|
||||
private boolean automaticallyReleaseConnectionToPool;
|
||||
|
||||
/** True if the socket connection is no longer needed by this engine. */
|
||||
private boolean connectionReleased;
|
||||
|
||||
/**
|
||||
* @param requestHeaders the client's supplied request headers. This class
|
||||
* creates a private copy that it can mutate.
|
||||
* @param connection the connection used for an intermediate response
|
||||
* immediately prior to this request/response pair, such as a same-host
|
||||
* redirect. This engine assumes ownership of the connection and must
|
||||
* release it when it is unneeded.
|
||||
*/
|
||||
public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
|
||||
Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
|
||||
this.policy = policy;
|
||||
this.method = method;
|
||||
this.connection = connection;
|
||||
this.requestBodyOut = requestBodyOut;
|
||||
|
||||
try {
|
||||
uri = Platform.get().toUriLenient(policy.getURL());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out what the response source will be, and opens a socket to that
|
||||
* source if necessary. Prepares the request headers and gets ready to start
|
||||
* writing the request body if it exists.
|
||||
*/
|
||||
public final void sendRequest() throws IOException {
|
||||
if (responseSource != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
prepareRawRequestHeaders();
|
||||
initResponseSource();
|
||||
if (policy.responseCache instanceof OkResponseCache) {
|
||||
((OkResponseCache) policy.responseCache).trackResponse(responseSource);
|
||||
}
|
||||
|
||||
// The raw response source may require the network, but the request
|
||||
// headers may forbid network use. In that case, dispose of the network
|
||||
// response and use a GATEWAY_TIMEOUT response instead, as specified
|
||||
// by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
|
||||
if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
|
||||
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||
Util.closeQuietly(cachedResponseBody);
|
||||
}
|
||||
this.responseSource = ResponseSource.CACHE;
|
||||
this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
|
||||
RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
|
||||
setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
|
||||
}
|
||||
|
||||
if (responseSource.requiresConnection()) {
|
||||
sendSocketRequest();
|
||||
} else if (connection != null) {
|
||||
policy.connectionPool.recycle(connection);
|
||||
connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the source for this response. It may be corrected later if the
|
||||
* request headers forbids network use.
|
||||
*/
|
||||
private void initResponseSource() throws IOException {
|
||||
responseSource = ResponseSource.NETWORK;
|
||||
if (!policy.getUseCaches() || policy.responseCache == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CacheResponse candidate =
|
||||
policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
|
||||
if (candidate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
|
||||
cachedResponseBody = candidate.getBody();
|
||||
if (!acceptCacheResponseType(candidate)
|
||||
|| responseHeadersMap == null
|
||||
|| cachedResponseBody == null) {
|
||||
Util.closeQuietly(cachedResponseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true);
|
||||
cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
|
||||
long now = System.currentTimeMillis();
|
||||
this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
|
||||
if (responseSource == ResponseSource.CACHE) {
|
||||
this.cacheResponse = candidate;
|
||||
setResponse(cachedResponseHeaders, cachedResponseBody);
|
||||
} else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||
this.cacheResponse = candidate;
|
||||
} else if (responseSource == ResponseSource.NETWORK) {
|
||||
Util.closeQuietly(cachedResponseBody);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSocketRequest() throws IOException {
|
||||
if (connection == null) {
|
||||
connect();
|
||||
}
|
||||
|
||||
if (transport != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
transport = (Transport) connection.newTransport(this);
|
||||
|
||||
if (hasRequestBody() && requestBodyOut == null) {
|
||||
// Create a request body if we don't have one already. We'll already
|
||||
// have one if we're retrying a failed POST.
|
||||
requestBodyOut = transport.createRequestBody();
|
||||
}
|
||||
}
|
||||
|
||||
/** Connect to the origin server either directly or via a proxy. */
|
||||
protected final void connect() throws IOException {
|
||||
if (connection != null) {
|
||||
return;
|
||||
}
|
||||
if (routeSelector == null) {
|
||||
String uriHost = uri.getHost();
|
||||
if (uriHost == null) {
|
||||
throw new UnknownHostException(uri.toString());
|
||||
}
|
||||
SSLSocketFactory sslSocketFactory = null;
|
||||
HostnameVerifier hostnameVerifier = null;
|
||||
if (uri.getScheme().equalsIgnoreCase("https")) {
|
||||
sslSocketFactory = policy.sslSocketFactory;
|
||||
hostnameVerifier = policy.hostnameVerifier;
|
||||
}
|
||||
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
|
||||
hostnameVerifier, policy.requestedProxy);
|
||||
routeSelector =
|
||||
new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
|
||||
}
|
||||
connection = routeSelector.next();
|
||||
if (!connection.isConnected()) {
|
||||
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
|
||||
policy.connectionPool.maybeShare(connection);
|
||||
}
|
||||
connected(connection);
|
||||
if (connection.getProxy() != policy.requestedProxy) {
|
||||
// Update the request line if the proxy changed; it may need a host name.
|
||||
requestHeaders.getHeaders().setRequestLine(getRequestLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a socket connection has been created or retrieved from the
|
||||
* pool. Subclasses use this hook to get a reference to the TLS data.
|
||||
*/
|
||||
protected void connected(Connection connection) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately before the transport transmits HTTP request headers.
|
||||
* This is used to observe the sent time should the request be cached.
|
||||
*/
|
||||
public void writingRequestHeaders() {
|
||||
if (sentRequestMillis != -1) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
sentRequestMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param body the response body, or null if it doesn't exist or isn't
|
||||
* available.
|
||||
*/
|
||||
private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
|
||||
if (this.responseBodyIn != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.responseHeaders = headers;
|
||||
if (body != null) {
|
||||
initContentStream(body);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasRequestBody() {
|
||||
return method.equals("POST") || method.equals("PUT");
|
||||
}
|
||||
|
||||
/** Returns the request body or null if this request doesn't have a body. */
|
||||
public final OutputStream getRequestBody() {
|
||||
if (responseSource == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return requestBodyOut;
|
||||
}
|
||||
|
||||
public final boolean hasResponse() {
|
||||
return responseHeaders != null;
|
||||
}
|
||||
|
||||
public final RequestHeaders getRequestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
public final ResponseHeaders getResponseHeaders() {
|
||||
if (responseHeaders == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
public final int getResponseCode() {
|
||||
if (responseHeaders == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return responseHeaders.getHeaders().getResponseCode();
|
||||
}
|
||||
|
||||
public final InputStream getResponseBody() {
|
||||
if (responseHeaders == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return responseBodyIn;
|
||||
}
|
||||
|
||||
public final CacheResponse getCacheResponse() {
|
||||
return cacheResponse;
|
||||
}
|
||||
|
||||
public final Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code cacheResponse} is of the right type. This
|
||||
* condition is necessary but not sufficient for the cached response to
|
||||
* be used.
|
||||
*/
|
||||
protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void maybeCache() throws IOException {
|
||||
// Are we caching at all?
|
||||
if (!policy.getUseCaches() || policy.responseCache == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Should we cache this response for this request?
|
||||
if (!responseHeaders.isCacheable(requestHeaders)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Offer this request to the cache.
|
||||
cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cause the socket connection to be released to the connection pool when
|
||||
* it is no longer needed. If it is already unneeded, it will be pooled
|
||||
* immediately. Otherwise the connection is held so that redirects can be
|
||||
* handled by the same connection.
|
||||
*/
|
||||
public final void automaticallyReleaseConnectionToPool() {
|
||||
automaticallyReleaseConnectionToPool = true;
|
||||
if (connection != null && connectionReleased) {
|
||||
policy.connectionPool.recycle(connection);
|
||||
connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases this engine so that its resources may be either reused or
|
||||
* closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
|
||||
* the connection will be used to follow a redirect.
|
||||
*/
|
||||
public final void release(boolean streamCancelled) {
|
||||
// If the response body comes from the cache, close it.
|
||||
if (responseBodyIn == cachedResponseBody) {
|
||||
Util.closeQuietly(responseBodyIn);
|
||||
}
|
||||
|
||||
if (!connectionReleased && connection != null) {
|
||||
connectionReleased = true;
|
||||
|
||||
if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
|
||||
responseTransferIn)) {
|
||||
Util.closeQuietly(connection);
|
||||
connection = null;
|
||||
} else if (automaticallyReleaseConnectionToPool) {
|
||||
policy.connectionPool.recycle(connection);
|
||||
connection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initContentStream(InputStream transferStream) throws IOException {
|
||||
responseTransferIn = transferStream;
|
||||
if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
|
||||
// If the response was transparently gzipped, remove the gzip header field
|
||||
// so clients don't double decompress. http://b/3009828
|
||||
//
|
||||
// Also remove the Content-Length in this case because it contains the
|
||||
// length 528 of the gzipped response. This isn't terribly useful and is
|
||||
// dangerous because 529 clients can query the content length, but not
|
||||
// the content encoding.
|
||||
responseHeaders.stripContentEncoding();
|
||||
responseHeaders.stripContentLength();
|
||||
responseBodyIn = new GZIPInputStream(transferStream);
|
||||
} else {
|
||||
responseBodyIn = transferStream;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the response must have a (possibly 0-length) body.
|
||||
* See RFC 2616 section 4.3.
|
||||
*/
|
||||
public final boolean hasResponseBody() {
|
||||
int responseCode = responseHeaders.getHeaders().getResponseCode();
|
||||
|
||||
// HEAD requests never yield a body regardless of the response headers.
|
||||
if (method.equals("HEAD")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
|
||||
&& responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
|
||||
&& responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the Content-Length or Transfer-Encoding headers disagree with the
|
||||
// response code, the response is malformed. For best compatibility, we
|
||||
// honor the headers.
|
||||
if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates requestHeaders with defaults and cookies.
|
||||
*
|
||||
* <p>This client doesn't specify a default {@code Accept} header because it
|
||||
* doesn't know what content types the application is interested in.
|
||||
*/
|
||||
private void prepareRawRequestHeaders() throws IOException {
|
||||
requestHeaders.getHeaders().setRequestLine(getRequestLine());
|
||||
|
||||
if (requestHeaders.getUserAgent() == null) {
|
||||
requestHeaders.setUserAgent(getDefaultUserAgent());
|
||||
}
|
||||
|
||||
if (requestHeaders.getHost() == null) {
|
||||
requestHeaders.setHost(getOriginAddress(policy.getURL()));
|
||||
}
|
||||
|
||||
if ((connection == null || connection.getHttpMinorVersion() != 0)
|
||||
&& requestHeaders.getConnection() == null) {
|
||||
requestHeaders.setConnection("Keep-Alive");
|
||||
}
|
||||
|
||||
if (requestHeaders.getAcceptEncoding() == null) {
|
||||
transparentGzip = true;
|
||||
requestHeaders.setAcceptEncoding("gzip");
|
||||
}
|
||||
|
||||
if (hasRequestBody() && requestHeaders.getContentType() == null) {
|
||||
requestHeaders.setContentType("application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
long ifModifiedSince = policy.getIfModifiedSince();
|
||||
if (ifModifiedSince != 0) {
|
||||
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
|
||||
}
|
||||
|
||||
CookieHandler cookieHandler = policy.cookieHandler;
|
||||
if (cookieHandler != null) {
|
||||
requestHeaders.addCookies(
|
||||
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request status line, like "GET / HTTP/1.1". This is exposed
|
||||
* to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
|
||||
* it needs to be set even if the transport is SPDY.
|
||||
*/
|
||||
String getRequestLine() {
|
||||
String protocol =
|
||||
(connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
|
||||
return method + " " + requestString() + " " + protocol;
|
||||
}
|
||||
|
||||
private String requestString() {
|
||||
URL url = policy.getURL();
|
||||
if (includeAuthorityInRequestLine()) {
|
||||
return url.toString();
|
||||
} else {
|
||||
return requestPath(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
|
||||
* empty, even if the request URL is. Includes the query component if it
|
||||
* exists.
|
||||
*/
|
||||
public static String requestPath(URL url) {
|
||||
String fileOnly = url.getFile();
|
||||
if (fileOnly == null) {
|
||||
return "/";
|
||||
} else if (!fileOnly.startsWith("/")) {
|
||||
return "/" + fileOnly;
|
||||
} else {
|
||||
return fileOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request line should contain the full URL with host
|
||||
* and port (like "GET http://android.com/foo HTTP/1.1") or only the path
|
||||
* (like "GET /foo HTTP/1.1").
|
||||
*
|
||||
* <p>This is non-final because for HTTPS it's never necessary to supply the
|
||||
* full URL, even if a proxy is in use.
|
||||
*/
|
||||
protected boolean includeAuthorityInRequestLine() {
|
||||
return connection == null
|
||||
? policy.usingProxy() // A proxy was requested.
|
||||
: connection.getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
|
||||
}
|
||||
|
||||
public static String getDefaultUserAgent() {
|
||||
String agent = System.getProperty("http.agent");
|
||||
return agent != null ? agent : ("Java" + System.getProperty("java.version"));
|
||||
}
|
||||
|
||||
public static String getOriginAddress(URL url) {
|
||||
int port = url.getPort();
|
||||
String result = url.getHost();
|
||||
if (port > 0 && port != getDefaultPort(url.getProtocol())) {
|
||||
result = result + ":" + port;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the remaining request header and body, parses the HTTP response
|
||||
* headers and starts reading the HTTP response body if it exists.
|
||||
*/
|
||||
public final void readResponse() throws IOException {
|
||||
if (hasResponse()) {
|
||||
responseHeaders.setResponseSource(responseSource);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseSource == null) {
|
||||
throw new IllegalStateException("readResponse() without sendRequest()");
|
||||
}
|
||||
|
||||
if (!responseSource.requiresConnection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sentRequestMillis == -1) {
|
||||
if (requestBodyOut instanceof RetryableOutputStream) {
|
||||
int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
|
||||
requestHeaders.setContentLength(contentLength);
|
||||
}
|
||||
transport.writeRequestHeaders();
|
||||
}
|
||||
|
||||
if (requestBodyOut != null) {
|
||||
requestBodyOut.close();
|
||||
if (requestBodyOut instanceof RetryableOutputStream) {
|
||||
transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
|
||||
}
|
||||
}
|
||||
|
||||
transport.flushRequest();
|
||||
|
||||
responseHeaders = transport.readResponseHeaders();
|
||||
responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
|
||||
responseHeaders.setResponseSource(responseSource);
|
||||
|
||||
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
|
||||
if (cachedResponseHeaders.validate(responseHeaders)) {
|
||||
release(false);
|
||||
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
|
||||
setResponse(combinedHeaders, cachedResponseBody);
|
||||
if (policy.responseCache instanceof OkResponseCache) {
|
||||
OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache;
|
||||
httpResponseCache.trackConditionalCacheHit();
|
||||
httpResponseCache.update(cacheResponse, policy.getHttpConnectionToCache());
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
Util.closeQuietly(cachedResponseBody);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasResponseBody()) {
|
||||
maybeCache(); // reentrant. this calls into user code which may call back into this!
|
||||
}
|
||||
|
||||
initContentStream(transport.getTransferStream(cacheRequest));
|
||||
}
|
||||
|
||||
protected TunnelRequest getTunnelConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void receiveHeaders(RawHeaders headers) throws IOException {
|
||||
CookieHandler cookieHandler = policy.cookieHandler;
|
||||
if (cookieHandler != null) {
|
||||
cookieHandler.put(uri, headers.toMultimap(true));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,608 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
import com.squareup.okhttp.OkResponseCache;
|
||||
import com.squareup.okhttp.ResponseSource;
|
||||
import com.squareup.okhttp.internal.Base64;
|
||||
import com.squareup.okhttp.internal.DiskLruCache;
|
||||
import com.squareup.okhttp.internal.StrictLineReader;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import 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;
|
||||
|
||||
/**
|
||||
* Cache responses in a directory on the file system. Most clients should use
|
||||
* {@code android.net.HttpResponseCache}, the stable, documented front end for
|
||||
* this.
|
||||
*/
|
||||
public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a conditional request hit by updating the stored cache response
|
||||
* with the headers from {@code httpConnection}. The cached response body is
|
||||
* not updated. If the stored response has changed since {@code
|
||||
* conditionalCacheHit} was returned, this does nothing.
|
||||
*/
|
||||
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public DiskLruCache getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
public synchronized int getWriteAbortCount() {
|
||||
return writeAbortCount;
|
||||
}
|
||||
|
||||
public synchronized int getWriteSuccessCount() {
|
||||
return writeSuccessCount;
|
||||
}
|
||||
|
||||
public synchronized void trackResponse(ResponseSource source) {
|
||||
requestCount++;
|
||||
|
||||
switch (source) {
|
||||
case CACHE:
|
||||
hitCount++;
|
||||
break;
|
||||
case CONDITIONAL_CACHE:
|
||||
case NETWORK:
|
||||
networkCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public 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.isEmpty()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,499 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.Connection;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheRequest;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.Socket;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||
|
||||
public final class HttpTransport implements Transport {
|
||||
/**
|
||||
* The maximum number of bytes to buffer when sending headers and a request
|
||||
* body. When the headers and body can be sent in a single write, the
|
||||
* request completes sooner. In one WiFi benchmark, using a large enough
|
||||
* buffer sped up some uploads by half.
|
||||
*/
|
||||
private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
|
||||
|
||||
/**
|
||||
* The timeout to use while discarding a stream of input data. Since this is
|
||||
* used for connection reuse, this timeout should be significantly less than
|
||||
* the time it takes to establish a new connection.
|
||||
*/
|
||||
private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
|
||||
|
||||
public static final int DEFAULT_CHUNK_LENGTH = 1024;
|
||||
|
||||
private final HttpEngine httpEngine;
|
||||
private final InputStream socketIn;
|
||||
private final OutputStream socketOut;
|
||||
|
||||
/**
|
||||
* This stream buffers the request headers and the request body when their
|
||||
* combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
|
||||
* we can save socket writes, which in turn saves a packet transmission.
|
||||
* This is socketOut if the request size is large or unknown.
|
||||
*/
|
||||
private OutputStream requestOut;
|
||||
|
||||
public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) {
|
||||
this.httpEngine = httpEngine;
|
||||
this.socketOut = outputStream;
|
||||
this.requestOut = outputStream;
|
||||
this.socketIn = inputStream;
|
||||
}
|
||||
|
||||
@Override public OutputStream createRequestBody() throws IOException {
|
||||
boolean chunked = httpEngine.requestHeaders.isChunked();
|
||||
if (!chunked
|
||||
&& httpEngine.policy.getChunkLength() > 0
|
||||
&& httpEngine.connection.getHttpMinorVersion() != 0) {
|
||||
httpEngine.requestHeaders.setChunked();
|
||||
chunked = true;
|
||||
}
|
||||
|
||||
// Stream a request body of unknown length.
|
||||
if (chunked) {
|
||||
int chunkLength = httpEngine.policy.getChunkLength();
|
||||
if (chunkLength == -1) {
|
||||
chunkLength = DEFAULT_CHUNK_LENGTH;
|
||||
}
|
||||
writeRequestHeaders();
|
||||
return new ChunkedOutputStream(requestOut, chunkLength);
|
||||
}
|
||||
|
||||
// Stream a request body of a known length.
|
||||
int fixedContentLength = httpEngine.policy.getFixedContentLength();
|
||||
if (fixedContentLength != -1) {
|
||||
httpEngine.requestHeaders.setContentLength(fixedContentLength);
|
||||
writeRequestHeaders();
|
||||
return new FixedLengthOutputStream(requestOut, fixedContentLength);
|
||||
}
|
||||
|
||||
// Buffer a request body of a known length.
|
||||
int contentLength = httpEngine.requestHeaders.getContentLength();
|
||||
if (contentLength != -1) {
|
||||
writeRequestHeaders();
|
||||
return new RetryableOutputStream(contentLength);
|
||||
}
|
||||
|
||||
// Buffer a request body of an unknown length. Don't write request
|
||||
// headers until the entire body is ready; otherwise we can't set the
|
||||
// Content-Length header correctly.
|
||||
return new RetryableOutputStream();
|
||||
}
|
||||
|
||||
@Override public void flushRequest() throws IOException {
|
||||
requestOut.flush();
|
||||
requestOut = socketOut;
|
||||
}
|
||||
|
||||
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
|
||||
requestBody.writeToSocket(requestOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the HTTP headers and sends them to the server.
|
||||
*
|
||||
* <p>For streaming requests with a body, headers must be prepared
|
||||
* <strong>before</strong> the output stream has been written to. Otherwise
|
||||
* the body would need to be buffered!
|
||||
*
|
||||
* <p>For non-streaming requests with a body, headers must be prepared
|
||||
* <strong>after</strong> the output stream has been written to and closed.
|
||||
* This ensures that the {@code Content-Length} header field receives the
|
||||
* proper value.
|
||||
*/
|
||||
public void writeRequestHeaders() throws IOException {
|
||||
httpEngine.writingRequestHeaders();
|
||||
int contentLength = httpEngine.requestHeaders.getContentLength();
|
||||
RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
|
||||
byte[] bytes = headersToSend.toBytes();
|
||||
|
||||
if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
|
||||
requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
|
||||
}
|
||||
|
||||
requestOut.write(bytes);
|
||||
}
|
||||
|
||||
@Override public ResponseHeaders readResponseHeaders() throws IOException {
|
||||
RawHeaders headers = RawHeaders.fromBytes(socketIn);
|
||||
httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
|
||||
httpEngine.receiveHeaders(headers);
|
||||
return new ResponseHeaders(httpEngine.uri, headers);
|
||||
}
|
||||
|
||||
public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
|
||||
InputStream responseBodyIn) {
|
||||
if (streamCancelled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We cannot reuse sockets that have incomplete output.
|
||||
if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the request specified that the connection shouldn't be reused, don't reuse it.
|
||||
if (httpEngine.requestHeaders.hasConnectionClose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the response specified that the connection shouldn't be reused, don't reuse it.
|
||||
if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseBodyIn != null) {
|
||||
return discardStream(httpEngine, responseBodyIn);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the response body so that the connection can be reused. This
|
||||
* needs to be done judiciously, since it delays the current request in
|
||||
* order to speed up a potential future request that may never occur.
|
||||
*/
|
||||
private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
|
||||
Connection connection = httpEngine.connection;
|
||||
if (connection == null) return false;
|
||||
Socket socket = connection.getSocket();
|
||||
if (socket == null) return false;
|
||||
try {
|
||||
int socketTimeout = socket.getSoTimeout();
|
||||
socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
|
||||
try {
|
||||
Util.skipAll(responseBodyIn);
|
||||
return true;
|
||||
} finally {
|
||||
socket.setSoTimeout(socketTimeout);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
|
||||
if (!httpEngine.hasResponseBody()) {
|
||||
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
|
||||
}
|
||||
|
||||
if (httpEngine.responseHeaders.isChunked()) {
|
||||
return new ChunkedInputStream(socketIn, cacheRequest, this);
|
||||
}
|
||||
|
||||
if (httpEngine.responseHeaders.getContentLength() != -1) {
|
||||
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
|
||||
httpEngine.responseHeaders.getContentLength());
|
||||
}
|
||||
|
||||
// Wrap the input stream from the connection (rather than just returning
|
||||
// "socketIn" directly here), so that we can control its use after the
|
||||
// reference escapes.
|
||||
return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine);
|
||||
}
|
||||
|
||||
/** An HTTP body with a fixed length known in advance. */
|
||||
private static final class FixedLengthOutputStream extends AbstractHttpOutputStream {
|
||||
private final OutputStream socketOut;
|
||||
private int bytesRemaining;
|
||||
|
||||
private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
|
||||
this.socketOut = socketOut;
|
||||
this.bytesRemaining = bytesRemaining;
|
||||
}
|
||||
|
||||
@Override public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||
checkNotClosed();
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
if (count > bytesRemaining) {
|
||||
throw new ProtocolException("expected " + bytesRemaining + " bytes but received " + count);
|
||||
}
|
||||
socketOut.write(buffer, offset, count);
|
||||
bytesRemaining -= count;
|
||||
}
|
||||
|
||||
@Override public void flush() throws IOException {
|
||||
if (closed) {
|
||||
return; // don't throw; this stream might have been closed on the caller's behalf
|
||||
}
|
||||
socketOut.flush();
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (bytesRemaining > 0) {
|
||||
throw new ProtocolException("unexpected end of stream");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An HTTP body with alternating chunk sizes and chunk bodies. Chunks are
|
||||
* buffered until {@code maxChunkLength} bytes are ready, at which point the
|
||||
* chunk is written and the buffer is cleared.
|
||||
*/
|
||||
private static final class ChunkedOutputStream extends AbstractHttpOutputStream {
|
||||
private static final byte[] CRLF = { '\r', '\n' };
|
||||
private static final byte[] HEX_DIGITS = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||
};
|
||||
private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
|
||||
|
||||
/** Scratch space for up to 8 hex digits, and then a constant CRLF. */
|
||||
private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' };
|
||||
|
||||
private final OutputStream socketOut;
|
||||
private final int maxChunkLength;
|
||||
private final ByteArrayOutputStream bufferedChunk;
|
||||
|
||||
private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) {
|
||||
this.socketOut = socketOut;
|
||||
this.maxChunkLength = Math.max(1, dataLength(maxChunkLength));
|
||||
this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of data that can be transmitted in a chunk whose total
|
||||
* length (data+headers) is {@code dataPlusHeaderLength}. This is presumably
|
||||
* useful to match sizes with wire-protocol packets.
|
||||
*/
|
||||
private int dataLength(int dataPlusHeaderLength) {
|
||||
int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data
|
||||
for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) {
|
||||
headerLength++;
|
||||
}
|
||||
return dataPlusHeaderLength - headerLength;
|
||||
}
|
||||
|
||||
@Override public synchronized void write(byte[] buffer, int offset, int count)
|
||||
throws IOException {
|
||||
checkNotClosed();
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
|
||||
while (count > 0) {
|
||||
int numBytesWritten;
|
||||
|
||||
if (bufferedChunk.size() > 0 || count < maxChunkLength) {
|
||||
// fill the buffered chunk and then maybe write that to the stream
|
||||
numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size());
|
||||
// TODO: skip unnecessary copies from buffer->bufferedChunk?
|
||||
bufferedChunk.write(buffer, offset, numBytesWritten);
|
||||
if (bufferedChunk.size() == maxChunkLength) {
|
||||
writeBufferedChunkToSocket();
|
||||
}
|
||||
} else {
|
||||
// write a single chunk of size maxChunkLength to the stream
|
||||
numBytesWritten = maxChunkLength;
|
||||
writeHex(numBytesWritten);
|
||||
socketOut.write(buffer, offset, numBytesWritten);
|
||||
socketOut.write(CRLF);
|
||||
}
|
||||
|
||||
offset += numBytesWritten;
|
||||
count -= numBytesWritten;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
|
||||
* followed by CRLF.
|
||||
*/
|
||||
private void writeHex(int i) throws IOException {
|
||||
int cursor = 8;
|
||||
do {
|
||||
hex[--cursor] = HEX_DIGITS[i & 0xf];
|
||||
} while ((i >>>= 4) != 0);
|
||||
socketOut.write(hex, cursor, hex.length - cursor);
|
||||
}
|
||||
|
||||
@Override public synchronized void flush() throws IOException {
|
||||
if (closed) {
|
||||
return; // don't throw; this stream might have been closed on the caller's behalf
|
||||
}
|
||||
writeBufferedChunkToSocket();
|
||||
socketOut.flush();
|
||||
}
|
||||
|
||||
@Override public synchronized void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
writeBufferedChunkToSocket();
|
||||
socketOut.write(FINAL_CHUNK);
|
||||
}
|
||||
|
||||
private void writeBufferedChunkToSocket() throws IOException {
|
||||
int size = bufferedChunk.size();
|
||||
if (size <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeHex(size);
|
||||
bufferedChunk.writeTo(socketOut);
|
||||
bufferedChunk.reset();
|
||||
socketOut.write(CRLF);
|
||||
}
|
||||
}
|
||||
|
||||
/** An HTTP body with a fixed length specified in advance. */
|
||||
private static class FixedLengthInputStream extends AbstractHttpInputStream {
|
||||
private int bytesRemaining;
|
||||
|
||||
public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
|
||||
int length) throws IOException {
|
||||
super(is, httpEngine, cacheRequest);
|
||||
bytesRemaining = length;
|
||||
if (bytesRemaining == 0) {
|
||||
endOfInput(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
checkNotClosed();
|
||||
if (bytesRemaining == 0) {
|
||||
return -1;
|
||||
}
|
||||
int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
|
||||
if (read == -1) {
|
||||
unexpectedEndOfInput(); // the server didn't supply the promised content length
|
||||
throw new ProtocolException("unexpected end of stream");
|
||||
}
|
||||
bytesRemaining -= read;
|
||||
cacheWrite(buffer, offset, read);
|
||||
if (bytesRemaining == 0) {
|
||||
endOfInput(false);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override public int available() throws IOException {
|
||||
checkNotClosed();
|
||||
return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (bytesRemaining != 0 && !discardStream(httpEngine, this)) {
|
||||
unexpectedEndOfInput();
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** An HTTP body with alternating chunk sizes and chunk bodies. */
|
||||
private static class ChunkedInputStream extends AbstractHttpInputStream {
|
||||
private static final int NO_CHUNK_YET = -1;
|
||||
private final HttpTransport transport;
|
||||
private int bytesRemainingInChunk = NO_CHUNK_YET;
|
||||
private boolean hasMoreChunks = true;
|
||||
|
||||
ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport)
|
||||
throws IOException {
|
||||
super(is, transport.httpEngine, cacheRequest);
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
checkNotClosed();
|
||||
|
||||
if (!hasMoreChunks) {
|
||||
return -1;
|
||||
}
|
||||
if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
|
||||
readChunkSize();
|
||||
if (!hasMoreChunks) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk));
|
||||
if (read == -1) {
|
||||
unexpectedEndOfInput(); // the server didn't supply the promised chunk length
|
||||
throw new IOException("unexpected end of stream");
|
||||
}
|
||||
bytesRemainingInChunk -= read;
|
||||
cacheWrite(buffer, offset, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
private void readChunkSize() throws IOException {
|
||||
// read the suffix of the previous chunk
|
||||
if (bytesRemainingInChunk != NO_CHUNK_YET) {
|
||||
Util.readAsciiLine(in);
|
||||
}
|
||||
String chunkSizeString = Util.readAsciiLine(in);
|
||||
int index = chunkSizeString.indexOf(";");
|
||||
if (index != -1) {
|
||||
chunkSizeString = chunkSizeString.substring(0, index);
|
||||
}
|
||||
try {
|
||||
bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
|
||||
}
|
||||
if (bytesRemainingInChunk == 0) {
|
||||
hasMoreChunks = false;
|
||||
RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
|
||||
RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
|
||||
httpEngine.receiveHeaders(rawResponseHeaders);
|
||||
endOfInput(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int available() throws IOException {
|
||||
checkNotClosed();
|
||||
if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(in.available(), bytesRemainingInChunk);
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (hasMoreChunks && !discardStream(httpEngine, this)) {
|
||||
unexpectedEndOfInput();
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,502 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.Connection;
|
||||
import com.squareup.okhttp.ConnectionPool;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.HttpRetryException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.ResponseCache;
|
||||
import java.net.SocketPermission;
|
||||
import java.net.URL;
|
||||
import java.security.Permission;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||
|
||||
/**
|
||||
* This implementation uses HttpEngine to send requests and receive responses.
|
||||
* This class may use multiple HttpEngines to follow redirects, authentication
|
||||
* retries, etc. to retrieve the final response body.
|
||||
*
|
||||
* <h3>What does 'connected' mean?</h3>
|
||||
* This class inherits a {@code connected} field from the superclass. That field
|
||||
* is <strong>not</strong> used to indicate not whether this URLConnection is
|
||||
* currently connected. Instead, it indicates whether a connection has ever been
|
||||
* attempted. Once a connection has been attempted, certain properties (request
|
||||
* header fields, request method, etc.) are immutable. Test the {@code
|
||||
* connection} field on this class for null/non-null to determine of an instance
|
||||
* is currently connected to a server.
|
||||
*/
|
||||
public class HttpURLConnectionImpl extends HttpURLConnection {
|
||||
|
||||
/** Numeric status code, 307: Temporary Redirect. */
|
||||
static final int HTTP_TEMP_REDIRECT = 307;
|
||||
|
||||
/**
|
||||
* How many redirects should we follow? Chrome follows 21; Firefox, curl,
|
||||
* and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
|
||||
*/
|
||||
private static final int MAX_REDIRECTS = 20;
|
||||
|
||||
private final boolean followProtocolRedirects;
|
||||
|
||||
/** The proxy requested by the client, or null for a proxy to be selected automatically. */
|
||||
final Proxy requestedProxy;
|
||||
|
||||
final ProxySelector proxySelector;
|
||||
final CookieHandler cookieHandler;
|
||||
final ResponseCache responseCache;
|
||||
final ConnectionPool connectionPool;
|
||||
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
|
||||
SSLSocketFactory sslSocketFactory;
|
||||
HostnameVerifier hostnameVerifier;
|
||||
|
||||
private final RawHeaders rawRequestHeaders = new RawHeaders();
|
||||
|
||||
private int redirectionCount;
|
||||
|
||||
protected IOException httpEngineFailure;
|
||||
protected HttpEngine httpEngine;
|
||||
|
||||
public HttpURLConnectionImpl(URL url, OkHttpClient client) {
|
||||
super(url);
|
||||
this.followProtocolRedirects = client.getFollowProtocolRedirects();
|
||||
this.requestedProxy = client.getProxy();
|
||||
this.proxySelector = client.getProxySelector();
|
||||
this.cookieHandler = client.getCookieHandler();
|
||||
this.responseCache = client.getResponseCache();
|
||||
this.connectionPool = client.getConnectionPool();
|
||||
this.sslSocketFactory = client.getSslSocketFactory();
|
||||
this.hostnameVerifier = client.getHostnameVerifier();
|
||||
}
|
||||
|
||||
@Override public final void connect() throws IOException {
|
||||
initHttpEngine();
|
||||
boolean success;
|
||||
do {
|
||||
success = execute(false);
|
||||
} while (!success);
|
||||
}
|
||||
|
||||
@Override public final void disconnect() {
|
||||
// Calling disconnect() before a connection exists should have no effect.
|
||||
if (httpEngine != null) {
|
||||
// We close the response body here instead of in
|
||||
// HttpEngine.release because that is called when input
|
||||
// has been completely read from the underlying socket.
|
||||
// However the response body can be a GZIPInputStream that
|
||||
// still has unread data.
|
||||
if (httpEngine.hasResponse()) {
|
||||
Util.closeQuietly(httpEngine.getResponseBody());
|
||||
}
|
||||
httpEngine.release(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream from the server in the case of error such as the
|
||||
* requested file (txt, htm, html) is not found on the remote server.
|
||||
*/
|
||||
@Override public final InputStream getErrorStream() {
|
||||
try {
|
||||
HttpEngine response = getResponse();
|
||||
if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
|
||||
return response.getResponseBody();
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the field at {@code position}. Returns null if there
|
||||
* are fewer than {@code position} headers.
|
||||
*/
|
||||
@Override public final String getHeaderField(int position) {
|
||||
try {
|
||||
return getResponse().getResponseHeaders().getHeaders().getValue(position);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the field corresponding to the {@code fieldName}, or
|
||||
* null if there is no such field. If the field has multiple values, the
|
||||
* last value is returned.
|
||||
*/
|
||||
@Override public final String getHeaderField(String fieldName) {
|
||||
try {
|
||||
RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
|
||||
return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public final String getHeaderFieldKey(int position) {
|
||||
try {
|
||||
return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public final Map<String, List<String>> getHeaderFields() {
|
||||
try {
|
||||
return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public final Map<String, List<String>> getRequestProperties() {
|
||||
if (connected) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot access request header fields after connection is set");
|
||||
}
|
||||
return rawRequestHeaders.toMultimap(false);
|
||||
}
|
||||
|
||||
@Override public final InputStream getInputStream() throws IOException {
|
||||
if (!doInput) {
|
||||
throw new ProtocolException("This protocol does not support input");
|
||||
}
|
||||
|
||||
HttpEngine response = getResponse();
|
||||
|
||||
// if the requested file does not exist, throw an exception formerly the
|
||||
// Error page from the server was returned if the requested file was
|
||||
// text/html this has changed to return FileNotFoundException for all
|
||||
// file types
|
||||
if (getResponseCode() >= HTTP_BAD_REQUEST) {
|
||||
throw new FileNotFoundException(url.toString());
|
||||
}
|
||||
|
||||
InputStream result = response.getResponseBody();
|
||||
if (result == null) {
|
||||
throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public final OutputStream getOutputStream() throws IOException {
|
||||
connect();
|
||||
|
||||
OutputStream result = httpEngine.getRequestBody();
|
||||
if (result == null) {
|
||||
throw new ProtocolException("method does not support a request body: " + method);
|
||||
} else if (httpEngine.hasResponse()) {
|
||||
throw new ProtocolException("cannot write request body after response has been read");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public final Permission getPermission() throws IOException {
|
||||
String hostName = getURL().getHost();
|
||||
int hostPort = Util.getEffectivePort(getURL());
|
||||
if (usingProxy()) {
|
||||
InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
|
||||
hostName = proxyAddress.getHostName();
|
||||
hostPort = proxyAddress.getPort();
|
||||
}
|
||||
return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
|
||||
}
|
||||
|
||||
@Override public final String getRequestProperty(String field) {
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
return rawRequestHeaders.get(field);
|
||||
}
|
||||
|
||||
private void initHttpEngine() throws IOException {
|
||||
if (httpEngineFailure != null) {
|
||||
throw httpEngineFailure;
|
||||
} else if (httpEngine != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
try {
|
||||
if (doOutput) {
|
||||
if (method.equals("GET")) {
|
||||
// they are requesting a stream to write to. This implies a POST method
|
||||
method = "POST";
|
||||
} else if (!method.equals("POST") && !method.equals("PUT")) {
|
||||
// If the request method is neither POST nor PUT, then you're not writing
|
||||
throw new ProtocolException(method + " does not support writing");
|
||||
}
|
||||
}
|
||||
httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
|
||||
} catch (IOException e) {
|
||||
httpEngineFailure = e;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpURLConnection getHttpConnectionToCache() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
|
||||
Connection connection, RetryableOutputStream requestBody) throws IOException {
|
||||
if (url.getProtocol().equals("http")) {
|
||||
return new HttpEngine(this, method, requestHeaders, connection, requestBody);
|
||||
} else if (url.getProtocol().equals("https")) {
|
||||
return new HttpsURLConnectionImpl.HttpsEngine(
|
||||
this, method, requestHeaders, connection, requestBody);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggressively tries to get the final HTTP response, potentially making
|
||||
* many HTTP requests in the process in order to cope with redirects and
|
||||
* authentication.
|
||||
*/
|
||||
private HttpEngine getResponse() throws IOException {
|
||||
initHttpEngine();
|
||||
|
||||
if (httpEngine.hasResponse()) {
|
||||
return httpEngine;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (!execute(true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Retry retry = processResponseHeaders();
|
||||
if (retry == Retry.NONE) {
|
||||
httpEngine.automaticallyReleaseConnectionToPool();
|
||||
return httpEngine;
|
||||
}
|
||||
|
||||
// The first request was insufficient. Prepare for another...
|
||||
String retryMethod = method;
|
||||
OutputStream requestBody = httpEngine.getRequestBody();
|
||||
|
||||
// Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
|
||||
// redirect should keep the same method, Chrome, Firefox and the
|
||||
// RI all issue GETs when following any redirect.
|
||||
int responseCode = getResponseCode();
|
||||
if (responseCode == HTTP_MULT_CHOICE
|
||||
|| responseCode == HTTP_MOVED_PERM
|
||||
|| responseCode == HTTP_MOVED_TEMP
|
||||
|| responseCode == HTTP_SEE_OTHER) {
|
||||
retryMethod = "GET";
|
||||
requestBody = null;
|
||||
}
|
||||
|
||||
if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
|
||||
throw new HttpRetryException("Cannot retry streamed HTTP body",
|
||||
httpEngine.getResponseCode());
|
||||
}
|
||||
|
||||
if (retry == Retry.DIFFERENT_CONNECTION) {
|
||||
httpEngine.automaticallyReleaseConnectionToPool();
|
||||
}
|
||||
|
||||
httpEngine.release(false);
|
||||
|
||||
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
|
||||
(RetryableOutputStream) requestBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and optionally reads a response. Returns true if the
|
||||
* request was successfully executed, and false if the request can be
|
||||
* retried. Throws an exception if the request failed permanently.
|
||||
*/
|
||||
private boolean execute(boolean readResponse) throws IOException {
|
||||
try {
|
||||
httpEngine.sendRequest();
|
||||
if (readResponse) {
|
||||
httpEngine.readResponse();
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
RouteSelector routeSelector = httpEngine.routeSelector;
|
||||
if (routeSelector != null && httpEngine.connection != null) {
|
||||
routeSelector.connectFailed(httpEngine.connection, e);
|
||||
}
|
||||
if (routeSelector == null && httpEngine.connection == null) {
|
||||
throw e; // If we failed before finding a route or a connection, give up.
|
||||
}
|
||||
|
||||
// The connection failure isn't fatal if there's another route to attempt.
|
||||
OutputStream requestBody = httpEngine.getRequestBody();
|
||||
if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody
|
||||
== null || requestBody instanceof RetryableOutputStream)) {
|
||||
httpEngine.release(true);
|
||||
httpEngine =
|
||||
newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody);
|
||||
httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
|
||||
return false;
|
||||
}
|
||||
httpEngineFailure = e;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRecoverable(IOException e) {
|
||||
// If the problem was a CertificateException from the X509TrustManager,
|
||||
// do not retry, we didn't have an abrupt server initiated exception.
|
||||
boolean sslFailure =
|
||||
e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
|
||||
boolean protocolFailure = e instanceof ProtocolException;
|
||||
return !sslFailure && !protocolFailure;
|
||||
}
|
||||
|
||||
HttpEngine getHttpEngine() {
|
||||
return httpEngine;
|
||||
}
|
||||
|
||||
enum Retry {
|
||||
NONE,
|
||||
SAME_CONNECTION,
|
||||
DIFFERENT_CONNECTION
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the retry action to take for the current response headers. The
|
||||
* headers, proxy and target URL or this connection may be adjusted to
|
||||
* prepare for a follow up request.
|
||||
*/
|
||||
private Retry processResponseHeaders() throws IOException {
|
||||
Proxy selectedProxy = httpEngine.connection != null
|
||||
? httpEngine.connection.getProxy()
|
||||
: requestedProxy;
|
||||
final int responseCode = getResponseCode();
|
||||
switch (responseCode) {
|
||||
case HTTP_PROXY_AUTH:
|
||||
if (selectedProxy.type() != Proxy.Type.HTTP) {
|
||||
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
|
||||
}
|
||||
// fall-through
|
||||
case HTTP_UNAUTHORIZED:
|
||||
boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
|
||||
httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
|
||||
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
|
||||
|
||||
case HTTP_MULT_CHOICE:
|
||||
case HTTP_MOVED_PERM:
|
||||
case HTTP_MOVED_TEMP:
|
||||
case HTTP_SEE_OTHER:
|
||||
case HTTP_TEMP_REDIRECT:
|
||||
if (!getInstanceFollowRedirects()) {
|
||||
return Retry.NONE;
|
||||
}
|
||||
if (++redirectionCount > MAX_REDIRECTS) {
|
||||
throw new ProtocolException("Too many redirects: " + redirectionCount);
|
||||
}
|
||||
if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
|
||||
// "If the 307 status code is received in response to a request other than GET or HEAD,
|
||||
// the user agent MUST NOT automatically redirect the request"
|
||||
return Retry.NONE;
|
||||
}
|
||||
String location = getHeaderField("Location");
|
||||
if (location == null) {
|
||||
return Retry.NONE;
|
||||
}
|
||||
URL previousUrl = url;
|
||||
url = new URL(previousUrl, location);
|
||||
if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
|
||||
return Retry.NONE; // Don't follow redirects to unsupported protocols.
|
||||
}
|
||||
boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
|
||||
if (!sameProtocol && !followProtocolRedirects) {
|
||||
return Retry.NONE; // This client doesn't follow redirects across protocols.
|
||||
}
|
||||
boolean sameHost = previousUrl.getHost().equals(url.getHost());
|
||||
boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
|
||||
if (sameHost && samePort && sameProtocol) {
|
||||
return Retry.SAME_CONNECTION;
|
||||
} else {
|
||||
return Retry.DIFFERENT_CONNECTION;
|
||||
}
|
||||
|
||||
default:
|
||||
return Retry.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
|
||||
final int getFixedContentLength() {
|
||||
return fixedContentLength;
|
||||
}
|
||||
|
||||
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
|
||||
final int getChunkLength() {
|
||||
return chunkLength;
|
||||
}
|
||||
|
||||
@Override public final boolean usingProxy() {
|
||||
return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
|
||||
}
|
||||
|
||||
@Override public String getResponseMessage() throws IOException {
|
||||
return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
|
||||
}
|
||||
|
||||
@Override public final int getResponseCode() throws IOException {
|
||||
return getResponse().getResponseCode();
|
||||
}
|
||||
|
||||
@Override public final void setRequestProperty(String field, String newValue) {
|
||||
if (connected) {
|
||||
throw new IllegalStateException("Cannot set request property after connection is made");
|
||||
}
|
||||
if (field == null) {
|
||||
throw new NullPointerException("field == null");
|
||||
}
|
||||
rawRequestHeaders.set(field, newValue);
|
||||
}
|
||||
|
||||
@Override public final void addRequestProperty(String field, String value) {
|
||||
if (connected) {
|
||||
throw new IllegalStateException("Cannot add request property after connection is made");
|
||||
}
|
||||
if (field == null) {
|
||||
throw new NullPointerException("field == null");
|
||||
}
|
||||
rawRequestHeaders.add(field, value);
|
||||
}
|
||||
}
|
@ -0,0 +1,458 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.Connection;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.TunnelRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheResponse;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.SecureCacheResponse;
|
||||
import java.net.URL;
|
||||
import java.security.Permission;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||
|
||||
public final class HttpsURLConnectionImpl extends HttpsURLConnection {
|
||||
|
||||
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
|
||||
private final HttpUrlConnectionDelegate delegate;
|
||||
|
||||
public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
|
||||
super(url);
|
||||
delegate = new HttpUrlConnectionDelegate(url, client);
|
||||
}
|
||||
|
||||
@Override public String getCipherSuite() {
|
||||
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||
if (cacheResponse != null) {
|
||||
return cacheResponse.getCipherSuite();
|
||||
}
|
||||
SSLSocket sslSocket = getSslSocket();
|
||||
if (sslSocket != null) {
|
||||
return sslSocket.getSession().getCipherSuite();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public Certificate[] getLocalCertificates() {
|
||||
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||
if (cacheResponse != null) {
|
||||
List<Certificate> result = cacheResponse.getLocalCertificateChain();
|
||||
return result != null ? result.toArray(new Certificate[result.size()]) : null;
|
||||
}
|
||||
SSLSocket sslSocket = getSslSocket();
|
||||
if (sslSocket != null) {
|
||||
return sslSocket.getSession().getLocalCertificates();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
|
||||
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||
if (cacheResponse != null) {
|
||||
List<Certificate> result = cacheResponse.getServerCertificateChain();
|
||||
return result != null ? result.toArray(new Certificate[result.size()]) : null;
|
||||
}
|
||||
SSLSocket sslSocket = getSslSocket();
|
||||
if (sslSocket != null) {
|
||||
return sslSocket.getSession().getPeerCertificates();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
|
||||
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||
if (cacheResponse != null) {
|
||||
return cacheResponse.getPeerPrincipal();
|
||||
}
|
||||
SSLSocket sslSocket = getSslSocket();
|
||||
if (sslSocket != null) {
|
||||
return sslSocket.getSession().getPeerPrincipal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public Principal getLocalPrincipal() {
|
||||
SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
|
||||
if (cacheResponse != null) {
|
||||
return cacheResponse.getLocalPrincipal();
|
||||
}
|
||||
SSLSocket sslSocket = getSslSocket();
|
||||
if (sslSocket != null) {
|
||||
return sslSocket.getSession().getLocalPrincipal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpEngine getHttpEngine() {
|
||||
return delegate.getHttpEngine();
|
||||
}
|
||||
|
||||
private SSLSocket getSslSocket() {
|
||||
if (delegate.httpEngine == null || delegate.httpEngine.sentRequestMillis == -1) {
|
||||
throw new IllegalStateException("Connection has not yet been established");
|
||||
}
|
||||
return delegate.httpEngine instanceof HttpsEngine
|
||||
? ((HttpsEngine) delegate.httpEngine).sslSocket
|
||||
: null; // Not HTTPS! Probably an https:// to http:// redirect.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
delegate.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
return delegate.getErrorStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestMethod() {
|
||||
return delegate.getRequestMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() throws IOException {
|
||||
return delegate.getResponseCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResponseMessage() throws IOException {
|
||||
return delegate.getResponseMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
delegate.setRequestMethod(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usingProxy() {
|
||||
return delegate.usingProxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getInstanceFollowRedirects() {
|
||||
return delegate.getInstanceFollowRedirects();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceFollowRedirects(boolean followRedirects) {
|
||||
delegate.setInstanceFollowRedirects(followRedirects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
connected = true;
|
||||
delegate.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAllowUserInteraction() {
|
||||
return delegate.getAllowUserInteraction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getContent() throws IOException {
|
||||
return delegate.getContent();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Spec does not generify
|
||||
@Override
|
||||
public Object getContent(Class[] types) throws IOException {
|
||||
return delegate.getContent(types);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentEncoding() {
|
||||
return delegate.getContentEncoding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return delegate.getContentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return delegate.getContentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDate() {
|
||||
return delegate.getDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDefaultUseCaches() {
|
||||
return delegate.getDefaultUseCaches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDoInput() {
|
||||
return delegate.getDoInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDoOutput() {
|
||||
return delegate.getDoOutput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getExpiration() {
|
||||
return delegate.getExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderField(int pos) {
|
||||
return delegate.getHeaderField(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
return delegate.getHeaderFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getRequestProperties() {
|
||||
return delegate.getRequestProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequestProperty(String field, String newValue) {
|
||||
delegate.addRequestProperty(field, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderField(String key) {
|
||||
return delegate.getHeaderField(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getHeaderFieldDate(String field, long defaultValue) {
|
||||
return delegate.getHeaderFieldDate(field, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeaderFieldInt(String field, int defaultValue) {
|
||||
return delegate.getHeaderFieldInt(field, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderFieldKey(int position) {
|
||||
return delegate.getHeaderFieldKey(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getIfModifiedSince() {
|
||||
return delegate.getIfModifiedSince();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return delegate.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastModified() {
|
||||
return delegate.getLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return delegate.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Permission getPermission() throws IOException {
|
||||
return delegate.getPermission();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestProperty(String field) {
|
||||
return delegate.getRequestProperty(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() {
|
||||
return delegate.getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseCaches() {
|
||||
return delegate.getUseCaches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllowUserInteraction(boolean newValue) {
|
||||
delegate.setAllowUserInteraction(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultUseCaches(boolean newValue) {
|
||||
delegate.setDefaultUseCaches(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDoInput(boolean newValue) {
|
||||
delegate.setDoInput(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDoOutput(boolean newValue) {
|
||||
delegate.setDoOutput(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIfModifiedSince(long newValue) {
|
||||
delegate.setIfModifiedSince(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String field, String newValue) {
|
||||
delegate.setRequestProperty(field, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseCaches(boolean newValue) {
|
||||
delegate.setUseCaches(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConnectTimeout(int timeoutMillis) {
|
||||
delegate.setConnectTimeout(timeoutMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConnectTimeout() {
|
||||
return delegate.getConnectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadTimeout(int timeoutMillis) {
|
||||
delegate.setReadTimeout(timeoutMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReadTimeout() {
|
||||
return delegate.getReadTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFixedLengthStreamingMode(int contentLength) {
|
||||
delegate.setFixedLengthStreamingMode(contentLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChunkedStreamingMode(int chunkLength) {
|
||||
delegate.setChunkedStreamingMode(chunkLength);
|
||||
}
|
||||
|
||||
@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
|
||||
delegate.hostnameVerifier = hostnameVerifier;
|
||||
}
|
||||
|
||||
@Override public HostnameVerifier getHostnameVerifier() {
|
||||
return delegate.hostnameVerifier;
|
||||
}
|
||||
|
||||
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
|
||||
delegate.sslSocketFactory = sslSocketFactory;
|
||||
}
|
||||
|
||||
@Override public SSLSocketFactory getSSLSocketFactory() {
|
||||
return delegate.sslSocketFactory;
|
||||
}
|
||||
|
||||
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
|
||||
private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
|
||||
super(url, client);
|
||||
}
|
||||
|
||||
@Override protected HttpURLConnection getHttpConnectionToCache() {
|
||||
return HttpsURLConnectionImpl.this;
|
||||
}
|
||||
|
||||
public SecureCacheResponse getSecureCacheResponse() {
|
||||
return httpEngine instanceof HttpsEngine
|
||||
? (SecureCacheResponse) httpEngine.getCacheResponse()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HttpsEngine extends HttpEngine {
|
||||
/**
|
||||
* Stash of HttpsEngine.connection.socket to implement requests like
|
||||
* {@link #getCipherSuite} even after the connection has been recycled.
|
||||
*/
|
||||
private SSLSocket sslSocket;
|
||||
|
||||
/**
|
||||
* @param policy the HttpURLConnectionImpl with connection configuration
|
||||
*/
|
||||
public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
|
||||
Connection connection, RetryableOutputStream requestBody)
|
||||
throws IOException {
|
||||
super(policy, method, requestHeaders, connection, requestBody);
|
||||
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
|
||||
}
|
||||
|
||||
@Override protected void connected(Connection connection) {
|
||||
this.sslSocket = (SSLSocket) connection.getSocket();
|
||||
}
|
||||
|
||||
@Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
|
||||
return cacheResponse instanceof SecureCacheResponse;
|
||||
}
|
||||
|
||||
@Override protected boolean includeAuthorityInRequestLine() {
|
||||
// Even if there is a proxy, it isn't involved. Always request just the file.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override protected TunnelRequest getTunnelConfig() {
|
||||
String userAgent = requestHeaders.getUserAgent();
|
||||
if (userAgent == null) {
|
||||
userAgent = getDefaultUserAgent();
|
||||
}
|
||||
|
||||
URL url = policy.getURL();
|
||||
return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
|
||||
requestHeaders.getProxyAuthorization());
|
||||
}
|
||||
}
|
||||
}
|
429
framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
Normal file
429
framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
Normal file
@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.internal.Platform;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ProtocolException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* The HTTP status and unparsed header fields of a single HTTP message. Values
|
||||
* are represented as uninterpreted strings; use {@link RequestHeaders} and
|
||||
* {@link ResponseHeaders} for interpreted headers. This class maintains the
|
||||
* order of the header fields within the HTTP message.
|
||||
*
|
||||
* <p>This class tracks fields line-by-line. A field with multiple comma-
|
||||
* separated values on the same line will be treated as a field with a single
|
||||
* value by this class. It is the caller's responsibility to detect and split
|
||||
* on commas if their field permits multiple values. This simplifies use of
|
||||
* single-valued fields whose values routinely contain commas, such as cookies
|
||||
* or dates.
|
||||
*
|
||||
* <p>This class trims whitespace from values. It never returns values with
|
||||
* leading or trailing whitespace.
|
||||
*/
|
||||
public final class RawHeaders {
|
||||
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
|
||||
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
|
||||
@Override public int compare(String a, String b) {
|
||||
if (a == b) {
|
||||
return 0;
|
||||
} else if (a == null) {
|
||||
return -1;
|
||||
} else if (b == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final List<String> namesAndValues = new ArrayList<String>(20);
|
||||
private String requestLine;
|
||||
private String statusLine;
|
||||
private int httpMinorVersion = 1;
|
||||
private int responseCode = -1;
|
||||
private String responseMessage;
|
||||
|
||||
public RawHeaders() {
|
||||
}
|
||||
|
||||
public RawHeaders(RawHeaders copyFrom) {
|
||||
namesAndValues.addAll(copyFrom.namesAndValues);
|
||||
requestLine = copyFrom.requestLine;
|
||||
statusLine = copyFrom.statusLine;
|
||||
httpMinorVersion = copyFrom.httpMinorVersion;
|
||||
responseCode = copyFrom.responseCode;
|
||||
responseMessage = copyFrom.responseMessage;
|
||||
}
|
||||
|
||||
/** Sets the request line (like "GET / HTTP/1.1"). */
|
||||
public void setRequestLine(String requestLine) {
|
||||
requestLine = requestLine.trim();
|
||||
this.requestLine = requestLine;
|
||||
}
|
||||
|
||||
/** Sets the response status line (like "HTTP/1.0 200 OK"). */
|
||||
public void setStatusLine(String statusLine) throws IOException {
|
||||
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
|
||||
if (this.responseMessage != null) {
|
||||
throw new IllegalStateException("statusLine is already set");
|
||||
}
|
||||
// We allow empty message without leading white space since some servers
|
||||
// do not send the white space when the message is empty.
|
||||
boolean hasMessage = statusLine.length() > 13;
|
||||
if (!statusLine.startsWith("HTTP/1.")
|
||||
|| statusLine.length() < 12
|
||||
|| statusLine.charAt(8) != ' '
|
||||
|| (hasMessage && statusLine.charAt(12) != ' ')) {
|
||||
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||
}
|
||||
int httpMinorVersion = statusLine.charAt(7) - '0';
|
||||
if (httpMinorVersion < 0 || httpMinorVersion > 9) {
|
||||
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||
}
|
||||
int responseCode;
|
||||
try {
|
||||
responseCode = Integer.parseInt(statusLine.substring(9, 12));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ProtocolException("Unexpected status line: " + statusLine);
|
||||
}
|
||||
this.responseMessage = hasMessage ? statusLine.substring(13) : "";
|
||||
this.responseCode = responseCode;
|
||||
this.statusLine = statusLine;
|
||||
this.httpMinorVersion = httpMinorVersion;
|
||||
}
|
||||
|
||||
public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
|
||||
String status = null;
|
||||
String version = null;
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
String name = namesAndValues.get(i);
|
||||
if (":status".equals(name)) {
|
||||
status = namesAndValues.get(i + 1);
|
||||
} else if (":version".equals(name)) {
|
||||
version = namesAndValues.get(i + 1);
|
||||
}
|
||||
}
|
||||
if (status == null || version == null) {
|
||||
throw new ProtocolException("Expected ':status' and ':version' headers not present");
|
||||
}
|
||||
setStatusLine(version + " " + status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param method like "GET", "POST", "HEAD", etc.
|
||||
* @param path like "/foo/bar.html"
|
||||
* @param version like "HTTP/1.1"
|
||||
* @param host like "www.android.com:1234"
|
||||
* @param scheme like "https"
|
||||
*/
|
||||
public void addSpdyRequestHeaders(String method, String path, String version, String host,
|
||||
String scheme) {
|
||||
// TODO: populate the statusLine for the client's benefit?
|
||||
add(":method", method);
|
||||
add(":scheme", scheme);
|
||||
add(":path", path);
|
||||
add(":version", version);
|
||||
add(":host", host);
|
||||
}
|
||||
|
||||
public String getStatusLine() {
|
||||
return statusLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
|
||||
* and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
|
||||
*/
|
||||
public int getHttpMinorVersion() {
|
||||
return httpMinorVersion != -1 ? httpMinorVersion : 1;
|
||||
}
|
||||
|
||||
/** Returns the HTTP status code or -1 if it is unknown. */
|
||||
public int getResponseCode() {
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
/** Returns the HTTP status message or null if it is unknown. */
|
||||
public String getResponseMessage() {
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HTTP header line containing a field name, a literal colon, and a
|
||||
* value.
|
||||
*/
|
||||
public void addLine(String line) {
|
||||
int index = line.indexOf(":");
|
||||
if (index == -1) {
|
||||
add("", line);
|
||||
} else {
|
||||
add(line.substring(0, index), line.substring(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a field with the specified value. */
|
||||
public void add(String fieldName, String value) {
|
||||
if (fieldName == null) {
|
||||
throw new IllegalArgumentException("fieldName == null");
|
||||
}
|
||||
if (value == null) {
|
||||
// Given null values, the RI sends a malformed field line like
|
||||
// "Accept\r\n". For platform compatibility and HTTP compliance, we
|
||||
// print a warning and ignore null values.
|
||||
Platform.get()
|
||||
.logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
|
||||
return;
|
||||
}
|
||||
namesAndValues.add(fieldName);
|
||||
namesAndValues.add(value.trim());
|
||||
}
|
||||
|
||||
public void removeAll(String fieldName) {
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
|
||||
namesAndValues.remove(i); // field name
|
||||
namesAndValues.remove(i); // value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addAll(String fieldName, List<String> headerFields) {
|
||||
for (String value : headerFields) {
|
||||
add(fieldName, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a field with the specified value. If the field is not found, it is
|
||||
* added. If the field is found, the existing values are replaced.
|
||||
*/
|
||||
public void set(String fieldName, String value) {
|
||||
removeAll(fieldName);
|
||||
add(fieldName, value);
|
||||
}
|
||||
|
||||
/** Returns the number of field values. */
|
||||
public int length() {
|
||||
return namesAndValues.size() / 2;
|
||||
}
|
||||
|
||||
/** Returns the field at {@code position} or null if that is out of range. */
|
||||
public String getFieldName(int index) {
|
||||
int fieldNameIndex = index * 2;
|
||||
if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
|
||||
return null;
|
||||
}
|
||||
return namesAndValues.get(fieldNameIndex);
|
||||
}
|
||||
|
||||
/** Returns the value at {@code index} or null if that is out of range. */
|
||||
public String getValue(int index) {
|
||||
int valueIndex = index * 2 + 1;
|
||||
if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
|
||||
return null;
|
||||
}
|
||||
return namesAndValues.get(valueIndex);
|
||||
}
|
||||
|
||||
/** Returns the last value corresponding to the specified field, or null. */
|
||||
public String get(String fieldName) {
|
||||
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
|
||||
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
|
||||
return namesAndValues.get(i + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param fieldNames a case-insensitive set of HTTP header field names. */
|
||||
public RawHeaders getAll(Set<String> fieldNames) {
|
||||
RawHeaders result = new RawHeaders();
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
String fieldName = namesAndValues.get(i);
|
||||
if (fieldNames.contains(fieldName)) {
|
||||
result.add(fieldName, namesAndValues.get(i + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns bytes of a request header for sending on an HTTP transport. */
|
||||
public byte[] toBytes() throws UnsupportedEncodingException {
|
||||
StringBuilder result = new StringBuilder(256);
|
||||
result.append(requestLine).append("\r\n");
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
result.append(namesAndValues.get(i))
|
||||
.append(": ")
|
||||
.append(namesAndValues.get(i + 1))
|
||||
.append("\r\n");
|
||||
}
|
||||
result.append("\r\n");
|
||||
return result.toString().getBytes("ISO-8859-1");
|
||||
}
|
||||
|
||||
/** Parses bytes of a response header from an HTTP transport. */
|
||||
public static RawHeaders fromBytes(InputStream in) throws IOException {
|
||||
RawHeaders headers;
|
||||
do {
|
||||
headers = new RawHeaders();
|
||||
headers.setStatusLine(Util.readAsciiLine(in));
|
||||
readHeaders(in, headers);
|
||||
} while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Reads headers or trailers into {@code out}. */
|
||||
public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
|
||||
// parse the result headers until the first blank line
|
||||
String line;
|
||||
while ((line = Util.readAsciiLine(in)).length() != 0) {
|
||||
out.addLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable map containing each field to its list of values. The
|
||||
* status line is mapped to null.
|
||||
*/
|
||||
public Map<String, List<String>> toMultimap(boolean response) {
|
||||
Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
String fieldName = namesAndValues.get(i);
|
||||
String value = namesAndValues.get(i + 1);
|
||||
|
||||
List<String> allValues = new ArrayList<String>();
|
||||
List<String> otherValues = result.get(fieldName);
|
||||
if (otherValues != null) {
|
||||
allValues.addAll(otherValues);
|
||||
}
|
||||
allValues.add(value);
|
||||
result.put(fieldName, Collections.unmodifiableList(allValues));
|
||||
}
|
||||
if (response && statusLine != null) {
|
||||
result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
|
||||
} else if (requestLine != null) {
|
||||
result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine)));
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from the given map of fields to values. If
|
||||
* present, the null field's last element will be used to set the status
|
||||
* line.
|
||||
*/
|
||||
public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
|
||||
throws IOException {
|
||||
if (!response) throw new UnsupportedOperationException();
|
||||
RawHeaders result = new RawHeaders();
|
||||
for (Entry<String, List<String>> entry : map.entrySet()) {
|
||||
String fieldName = entry.getKey();
|
||||
List<String> values = entry.getValue();
|
||||
if (fieldName != null) {
|
||||
result.addAll(fieldName, values);
|
||||
} else if (!values.isEmpty()) {
|
||||
result.setStatusLine(values.get(values.size() - 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of alternating names and values. Names are all lower case.
|
||||
* No names are repeated. If any name has multiple values, they are
|
||||
* concatenated using "\0" as a delimiter.
|
||||
*/
|
||||
public List<String> toNameValueBlock() {
|
||||
Set<String> names = new HashSet<String>();
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
String name = namesAndValues.get(i).toLowerCase(Locale.US);
|
||||
String value = namesAndValues.get(i + 1);
|
||||
|
||||
// TODO: promote this check to where names and values are created
|
||||
if (name.length() == 0
|
||||
|| 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.
|
||||
if (name.equals("connection")
|
||||
|| name.equals("host")
|
||||
|| name.equals("keep-alive")
|
||||
|| name.equals("proxy-connection")
|
||||
|| name.equals("transfer-encoding")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we haven't seen this name before, add the pair to the end of the list...
|
||||
if (names.add(name)) {
|
||||
result.add(name);
|
||||
result.add(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ...otherwise concatenate the existing values and this value.
|
||||
for (int j = 0; j < result.size(); j += 2) {
|
||||
if (name.equals(result.get(j))) {
|
||||
result.set(j + 1, result.get(j + 1) + "\0" + value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
|
||||
if (nameValueBlock.size() % 2 != 0) {
|
||||
throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
|
||||
}
|
||||
RawHeaders result = new RawHeaders();
|
||||
for (int i = 0; i < nameValueBlock.size(); i += 2) {
|
||||
String name = nameValueBlock.get(i);
|
||||
String values = nameValueBlock.get(i + 1);
|
||||
for (int start = 0; start < values.length(); ) {
|
||||
int end = values.indexOf('\0', start);
|
||||
if (end == -1) {
|
||||
end = values.length();
|
||||
}
|
||||
result.namesAndValues.add(name);
|
||||
result.namesAndValues.add(values.substring(start, end));
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Parsed HTTP request headers. */
|
||||
final class RequestHeaders {
|
||||
private final URI uri;
|
||||
private final RawHeaders headers;
|
||||
|
||||
/** Don't use a cache to satisfy this request. */
|
||||
private boolean noCache;
|
||||
private int maxAgeSeconds = -1;
|
||||
private int maxStaleSeconds = -1;
|
||||
private int minFreshSeconds = -1;
|
||||
|
||||
/**
|
||||
* This field's name "only-if-cached" is misleading. It actually means "do
|
||||
* not use the network". It is set by a client who only wants to make a
|
||||
* request if it can be fully satisfied by the cache. Cached responses that
|
||||
* would require validation (ie. conditional gets) are not permitted if this
|
||||
* header is set.
|
||||
*/
|
||||
private boolean onlyIfCached;
|
||||
|
||||
/**
|
||||
* True if the request contains an authorization field. Although this isn't
|
||||
* necessarily a shared cache, it follows the spec's strict requirements for
|
||||
* shared caches.
|
||||
*/
|
||||
private boolean hasAuthorization;
|
||||
|
||||
private int contentLength = -1;
|
||||
private String transferEncoding;
|
||||
private String userAgent;
|
||||
private String host;
|
||||
private String connection;
|
||||
private String acceptEncoding;
|
||||
private String contentType;
|
||||
private String ifModifiedSince;
|
||||
private String ifNoneMatch;
|
||||
private String proxyAuthorization;
|
||||
|
||||
public RequestHeaders(URI uri, RawHeaders headers) {
|
||||
this.uri = uri;
|
||||
this.headers = headers;
|
||||
|
||||
HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
|
||||
@Override public void handle(String directive, String parameter) {
|
||||
if ("no-cache".equalsIgnoreCase(directive)) {
|
||||
noCache = true;
|
||||
} else if ("max-age".equalsIgnoreCase(directive)) {
|
||||
maxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||
} else if ("max-stale".equalsIgnoreCase(directive)) {
|
||||
maxStaleSeconds = HeaderParser.parseSeconds(parameter);
|
||||
} else if ("min-fresh".equalsIgnoreCase(directive)) {
|
||||
minFreshSeconds = HeaderParser.parseSeconds(parameter);
|
||||
} else if ("only-if-cached".equalsIgnoreCase(directive)) {
|
||||
onlyIfCached = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < headers.length(); i++) {
|
||||
String fieldName = headers.getFieldName(i);
|
||||
String value = headers.getValue(i);
|
||||
if ("Cache-Control".equalsIgnoreCase(fieldName)) {
|
||||
HeaderParser.parseCacheControl(value, handler);
|
||||
} else if ("Pragma".equalsIgnoreCase(fieldName)) {
|
||||
if ("no-cache".equalsIgnoreCase(value)) {
|
||||
noCache = true;
|
||||
}
|
||||
} else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
|
||||
ifNoneMatch = value;
|
||||
} else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
|
||||
ifModifiedSince = value;
|
||||
} else if ("Authorization".equalsIgnoreCase(fieldName)) {
|
||||
hasAuthorization = true;
|
||||
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
|
||||
try {
|
||||
contentLength = Integer.parseInt(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
|
||||
transferEncoding = value;
|
||||
} else if ("User-Agent".equalsIgnoreCase(fieldName)) {
|
||||
userAgent = value;
|
||||
} else if ("Host".equalsIgnoreCase(fieldName)) {
|
||||
host = value;
|
||||
} else if ("Connection".equalsIgnoreCase(fieldName)) {
|
||||
connection = value;
|
||||
} else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
|
||||
acceptEncoding = value;
|
||||
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
|
||||
contentType = value;
|
||||
} else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
|
||||
proxyAuthorization = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isChunked() {
|
||||
return "chunked".equalsIgnoreCase(transferEncoding);
|
||||
}
|
||||
|
||||
public boolean hasConnectionClose() {
|
||||
return "close".equalsIgnoreCase(connection);
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public RawHeaders getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public boolean isNoCache() {
|
||||
return noCache;
|
||||
}
|
||||
|
||||
public int getMaxAgeSeconds() {
|
||||
return maxAgeSeconds;
|
||||
}
|
||||
|
||||
public int getMaxStaleSeconds() {
|
||||
return maxStaleSeconds;
|
||||
}
|
||||
|
||||
public int getMinFreshSeconds() {
|
||||
return minFreshSeconds;
|
||||
}
|
||||
|
||||
public boolean isOnlyIfCached() {
|
||||
return onlyIfCached;
|
||||
}
|
||||
|
||||
public boolean hasAuthorization() {
|
||||
return hasAuthorization;
|
||||
}
|
||||
|
||||
public int getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public String getTransferEncoding() {
|
||||
return transferEncoding;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public String getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public String getAcceptEncoding() {
|
||||
return acceptEncoding;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public String getIfModifiedSince() {
|
||||
return ifModifiedSince;
|
||||
}
|
||||
|
||||
public String getIfNoneMatch() {
|
||||
return ifNoneMatch;
|
||||
}
|
||||
|
||||
public String getProxyAuthorization() {
|
||||
return proxyAuthorization;
|
||||
}
|
||||
|
||||
public void setChunked() {
|
||||
if (this.transferEncoding != null) {
|
||||
headers.removeAll("Transfer-Encoding");
|
||||
}
|
||||
headers.add("Transfer-Encoding", "chunked");
|
||||
this.transferEncoding = "chunked";
|
||||
}
|
||||
|
||||
public void setContentLength(int contentLength) {
|
||||
if (this.contentLength != -1) {
|
||||
headers.removeAll("Content-Length");
|
||||
}
|
||||
headers.add("Content-Length", Integer.toString(contentLength));
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
public void setUserAgent(String userAgent) {
|
||||
if (this.userAgent != null) {
|
||||
headers.removeAll("User-Agent");
|
||||
}
|
||||
headers.add("User-Agent", userAgent);
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
if (this.host != null) {
|
||||
headers.removeAll("Host");
|
||||
}
|
||||
headers.add("Host", host);
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public void setConnection(String connection) {
|
||||
if (this.connection != null) {
|
||||
headers.removeAll("Connection");
|
||||
}
|
||||
headers.add("Connection", connection);
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public void setAcceptEncoding(String acceptEncoding) {
|
||||
if (this.acceptEncoding != null) {
|
||||
headers.removeAll("Accept-Encoding");
|
||||
}
|
||||
headers.add("Accept-Encoding", acceptEncoding);
|
||||
this.acceptEncoding = acceptEncoding;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
if (this.contentType != null) {
|
||||
headers.removeAll("Content-Type");
|
||||
}
|
||||
headers.add("Content-Type", contentType);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public void setIfModifiedSince(Date date) {
|
||||
if (ifModifiedSince != null) {
|
||||
headers.removeAll("If-Modified-Since");
|
||||
}
|
||||
String formattedDate = HttpDate.format(date);
|
||||
headers.add("If-Modified-Since", formattedDate);
|
||||
ifModifiedSince = formattedDate;
|
||||
}
|
||||
|
||||
public void setIfNoneMatch(String ifNoneMatch) {
|
||||
if (this.ifNoneMatch != null) {
|
||||
headers.removeAll("If-None-Match");
|
||||
}
|
||||
headers.add("If-None-Match", ifNoneMatch);
|
||||
this.ifNoneMatch = ifNoneMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request contains conditions that save the server from
|
||||
* sending a response that the client has locally. When the caller adds
|
||||
* conditions, this cache won't participate in the request.
|
||||
*/
|
||||
public boolean hasConditions() {
|
||||
return ifModifiedSince != null || ifNoneMatch != null;
|
||||
}
|
||||
|
||||
public void addCookies(Map<String, List<String>> allCookieHeaders) {
|
||||
for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
|
||||
headers.addAll(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,496 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.ResponseSource;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.equal;
|
||||
|
||||
/** Parsed HTTP response headers. */
|
||||
final class ResponseHeaders {
|
||||
|
||||
/** HTTP header name for the local time when the request was sent. */
|
||||
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
|
||||
|
||||
/** HTTP header name for the local time when the response was received. */
|
||||
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
|
||||
|
||||
/** HTTP synthetic header with the response source. */
|
||||
static final String RESPONSE_SOURCE = "X-Android-Response-Source";
|
||||
|
||||
private final URI uri;
|
||||
private final RawHeaders headers;
|
||||
|
||||
/** The server's time when this response was served, if known. */
|
||||
private Date servedDate;
|
||||
|
||||
/** The last modified date of the response, if known. */
|
||||
private Date lastModified;
|
||||
|
||||
/**
|
||||
* The expiration date of the response, if known. If both this field and the
|
||||
* max age are set, the max age is preferred.
|
||||
*/
|
||||
private Date expires;
|
||||
|
||||
/**
|
||||
* Extension header set by HttpURLConnectionImpl specifying the timestamp
|
||||
* when the HTTP request was first initiated.
|
||||
*/
|
||||
private long sentRequestMillis;
|
||||
|
||||
/**
|
||||
* Extension header set by HttpURLConnectionImpl specifying the timestamp
|
||||
* when the HTTP response was first received.
|
||||
*/
|
||||
private long receivedResponseMillis;
|
||||
|
||||
/**
|
||||
* In the response, this field's name "no-cache" is misleading. It doesn't
|
||||
* prevent us from caching the response; it only means we have to validate
|
||||
* the response with the origin server before returning it. We can do this
|
||||
* with a conditional get.
|
||||
*/
|
||||
private boolean noCache;
|
||||
|
||||
/** If true, this response should not be cached. */
|
||||
private boolean noStore;
|
||||
|
||||
/**
|
||||
* The duration past the response's served date that it can be served
|
||||
* without validation.
|
||||
*/
|
||||
private int maxAgeSeconds = -1;
|
||||
|
||||
/**
|
||||
* The "s-maxage" directive is the max age for shared caches. Not to be
|
||||
* confused with "max-age" for non-shared caches, As in Firefox and Chrome,
|
||||
* this directive is not honored by this cache.
|
||||
*/
|
||||
private int sMaxAgeSeconds = -1;
|
||||
|
||||
/**
|
||||
* This request header field's name "only-if-cached" is misleading. It
|
||||
* actually means "do not use the network". It is set by a client who only
|
||||
* wants to make a request if it can be fully satisfied by the cache.
|
||||
* Cached responses that would require validation (ie. conditional gets) are
|
||||
* not permitted if this header is set.
|
||||
*/
|
||||
private boolean isPublic;
|
||||
private boolean mustRevalidate;
|
||||
private String etag;
|
||||
private int ageSeconds = -1;
|
||||
|
||||
/** Case-insensitive set of field names. */
|
||||
private Set<String> varyFields = Collections.emptySet();
|
||||
|
||||
private String contentEncoding;
|
||||
private String transferEncoding;
|
||||
private int contentLength = -1;
|
||||
private String connection;
|
||||
|
||||
public ResponseHeaders(URI uri, RawHeaders headers) {
|
||||
this.uri = uri;
|
||||
this.headers = headers;
|
||||
|
||||
HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
|
||||
@Override public void handle(String directive, String parameter) {
|
||||
if ("no-cache".equalsIgnoreCase(directive)) {
|
||||
noCache = true;
|
||||
} else if ("no-store".equalsIgnoreCase(directive)) {
|
||||
noStore = true;
|
||||
} else if ("max-age".equalsIgnoreCase(directive)) {
|
||||
maxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||
} else if ("s-maxage".equalsIgnoreCase(directive)) {
|
||||
sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
|
||||
} else if ("public".equalsIgnoreCase(directive)) {
|
||||
isPublic = true;
|
||||
} else if ("must-revalidate".equalsIgnoreCase(directive)) {
|
||||
mustRevalidate = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < headers.length(); i++) {
|
||||
String fieldName = headers.getFieldName(i);
|
||||
String value = headers.getValue(i);
|
||||
if ("Cache-Control".equalsIgnoreCase(fieldName)) {
|
||||
HeaderParser.parseCacheControl(value, handler);
|
||||
} else if ("Date".equalsIgnoreCase(fieldName)) {
|
||||
servedDate = HttpDate.parse(value);
|
||||
} else if ("Expires".equalsIgnoreCase(fieldName)) {
|
||||
expires = HttpDate.parse(value);
|
||||
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
|
||||
lastModified = HttpDate.parse(value);
|
||||
} else if ("ETag".equalsIgnoreCase(fieldName)) {
|
||||
etag = value;
|
||||
} else if ("Pragma".equalsIgnoreCase(fieldName)) {
|
||||
if ("no-cache".equalsIgnoreCase(value)) {
|
||||
noCache = true;
|
||||
}
|
||||
} else if ("Age".equalsIgnoreCase(fieldName)) {
|
||||
ageSeconds = HeaderParser.parseSeconds(value);
|
||||
} else if ("Vary".equalsIgnoreCase(fieldName)) {
|
||||
// Replace the immutable empty set with something we can mutate.
|
||||
if (varyFields.isEmpty()) {
|
||||
varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
}
|
||||
for (String varyField : value.split(",")) {
|
||||
varyFields.add(varyField.trim());
|
||||
}
|
||||
} else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
|
||||
contentEncoding = value;
|
||||
} else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
|
||||
transferEncoding = value;
|
||||
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
|
||||
try {
|
||||
contentLength = Integer.parseInt(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} else if ("Connection".equalsIgnoreCase(fieldName)) {
|
||||
connection = value;
|
||||
} else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
|
||||
sentRequestMillis = Long.parseLong(value);
|
||||
} else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
|
||||
receivedResponseMillis = Long.parseLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isContentEncodingGzip() {
|
||||
return "gzip".equalsIgnoreCase(contentEncoding);
|
||||
}
|
||||
|
||||
public void stripContentEncoding() {
|
||||
contentEncoding = null;
|
||||
headers.removeAll("Content-Encoding");
|
||||
}
|
||||
|
||||
public void stripContentLength() {
|
||||
contentLength = -1;
|
||||
headers.removeAll("Content-Length");
|
||||
}
|
||||
|
||||
public boolean isChunked() {
|
||||
return "chunked".equalsIgnoreCase(transferEncoding);
|
||||
}
|
||||
|
||||
public boolean hasConnectionClose() {
|
||||
return "close".equalsIgnoreCase(connection);
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public RawHeaders getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public Date getServedDate() {
|
||||
return servedDate;
|
||||
}
|
||||
|
||||
public Date getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
public Date getExpires() {
|
||||
return expires;
|
||||
}
|
||||
|
||||
public boolean isNoCache() {
|
||||
return noCache;
|
||||
}
|
||||
|
||||
public boolean isNoStore() {
|
||||
return noStore;
|
||||
}
|
||||
|
||||
public int getMaxAgeSeconds() {
|
||||
return maxAgeSeconds;
|
||||
}
|
||||
|
||||
public int getSMaxAgeSeconds() {
|
||||
return sMaxAgeSeconds;
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
return isPublic;
|
||||
}
|
||||
|
||||
public boolean isMustRevalidate() {
|
||||
return mustRevalidate;
|
||||
}
|
||||
|
||||
public String getEtag() {
|
||||
return etag;
|
||||
}
|
||||
|
||||
public Set<String> getVaryFields() {
|
||||
return varyFields;
|
||||
}
|
||||
|
||||
public String getContentEncoding() {
|
||||
return contentEncoding;
|
||||
}
|
||||
|
||||
public int getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public String getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
|
||||
this.sentRequestMillis = sentRequestMillis;
|
||||
headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
|
||||
this.receivedResponseMillis = receivedResponseMillis;
|
||||
headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
|
||||
}
|
||||
|
||||
public void setResponseSource(ResponseSource responseSource) {
|
||||
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current age of the response, in milliseconds. The calculation
|
||||
* is specified by RFC 2616, 13.2.3 Age Calculations.
|
||||
*/
|
||||
private long computeAge(long nowMillis) {
|
||||
long apparentReceivedAge =
|
||||
servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0;
|
||||
long receivedAge =
|
||||
ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
|
||||
: apparentReceivedAge;
|
||||
long responseDuration = receivedResponseMillis - sentRequestMillis;
|
||||
long residentDuration = nowMillis - receivedResponseMillis;
|
||||
return receivedAge + responseDuration + residentDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of milliseconds that the response was fresh for,
|
||||
* starting from the served date.
|
||||
*/
|
||||
private long computeFreshnessLifetime() {
|
||||
if (maxAgeSeconds != -1) {
|
||||
return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
|
||||
} else if (expires != null) {
|
||||
long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
|
||||
long delta = expires.getTime() - servedMillis;
|
||||
return delta > 0 ? delta : 0;
|
||||
} else if (lastModified != null && uri.getRawQuery() == null) {
|
||||
// As recommended by the HTTP RFC and implemented in Firefox, the
|
||||
// max age of a document should be defaulted to 10% of the
|
||||
// document's age at the time it was served. Default expiration
|
||||
// dates aren't used for URIs containing a query.
|
||||
long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
|
||||
long delta = servedMillis - lastModified.getTime();
|
||||
return delta > 0 ? (delta / 10) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if computeFreshnessLifetime used a heuristic. If we used a
|
||||
* heuristic to serve a cached response older than 24 hours, we are required
|
||||
* to attach a warning.
|
||||
*/
|
||||
private boolean isFreshnessLifetimeHeuristic() {
|
||||
return maxAgeSeconds == -1 && expires == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this response can be stored to later serve another
|
||||
* request.
|
||||
*/
|
||||
public boolean isCacheable(RequestHeaders request) {
|
||||
// Always go to network for uncacheable response codes (RFC 2616, 13.4),
|
||||
// This implementation doesn't support caching partial content.
|
||||
int responseCode = headers.getResponseCode();
|
||||
if (responseCode != HttpURLConnection.HTTP_OK
|
||||
&& responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
|
||||
&& responseCode != HttpURLConnection.HTTP_MULT_CHOICE
|
||||
&& responseCode != HttpURLConnection.HTTP_MOVED_PERM
|
||||
&& responseCode != HttpURLConnection.HTTP_GONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Responses to authorized requests aren't cacheable unless they include
|
||||
// a 'public', 'must-revalidate' or 's-maxage' directive.
|
||||
if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noStore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a Vary header contains an asterisk. Such responses cannot
|
||||
* be cached.
|
||||
*/
|
||||
public boolean hasVaryAll() {
|
||||
return varyFields.contains("*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if none of the Vary headers on this response have changed
|
||||
* between {@code cachedRequest} and {@code newRequest}.
|
||||
*/
|
||||
public boolean varyMatches(Map<String, List<String>> cachedRequest,
|
||||
Map<String, List<String>> newRequest) {
|
||||
for (String field : varyFields) {
|
||||
if (!equal(cachedRequest.get(field), newRequest.get(field))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the source to satisfy {@code request} given this cached response. */
|
||||
public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
|
||||
// If this response shouldn't have been stored, it should never be used
|
||||
// as a response source. This check should be redundant as long as the
|
||||
// persistence store is well-behaved and the rules are constant.
|
||||
if (!isCacheable(request)) {
|
||||
return ResponseSource.NETWORK;
|
||||
}
|
||||
|
||||
if (request.isNoCache() || request.hasConditions()) {
|
||||
return ResponseSource.NETWORK;
|
||||
}
|
||||
|
||||
long ageMillis = computeAge(nowMillis);
|
||||
long freshMillis = computeFreshnessLifetime();
|
||||
|
||||
if (request.getMaxAgeSeconds() != -1) {
|
||||
freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
|
||||
}
|
||||
|
||||
long minFreshMillis = 0;
|
||||
if (request.getMinFreshSeconds() != -1) {
|
||||
minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
|
||||
}
|
||||
|
||||
long maxStaleMillis = 0;
|
||||
if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
|
||||
maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
|
||||
}
|
||||
|
||||
if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
|
||||
if (ageMillis + minFreshMillis >= freshMillis) {
|
||||
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
|
||||
}
|
||||
if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
|
||||
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
|
||||
}
|
||||
return ResponseSource.CACHE;
|
||||
}
|
||||
|
||||
if (lastModified != null) {
|
||||
request.setIfModifiedSince(lastModified);
|
||||
} else if (servedDate != null) {
|
||||
request.setIfModifiedSince(servedDate);
|
||||
}
|
||||
|
||||
if (etag != null) {
|
||||
request.setIfNoneMatch(etag);
|
||||
}
|
||||
|
||||
return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cached response should be used; false if the
|
||||
* network response should be used.
|
||||
*/
|
||||
public boolean validate(ResponseHeaders networkResponse) {
|
||||
if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The HTTP spec says that if the network's response is older than our
|
||||
// cached response, we may return the cache's response. Like Chrome (but
|
||||
// unlike Firefox), this client prefers to return the newer response.
|
||||
if (lastModified != null
|
||||
&& networkResponse.lastModified != null
|
||||
&& networkResponse.lastModified.getTime() < lastModified.getTime()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines this cached header with a network header as defined by RFC 2616,
|
||||
* 13.5.3.
|
||||
*/
|
||||
public ResponseHeaders combine(ResponseHeaders network) throws IOException {
|
||||
RawHeaders result = new RawHeaders();
|
||||
result.setStatusLine(headers.getStatusLine());
|
||||
|
||||
for (int i = 0; i < headers.length(); i++) {
|
||||
String fieldName = headers.getFieldName(i);
|
||||
String value = headers.getValue(i);
|
||||
if ("Warning".equals(fieldName) && value.startsWith("1")) {
|
||||
continue; // drop 100-level freshness warnings
|
||||
}
|
||||
if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
|
||||
result.add(fieldName, value);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < network.headers.length(); i++) {
|
||||
String fieldName = network.headers.getFieldName(i);
|
||||
if (isEndToEnd(fieldName)) {
|
||||
result.add(fieldName, network.headers.getValue(i));
|
||||
}
|
||||
}
|
||||
|
||||
return new ResponseHeaders(uri, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code fieldName} is an end-to-end HTTP header, as
|
||||
* defined by RFC 2616, 13.5.1.
|
||||
*/
|
||||
private static boolean isEndToEnd(String fieldName) {
|
||||
return !"Connection".equalsIgnoreCase(fieldName)
|
||||
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
|
||||
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
|
||||
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
|
||||
&& !"TE".equalsIgnoreCase(fieldName)
|
||||
&& !"Trailers".equalsIgnoreCase(fieldName)
|
||||
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
|
||||
&& !"Upgrade".equalsIgnoreCase(fieldName);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||
|
||||
/**
|
||||
* An HTTP request body that's completely buffered in memory. This allows
|
||||
* the post body to be transparently re-sent if the HTTP request must be
|
||||
* sent multiple times.
|
||||
*/
|
||||
final class RetryableOutputStream extends AbstractHttpOutputStream {
|
||||
private final int limit;
|
||||
private final ByteArrayOutputStream content;
|
||||
|
||||
public RetryableOutputStream(int limit) {
|
||||
this.limit = limit;
|
||||
this.content = new ByteArrayOutputStream(limit);
|
||||
}
|
||||
|
||||
public RetryableOutputStream() {
|
||||
this.limit = -1;
|
||||
this.content = new ByteArrayOutputStream();
|
||||
}
|
||||
|
||||
@Override public synchronized void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (content.size() < limit) {
|
||||
throw new ProtocolException(
|
||||
"content-length promised " + limit + " bytes, but received " + content.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override public synchronized void write(byte[] buffer, int offset, int count)
|
||||
throws IOException {
|
||||
checkNotClosed();
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
if (limit != -1 && content.size() > limit - count) {
|
||||
throw new ProtocolException("exceeded content-length limit of " + limit + " bytes");
|
||||
}
|
||||
content.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public synchronized int contentLength() throws IOException {
|
||||
close();
|
||||
return content.size();
|
||||
}
|
||||
|
||||
public void writeToSocket(OutputStream socketOut) throws IOException {
|
||||
content.writeTo(socketOut);
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.Address;
|
||||
import com.squareup.okhttp.Connection;
|
||||
import com.squareup.okhttp.ConnectionPool;
|
||||
import com.squareup.okhttp.internal.Dns;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.getEffectivePort;
|
||||
|
||||
/**
|
||||
* Selects routes to connect to an origin server. Each connection requires a
|
||||
* choice of proxy server, IP address, and TLS mode. Connections may also be
|
||||
* recycled.
|
||||
*/
|
||||
public final class RouteSelector {
|
||||
/** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
|
||||
private static final int TLS_MODE_MODERN = 1;
|
||||
/** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
|
||||
private static final int TLS_MODE_COMPATIBLE = 0;
|
||||
/** No TLS mode. */
|
||||
private static final int TLS_MODE_NULL = -1;
|
||||
|
||||
private final Address address;
|
||||
private final URI uri;
|
||||
private final ProxySelector proxySelector;
|
||||
private final ConnectionPool pool;
|
||||
private final Dns dns;
|
||||
|
||||
/* The most recently attempted route. */
|
||||
private Proxy lastProxy;
|
||||
private InetSocketAddress lastInetSocketAddress;
|
||||
|
||||
/* State for negotiating the next proxy to use. */
|
||||
private boolean hasNextProxy;
|
||||
private Proxy userSpecifiedProxy;
|
||||
private Iterator<Proxy> proxySelectorProxies;
|
||||
|
||||
/* State for negotiating the next InetSocketAddress to use. */
|
||||
private InetAddress[] socketAddresses;
|
||||
private int nextSocketAddressIndex;
|
||||
private String socketHost;
|
||||
private int socketPort;
|
||||
|
||||
/* State for negotiating the next TLS configuration */
|
||||
private int nextTlsMode = TLS_MODE_NULL;
|
||||
|
||||
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
|
||||
Dns dns) {
|
||||
this.address = address;
|
||||
this.uri = uri;
|
||||
this.proxySelector = proxySelector;
|
||||
this.pool = pool;
|
||||
this.dns = dns;
|
||||
|
||||
resetNextProxy(uri, address.getProxy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there's another route to attempt. Every address has at
|
||||
* least one route.
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next route address to attempt.
|
||||
*
|
||||
* @throws NoSuchElementException if there are no more routes to attempt.
|
||||
*/
|
||||
public Connection next() throws IOException {
|
||||
// Always prefer pooled connections over new connections.
|
||||
Connection pooled = pool.get(address);
|
||||
if (pooled != null) {
|
||||
return pooled;
|
||||
}
|
||||
|
||||
// Compute the next route to attempt.
|
||||
if (!hasNextTlsMode()) {
|
||||
if (!hasNextInetSocketAddress()) {
|
||||
if (!hasNextProxy()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
lastProxy = nextProxy();
|
||||
resetNextInetSocketAddress(lastProxy);
|
||||
}
|
||||
lastInetSocketAddress = nextInetSocketAddress();
|
||||
resetNextTlsMode();
|
||||
}
|
||||
boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
|
||||
|
||||
return new Connection(address, lastProxy, lastInetSocketAddress, modernTls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clients should invoke this method when they encounter a connectivity
|
||||
* failure on a connection returned by this route selector.
|
||||
*/
|
||||
public void connectFailed(Connection connection, IOException failure) {
|
||||
if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
|
||||
// Tell the proxy selector when we fail to connect on a fresh connection.
|
||||
proxySelector.connectFailed(uri, connection.getProxy().address(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets {@link #nextProxy} to the first option. */
|
||||
private void resetNextProxy(URI uri, Proxy proxy) {
|
||||
this.hasNextProxy = true; // This includes NO_PROXY!
|
||||
if (proxy != null) {
|
||||
this.userSpecifiedProxy = proxy;
|
||||
} else {
|
||||
List<Proxy> proxyList = proxySelector.select(uri);
|
||||
if (proxyList != null) {
|
||||
this.proxySelectorProxies = proxyList.iterator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if there's another proxy to try. */
|
||||
private boolean hasNextProxy() {
|
||||
return hasNextProxy;
|
||||
}
|
||||
|
||||
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
|
||||
private Proxy nextProxy() {
|
||||
// If the user specifies a proxy, try that and only that.
|
||||
if (userSpecifiedProxy != null) {
|
||||
hasNextProxy = false;
|
||||
return userSpecifiedProxy;
|
||||
}
|
||||
|
||||
// Try each of the ProxySelector choices until one connection succeeds. If none succeed
|
||||
// then we'll try a direct connection below.
|
||||
if (proxySelectorProxies != null) {
|
||||
while (proxySelectorProxies.hasNext()) {
|
||||
Proxy candidate = proxySelectorProxies.next();
|
||||
if (candidate.type() != Proxy.Type.DIRECT) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally try a direct connection.
|
||||
hasNextProxy = false;
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
|
||||
/** Resets {@link #nextInetSocketAddress} to the first option. */
|
||||
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
|
||||
socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
|
||||
|
||||
if (proxy.type() == Proxy.Type.DIRECT) {
|
||||
socketHost = uri.getHost();
|
||||
socketPort = getEffectivePort(uri);
|
||||
} else {
|
||||
SocketAddress proxyAddress = proxy.address();
|
||||
if (!(proxyAddress instanceof InetSocketAddress)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
|
||||
}
|
||||
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
|
||||
socketHost = proxySocketAddress.getHostName();
|
||||
socketPort = proxySocketAddress.getPort();
|
||||
}
|
||||
|
||||
// Try each address for best behavior in mixed IPv4/IPv6 environments.
|
||||
socketAddresses = dns.getAllByName(socketHost);
|
||||
nextSocketAddressIndex = 0;
|
||||
}
|
||||
|
||||
/** Returns true if there's another socket address to try. */
|
||||
private boolean hasNextInetSocketAddress() {
|
||||
return socketAddresses != null;
|
||||
}
|
||||
|
||||
/** Returns the next socket address to try. */
|
||||
private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
|
||||
InetSocketAddress result =
|
||||
new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort);
|
||||
if (nextSocketAddressIndex == socketAddresses.length) {
|
||||
socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
|
||||
nextSocketAddressIndex = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Resets {@link #nextTlsMode} to the first option. */
|
||||
private void resetNextTlsMode() {
|
||||
nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE;
|
||||
}
|
||||
|
||||
/** Returns true if there's another TLS mode to try. */
|
||||
private boolean hasNextTlsMode() {
|
||||
return nextTlsMode != TLS_MODE_NULL;
|
||||
}
|
||||
|
||||
/** Returns the next TLS mode to try. */
|
||||
private int nextTlsMode() {
|
||||
if (nextTlsMode == TLS_MODE_MODERN) {
|
||||
nextTlsMode = TLS_MODE_COMPATIBLE;
|
||||
return TLS_MODE_MODERN;
|
||||
} else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
|
||||
nextTlsMode = TLS_MODE_NULL; // So that hasNextTlsMode() returns false.
|
||||
return TLS_MODE_COMPATIBLE;
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import com.squareup.okhttp.internal.spdy.SpdyConnection;
|
||||
import com.squareup.okhttp.internal.spdy.SpdyStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheRequest;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
public final class SpdyTransport implements Transport {
|
||||
private final HttpEngine httpEngine;
|
||||
private final SpdyConnection spdyConnection;
|
||||
private SpdyStream stream;
|
||||
|
||||
public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
|
||||
this.httpEngine = httpEngine;
|
||||
this.spdyConnection = spdyConnection;
|
||||
}
|
||||
|
||||
@Override public OutputStream createRequestBody() throws IOException {
|
||||
// TODO: if we aren't streaming up to the server, we should buffer the whole request
|
||||
writeRequestHeaders();
|
||||
return stream.getOutputStream();
|
||||
}
|
||||
|
||||
@Override public void writeRequestHeaders() throws IOException {
|
||||
if (stream != null) {
|
||||
return;
|
||||
}
|
||||
httpEngine.writingRequestHeaders();
|
||||
RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
|
||||
String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
|
||||
URL url = httpEngine.policy.getURL();
|
||||
requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), version,
|
||||
HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
|
||||
boolean hasRequestBody = httpEngine.hasRequestBody();
|
||||
boolean hasResponseBody = true;
|
||||
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
|
||||
hasResponseBody);
|
||||
stream.setReadTimeout(httpEngine.policy.getReadTimeout());
|
||||
}
|
||||
|
||||
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override public void flushRequest() throws IOException {
|
||||
stream.getOutputStream().close();
|
||||
}
|
||||
|
||||
@Override public ResponseHeaders readResponseHeaders() throws IOException {
|
||||
List<String> nameValueBlock = stream.getResponseHeaders();
|
||||
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
|
||||
rawHeaders.computeResponseStatusLineFromSpdyHeaders();
|
||||
httpEngine.receiveHeaders(rawHeaders);
|
||||
return new ResponseHeaders(httpEngine.uri, rawHeaders);
|
||||
}
|
||||
|
||||
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
|
||||
return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
|
||||
}
|
||||
|
||||
@Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
|
||||
InputStream responseBodyIn) {
|
||||
if (streamCancelled) {
|
||||
if (stream != null) {
|
||||
stream.closeLater(SpdyStream.RST_CANCEL);
|
||||
return true;
|
||||
} else {
|
||||
// If stream is null, it either means that writeRequestHeaders wasn't called
|
||||
// or that SpdyConnection#newStream threw an IOEXception. In both cases there's
|
||||
// nothing to do here and this stream can't be reused.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.CacheRequest;
|
||||
|
||||
interface Transport {
|
||||
/**
|
||||
* Returns an output stream where the request body can be written. The
|
||||
* returned stream will of one of two types:
|
||||
* <ul>
|
||||
* <li><strong>Direct.</strong> Bytes are written to the socket and
|
||||
* forgotten. This is most efficient, particularly for large request
|
||||
* bodies. The returned stream may be buffered; the caller must call
|
||||
* {@link #flushRequest} before reading the response.</li>
|
||||
* <li><strong>Buffered.</strong> Bytes are written to an in memory
|
||||
* buffer, and must be explicitly flushed with a call to {@link
|
||||
* #writeRequestBody}. This allows HTTP authorization (401, 407)
|
||||
* responses to be retransmitted transparently.</li>
|
||||
* </ul>
|
||||
*/
|
||||
// TODO: don't bother retransmitting the request body? It's quite a corner
|
||||
// case and there's uncertainty whether Firefox or Chrome do this
|
||||
OutputStream createRequestBody() throws IOException;
|
||||
|
||||
/** This should update the HTTP engine's sentRequestMillis field. */
|
||||
void writeRequestHeaders() throws IOException;
|
||||
|
||||
/**
|
||||
* Sends the request body returned by {@link #createRequestBody} to the
|
||||
* remote peer.
|
||||
*/
|
||||
void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
|
||||
|
||||
/** Flush the request body to the underlying socket. */
|
||||
void flushRequest() throws IOException;
|
||||
|
||||
/** Read response headers and update the cookie manager. */
|
||||
ResponseHeaders readResponseHeaders() throws IOException;
|
||||
|
||||
// TODO: make this the content stream?
|
||||
InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
|
||||
|
||||
/** Returns true if the underlying connection can be recycled. */
|
||||
boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut,
|
||||
InputStream responseBodyIn);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.okhttp.internal.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.CacheRequest;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||
|
||||
/** An HTTP message body terminated by the end of the underlying stream. */
|
||||
final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
|
||||
private boolean inputExhausted;
|
||||
|
||||
UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine)
|
||||
throws IOException {
|
||||
super(is, httpEngine, cacheRequest);
|
||||
}
|
||||
|
||||
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
checkOffsetAndCount(buffer.length, offset, count);
|
||||
checkNotClosed();
|
||||
if (in == null || inputExhausted) {
|
||||
return -1;
|
||||
}
|
||||
int read = in.read(buffer, offset, count);
|
||||
if (read == -1) {
|
||||
inputExhausted = true;
|
||||
endOfInput(false);
|
||||
return -1;
|
||||
}
|
||||
cacheWrite(buffer, offset, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override public int available() throws IOException {
|
||||
checkNotClosed();
|
||||
return in == null ? 0 : in.available();
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (!inputExhausted) {
|
||||
unexpectedEndOfInput();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.spdy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Listener to be notified when a connected peer creates a new stream. */
|
||||
public interface IncomingStreamHandler {
|
||||
IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
|
||||
@Override public void receive(SpdyStream stream) throws IOException {
|
||||
stream.close(SpdyStream.RST_REFUSED_STREAM);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a new stream from this connection's peer. Implementations should
|
||||
* respond by either {@link SpdyStream#reply replying to the stream} or
|
||||
* {@link SpdyStream#close closing it}. This response does not need to be
|
||||
* synchronous.
|
||||
*/
|
||||
void receive(SpdyStream stream) throws IOException;
|
||||
}
|
71
framework/src/com/squareup/okhttp/internal/spdy/Ping.java
Normal file
71
framework/src/com/squareup/okhttp/internal/spdy/Ping.java
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.spdy;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A locally-originated ping.
|
||||
*/
|
||||
public final class Ping {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private long sent = -1;
|
||||
private long received = -1;
|
||||
|
||||
Ping() {
|
||||
}
|
||||
|
||||
void send() {
|
||||
if (sent != -1) throw new IllegalStateException();
|
||||
sent = System.nanoTime();
|
||||
}
|
||||
|
||||
void receive() {
|
||||
if (received != -1 || sent == -1) throw new IllegalStateException();
|
||||
received = System.nanoTime();
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (received != -1 || sent == -1) throw new IllegalStateException();
|
||||
received = sent - 1;
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the round trip time for this ping in nanoseconds, waiting for the
|
||||
* response to arrive if necessary. Returns -1 if the response was
|
||||
* cancelled.
|
||||
*/
|
||||
public long roundTripTime() throws InterruptedException {
|
||||
latch.await();
|
||||
return received - sent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the round trip time for this ping in nanoseconds, or -1 if the
|
||||
* response was cancelled, or -2 if the timeout elapsed before the round
|
||||
* trip completed.
|
||||
*/
|
||||
public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
if (latch.await(timeout, unit)) {
|
||||
return received - sent;
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
174
framework/src/com/squareup/okhttp/internal/spdy/Settings.java
Normal file
174
framework/src/com/squareup/okhttp/internal/spdy/Settings.java
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.spdy;
|
||||
|
||||
final class Settings {
|
||||
/**
|
||||
* From the spdy/3 spec, the default initial window size for all streams is
|
||||
* 64 KiB. (Chrome 25 uses 10 MiB).
|
||||
*/
|
||||
static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024;
|
||||
|
||||
/** Peer request to clear durable settings. */
|
||||
static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
|
||||
|
||||
/** Sent by servers only. The peer requests this setting persisted for future connections. */
|
||||
static final int PERSIST_VALUE = 0x1;
|
||||
/** Sent by clients only. The client is reminding the server of a persisted value. */
|
||||
static final int PERSISTED = 0x2;
|
||||
|
||||
/** Sender's estimate of max incoming kbps. */
|
||||
static final int UPLOAD_BANDWIDTH = 0x1;
|
||||
/** Sender's estimate of max outgoing kbps. */
|
||||
static final int DOWNLOAD_BANDWIDTH = 0x2;
|
||||
/** Sender's estimate of milliseconds between sending a request and receiving a response. */
|
||||
static final int ROUND_TRIP_TIME = 0x3;
|
||||
/** Sender's maximum number of concurrent streams. */
|
||||
static final int MAX_CONCURRENT_STREAMS = 0x4;
|
||||
/** Current CWND in Packets. */
|
||||
static final int CURRENT_CWND = 0x5;
|
||||
/** Retransmission rate. Percentage */
|
||||
static final int DOWNLOAD_RETRANS_RATE = 0x6;
|
||||
/** Window size in bytes. */
|
||||
static final int INITIAL_WINDOW_SIZE = 0x7;
|
||||
/** Window size in bytes. */
|
||||
static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8;
|
||||
/** Total number of settings. */
|
||||
static final int COUNT = 0x9;
|
||||
|
||||
/** Bitfield of which flags that values. */
|
||||
private int set;
|
||||
|
||||
/** Bitfield of flags that have {@link #PERSIST_VALUE}. */
|
||||
private int persistValue;
|
||||
|
||||
/** Bitfield of flags that have {@link #PERSISTED}. */
|
||||
private int persisted;
|
||||
|
||||
/** Flag values. */
|
||||
private final int[] values = new int[COUNT];
|
||||
|
||||
void set(int id, int idFlags, int value) {
|
||||
if (id >= values.length) {
|
||||
return; // Discard unknown settings.
|
||||
}
|
||||
|
||||
int bit = 1 << id;
|
||||
set |= bit;
|
||||
if ((idFlags & PERSIST_VALUE) != 0) {
|
||||
persistValue |= bit;
|
||||
} else {
|
||||
persistValue &= ~bit;
|
||||
}
|
||||
if ((idFlags & PERSISTED) != 0) {
|
||||
persisted |= bit;
|
||||
} else {
|
||||
persisted &= ~bit;
|
||||
}
|
||||
|
||||
values[id] = value;
|
||||
}
|
||||
|
||||
/** Returns true if a value has been assigned for the setting {@code id}. */
|
||||
boolean isSet(int id) {
|
||||
int bit = 1 << id;
|
||||
return (set & bit) != 0;
|
||||
}
|
||||
|
||||
/** Returns the value for the setting {@code id}, or 0 if unset. */
|
||||
int get(int id) {
|
||||
return values[id];
|
||||
}
|
||||
|
||||
/** Returns the flags for the setting {@code id}, or 0 if unset. */
|
||||
int flags(int id) {
|
||||
int result = 0;
|
||||
if (isPersisted(id)) result |= Settings.PERSISTED;
|
||||
if (persistValue(id)) result |= Settings.PERSIST_VALUE;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the number of settings that have values assigned. */
|
||||
int size() {
|
||||
return Integer.bitCount(set);
|
||||
}
|
||||
|
||||
int getUploadBandwidth(int defaultValue) {
|
||||
int bit = 1 << UPLOAD_BANDWIDTH;
|
||||
return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
|
||||
}
|
||||
|
||||
int getDownloadBandwidth(int defaultValue) {
|
||||
int bit = 1 << DOWNLOAD_BANDWIDTH;
|
||||
return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
|
||||
}
|
||||
|
||||
int getRoundTripTime(int defaultValue) {
|
||||
int bit = 1 << ROUND_TRIP_TIME;
|
||||
return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
|
||||
}
|
||||
|
||||
int getMaxConcurrentStreams(int defaultValue) {
|
||||
int bit = 1 << MAX_CONCURRENT_STREAMS;
|
||||
return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
|
||||
}
|
||||
|
||||
int getCurrentCwnd(int defaultValue) {
|
||||
int bit = 1 << CURRENT_CWND;
|
||||
return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
|
||||
}
|
||||
|
||||
int getDownloadRetransRate(int defaultValue) {
|
||||
int bit = 1 << DOWNLOAD_RETRANS_RATE;
|
||||
return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
|
||||
}
|
||||
|
||||
int getInitialWindowSize(int defaultValue) {
|
||||
int bit = 1 << INITIAL_WINDOW_SIZE;
|
||||
return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
|
||||
}
|
||||
|
||||
int getClientCertificateVectorSize(int defaultValue) {
|
||||
int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
|
||||
return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user agent should use this setting in future SPDY
|
||||
* connections to the same host.
|
||||
*/
|
||||
boolean persistValue(int id) {
|
||||
int bit = 1 << id;
|
||||
return (persistValue & bit) != 0;
|
||||
}
|
||||
|
||||
/** Returns true if this setting was persisted. */
|
||||
boolean isPersisted(int id) {
|
||||
int bit = 1 << id;
|
||||
return (persisted & bit) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes {@code other} into this. If any setting is populated by this and
|
||||
* {@code other}, the value and flags from {@code other} will be kept.
|
||||
*/
|
||||
void merge(Settings other) {
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
if (!other.isSet(i)) continue;
|
||||
set(i, other.flags(i), other.get(i));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,579 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.spdy;
|
||||
|
||||
import com.squareup.okhttp.internal.NamedRunnable;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.util.concurrent.Executors.defaultThreadFactory;
|
||||
|
||||
/**
|
||||
* A socket connection to a remote peer. A connection hosts streams which can
|
||||
* send and receive data.
|
||||
*
|
||||
* <p>Many methods in this API are <strong>synchronous:</strong> the call is
|
||||
* completed before the method returns. This is typical for Java but atypical
|
||||
* for SPDY. This is motivated by exception transparency: an IOException that
|
||||
* was triggered by a certain caller can be caught and handled by that caller.
|
||||
*/
|
||||
public final class SpdyConnection implements Closeable {
|
||||
|
||||
// Internal state of this connection is guarded by 'this'. No blocking
|
||||
// operations may be performed while holding this lock!
|
||||
//
|
||||
// Socket writes are guarded by spdyWriter.
|
||||
//
|
||||
// Socket reads are unguarded but are only made by the reader thread.
|
||||
//
|
||||
// Certain operations (like SYN_STREAM) need to synchronize on both the
|
||||
// spdyWriter (to do blocking I/O) and this (to create streams). Such
|
||||
// operations must synchronize on 'this' last. This ensures that we never
|
||||
// wait for a blocking operation while holding 'this'.
|
||||
|
||||
static final int FLAG_FIN = 0x1;
|
||||
static final int FLAG_UNIDIRECTIONAL = 0x2;
|
||||
|
||||
static final int TYPE_DATA = 0x0;
|
||||
static final int TYPE_SYN_STREAM = 0x1;
|
||||
static final int TYPE_SYN_REPLY = 0x2;
|
||||
static final int TYPE_RST_STREAM = 0x3;
|
||||
static final int TYPE_SETTINGS = 0x4;
|
||||
static final int TYPE_NOOP = 0x5;
|
||||
static final int TYPE_PING = 0x6;
|
||||
static final int TYPE_GOAWAY = 0x7;
|
||||
static final int TYPE_HEADERS = 0x8;
|
||||
static final int TYPE_WINDOW_UPDATE = 0x9;
|
||||
static final int TYPE_CREDENTIAL = 0x10;
|
||||
static final int VERSION = 3;
|
||||
|
||||
static final int GOAWAY_OK = 0;
|
||||
static final int GOAWAY_PROTOCOL_ERROR = 1;
|
||||
static final int GOAWAY_INTERNAL_ERROR = 2;
|
||||
|
||||
private static final ExecutorService executor =
|
||||
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>(), defaultThreadFactory());
|
||||
|
||||
/** True if this peer initiated the connection. */
|
||||
final boolean client;
|
||||
|
||||
/**
|
||||
* User code to run in response to an incoming stream. Callbacks must not be
|
||||
* run on the callback executor.
|
||||
*/
|
||||
private final IncomingStreamHandler handler;
|
||||
private final SpdyReader spdyReader;
|
||||
private final SpdyWriter spdyWriter;
|
||||
|
||||
private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
|
||||
private final String hostName;
|
||||
private int lastGoodStreamId;
|
||||
private int nextStreamId;
|
||||
private boolean shutdown;
|
||||
private long idleStartTimeNs = System.nanoTime();
|
||||
|
||||
/** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
|
||||
private Map<Integer, Ping> pings;
|
||||
private int nextPingId;
|
||||
|
||||
/** Lazily-created settings for this connection. */
|
||||
Settings settings;
|
||||
|
||||
private SpdyConnection(Builder builder) {
|
||||
client = builder.client;
|
||||
handler = builder.handler;
|
||||
spdyReader = new SpdyReader(builder.in);
|
||||
spdyWriter = new SpdyWriter(builder.out);
|
||||
nextStreamId = builder.client ? 1 : 2;
|
||||
nextPingId = builder.client ? 1 : 2;
|
||||
|
||||
hostName = builder.hostName;
|
||||
|
||||
new Thread(new Reader(), "Spdy Reader " + hostName).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of {@link SpdyStream#isOpen() open streams} on this
|
||||
* connection.
|
||||
*/
|
||||
public synchronized int openStreamCount() {
|
||||
return streams.size();
|
||||
}
|
||||
|
||||
private synchronized SpdyStream getStream(int id) {
|
||||
return streams.get(id);
|
||||
}
|
||||
|
||||
synchronized SpdyStream removeStream(int streamId) {
|
||||
SpdyStream stream = streams.remove(streamId);
|
||||
if (stream != null && streams.isEmpty()) {
|
||||
setIdle(true);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
private void setIdle(boolean value) {
|
||||
idleStartTimeNs = value ? System.nanoTime() : 0L;
|
||||
}
|
||||
|
||||
/** Returns true if this connection is idle. */
|
||||
public boolean isIdle() {
|
||||
return idleStartTimeNs != 0L;
|
||||
}
|
||||
|
||||
/** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
|
||||
public long getIdleStartTimeNs() {
|
||||
return idleStartTimeNs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new locally-initiated stream.
|
||||
*
|
||||
* @param out true to create an output stream that we can use to send data
|
||||
* to the remote peer. Corresponds to {@code FLAG_FIN}.
|
||||
* @param in true to create an input stream that the remote peer can use to
|
||||
* send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}.
|
||||
*/
|
||||
public SpdyStream newStream(List<String> requestHeaders, boolean out, boolean in)
|
||||
throws IOException {
|
||||
int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
|
||||
int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream?
|
||||
int priority = 0; // TODO: permit the caller to specify a priority?
|
||||
int slot = 0; // TODO: permit the caller to specify a slot?
|
||||
SpdyStream stream;
|
||||
int streamId;
|
||||
|
||||
synchronized (spdyWriter) {
|
||||
synchronized (this) {
|
||||
if (shutdown) {
|
||||
throw new IOException("shutdown");
|
||||
}
|
||||
streamId = nextStreamId;
|
||||
nextStreamId += 2;
|
||||
stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, settings);
|
||||
if (stream.isOpen()) {
|
||||
streams.put(streamId, stream);
|
||||
setIdle(false);
|
||||
}
|
||||
}
|
||||
|
||||
spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, requestHeaders);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
void writeSynReply(int streamId, int flags, List<String> alternating) throws IOException {
|
||||
spdyWriter.synReply(flags, streamId, alternating);
|
||||
}
|
||||
|
||||
/** Writes a complete data frame. */
|
||||
void writeFrame(byte[] bytes, int offset, int length) throws IOException {
|
||||
synchronized (spdyWriter) {
|
||||
spdyWriter.out.write(bytes, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
void writeSynResetLater(final int streamId, final int statusCode) {
|
||||
executor.submit(
|
||||
new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
|
||||
@Override public void execute() {
|
||||
try {
|
||||
writeSynReset(streamId, statusCode);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void writeSynReset(int streamId, int statusCode) throws IOException {
|
||||
spdyWriter.rstStream(streamId, statusCode);
|
||||
}
|
||||
|
||||
void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
|
||||
executor.submit(
|
||||
new NamedRunnable(String.format("Spdy Writer %s stream %d", hostName, streamId)) {
|
||||
@Override public void execute() {
|
||||
try {
|
||||
writeWindowUpdate(streamId, deltaWindowSize);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
|
||||
spdyWriter.windowUpdate(streamId, deltaWindowSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping frame to the peer. Use the returned object to await the
|
||||
* ping's response and observe its round trip time.
|
||||
*/
|
||||
public Ping ping() throws IOException {
|
||||
Ping ping = new Ping();
|
||||
int pingId;
|
||||
synchronized (this) {
|
||||
if (shutdown) {
|
||||
throw new IOException("shutdown");
|
||||
}
|
||||
pingId = nextPingId;
|
||||
nextPingId += 2;
|
||||
if (pings == null) pings = new HashMap<Integer, Ping>();
|
||||
pings.put(pingId, ping);
|
||||
}
|
||||
writePing(pingId, ping);
|
||||
return ping;
|
||||
}
|
||||
|
||||
private void writePingLater(final int streamId, final Ping ping) {
|
||||
executor.submit(new NamedRunnable(String.format("Spdy Writer %s ping %d", hostName, streamId)) {
|
||||
@Override public void execute() {
|
||||
try {
|
||||
writePing(streamId, ping);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void writePing(int id, Ping ping) throws IOException {
|
||||
synchronized (spdyWriter) {
|
||||
// Observe the sent time immediately before performing I/O.
|
||||
if (ping != null) ping.send();
|
||||
spdyWriter.ping(0, id);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized Ping removePing(int id) {
|
||||
return pings != null ? pings.remove(id) : null;
|
||||
}
|
||||
|
||||
/** Sends a noop frame to the peer. */
|
||||
public void noop() throws IOException {
|
||||
spdyWriter.noop();
|
||||
}
|
||||
|
||||
public void flush() throws IOException {
|
||||
synchronized (spdyWriter) {
|
||||
spdyWriter.out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Degrades this connection such that new streams can neither be created
|
||||
* locally, nor accepted from the remote peer. Existing streams are not
|
||||
* impacted. This is intended to permit an endpoint to gracefully stop
|
||||
* accepting new requests without harming previously established streams.
|
||||
*
|
||||
* @param statusCode one of {@link #GOAWAY_OK}, {@link
|
||||
* #GOAWAY_INTERNAL_ERROR} or {@link #GOAWAY_PROTOCOL_ERROR}.
|
||||
*/
|
||||
public void shutdown(int statusCode) throws IOException {
|
||||
synchronized (spdyWriter) {
|
||||
int lastGoodStreamId;
|
||||
synchronized (this) {
|
||||
if (shutdown) {
|
||||
return;
|
||||
}
|
||||
shutdown = true;
|
||||
lastGoodStreamId = this.lastGoodStreamId;
|
||||
}
|
||||
spdyWriter.goAway(0, lastGoodStreamId, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this connection. This cancels all open streams and unanswered
|
||||
* pings. It closes the underlying input and output streams and shuts down
|
||||
* internal executor services.
|
||||
*/
|
||||
@Override public void close() throws IOException {
|
||||
close(GOAWAY_OK, SpdyStream.RST_CANCEL);
|
||||
}
|
||||
|
||||
private void close(int shutdownStatusCode, int rstStatusCode) throws IOException {
|
||||
assert (!Thread.holdsLock(this));
|
||||
IOException thrown = null;
|
||||
try {
|
||||
shutdown(shutdownStatusCode);
|
||||
} catch (IOException e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
SpdyStream[] streamsToClose = null;
|
||||
Ping[] pingsToCancel = null;
|
||||
synchronized (this) {
|
||||
if (!streams.isEmpty()) {
|
||||
streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
|
||||
streams.clear();
|
||||
setIdle(false);
|
||||
}
|
||||
if (pings != null) {
|
||||
pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
|
||||
pings = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (streamsToClose != null) {
|
||||
for (SpdyStream stream : streamsToClose) {
|
||||
try {
|
||||
stream.close(rstStatusCode);
|
||||
} catch (IOException e) {
|
||||
if (thrown != null) thrown = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pingsToCancel != null) {
|
||||
for (Ping ping : pingsToCancel) {
|
||||
ping.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
spdyReader.close();
|
||||
} catch (IOException e) {
|
||||
thrown = e;
|
||||
}
|
||||
try {
|
||||
spdyWriter.close();
|
||||
} catch (IOException e) {
|
||||
if (thrown == null) thrown = e;
|
||||
}
|
||||
|
||||
if (thrown != null) throw thrown;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String hostName;
|
||||
private InputStream in;
|
||||
private OutputStream out;
|
||||
private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
|
||||
public boolean client;
|
||||
|
||||
public Builder(boolean client, Socket socket) throws IOException {
|
||||
this("", client, socket.getInputStream(), socket.getOutputStream());
|
||||
}
|
||||
|
||||
public Builder(boolean client, InputStream in, OutputStream out) {
|
||||
this("", client, in, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param client true if this peer initiated the connection; false if
|
||||
* this peer accepted the connection.
|
||||
*/
|
||||
public Builder(String hostName, boolean client, Socket socket) throws IOException {
|
||||
this(hostName, client, socket.getInputStream(), socket.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param client true if this peer initiated the connection; false if this
|
||||
* peer accepted the connection.
|
||||
*/
|
||||
public Builder(String hostName, boolean client, InputStream in, OutputStream out) {
|
||||
this.hostName = hostName;
|
||||
this.client = client;
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public Builder handler(IncomingStreamHandler handler) {
|
||||
this.handler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpdyConnection build() {
|
||||
return new SpdyConnection(this);
|
||||
}
|
||||
}
|
||||
|
||||
private class Reader implements Runnable, SpdyReader.Handler {
|
||||
@Override public void run() {
|
||||
int shutdownStatusCode = GOAWAY_INTERNAL_ERROR;
|
||||
int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR;
|
||||
try {
|
||||
while (spdyReader.nextFrame(this)) {
|
||||
}
|
||||
shutdownStatusCode = GOAWAY_OK;
|
||||
rstStatusCode = SpdyStream.RST_CANCEL;
|
||||
} catch (IOException e) {
|
||||
shutdownStatusCode = GOAWAY_PROTOCOL_ERROR;
|
||||
rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR;
|
||||
} finally {
|
||||
try {
|
||||
close(shutdownStatusCode, rstStatusCode);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void data(int flags, int streamId, InputStream in, int length)
|
||||
throws IOException {
|
||||
SpdyStream dataStream = getStream(streamId);
|
||||
if (dataStream == null) {
|
||||
writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
|
||||
Util.skipByReading(in, length);
|
||||
return;
|
||||
}
|
||||
dataStream.receiveData(in, length);
|
||||
if ((flags & SpdyConnection.FLAG_FIN) != 0) {
|
||||
dataStream.receiveFin();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
|
||||
List<String> nameValueBlock) {
|
||||
final SpdyStream synStream;
|
||||
final SpdyStream previous;
|
||||
synchronized (SpdyConnection.this) {
|
||||
synStream =
|
||||
new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock,
|
||||
settings);
|
||||
if (shutdown) {
|
||||
return;
|
||||
}
|
||||
lastGoodStreamId = streamId;
|
||||
previous = streams.put(streamId, synStream);
|
||||
}
|
||||
if (previous != null) {
|
||||
previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
|
||||
removeStream(streamId);
|
||||
return;
|
||||
}
|
||||
|
||||
executor.submit(
|
||||
new NamedRunnable(String.format("Callback %s stream %d", hostName, streamId)) {
|
||||
@Override public void execute() {
|
||||
try {
|
||||
handler.receive(synStream);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override public void synReply(int flags, int streamId, List<String> nameValueBlock)
|
||||
throws IOException {
|
||||
SpdyStream replyStream = getStream(streamId);
|
||||
if (replyStream == null) {
|
||||
writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
|
||||
return;
|
||||
}
|
||||
replyStream.receiveReply(nameValueBlock);
|
||||
if ((flags & SpdyConnection.FLAG_FIN) != 0) {
|
||||
replyStream.receiveFin();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void headers(int flags, int streamId, List<String> nameValueBlock)
|
||||
throws IOException {
|
||||
SpdyStream replyStream = getStream(streamId);
|
||||
if (replyStream != null) {
|
||||
replyStream.receiveHeaders(nameValueBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void rstStream(int flags, int streamId, int statusCode) {
|
||||
SpdyStream rstStream = removeStream(streamId);
|
||||
if (rstStream != null) {
|
||||
rstStream.receiveRstStream(statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void settings(int flags, Settings newSettings) {
|
||||
SpdyStream[] streamsToNotify = null;
|
||||
synchronized (SpdyConnection.this) {
|
||||
if (settings == null || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
|
||||
settings = newSettings;
|
||||
} else {
|
||||
settings.merge(newSettings);
|
||||
}
|
||||
if (!streams.isEmpty()) {
|
||||
streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]);
|
||||
}
|
||||
}
|
||||
if (streamsToNotify != null) {
|
||||
for (SpdyStream stream : streamsToNotify) {
|
||||
// The synchronization here is ugly. We need to synchronize on 'this' to guard
|
||||
// reads to 'settings'. We synchronize on 'stream' to guard the state change.
|
||||
// And we need to acquire the 'stream' lock first, since that may block.
|
||||
synchronized (stream) {
|
||||
synchronized (this) {
|
||||
stream.receiveSettings(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void noop() {
|
||||
}
|
||||
|
||||
@Override public void ping(int flags, int streamId) {
|
||||
if (client != (streamId % 2 == 1)) {
|
||||
// Respond to a client ping if this is a server and vice versa.
|
||||
writePingLater(streamId, null);
|
||||
} else {
|
||||
Ping ping = removePing(streamId);
|
||||
if (ping != null) {
|
||||
ping.receive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
|
||||
synchronized (SpdyConnection.this) {
|
||||
shutdown = true;
|
||||
|
||||
// Fail all streams created after the last good stream ID.
|
||||
for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
|
||||
i.hasNext(); ) {
|
||||
Map.Entry<Integer, SpdyStream> entry = i.next();
|
||||
int streamId = entry.getKey();
|
||||
if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
|
||||
entry.getValue().receiveRstStream(SpdyStream.RST_REFUSED_STREAM);
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
|
||||
SpdyStream stream = getStream(streamId);
|
||||
if (stream != null) {
|
||||
stream.receiveWindowUpdate(deltaWindowSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
318
framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
Normal file
318
framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
Normal file
@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.spdy;
|
||||
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
/** Read spdy/3 frames. */
|
||||
final class SpdyReader implements Closeable {
|
||||
static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
|
||||
+ "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
|
||||
+ "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
|
||||
+ "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
|
||||
+ "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
|
||||
+ "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
|
||||
+ "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
|
||||
+ "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
|
||||
+ "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
|
||||
+ "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
|
||||
+ "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
|
||||
+ "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
|
||||
+ "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
|
||||
+ "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
|
||||
+ "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
|
||||
+ "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
|
||||
+ "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
|
||||
+ "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
|
||||
+ "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
|
||||
+ "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
|
||||
+ "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
|
||||
+ "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
|
||||
+ "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
|
||||
+ "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
|
||||
+ "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
|
||||
+ "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
|
||||
+ "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
|
||||
+ "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
|
||||
+ "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
|
||||
+ "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
|
||||
+ "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
|
||||
+ ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
|
||||
+ "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8);
|
||||
|
||||
private final DataInputStream in;
|
||||
private final DataInputStream nameValueBlockIn;
|
||||
private int compressedLimit;
|
||||
|
||||
SpdyReader(InputStream in) {
|
||||
this.in = new DataInputStream(in);
|
||||
this.nameValueBlockIn = newNameValueBlockStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the next frame to {@code handler}. Returns true unless there are no
|
||||
* more frames on the stream.
|
||||
*/
|
||||
public boolean nextFrame(Handler handler) throws IOException {
|
||||
int w1;
|
||||
try {
|
||||
w1 = in.readInt();
|
||||
} catch (IOException e) {
|
||||
return false; // This might be a normal socket close.
|
||||
}
|
||||
int w2 = in.readInt();
|
||||
|
||||
boolean control = (w1 & 0x80000000) != 0;
|
||||
int flags = (w2 & 0xff000000) >>> 24;
|
||||
int length = (w2 & 0xffffff);
|
||||
|
||||
if (control) {
|
||||
int version = (w1 & 0x7fff0000) >>> 16;
|
||||
int type = (w1 & 0xffff);
|
||||
|
||||
if (version != 3) {
|
||||
throw new ProtocolException("version != 3: " + version);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case SpdyConnection.TYPE_SYN_STREAM:
|
||||
readSynStream(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_SYN_REPLY:
|
||||
readSynReply(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_RST_STREAM:
|
||||
readRstStream(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_SETTINGS:
|
||||
readSettings(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_NOOP:
|
||||
if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
|
||||
handler.noop();
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_PING:
|
||||
readPing(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_GOAWAY:
|
||||
readGoAway(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_HEADERS:
|
||||
readHeaders(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_WINDOW_UPDATE:
|
||||
readWindowUpdate(handler, flags, length);
|
||||
return true;
|
||||
|
||||
case SpdyConnection.TYPE_CREDENTIAL:
|
||||
Util.skipByReading(in, length);
|
||||
throw new UnsupportedOperationException("TODO"); // TODO: implement
|
||||
|
||||
default:
|
||||
throw new IOException("Unexpected frame");
|
||||
}
|
||||
} else {
|
||||
int streamId = w1 & 0x7fffffff;
|
||||
handler.data(flags, streamId, in, length);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void readSynStream(Handler handler, int flags, int length) throws IOException {
|
||||
int w1 = in.readInt();
|
||||
int w2 = in.readInt();
|
||||
int s3 = in.readShort();
|
||||
int streamId = w1 & 0x7fffffff;
|
||||
int associatedStreamId = w2 & 0x7fffffff;
|
||||
int priority = (s3 & 0xe000) >>> 13;
|
||||
int slot = s3 & 0xff;
|
||||
List<String> nameValueBlock = readNameValueBlock(length - 10);
|
||||
handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock);
|
||||
}
|
||||
|
||||
private void readSynReply(Handler handler, int flags, int length) throws IOException {
|
||||
int w1 = in.readInt();
|
||||
int streamId = w1 & 0x7fffffff;
|
||||
List<String> nameValueBlock = readNameValueBlock(length - 4);
|
||||
handler.synReply(flags, streamId, nameValueBlock);
|
||||
}
|
||||
|
||||
private void readRstStream(Handler handler, int flags, int length) throws IOException {
|
||||
if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length);
|
||||
int streamId = in.readInt() & 0x7fffffff;
|
||||
int statusCode = in.readInt();
|
||||
handler.rstStream(flags, streamId, statusCode);
|
||||
}
|
||||
|
||||
private void readHeaders(Handler handler, int flags, int length) throws IOException {
|
||||
int w1 = in.readInt();
|
||||
int streamId = w1 & 0x7fffffff;
|
||||
List<String> nameValueBlock = readNameValueBlock(length - 4);
|
||||
handler.headers(flags, streamId, nameValueBlock);
|
||||
}
|
||||
|
||||
private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
|
||||
if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length);
|
||||
int w1 = in.readInt();
|
||||
int w2 = in.readInt();
|
||||
int streamId = w1 & 0x7fffffff;
|
||||
int deltaWindowSize = w2 & 0x7fffffff;
|
||||
handler.windowUpdate(flags, streamId, deltaWindowSize);
|
||||
}
|
||||
|
||||
private DataInputStream newNameValueBlockStream() {
|
||||
// Limit the inflater input stream to only those bytes in the Name/Value block.
|
||||
final InputStream throttleStream = new InputStream() {
|
||||
@Override public int read() throws IOException {
|
||||
return Util.readSingleByte(this);
|
||||
}
|
||||
|
||||
@Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
|
||||
byteCount = Math.min(byteCount, compressedLimit);
|
||||
int consumed = in.read(buffer, offset, byteCount);
|
||||
compressedLimit -= consumed;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
in.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass inflater to install a dictionary when it's needed.
|
||||
Inflater inflater = new Inflater() {
|
||||
@Override
|
||||
public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
|
||||
int result = super.inflate(buffer, offset, count);
|
||||
if (result == 0 && needsDictionary()) {
|
||||
setDictionary(DICTIONARY);
|
||||
result = super.inflate(buffer, offset, count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
|
||||
}
|
||||
|
||||
private List<String> readNameValueBlock(int length) throws IOException {
|
||||
this.compressedLimit += length;
|
||||
try {
|
||||
int numberOfPairs = nameValueBlockIn.readInt();
|
||||
if (numberOfPairs < 0) {
|
||||
Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs);
|
||||
throw ioException("numberOfPairs < 0");
|
||||
}
|
||||
List<String> entries = new ArrayList<String>(numberOfPairs * 2);
|
||||
for (int i = 0; i < numberOfPairs; i++) {
|
||||
String name = readString();
|
||||
String values = readString();
|
||||
if (name.length() == 0) throw ioException("name.length == 0");
|
||||
if (values.length() == 0) throw ioException("values.length == 0");
|
||||
entries.add(name);
|
||||
entries.add(values);
|
||||
}
|
||||
|
||||
if (compressedLimit != 0) {
|
||||
Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit);
|
||||
}
|
||||
|
||||
return entries;
|
||||
} catch (DataFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readString() throws DataFormatException, IOException {
|
||||
int length = nameValueBlockIn.readInt();
|
||||
byte[] bytes = new byte[length];
|
||||
Util.readFully(nameValueBlockIn, bytes);
|
||||
return new String(bytes, 0, length, "UTF-8");
|
||||
}
|
||||
|
||||
private void readPing(Handler handler, int flags, int length) throws IOException {
|
||||
if (length != 4) throw ioException("TYPE_PING length: %d != 4", length);
|
||||
int id = in.readInt();
|
||||
handler.ping(flags, id);
|
||||
}
|
||||
|
||||
private void readGoAway(Handler handler, int flags, int length) throws IOException {
|
||||
if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length);
|
||||
int lastGoodStreamId = in.readInt() & 0x7fffffff;
|
||||
int statusCode = in.readInt();
|
||||
handler.goAway(flags, lastGoodStreamId, statusCode);
|
||||
}
|
||||
|
||||
private void readSettings(Handler handler, int flags, int length) throws IOException {
|
||||
int numberOfEntries = in.readInt();
|
||||
if (length != 4 + 8 * numberOfEntries) {
|
||||
throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries);
|
||||
}
|
||||
Settings settings = new Settings();
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
int w1 = in.readInt();
|
||||
int value = in.readInt();
|
||||
int idFlags = (w1 & 0xff000000) >>> 24;
|
||||
int id = w1 & 0xffffff;
|
||||
settings.set(id, idFlags, value);
|
||||
}
|
||||
handler.settings(flags, settings);
|
||||
}
|
||||
|
||||
private static IOException ioException(String message, Object... args) throws IOException {
|
||||
throw new IOException(String.format(message, args));
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
Util.closeAll(in, nameValueBlockIn);
|
||||
}
|
||||
|
||||
public interface Handler {
|
||||
void data(int flags, int streamId, InputStream in, int length) throws IOException;
|
||||
|
||||
void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
|
||||
List<String> nameValueBlock);
|
||||
|
||||
void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
|
||||
void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
|
||||
void rstStream(int flags, int streamId, int statusCode);
|
||||
void settings(int flags, Settings settings);
|
||||
void noop();
|
||||
void ping(int flags, int streamId);
|
||||
void goAway(int flags, int lastGoodStreamId, int statusCode);
|
||||
void windowUpdate(int flags, int streamId, int deltaWindowSize);
|
||||
}
|
||||
}
|
733
framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java
Normal file
733
framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java
Normal file
@ -0,0 +1,733 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.spdy;
|
||||
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
|
||||
import static com.squareup.okhttp.internal.Util.pokeInt;
|
||||
import static java.nio.ByteOrder.BIG_ENDIAN;
|
||||
|
||||
/** A logical bidirectional stream. */
|
||||
public final class SpdyStream {
|
||||
|
||||
// Internal state is guarded by this. No long-running or potentially
|
||||
// blocking operations are performed while the lock is held.
|
||||
|
||||
private static final int DATA_FRAME_HEADER_LENGTH = 8;
|
||||
|
||||
private static final String[] STATUS_CODE_NAMES = {
|
||||
null,
|
||||
"PROTOCOL_ERROR",
|
||||
"INVALID_STREAM",
|
||||
"REFUSED_STREAM",
|
||||
"UNSUPPORTED_VERSION",
|
||||
"CANCEL",
|
||||
"INTERNAL_ERROR",
|
||||
"FLOW_CONTROL_ERROR",
|
||||
"STREAM_IN_USE",
|
||||
"STREAM_ALREADY_CLOSED",
|
||||
"INVALID_CREDENTIALS",
|
||||
"FRAME_TOO_LARGE"
|
||||
};
|
||||
|
||||
public static final int RST_PROTOCOL_ERROR = 1;
|
||||
public static final int RST_INVALID_STREAM = 2;
|
||||
public static final int RST_REFUSED_STREAM = 3;
|
||||
public static final int RST_UNSUPPORTED_VERSION = 4;
|
||||
public static final int RST_CANCEL = 5;
|
||||
public static final int RST_INTERNAL_ERROR = 6;
|
||||
public static final int RST_FLOW_CONTROL_ERROR = 7;
|
||||
public static final int RST_STREAM_IN_USE = 8;
|
||||
public static final int RST_STREAM_ALREADY_CLOSED = 9;
|
||||
public static final int RST_INVALID_CREDENTIALS = 10;
|
||||
public static final int RST_FRAME_TOO_LARGE = 11;
|
||||
|
||||
/**
|
||||
* The number of unacknowledged bytes at which the input stream will send
|
||||
* the peer a {@code WINDOW_UPDATE} frame. Must be less than this client's
|
||||
* window size, otherwise the remote peer will stop sending data on this
|
||||
* stream. (Chrome 25 uses 5 MiB.)
|
||||
*/
|
||||
public static final int WINDOW_UPDATE_THRESHOLD = Settings.DEFAULT_INITIAL_WINDOW_SIZE / 2;
|
||||
|
||||
private final int id;
|
||||
private final SpdyConnection connection;
|
||||
private final int priority;
|
||||
private final int slot;
|
||||
private long readTimeoutMillis = 0;
|
||||
private int writeWindowSize;
|
||||
|
||||
/** Headers sent by the stream initiator. Immutable and non null. */
|
||||
private final List<String> requestHeaders;
|
||||
|
||||
/** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
|
||||
private List<String> responseHeaders;
|
||||
|
||||
private final SpdyDataInputStream in = new SpdyDataInputStream();
|
||||
private final SpdyDataOutputStream out = new SpdyDataOutputStream();
|
||||
|
||||
/**
|
||||
* The reason why this stream was abnormally closed. If there are multiple
|
||||
* reasons to abnormally close this stream (such as both peers closing it
|
||||
* near-simultaneously) then this is the first reason known to this peer.
|
||||
*/
|
||||
private int rstStatusCode = -1;
|
||||
|
||||
SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
|
||||
List<String> requestHeaders, Settings settings) {
|
||||
if (connection == null) throw new NullPointerException("connection == null");
|
||||
if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
|
||||
this.id = id;
|
||||
this.connection = connection;
|
||||
this.priority = priority;
|
||||
this.slot = slot;
|
||||
this.requestHeaders = requestHeaders;
|
||||
|
||||
if (isLocallyInitiated()) {
|
||||
// I am the sender
|
||||
in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
|
||||
out.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
|
||||
} else {
|
||||
// I am the receiver
|
||||
in.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
|
||||
out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
|
||||
}
|
||||
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this stream is open. A stream is open until either:
|
||||
* <ul>
|
||||
* <li>A {@code SYN_RESET} frame abnormally terminates the stream.
|
||||
* <li>Both input and output streams have transmitted all data and
|
||||
* headers.
|
||||
* </ul>
|
||||
* Note that the input stream may continue to yield data even after a stream
|
||||
* reports itself as not open. This is because input data is buffered.
|
||||
*/
|
||||
public synchronized boolean isOpen() {
|
||||
if (rstStatusCode != -1) {
|
||||
return false;
|
||||
}
|
||||
if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns true if this stream was created by this peer. */
|
||||
public boolean isLocallyInitiated() {
|
||||
boolean streamIsClient = (id % 2 == 1);
|
||||
return connection.client == streamIsClient;
|
||||
}
|
||||
|
||||
public SpdyConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public List<String> getRequestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stream's response headers, blocking if necessary if they
|
||||
* have not been received yet.
|
||||
*/
|
||||
public synchronized List<String> getResponseHeaders() throws IOException {
|
||||
try {
|
||||
while (responseHeaders == null && rstStatusCode == -1) {
|
||||
wait();
|
||||
}
|
||||
if (responseHeaders != null) {
|
||||
return responseHeaders;
|
||||
}
|
||||
throw new IOException("stream was reset: " + rstStatusString());
|
||||
} catch (InterruptedException e) {
|
||||
InterruptedIOException rethrow = new InterruptedIOException();
|
||||
rethrow.initCause(e);
|
||||
throw rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reason why this stream was closed, or -1 if it closed
|
||||
* normally or has not yet been closed. Valid reasons are {@link
|
||||
* #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link
|
||||
* #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link
|
||||
* #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link
|
||||
* #RST_FLOW_CONTROL_ERROR}.
|
||||
*/
|
||||
public synchronized int getRstStatusCode() {
|
||||
return rstStatusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a reply to an incoming stream.
|
||||
*
|
||||
* @param out true to create an output stream that we can use to send data
|
||||
* to the remote peer. Corresponds to {@code FLAG_FIN}.
|
||||
*/
|
||||
public void reply(List<String> responseHeaders, boolean out) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
int flags = 0;
|
||||
synchronized (this) {
|
||||
if (responseHeaders == null) {
|
||||
throw new NullPointerException("responseHeaders == null");
|
||||
}
|
||||
if (isLocallyInitiated()) {
|
||||
throw new IllegalStateException("cannot reply to a locally initiated stream");
|
||||
}
|
||||
if (this.responseHeaders != null) {
|
||||
throw new IllegalStateException("reply already sent");
|
||||
}
|
||||
this.responseHeaders = responseHeaders;
|
||||
if (!out) {
|
||||
this.out.finished = true;
|
||||
flags |= SpdyConnection.FLAG_FIN;
|
||||
}
|
||||
}
|
||||
connection.writeSynReply(id, flags, responseHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum time to wait on input stream reads before failing with a
|
||||
* {@code SocketTimeoutException}, or {@code 0} to wait indefinitely.
|
||||
*/
|
||||
public void setReadTimeout(long readTimeoutMillis) {
|
||||
this.readTimeoutMillis = readTimeoutMillis;
|
||||
}
|
||||
|
||||
public long getReadTimeoutMillis() {
|
||||
return readTimeoutMillis;
|
||||
}
|
||||
|
||||
/** Returns an input stream that can be used to read data from the peer. */
|
||||
public InputStream getInputStream() {
|
||||
return in;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an output stream that can be used to write data to the peer.
|
||||
*
|
||||
* @throws IllegalStateException if this stream was initiated by the peer
|
||||
* and a {@link #reply} has not yet been sent.
|
||||
*/
|
||||
public OutputStream getOutputStream() {
|
||||
synchronized (this) {
|
||||
if (responseHeaders == null && !isLocallyInitiated()) {
|
||||
throw new IllegalStateException("reply before requesting the output stream");
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abnormally terminate this stream. This blocks until the {@code RST_STREAM}
|
||||
* frame has been transmitted.
|
||||
*/
|
||||
public void close(int rstStatusCode) throws IOException {
|
||||
if (!closeInternal(rstStatusCode)) {
|
||||
return; // Already closed.
|
||||
}
|
||||
connection.writeSynReset(id, rstStatusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abnormally terminate this stream. This enqueues a {@code RST_STREAM}
|
||||
* frame and returns immediately.
|
||||
*/
|
||||
public void closeLater(int rstStatusCode) {
|
||||
if (!closeInternal(rstStatusCode)) {
|
||||
return; // Already closed.
|
||||
}
|
||||
connection.writeSynResetLater(id, rstStatusCode);
|
||||
}
|
||||
|
||||
/** Returns true if this stream was closed. */
|
||||
private boolean closeInternal(int rstStatusCode) {
|
||||
assert (!Thread.holdsLock(this));
|
||||
synchronized (this) {
|
||||
if (this.rstStatusCode != -1) {
|
||||
return false;
|
||||
}
|
||||
if (in.finished && out.finished) {
|
||||
return false;
|
||||
}
|
||||
this.rstStatusCode = rstStatusCode;
|
||||
notifyAll();
|
||||
}
|
||||
connection.removeStream(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void receiveReply(List<String> strings) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
boolean streamInUseError = false;
|
||||
boolean open = true;
|
||||
synchronized (this) {
|
||||
if (isLocallyInitiated() && responseHeaders == null) {
|
||||
responseHeaders = strings;
|
||||
open = isOpen();
|
||||
notifyAll();
|
||||
} else {
|
||||
streamInUseError = true;
|
||||
}
|
||||
}
|
||||
if (streamInUseError) {
|
||||
closeLater(SpdyStream.RST_STREAM_IN_USE);
|
||||
} else if (!open) {
|
||||
connection.removeStream(id);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveHeaders(List<String> headers) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
boolean protocolError = false;
|
||||
synchronized (this) {
|
||||
if (responseHeaders != null) {
|
||||
List<String> newHeaders = new ArrayList<String>();
|
||||
newHeaders.addAll(responseHeaders);
|
||||
newHeaders.addAll(headers);
|
||||
this.responseHeaders = newHeaders;
|
||||
} else {
|
||||
protocolError = true;
|
||||
}
|
||||
}
|
||||
if (protocolError) {
|
||||
closeLater(SpdyStream.RST_PROTOCOL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveData(InputStream in, int length) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
this.in.receive(in, length);
|
||||
}
|
||||
|
||||
void receiveFin() {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
boolean open;
|
||||
synchronized (this) {
|
||||
this.in.finished = true;
|
||||
open = isOpen();
|
||||
notifyAll();
|
||||
}
|
||||
if (!open) {
|
||||
connection.removeStream(id);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void receiveRstStream(int statusCode) {
|
||||
if (rstStatusCode == -1) {
|
||||
rstStatusCode = statusCode;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void setSettings(Settings settings) {
|
||||
assert (Thread.holdsLock(connection)); // Because 'settings' is guarded by 'connection'.
|
||||
this.writeWindowSize =
|
||||
settings != null ? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE)
|
||||
: Settings.DEFAULT_INITIAL_WINDOW_SIZE;
|
||||
}
|
||||
|
||||
void receiveSettings(Settings settings) {
|
||||
assert (Thread.holdsLock(this));
|
||||
setSettings(settings);
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized void receiveWindowUpdate(int deltaWindowSize) {
|
||||
out.unacknowledgedBytes -= deltaWindowSize;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
private String rstStatusString() {
|
||||
return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length
|
||||
? STATUS_CODE_NAMES[rstStatusCode] : Integer.toString(rstStatusCode);
|
||||
}
|
||||
|
||||
int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
int getSlot() {
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* An input stream that reads the incoming data frames of a stream. Although
|
||||
* this class uses synchronization to safely receive incoming data frames,
|
||||
* it is not intended for use by multiple readers.
|
||||
*/
|
||||
private final class SpdyDataInputStream extends InputStream {
|
||||
// Store incoming data bytes in a circular buffer. When the buffer is
|
||||
// empty, pos == -1. Otherwise pos is the first byte to read and limit
|
||||
// is the first byte to write.
|
||||
//
|
||||
// { - - - X X X X - - - }
|
||||
// ^ ^
|
||||
// pos limit
|
||||
//
|
||||
// { X X X - - - - X X X }
|
||||
// ^ ^
|
||||
// limit pos
|
||||
|
||||
private final byte[] buffer = new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE];
|
||||
|
||||
/** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */
|
||||
private int pos = -1;
|
||||
|
||||
/** the last byte to be read. Never buffer.length */
|
||||
private int limit;
|
||||
|
||||
/** True if the caller has closed this stream. */
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* True if either side has cleanly shut down this stream. We will
|
||||
* receive no more bytes beyond those already in the buffer.
|
||||
*/
|
||||
private boolean finished;
|
||||
|
||||
/**
|
||||
* The total number of bytes consumed by the application (with {@link
|
||||
* #read}), but not yet acknowledged by sending a {@code WINDOW_UPDATE}
|
||||
* frame.
|
||||
*/
|
||||
private int unacknowledgedBytes = 0;
|
||||
|
||||
@Override public int available() throws IOException {
|
||||
synchronized (SpdyStream.this) {
|
||||
checkNotClosed();
|
||||
if (pos == -1) {
|
||||
return 0;
|
||||
} else if (limit > pos) {
|
||||
return limit - pos;
|
||||
} else {
|
||||
return limit + (buffer.length - pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int read() throws IOException {
|
||||
return Util.readSingleByte(this);
|
||||
}
|
||||
|
||||
@Override public int read(byte[] b, int offset, int count) throws IOException {
|
||||
synchronized (SpdyStream.this) {
|
||||
checkOffsetAndCount(b.length, offset, count);
|
||||
waitUntilReadable();
|
||||
checkNotClosed();
|
||||
|
||||
if (pos == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int copied = 0;
|
||||
|
||||
// drain from [pos..buffer.length)
|
||||
if (limit <= pos) {
|
||||
int bytesToCopy = Math.min(count, buffer.length - pos);
|
||||
System.arraycopy(buffer, pos, b, offset, bytesToCopy);
|
||||
pos += bytesToCopy;
|
||||
copied += bytesToCopy;
|
||||
if (pos == buffer.length) {
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// drain from [pos..limit)
|
||||
if (copied < count) {
|
||||
int bytesToCopy = Math.min(limit - pos, count - copied);
|
||||
System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy);
|
||||
pos += bytesToCopy;
|
||||
copied += bytesToCopy;
|
||||
}
|
||||
|
||||
// Flow control: notify the peer that we're ready for more data!
|
||||
unacknowledgedBytes += copied;
|
||||
if (unacknowledgedBytes >= WINDOW_UPDATE_THRESHOLD) {
|
||||
connection.writeWindowUpdateLater(id, unacknowledgedBytes);
|
||||
unacknowledgedBytes = 0;
|
||||
}
|
||||
|
||||
if (pos == limit) {
|
||||
pos = -1;
|
||||
limit = 0;
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns once the input stream is either readable or finished. Throws
|
||||
* a {@link SocketTimeoutException} if the read timeout elapses before
|
||||
* that happens.
|
||||
*/
|
||||
private void waitUntilReadable() throws IOException {
|
||||
long start = 0;
|
||||
long remaining = 0;
|
||||
if (readTimeoutMillis != 0) {
|
||||
start = (System.nanoTime() / 1000000);
|
||||
remaining = readTimeoutMillis;
|
||||
}
|
||||
try {
|
||||
while (pos == -1 && !finished && !closed && rstStatusCode == -1) {
|
||||
if (readTimeoutMillis == 0) {
|
||||
SpdyStream.this.wait();
|
||||
} else if (remaining > 0) {
|
||||
SpdyStream.this.wait(remaining);
|
||||
remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
|
||||
} else {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
}
|
||||
|
||||
void receive(InputStream in, int byteCount) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
|
||||
if (byteCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int pos;
|
||||
int limit;
|
||||
int firstNewByte;
|
||||
boolean finished;
|
||||
boolean flowControlError;
|
||||
synchronized (SpdyStream.this) {
|
||||
finished = this.finished;
|
||||
pos = this.pos;
|
||||
firstNewByte = this.limit;
|
||||
limit = this.limit;
|
||||
flowControlError = byteCount > buffer.length - available();
|
||||
}
|
||||
|
||||
// If the peer sends more data than we can handle, discard it and close the connection.
|
||||
if (flowControlError) {
|
||||
Util.skipByReading(in, byteCount);
|
||||
closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Discard data received after the stream is finished. It's probably a benign race.
|
||||
if (finished) {
|
||||
Util.skipByReading(in, byteCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill the buffer without holding any locks. First fill [limit..buffer.length) if that
|
||||
// won't overwrite unread data. Then fill [limit..pos). We can't hold a lock, otherwise
|
||||
// writes will be blocked until reads complete.
|
||||
if (pos < limit) {
|
||||
int firstCopyCount = Math.min(byteCount, buffer.length - limit);
|
||||
Util.readFully(in, buffer, limit, firstCopyCount);
|
||||
limit += firstCopyCount;
|
||||
byteCount -= firstCopyCount;
|
||||
if (limit == buffer.length) {
|
||||
limit = 0;
|
||||
}
|
||||
}
|
||||
if (byteCount > 0) {
|
||||
Util.readFully(in, buffer, limit, byteCount);
|
||||
limit += byteCount;
|
||||
}
|
||||
|
||||
synchronized (SpdyStream.this) {
|
||||
// Update the new limit, and mark the position as readable if necessary.
|
||||
this.limit = limit;
|
||||
if (this.pos == -1) {
|
||||
this.pos = firstNewByte;
|
||||
SpdyStream.this.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
synchronized (SpdyStream.this) {
|
||||
closed = true;
|
||||
SpdyStream.this.notifyAll();
|
||||
}
|
||||
cancelStreamIfNecessary();
|
||||
}
|
||||
|
||||
private void checkNotClosed() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("stream closed");
|
||||
}
|
||||
if (rstStatusCode != -1) {
|
||||
throw new IOException("stream was reset: " + rstStatusString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelStreamIfNecessary() throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
boolean open;
|
||||
boolean cancel;
|
||||
synchronized (this) {
|
||||
cancel = !in.finished && in.closed && (out.finished || out.closed);
|
||||
open = isOpen();
|
||||
}
|
||||
if (cancel) {
|
||||
// RST this stream to prevent additional data from being sent. This
|
||||
// is safe because the input stream is closed (we won't use any
|
||||
// further bytes) and the output stream is either finished or closed
|
||||
// (so RSTing both streams doesn't cause harm).
|
||||
SpdyStream.this.close(RST_CANCEL);
|
||||
} else if (!open) {
|
||||
connection.removeStream(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An output stream that writes outgoing data frames of a stream. This class
|
||||
* is not thread safe.
|
||||
*/
|
||||
private final class SpdyDataOutputStream extends OutputStream {
|
||||
private final byte[] buffer = new byte[8192];
|
||||
private int pos = DATA_FRAME_HEADER_LENGTH;
|
||||
|
||||
/** True if the caller has closed this stream. */
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* True if either side has cleanly shut down this stream. We shall send
|
||||
* no more bytes.
|
||||
*/
|
||||
private boolean finished;
|
||||
|
||||
/**
|
||||
* The total number of bytes written out to the peer, but not yet
|
||||
* acknowledged with an incoming {@code WINDOW_UPDATE} frame. Writes
|
||||
* block if they cause this to exceed the {@code WINDOW_SIZE}.
|
||||
*/
|
||||
private int unacknowledgedBytes = 0;
|
||||
|
||||
@Override public void write(int b) throws IOException {
|
||||
Util.writeSingleByte(this, b);
|
||||
}
|
||||
|
||||
@Override public void write(byte[] bytes, int offset, int count) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
checkOffsetAndCount(bytes.length, offset, count);
|
||||
checkNotClosed();
|
||||
|
||||
while (count > 0) {
|
||||
if (pos == buffer.length) {
|
||||
writeFrame(false);
|
||||
}
|
||||
int bytesToCopy = Math.min(count, buffer.length - pos);
|
||||
System.arraycopy(bytes, offset, buffer, pos, bytesToCopy);
|
||||
pos += bytesToCopy;
|
||||
offset += bytesToCopy;
|
||||
count -= bytesToCopy;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void flush() throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
checkNotClosed();
|
||||
if (pos > DATA_FRAME_HEADER_LENGTH) {
|
||||
writeFrame(false);
|
||||
connection.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
synchronized (SpdyStream.this) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
writeFrame(true);
|
||||
connection.flush();
|
||||
cancelStreamIfNecessary();
|
||||
}
|
||||
|
||||
private void writeFrame(boolean last) throws IOException {
|
||||
assert (!Thread.holdsLock(SpdyStream.this));
|
||||
|
||||
int length = pos - DATA_FRAME_HEADER_LENGTH;
|
||||
synchronized (SpdyStream.this) {
|
||||
waitUntilWritable(length, last);
|
||||
unacknowledgedBytes += length;
|
||||
}
|
||||
int flags = 0;
|
||||
if (last) {
|
||||
flags |= SpdyConnection.FLAG_FIN;
|
||||
}
|
||||
pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN);
|
||||
pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN);
|
||||
connection.writeFrame(buffer, 0, pos);
|
||||
pos = DATA_FRAME_HEADER_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns once the peer is ready to receive {@code count} bytes.
|
||||
*
|
||||
* @throws IOException if the stream was finished or closed, or the
|
||||
* thread was interrupted.
|
||||
*/
|
||||
private void waitUntilWritable(int count, boolean last) throws IOException {
|
||||
try {
|
||||
while (unacknowledgedBytes + count >= writeWindowSize) {
|
||||
SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
|
||||
|
||||
// The stream may have been closed or reset while we were waiting!
|
||||
if (!last && closed) {
|
||||
throw new IOException("stream closed");
|
||||
} else if (finished) {
|
||||
throw new IOException("stream finished");
|
||||
} else if (rstStatusCode != -1) {
|
||||
throw new IOException("stream was reset: " + rstStatusString());
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNotClosed() throws IOException {
|
||||
synchronized (SpdyStream.this) {
|
||||
if (closed) {
|
||||
throw new IOException("stream closed");
|
||||
} else if (finished) {
|
||||
throw new IOException("stream finished");
|
||||
} else if (rstStatusCode != -1) {
|
||||
throw new IOException("stream was reset: " + rstStatusString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java
Normal file
176
framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.okhttp.internal.spdy;
|
||||
|
||||
import com.squareup.okhttp.internal.Platform;
|
||||
import com.squareup.okhttp.internal.Util;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
/** Write spdy/3 frames. */
|
||||
final class SpdyWriter implements Closeable {
|
||||
final DataOutputStream out;
|
||||
private final ByteArrayOutputStream nameValueBlockBuffer;
|
||||
private final DataOutputStream nameValueBlockOut;
|
||||
|
||||
SpdyWriter(OutputStream out) {
|
||||
this.out = new DataOutputStream(out);
|
||||
|
||||
Deflater deflater = new Deflater();
|
||||
deflater.setDictionary(SpdyReader.DICTIONARY);
|
||||
nameValueBlockBuffer = new ByteArrayOutputStream();
|
||||
nameValueBlockOut = new DataOutputStream(
|
||||
Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
|
||||
}
|
||||
|
||||
public synchronized void synStream(int flags, int streamId, int associatedStreamId, int priority,
|
||||
int slot, List<String> nameValueBlock) throws IOException {
|
||||
writeNameValueBlockToBuffer(nameValueBlock);
|
||||
int length = 10 + nameValueBlockBuffer.size();
|
||||
int type = SpdyConnection.TYPE_SYN_STREAM;
|
||||
|
||||
int unused = 0;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(streamId & 0x7fffffff);
|
||||
out.writeInt(associatedStreamId & 0x7fffffff);
|
||||
out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
|
||||
nameValueBlockBuffer.writeTo(out);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void synReply(int flags, int streamId, List<String> nameValueBlock)
|
||||
throws IOException {
|
||||
writeNameValueBlockToBuffer(nameValueBlock);
|
||||
int type = SpdyConnection.TYPE_SYN_REPLY;
|
||||
int length = nameValueBlockBuffer.size() + 4;
|
||||
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(streamId & 0x7fffffff);
|
||||
nameValueBlockBuffer.writeTo(out);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void headers(int flags, int streamId, List<String> nameValueBlock)
|
||||
throws IOException {
|
||||
writeNameValueBlockToBuffer(nameValueBlock);
|
||||
int type = SpdyConnection.TYPE_HEADERS;
|
||||
int length = nameValueBlockBuffer.size() + 4;
|
||||
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(streamId & 0x7fffffff);
|
||||
nameValueBlockBuffer.writeTo(out);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void rstStream(int streamId, int statusCode) throws IOException {
|
||||
int flags = 0;
|
||||
int type = SpdyConnection.TYPE_RST_STREAM;
|
||||
int length = 8;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(streamId & 0x7fffffff);
|
||||
out.writeInt(statusCode);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void data(int flags, int streamId, byte[] data) throws IOException {
|
||||
int length = data.length;
|
||||
out.writeInt(streamId & 0x7fffffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.write(data);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void writeNameValueBlockToBuffer(List<String> nameValueBlock) throws IOException {
|
||||
nameValueBlockBuffer.reset();
|
||||
int numberOfPairs = nameValueBlock.size() / 2;
|
||||
nameValueBlockOut.writeInt(numberOfPairs);
|
||||
for (String s : nameValueBlock) {
|
||||
nameValueBlockOut.writeInt(s.length());
|
||||
nameValueBlockOut.write(s.getBytes("UTF-8"));
|
||||
}
|
||||
nameValueBlockOut.flush();
|
||||
}
|
||||
|
||||
public synchronized void settings(int flags, Settings settings) throws IOException {
|
||||
int type = SpdyConnection.TYPE_SETTINGS;
|
||||
int size = settings.size();
|
||||
int length = 4 + size * 8;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(size);
|
||||
for (int i = 0; i <= Settings.COUNT; i++) {
|
||||
if (!settings.isSet(i)) continue;
|
||||
int settingsFlags = settings.flags(i);
|
||||
out.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff));
|
||||
out.writeInt(settings.get(i));
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void noop() throws IOException {
|
||||
int type = SpdyConnection.TYPE_NOOP;
|
||||
int length = 0;
|
||||
int flags = 0;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void ping(int flags, int id) throws IOException {
|
||||
int type = SpdyConnection.TYPE_PING;
|
||||
int length = 4;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(id);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode)
|
||||
throws IOException {
|
||||
int type = SpdyConnection.TYPE_GOAWAY;
|
||||
int length = 8;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(lastGoodStreamId);
|
||||
out.writeInt(statusCode);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public synchronized void windowUpdate(int streamId, int deltaWindowSize) throws IOException {
|
||||
int type = SpdyConnection.TYPE_WINDOW_UPDATE;
|
||||
int flags = 0;
|
||||
int length = 8;
|
||||
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
|
||||
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
|
||||
out.writeInt(streamId);
|
||||
out.writeInt(deltaWindowSize);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
Util.closeAll(out, nameValueBlockOut);
|
||||
}
|
||||
}
|
@ -64,6 +64,8 @@ import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.webkit.CookieManager;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
|
||||
public class FileTransfer extends CordovaPlugin {
|
||||
|
||||
private static final String LOG_TAG = "FileTransfer";
|
||||
@ -79,6 +81,8 @@ public class FileTransfer extends CordovaPlugin {
|
||||
private static HashMap<String, RequestContext> activeRequests = new HashMap<String, RequestContext>();
|
||||
private static final int MAX_BUFFER_SIZE = 16 * 1024;
|
||||
|
||||
private static OkHttpClient httpClient = new OkHttpClient();
|
||||
|
||||
private static final class RequestContext {
|
||||
String source;
|
||||
String target;
|
||||
@ -327,13 +331,13 @@ public class FileTransfer extends CordovaPlugin {
|
||||
if (useHttps) {
|
||||
// Using standard HTTPS connection. Will not allow self signed certificate
|
||||
if (!trustEveryone) {
|
||||
conn = (HttpsURLConnection) url.openConnection();
|
||||
conn = (HttpsURLConnection) httpClient.open(url);
|
||||
}
|
||||
// Use our HTTPS connection that blindly trusts everyone.
|
||||
// This should only be used in debug environments
|
||||
else {
|
||||
// Setup the HTTPS connection class to trust everyone
|
||||
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
|
||||
HttpsURLConnection https = (HttpsURLConnection) httpClient.open(url);
|
||||
oldSocketFactory = trustAllHosts(https);
|
||||
// Save the current hostnameVerifier
|
||||
oldHostnameVerifier = https.getHostnameVerifier();
|
||||
@ -344,7 +348,7 @@ public class FileTransfer extends CordovaPlugin {
|
||||
}
|
||||
// Return a standard HTTP connection
|
||||
else {
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn = httpClient.open(url);
|
||||
}
|
||||
|
||||
// Allow Inputs
|
||||
@ -749,13 +753,13 @@ public class FileTransfer extends CordovaPlugin {
|
||||
if (useHttps) {
|
||||
// Using standard HTTPS connection. Will not allow self signed certificate
|
||||
if (!trustEveryone) {
|
||||
connection = (HttpsURLConnection) url.openConnection();
|
||||
connection = (HttpsURLConnection) httpClient.open(url);
|
||||
}
|
||||
// Use our HTTPS connection that blindly trusts everyone.
|
||||
// This should only be used in debug environments
|
||||
else {
|
||||
// Setup the HTTPS connection class to trust everyone
|
||||
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
|
||||
HttpsURLConnection https = (HttpsURLConnection) httpClient.open(url);
|
||||
oldSocketFactory = trustAllHosts(https);
|
||||
// Save the current hostnameVerifier
|
||||
oldHostnameVerifier = https.getHostnameVerifier();
|
||||
@ -766,7 +770,8 @@ public class FileTransfer extends CordovaPlugin {
|
||||
}
|
||||
// Return a standard HTTP connection
|
||||
else {
|
||||
connection = url.openConnection();
|
||||
connection = httpClient.open(url);
|
||||
|
||||
}
|
||||
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
|
Loading…
Reference in New Issue
Block a user