CB-5799 Update version of OkHTTP to 1.3

This commit is contained in:
Andrew Grieve 2014-01-15 11:36:18 -05:00
parent a643c3dba6
commit e16cab6b9c
66 changed files with 4911 additions and 2166 deletions

33
framework/src/com/squareup/okhttp/Address.java Normal file → Executable file
View File

@ -15,8 +15,10 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import java.net.Proxy;
import java.net.UnknownHostException;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
@ -38,16 +40,23 @@ public final class Address {
final int uriPort;
final SSLSocketFactory sslSocketFactory;
final HostnameVerifier hostnameVerifier;
final OkAuthenticator authenticator;
final List<String> transports;
public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy,
List<String> transports) throws UnknownHostException {
if (uriHost == null) throw new NullPointerException("uriHost == null");
if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
if (transports == null) throw new IllegalArgumentException("transports == null");
this.proxy = proxy;
this.uriHost = uriHost;
this.uriPort = uriPort;
this.sslSocketFactory = sslSocketFactory;
this.hostnameVerifier = hostnameVerifier;
this.authenticator = authenticator;
this.transports = Util.immutableList(transports);
}
/** Returns the hostname of the origin server. */
@ -79,6 +88,22 @@ public final class Address {
return hostnameVerifier;
}
/**
* Returns the client's authenticator. This method never returns null.
*/
public OkAuthenticator getAuthenticator() {
return authenticator;
}
/**
* Returns the client's transports. This method always returns a non-null list
* that contains "http/1.1", possibly among other transports.
*/
public List<String> getTransports() {
return transports;
}
/**
* Returns this address's explicitly-specified HTTP proxy, or null to
* delegate to the HTTP client's proxy selector.
@ -94,7 +119,9 @@ public final class Address {
&& this.uriHost.equals(that.uriHost)
&& this.uriPort == that.uriPort
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier);
&& equal(this.hostnameVerifier, that.hostnameVerifier)
&& equal(this.authenticator, that.authenticator)
&& equal(this.transports, that.transports);
}
return false;
}
@ -105,7 +132,9 @@ public final class Address {
result = 31 * result + uriPort;
result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0);
result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
result = 31 * result + transports.hashCode();
return result;
}
}

78
framework/src/com/squareup/okhttp/Connection.java Normal file → Executable file
View File

@ -31,6 +31,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import javax.net.ssl.SSLSocket;
@ -92,24 +93,20 @@ public final class Connection implements Closeable {
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
throws IOException {
if (connected) {
throw new IllegalStateException("already connected");
}
connected = true;
if (connected) throw new IllegalStateException("already connected");
socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
socket.connect(route.inetSocketAddress, connectTimeout);
Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
socket.setSoTimeout(readTimeout);
in = socket.getInputStream();
out = socket.getOutputStream();
if (route.address.sslSocketFactory != null) {
upgradeToTls(tunnelRequest);
} else {
streamWrapper();
}
// Use MTU-sized buffers to send fewer packets.
int mtu = Platform.get().getMtu(socket);
in = new BufferedInputStream(in, mtu);
out = new BufferedOutputStream(out, mtu);
connected = true;
}
/**
@ -134,7 +131,8 @@ public final class Connection implements Closeable {
platform.supportTlsIntolerantServer(sslSocket);
}
if (route.modernTls) {
boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
if (useNpn) {
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
}
@ -148,14 +146,15 @@ public final class Connection implements Closeable {
out = sslSocket.getOutputStream();
in = sslSocket.getInputStream();
streamWrapper();
byte[] selectedProtocol;
if (route.modernTls
&& (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (Arrays.equals(selectedProtocol, SPDY3)) {
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
.build();
spdyConnection.sendConnectionHeader();
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
throw new IOException(
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
@ -190,6 +189,39 @@ public final class Connection implements Closeable {
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
}
/**
* Returns true if we are confident that we can read data from this
* connection. This is more expensive and more accurate than {@link
* #isAlive()}; callers should check {@link #isAlive()} first.
*/
public boolean isReadable() {
if (!(in instanceof BufferedInputStream)) {
return true; // Optimistic.
}
if (isSpdy()) {
return true; // Optimistic. We can't test SPDY because its streams are in use.
}
BufferedInputStream bufferedInputStream = (BufferedInputStream) in;
try {
int readTimeout = socket.getSoTimeout();
try {
socket.setSoTimeout(1);
bufferedInputStream.mark(1);
if (bufferedInputStream.read() == -1) {
return false; // Stream is exhausted; socket is closed.
}
bufferedInputStream.reset();
return true;
} finally {
socket.setSoTimeout(readTimeout);
}
} catch (SocketTimeoutException ignored) {
return true; // Read timed out; socket is good.
} catch (IOException e) {
return false; // Couldn't read; socket is closed.
}
}
public void resetIdleStartTime() {
if (spdyConnection != null) {
throw new IllegalStateException("spdyConnection != null");
@ -207,7 +239,7 @@ public final class Connection implements Closeable {
* {@code keepAliveDurationNs}.
*/
public boolean isExpired(long keepAliveDurationNs) {
return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs;
}
/**
@ -220,7 +252,8 @@ public final class Connection implements Closeable {
/** Returns the transport appropriate for this connection. */
public Object newTransport(HttpEngine httpEngine) throws IOException {
return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
return (spdyConnection != null)
? new SpdyTransport(httpEngine, spdyConnection)
: new HttpTransport(httpEngine, out, in);
}
@ -258,6 +291,11 @@ public final class Connection implements Closeable {
return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
}
public void updateReadTimeout(int newTimeout) throws IOException {
if (!connected) throw new IllegalStateException("updateReadTimeout - not connected");
socket.setSoTimeout(newTimeout);
}
/**
* To make an HTTPS connection over an HTTP proxy, send an unencrypted
* CONNECT request to create the proxy connection. This may need to be
@ -275,8 +313,9 @@ public final class Connection implements Closeable {
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, route.proxy, url);
boolean credentialsFound = HttpAuthenticator.processAuthHeader(
route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
route.proxy, url);
if (credentialsFound) {
continue;
} else {
@ -288,4 +327,9 @@ public final class Connection implements Closeable {
}
}
}
private void streamWrapper() throws IOException {
in = new BufferedInputStream(in, 4096);
out = new BufferedOutputStream(out, 256);
}
}

9
framework/src/com/squareup/okhttp/ConnectionPool.java Normal file → Executable file
View File

@ -80,8 +80,9 @@ public class ConnectionPool {
private final LinkedList<Connection> connections = new LinkedList<Connection>();
/** We use a single background thread to cleanup expired connections. */
private final ExecutorService executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
Util.daemonThreadFactory("OkHttp ConnectionPool"));
private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
@Override public Void call() throws Exception {
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
@ -215,8 +216,6 @@ public class ConnectionPool {
* <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;
}
@ -239,6 +238,8 @@ public class ConnectionPool {
connections.addFirst(connection);
connection.resetIdleStartTime();
}
executorService.submit(connectionsCleanupCallable);
}
/**

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.ResponseHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
final class Dispatcher {
// TODO: thread pool size should be configurable; possibly configurable per host.
private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>();
public synchronized void enqueue(
OkHttpClient client, Request request, Response.Receiver responseReceiver) {
Job job = new Job(this, client, request, responseReceiver);
List<Job> jobsForTag = enqueuedJobs.get(request.tag());
if (jobsForTag == null) {
jobsForTag = new ArrayList<Job>(2);
enqueuedJobs.put(request.tag(), jobsForTag);
}
jobsForTag.add(job);
executorService.execute(job);
}
public synchronized void cancel(Object tag) {
List<Job> jobs = enqueuedJobs.remove(tag);
if (jobs == null) return;
for (Job job : jobs) {
executorService.remove(job);
}
}
synchronized void finished(Job job) {
List<Job> jobs = enqueuedJobs.get(job.tag());
if (jobs != null) jobs.remove(job);
}
static class RealResponseBody extends Response.Body {
private final ResponseHeaders responseHeaders;
private final InputStream in;
RealResponseBody(ResponseHeaders responseHeaders, InputStream in) {
this.responseHeaders = responseHeaders;
this.in = in;
}
@Override public boolean ready() throws IOException {
return true;
}
@Override public MediaType contentType() {
String contentType = responseHeaders.getContentType();
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
return responseHeaders.getContentLength();
}
@Override public InputStream byteStream() throws IOException {
return in;
}
}
}

View File

@ -0,0 +1,59 @@
/*
* 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;
/**
* A failure attempting to retrieve an HTTP response.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ class Failure {
private final Request request;
private final Throwable exception;
private Failure(Builder builder) {
this.request = builder.request;
this.exception = builder.exception;
}
public Request request() {
return request;
}
public Throwable exception() {
return exception;
}
public static class Builder {
private Request request;
private Throwable exception;
public Builder request(Request request) {
this.request = request;
return this;
}
public Builder exception(Throwable exception) {
this.exception = exception;
return this;
}
public Failure build() {
return new Failure(this);
}
}
}

119
framework/src/com/squareup/okhttp/HttpResponseCache.java Normal file → Executable file
View File

@ -22,8 +22,8 @@ import com.squareup.okhttp.internal.StrictLineReader;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsEngine;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
import com.squareup.okhttp.internal.http.OkResponseCache;
import com.squareup.okhttp.internal.http.RawHeaders;
import com.squareup.okhttp.internal.http.ResponseHeaders;
import java.io.BufferedWriter;
@ -35,7 +35,6 @@ 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;
@ -44,8 +43,6 @@ 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;
@ -55,8 +52,8 @@ 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 javax.net.ssl.SSLSocket;
import static com.squareup.okhttp.internal.Util.US_ASCII;
import static com.squareup.okhttp.internal.Util.UTF_8;
@ -119,9 +116,6 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
* }</pre>
*/
public final class HttpResponseCache extends ResponseCache {
private static final char[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
// TODO: add APIs to iterate the cache?
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
@ -153,6 +147,10 @@ public final class HttpResponseCache extends ResponseCache {
return HttpResponseCache.this.put(uri, connection);
}
@Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
HttpResponseCache.this.maybeRemove(requestMethod, uri);
}
@Override public void update(
CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
HttpResponseCache.this.update(conditionalCacheHit, connection);
@ -172,26 +170,7 @@ public final class HttpResponseCache extends ResponseCache {
}
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);
return Util.hash(uri.toString());
}
@Override public CacheResponse get(URI uri, String requestMethod,
@ -226,17 +205,11 @@ public final class HttpResponseCache extends ResponseCache {
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.
}
if (maybeRemove(requestMethod, uri)) {
return null;
} else if (!requestMethod.equals("GET")) {
}
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.
@ -259,7 +232,7 @@ public final class HttpResponseCache extends ResponseCache {
Entry entry = new Entry(uri, varyHeaders, httpConnection);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key);
editor = cache.edit(uriToKey(uri));
if (editor == null) {
return null;
}
@ -271,6 +244,23 @@ public final class HttpResponseCache extends ResponseCache {
}
}
/**
* Returns true if the supplied {@code requestMethod} potentially invalidates an entry in the
* cache.
*/
private boolean maybeRemove(String requestMethod, URI uri) {
if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
"DELETE")) {
try {
cache.remove(uriToKey(uri));
} catch (IOException ignored) {
// The cache cannot be written.
}
return true;
}
return false;
}
private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException {
HttpEngine httpEngine = getHttpEngine(httpConnection);
@ -331,6 +321,30 @@ public final class HttpResponseCache extends ResponseCache {
return writeSuccessCount;
}
public long getSize() {
return cache.size();
}
public long getMaxSize() {
return cache.getMaxSize();
}
public void flush() throws IOException {
cache.flush();
}
public void close() throws IOException {
cache.close();
}
public File getDirectory() {
return cache.getDirectory();
}
public boolean isClosed() {
return cache.isClosed();
}
private synchronized void trackResponse(ResponseSource source) {
requestCount++;
@ -383,8 +397,7 @@ public final class HttpResponseCache extends ResponseCache {
editor.commit();
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
@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);
@ -513,16 +526,16 @@ public final class HttpResponseCache extends ResponseCache {
this.requestMethod = httpConnection.getRequestMethod();
this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
if (isHttps()) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
cipherSuite = httpsConnection.getCipherSuite();
SSLSocket sslSocket = getSslSocket(httpConnection);
if (sslSocket != null) {
cipherSuite = sslSocket.getSession().getCipherSuite();
Certificate[] peerCertificatesNonFinal = null;
try {
peerCertificatesNonFinal = httpsConnection.getServerCertificates();
peerCertificatesNonFinal = sslSocket.getSession().getPeerCertificates();
} catch (SSLPeerUnverifiedException ignored) {
}
peerCertificates = peerCertificatesNonFinal;
localCertificates = httpsConnection.getLocalCertificates();
localCertificates = sslSocket.getSession().getLocalCertificates();
} else {
cipherSuite = null;
peerCertificates = null;
@ -530,6 +543,22 @@ public final class HttpResponseCache extends ResponseCache {
}
}
/**
* Returns the SSL socket used by {@code httpConnection} for HTTPS, nor null
* if the connection isn't using HTTPS. Since we permit redirects across
* protocols (HTTP to HTTPS or vice versa), the implementation type of the
* connection doesn't necessarily match the implementation type of its HTTP
* engine.
*/
private SSLSocket getSslSocket(HttpURLConnection httpConnection) {
HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl
? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine()
: ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
return engine instanceof HttpsEngine
? ((HttpsEngine) engine).getSslSocket()
: null;
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
OutputStream out = editor.newOutputStream(ENTRY_METADATA);
Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));

View File

@ -0,0 +1,232 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.HttpTransport;
import com.squareup.okhttp.internal.http.HttpsEngine;
import com.squareup.okhttp.internal.http.Policy;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_TEMP_REDIRECT;
import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED;
final class Job implements Runnable, Policy {
private final Dispatcher dispatcher;
private final OkHttpClient client;
private final Response.Receiver responseReceiver;
/** The request; possibly a consequence of redirects or auth headers. */
private Request request;
public Job(Dispatcher dispatcher, OkHttpClient client, Request request,
Response.Receiver responseReceiver) {
this.dispatcher = dispatcher;
this.client = client;
this.request = request;
this.responseReceiver = responseReceiver;
}
@Override public int getChunkLength() {
return request.body().contentLength() == -1 ? HttpTransport.DEFAULT_CHUNK_LENGTH : -1;
}
@Override public long getFixedContentLength() {
return request.body().contentLength();
}
@Override public boolean getUseCaches() {
return false; // TODO.
}
@Override public HttpURLConnection getHttpConnectionToCache() {
return null;
}
@Override public URL getURL() {
return request.url();
}
@Override public long getIfModifiedSince() {
return 0; // For HttpURLConnection only. We let the cache drive this.
}
@Override public boolean usingProxy() {
return false; // We let the connection decide this.
}
@Override public void setSelectedProxy(Proxy proxy) {
// Do nothing.
}
Object tag() {
return request.tag();
}
@Override public void run() {
try {
Response response = execute();
responseReceiver.onResponse(response);
} catch (IOException e) {
responseReceiver.onFailure(new Failure.Builder()
.request(request)
.exception(e)
.build());
} finally {
// TODO: close the response body
// TODO: release the HTTP engine (potentially multiple!)
dispatcher.finished(this);
}
}
private Response execute() throws IOException {
Connection connection = null;
Response redirectedBy = null;
while (true) {
HttpEngine engine = newEngine(connection);
Request.Body body = request.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType == null) throw new IllegalStateException("contentType == null");
if (engine.getRequestHeaders().getContentType() == null) {
engine.getRequestHeaders().setContentType(contentType.toString());
}
}
engine.sendRequest();
if (body != null) {
body.writeTo(engine.getRequestBody());
}
engine.readResponse();
int responseCode = engine.getResponseCode();
Dispatcher.RealResponseBody responseBody = new Dispatcher.RealResponseBody(
engine.getResponseHeaders(), engine.getResponseBody());
Response response = new Response.Builder(request, responseCode)
.rawHeaders(engine.getResponseHeaders().getHeaders())
.body(responseBody)
.redirectedBy(redirectedBy)
.build();
Request redirect = processResponse(engine, response);
if (redirect == null) {
engine.automaticallyReleaseConnectionToPool();
return response;
}
// TODO: fail if too many redirects
// TODO: fail if not following redirects
// TODO: release engine
connection = sameConnection(request, redirect) ? engine.getConnection() : null;
redirectedBy = response;
request = redirect;
}
}
HttpEngine newEngine(Connection connection) throws IOException {
String protocol = request.url().getProtocol();
RawHeaders requestHeaders = request.rawHeaders();
if (protocol.equals("http")) {
return new HttpEngine(client, this, request.method(), requestHeaders, connection, null);
} else if (protocol.equals("https")) {
return new HttpsEngine(client, this, request.method(), requestHeaders, connection, null);
} else {
throw new AssertionError();
}
}
/**
* Figures out the HTTP request to make in response to receiving {@code
* response}. This will either add authentication headers or follow
* redirects. If a follow-up is either unnecessary or not applicable, this
* returns null.
*/
private Request processResponse(HttpEngine engine, Response response) throws IOException {
Request request = response.request();
Proxy selectedProxy = engine.getConnection() != null
? engine.getConnection().getRoute().getProxy()
: client.getProxy();
int responseCode = response.code();
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:
RawHeaders successorRequestHeaders = request.rawHeaders();
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
response.code(), response.rawHeaders(), successorRequestHeaders, selectedProxy,
this.request.url());
return credentialsFound
? request.newBuilder().rawHeaders(successorRequestHeaders).build()
: null;
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
String method = request.method();
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 null;
}
String location = response.header("Location");
if (location == null) {
return null;
}
URL url = new URL(request.url(), location);
if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
return null; // Don't follow redirects to unsupported protocols.
}
return this.request.newBuilder().url(url).build();
default:
return null;
}
}
private boolean sameConnection(Request a, Request b) {
return a.url().getHost().equals(b.url().getHost())
&& getEffectivePort(a.url()) == getEffectivePort(b.url())
&& a.url().getProtocol().equals(b.url().getProtocol());
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type,
* appropriate to describe the content type of an HTTP request or response body.
*/
public final class MediaType {
private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)";
private static final String QUOTED = "\"([^\"]*)\"";
private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
private static final Pattern PARAMETER = Pattern.compile(
";\\s*" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + ")");
private final String mediaType;
private final String type;
private final String subtype;
private final String charset;
private MediaType(String mediaType, String type, String subtype, String charset) {
this.mediaType = mediaType;
this.type = type;
this.subtype = subtype;
this.charset = charset;
}
/**
* Returns a media type for {@code string}, or null if {@code string} is not a
* well-formed media type.
*/
public static MediaType parse(String string) {
Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
if (!typeSubtype.lookingAt()) return null;
String type = typeSubtype.group(1).toLowerCase(Locale.US);
String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
String charset = null;
Matcher parameter = PARAMETER.matcher(string);
for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
parameter.region(s, string.length());
if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
if (charset != null) throw new IllegalArgumentException("Multiple charsets: " + string);
charset = parameter.group(2) != null
? parameter.group(2) // Value is a token.
: parameter.group(3); // Value is a quoted string.
}
return new MediaType(string, type, subtype, charset);
}
/**
* Returns the high-level media type, such as "text", "image", "audio",
* "video", or "application".
*/
public String type() {
return type;
}
/**
* Returns a specific media subtype, such as "plain" or "png", "mpeg",
* "mp4" or "xml".
*/
public String subtype() {
return subtype;
}
/**
* Returns the charset of this media type, or null if this media type doesn't
* specify a charset.
*/
public Charset charset() {
return charset != null ? Charset.forName(charset) : null;
}
/**
* Returns the charset of this media type, or {@code defaultValue} if this
* media type doesn't specify a charset.
*/
public Charset charset(Charset defaultValue) {
return charset != null ? Charset.forName(charset) : defaultValue;
}
/**
* Returns the encoded media type, like "text/plain; charset=utf-8",
* appropriate for use in a Content-Type header.
*/
@Override public String toString() {
return mediaType;
}
@Override public boolean equals(Object o) {
return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType);
}
@Override public int hashCode() {
return mediaType.hashCode();
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Base64;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
/**
* Responds to authentication challenges from the remote web or proxy server by
* returning credentials.
*/
public interface OkAuthenticator {
/**
* Returns a credential that satisfies the authentication challenge made by
* {@code url}. Returns null if the challenge cannot be satisfied. This method
* is called in response to an HTTP 401 unauthorized status code sent by the
* origin server.
*
* @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP
* response.
*/
Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
/**
* Returns a credential that satisfies the authentication challenge made by
* {@code proxy}. Returns null if the challenge cannot be satisfied. This
* method is called in response to an HTTP 401 unauthorized status code sent
* by the proxy server.
*
* @param challenges parsed "Proxy-Authenticate" challenge headers from the
* HTTP response.
*/
Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
/** An RFC 2617 challenge. */
public final class Challenge {
private final String scheme;
private final String realm;
public Challenge(String scheme, String realm) {
this.scheme = scheme;
this.realm = realm;
}
/** Returns the authentication scheme, like {@code Basic}. */
public String getScheme() {
return scheme;
}
/** Returns the protection space. */
public String getRealm() {
return realm;
}
@Override public boolean equals(Object o) {
return o instanceof Challenge
&& ((Challenge) o).scheme.equals(scheme)
&& ((Challenge) o).realm.equals(realm);
}
@Override public int hashCode() {
return scheme.hashCode() + 31 * realm.hashCode();
}
@Override public String toString() {
return scheme + " realm=\"" + realm + "\"";
}
}
/** An RFC 2617 credential. */
public final class Credential {
private final String headerValue;
private Credential(String headerValue) {
this.headerValue = headerValue;
}
/** Returns an auth credential for the Basic scheme. */
public static Credential basic(String userName, String password) {
try {
String usernameAndPassword = userName + ":" + password;
byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
String encoded = Base64.encode(bytes);
return new Credential("Basic " + encoded);
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
public String getHeaderValue() {
return headerValue;
}
@Override public boolean equals(Object o) {
return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue);
}
@Override public int hashCode() {
return headerValue.hashCode();
}
@Override public String toString() {
return headerValue;
}
}
}

228
framework/src/com/squareup/okhttp/OkHttpClient.java Normal file → Executable file
View File

@ -15,34 +15,105 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
import com.squareup.okhttp.internal.http.OkResponseCache;
import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ResponseCache;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
/** Configures and creates HTTP connections. */
public final class OkHttpClient {
public final class OkHttpClient implements URLStreamHandlerFactory {
private static final List<String> DEFAULT_TRANSPORTS
= Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
private final RouteDatabase routeDatabase;
private final Dispatcher dispatcher;
private Proxy proxy;
private Set<Route> failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
private List<String> transports;
private ProxySelector proxySelector;
private CookieHandler cookieHandler;
private ResponseCache responseCache;
private SSLSocketFactory sslSocketFactory;
private HostnameVerifier hostnameVerifier;
private OkAuthenticator authenticator;
private ConnectionPool connectionPool;
private boolean followProtocolRedirects = true;
private int connectTimeout;
private int readTimeout;
public OkHttpClient() {
routeDatabase = new RouteDatabase();
dispatcher = new Dispatcher();
}
private OkHttpClient(OkHttpClient copyFrom) {
routeDatabase = copyFrom.routeDatabase;
dispatcher = copyFrom.dispatcher;
}
/**
* Sets the default connect timeout for new connections. A value of 0 means no timeout.
*
* @see URLConnection#setConnectTimeout(int)
*/
public void setConnectTimeout(long timeout, TimeUnit unit) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout < 0");
}
if (unit == null) {
throw new IllegalArgumentException("unit == null");
}
long millis = unit.toMillis(timeout);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Timeout too large.");
}
connectTimeout = (int) millis;
}
/** Default connect timeout (in milliseconds). */
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Sets the default read timeout for new connections. A value of 0 means no timeout.
*
* @see URLConnection#setReadTimeout(int)
*/
public void setReadTimeout(long timeout, TimeUnit unit) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout < 0");
}
if (unit == null) {
throw new IllegalArgumentException("unit == null");
}
long millis = unit.toMillis(timeout);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Timeout too large.");
}
readTimeout = (int) millis;
}
/** Default read timeout (in milliseconds). */
public int getReadTimeout() {
return readTimeout;
}
/**
* Sets the HTTP proxy that will be used by connections created by this
@ -108,7 +179,7 @@ public final class OkHttpClient {
return responseCache;
}
private OkResponseCache okResponseCache() {
public OkResponseCache getOkResponseCache() {
if (responseCache instanceof HttpResponseCache) {
return ((HttpResponseCache) responseCache).okResponseCache;
} else if (responseCache != null) {
@ -124,7 +195,7 @@ public final class OkHttpClient {
* <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
* system-wide default} SSL socket factory will be used.
*/
public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
return this;
}
@ -149,6 +220,22 @@ public final class OkHttpClient {
return hostnameVerifier;
}
/**
* Sets the authenticator used to respond to challenges from the remote web
* server or proxy server.
*
* <p>If unset, the {@link java.net.Authenticator#setDefault system-wide default}
* authenticator will be used.
*/
public OkHttpClient setAuthenticator(OkAuthenticator authenticator) {
this.authenticator = authenticator;
return this;
}
public OkAuthenticator getAuthenticator() {
return authenticator;
}
/**
* Sets the connection pool used to recycle HTTP and HTTPS connections.
*
@ -180,16 +267,86 @@ public final class OkHttpClient {
return followProtocolRedirects;
}
public RouteDatabase getRoutesDatabase() {
return routeDatabase;
}
/**
* Configure the transports used by this client to communicate with remote
* servers. By default this client will prefer the most efficient transport
* available, falling back to more ubiquitous transports. Applications should
* only call this method to avoid specific compatibility problems, such as web
* servers that behave incorrectly when SPDY is enabled.
*
* <p>The following transports are currently supported:
* <ul>
* <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
* <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3">spdy/3</a>
* </ul>
*
* <p><strong>This is an evolving set.</strong> Future releases may drop
* support for transitional transports (like spdy/3), in favor of their
* successors (spdy/4 or http/2.0). The http/1.1 transport will never be
* dropped.
*
* <p>If multiple protocols are specified, <a
* href="https://technotes.googlecode.com/git/nextprotoneg.html">NPN</a> will
* be used to negotiate a transport. Future releases may use another mechanism
* (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
* to negotiate a transport.
*
* @param transports the transports to use, in order of preference. The list
* must contain "http/1.1". It must not contain null.
*/
public OkHttpClient setTransports(List<String> transports) {
transports = Util.immutableList(transports);
if (!transports.contains("http/1.1")) {
throw new IllegalArgumentException("transports doesn't contain http/1.1: " + transports);
}
if (transports.contains(null)) {
throw new IllegalArgumentException("transports must not contain null");
}
if (transports.contains("")) {
throw new IllegalArgumentException("transports contains an empty string");
}
this.transports = transports;
return this;
}
public List<String> getTransports() {
return transports;
}
/**
* Schedules {@code request} to be executed.
*/
/* OkHttp 2.0: public */ void enqueue(Request request, Response.Receiver responseReceiver) {
// Create the HttpURLConnection immediately so the enqueued job gets the current settings of
// this client. Otherwise changes to this client (socket factory, redirect policy, etc.) may
// incorrectly be reflected in the request when it is dispatched later.
dispatcher.enqueue(copyWithDefaults(), request, responseReceiver);
}
/**
* Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
* in flight might not be canceled.
*/
/* OkHttp 2.0: public */ void cancel(Object tag) {
dispatcher.cancel(tag);
}
public HttpURLConnection open(URL url) {
return open(url, proxy);
}
HttpURLConnection open(URL url, Proxy proxy) {
String protocol = url.getProtocol();
OkHttpClient copy = copyWithDefaults();
if (protocol.equals("http")) {
return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else if (protocol.equals("https")) {
return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else {
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
copy.proxy = proxy;
if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
/**
@ -197,9 +354,8 @@ public final class OkHttpClient {
* each field that hasn't been explicitly configured.
*/
private OkHttpClient copyWithDefaults() {
OkHttpClient result = new OkHttpClient();
OkHttpClient result = new OkHttpClient(this);
result.proxy = proxy;
result.failedRoutes = failedRoutes;
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
@ -208,9 +364,45 @@ public final class OkHttpClient {
: HttpsURLConnection.getDefaultSSLSocketFactory();
result.hostnameVerifier = hostnameVerifier != null
? hostnameVerifier
: HttpsURLConnection.getDefaultHostnameVerifier();
: OkHostnameVerifier.INSTANCE;
result.authenticator = authenticator != null
? authenticator
: HttpAuthenticator.SYSTEM_DEFAULT;
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
result.followProtocolRedirects = followProtocolRedirects;
result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
result.connectTimeout = connectTimeout;
result.readTimeout = readTimeout;
return result;
}
/**
* Creates a URLStreamHandler as a {@link URL#setURLStreamHandlerFactory}.
*
* <p>This code configures OkHttp to handle all HTTP and HTTPS connections
* created with {@link URL#openConnection()}: <pre> {@code
*
* OkHttpClient okHttpClient = new OkHttpClient();
* URL.setURLStreamHandlerFactory(okHttpClient);
* }</pre>
*/
public URLStreamHandler createURLStreamHandler(final String protocol) {
if (!protocol.equals("http") && !protocol.equals("https")) return null;
return new URLStreamHandler() {
@Override protected URLConnection openConnection(URL url) {
return open(url);
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) {
return open(url, proxy);
}
@Override protected int getDefaultPort() {
if (protocol.equals("http")) return 80;
if (protocol.equals("https")) return 443;
throw new AssertionError();
}
};
}
}

36
framework/src/com/squareup/okhttp/OkResponseCache.java Normal file → Executable file
View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 The Android Open Source Project
* 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.
@ -16,23 +16,41 @@
package com.squareup.okhttp;
import java.io.IOException;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
/**
* 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.
* An extended response cache API. Unlike {@link java.net.ResponseCache}, this
* interface supports conditional caching and statistics.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
public interface OkResponseCache {
CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
throws IOException;
/** Track an HTTP response being satisfied by {@code source}. */
void trackResponse(ResponseSource source);
CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
/** Remove any cache entries for the supplied {@code uri} if the request method invalidates. */
void maybeRemove(String requestMethod, URI uri) throws IOException;
/**
* Handles a conditional request hit by updating the stored cache response
* with the headers from {@code httpConnection}. The cached response body is
* not updated. If the stored response has changed since {@code
* conditionalCacheHit} was returned, this does nothing.
*/
void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
/** Track an conditional GET that was satisfied by this cache. */
void trackConditionalCacheHit();
/** Updates stored HTTP headers using a hit on a conditional GET. */
void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException;
/** Track an HTTP response being satisfied by {@code source}. */
void trackResponse(ResponseSource source);
}

View File

@ -0,0 +1,284 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Set;
/**
* An HTTP request. Instances of this class are immutable if their {@link #body}
* is null or itself immutable.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ final class Request {
private final URL url;
private final String method;
private final RawHeaders headers;
private final Body body;
private final Object tag;
private Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = new RawHeaders(builder.headers);
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
}
public URL url() {
return url;
}
public String urlString() {
return url.toString();
}
public String method() {
return method;
}
public String header(String name) {
return headers.get(name);
}
public List<String> headers(String name) {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
RawHeaders rawHeaders() {
return new RawHeaders(headers);
}
public int headerCount() {
return headers.length();
}
public String headerName(int index) {
return headers.getFieldName(index);
}
public String headerValue(int index) {
return headers.getValue(index);
}
public Body body() {
return body;
}
public Object tag() {
return tag;
}
Builder newBuilder() {
return new Builder(url)
.method(method, body)
.rawHeaders(headers)
.tag(tag);
}
public abstract static class Body {
/** Returns the Content-Type header for this body. */
public abstract MediaType contentType();
/**
* Returns the number of bytes that will be written to {@code out} in a call
* to {@link #writeTo}, or -1 if that count is unknown.
*/
public long contentLength() {
return -1;
}
/** Writes the content of this request to {@code out}. */
public abstract void writeTo(OutputStream out) throws IOException;
/**
* Returns a new request body that transmits {@code content}. If {@code
* contentType} lacks a charset, this will use UTF-8.
*/
public static Body create(MediaType contentType, String content) {
contentType = contentType.charset() != null
? contentType
: MediaType.parse(contentType + "; charset=utf-8");
try {
byte[] bytes = content.getBytes(contentType.charset().name());
return create(contentType, bytes);
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
/** Returns a new request body that transmits {@code content}. */
public static Body create(final MediaType contentType, final byte[] content) {
if (contentType == null) throw new NullPointerException("contentType == null");
if (content == null) throw new NullPointerException("content == null");
return new Body() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return content.length;
}
@Override public void writeTo(OutputStream out) throws IOException {
out.write(content);
}
};
}
/** Returns a new request body that transmits the content of {@code file}. */
public static Body create(final MediaType contentType, final File file) {
if (contentType == null) throw new NullPointerException("contentType == null");
if (file == null) throw new NullPointerException("content == null");
return new Body() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(OutputStream out) throws IOException {
long length = contentLength();
if (length == 0) return;
InputStream in = null;
try {
in = new FileInputStream(file);
byte[] buffer = new byte[(int) Math.min(8192, length)];
for (int c; (c = in.read(buffer)) != -1; ) {
out.write(buffer, 0, c);
}
} finally {
Util.closeQuietly(in);
}
}
};
}
}
public static class Builder {
private URL url;
private String method = "GET";
private RawHeaders headers = new RawHeaders();
private Body body;
private Object tag;
public Builder(String url) {
url(url);
}
public Builder(URL url) {
url(url);
}
public Builder url(String url) {
try {
this.url = new URL(url);
return this;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Malformed URL: " + url);
}
}
public Builder url(URL url) {
if (url == null) throw new IllegalStateException("url == null");
this.url = url;
return this;
}
/**
* Sets the header named {@code name} to {@code value}. If this request
* already has any headers with that name, they are all replaced.
*/
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Cookie".
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
return this;
}
Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
return this;
}
public Builder get() {
return method("GET", null);
}
public Builder head() {
return method("HEAD", null);
}
public Builder post(Body body) {
return method("POST", body);
}
public Builder put(Body body) {
return method("PUT", body);
}
public Builder method(String method, Body body) {
if (method == null || method.length() == 0) {
throw new IllegalArgumentException("method == null || method.length() == 0");
}
this.method = method;
this.body = body;
return this;
}
/**
* Attaches {@code tag} to the request. It can be used later to cancel the
* request. If the tag is unspecified or null, the request is canceled by
* using the request itself as the tag.
*/
public Builder tag(Object tag) {
this.tag = tag;
return this;
}
public Request build() {
return new Request(this);
}
}
}

View File

@ -0,0 +1,290 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Set;
import static com.squareup.okhttp.internal.Util.UTF_8;
/**
* An HTTP response. Instances of this class are not immutable: the response
* body is a one-shot value that may be consumed only once. All other properties
* are immutable.
*
* <h3>Warning: Experimental OkHttp 2.0 API</h3>
* This class is in beta. APIs are subject to change!
*/
/* OkHttp 2.0: public */ final class Response {
private final Request request;
private final int code;
private final RawHeaders headers;
private final Body body;
private final Response redirectedBy;
private Response(Builder builder) {
this.request = builder.request;
this.code = builder.code;
this.headers = new RawHeaders(builder.headers);
this.body = builder.body;
this.redirectedBy = builder.redirectedBy;
}
/**
* The wire-level request that initiated this HTTP response. This is usually
* <strong>not</strong> the same request instance provided to the HTTP client:
* <ul>
* <li>It may be transformed by the HTTP client. For example, the client
* may have added its own {@code Content-Encoding} header to enable
* response compression.
* <li>It may be the request generated in response to an HTTP redirect.
* In this case the request URL may be different than the initial
* request URL.
* </ul>
*/
public Request request() {
return request;
}
public int code() {
return code;
}
public String header(String name) {
return header(name, null);
}
public String header(String name, String defaultValue) {
String result = headers.get(name);
return result != null ? result : defaultValue;
}
public List<String> headers(String name) {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
public int headerCount() {
return headers.length();
}
public String headerName(int index) {
return headers.getFieldName(index);
}
RawHeaders rawHeaders() {
return new RawHeaders(headers);
}
public String headerValue(int index) {
return headers.getValue(index);
}
public Body body() {
return body;
}
/**
* Returns the response for the HTTP redirect that triggered this response, or
* null if this response wasn't triggered by an automatic redirect. The body
* of the returned response should not be read because it has already been
* consumed by the redirecting client.
*/
public Response redirectedBy() {
return redirectedBy;
}
public abstract static class Body {
/** Multiple calls to {@link #charStream()} must return the same instance. */
private Reader reader;
/**
* Returns true if further data from this response body should be read at
* this time. For asynchronous transports like SPDY and HTTP/2.0, this will
* return false once all locally-available body bytes have been read.
*
* <p>Clients with many concurrent downloads can use this method to reduce
* the number of idle threads blocking on reads. See {@link
* Receiver#onResponse} for details.
*/
// <h3>Body.ready() vs. InputStream.available()</h3>
// TODO: Can we fix response bodies to implement InputStream.available well?
// The deflater implementation is broken by default but we could do better.
public abstract boolean ready() throws IOException;
public abstract MediaType contentType();
/**
* Returns the number of bytes in that will returned by {@link #bytes}, or
* {@link #byteStream}, or -1 if unknown.
*/
public abstract long contentLength();
public abstract InputStream byteStream() throws IOException;
public final byte[] bytes() throws IOException {
long contentLength = contentLength();
if (contentLength > Integer.MAX_VALUE) {
throw new IOException("Cannot buffer entire body for content length: " + contentLength);
}
if (contentLength != -1) {
byte[] content = new byte[(int) contentLength];
InputStream in = byteStream();
Util.readFully(in, content);
if (in.read() != -1) throw new IOException("Content-Length and stream length disagree");
return content;
} else {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Util.copy(byteStream(), out);
return out.toByteArray();
}
}
/**
* Returns the response as a character stream decoded with the charset
* of the Content-Type header. If that header is either absent or lacks a
* charset, this will attempt to decode the response body as UTF-8.
*/
public final Reader charStream() throws IOException {
if (reader == null) {
reader = new InputStreamReader(byteStream(), charset());
}
return reader;
}
/**
* Returns the response as a string decoded with the charset of the
* Content-Type header. If that header is either absent or lacks a charset,
* this will attempt to decode the response body as UTF-8.
*/
public final String string() throws IOException {
return new String(bytes(), charset().name());
}
private Charset charset() {
MediaType contentType = contentType();
return contentType != null ? contentType.charset(UTF_8) : UTF_8;
}
}
public interface Receiver {
/**
* Called when the request could not be executed due to a connectivity
* problem or timeout. Because networks can fail during an exchange, it is
* possible that the remote server accepted the request before the failure.
*/
void onFailure(Failure failure);
/**
* Called when the HTTP response was successfully returned by the remote
* server. The receiver may proceed to read the response body with the
* response's {@link #body} method.
*
* <p>Note that transport-layer success (receiving a HTTP response code,
* headers and body) does not necessarily indicate application-layer
* success: {@code response} may still indicate an unhappy HTTP response
* code like 404 or 500.
*
* <h3>Non-blocking responses</h3>
*
* <p>Receivers do not need to block while waiting for the response body to
* download. Instead, they can get called back as data arrives. Use {@link
* Body#ready} to check if bytes should be read immediately. While there is
* data ready, read it. If there isn't, return false: receivers will be
* called back with {@code onResponse()} as additional data is downloaded.
*
* <p>Return true to indicate that the receiver has finished handling the
* response body. If the response body has unread data, it will be
* discarded.
*
* <p>When the response body has been fully consumed the returned value is
* undefined.
*
* <p>The current implementation of {@link Body#ready} always returns true
* when the underlying transport is HTTP/1. This results in blocking on that
* transport. For effective non-blocking your server must support SPDY or
* HTTP/2.
*/
boolean onResponse(Response response) throws IOException;
}
public static class Builder {
private final Request request;
private final int code;
private RawHeaders headers = new RawHeaders();
private Body body;
private Response redirectedBy;
public Builder(Request request, int code) {
if (request == null) throw new IllegalArgumentException("request == null");
if (code <= 0) throw new IllegalArgumentException("code <= 0");
this.request = request;
this.code = code;
}
/**
* Sets the header named {@code name} to {@code value}. If this request
* already has any headers with that name, they are all replaced.
*/
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Set-Cookie".
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
return this;
}
Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
return this;
}
public Builder body(Body body) {
this.body = body;
return this;
}
public Builder redirectedBy(Response redirectedBy) {
this.redirectedBy = redirectedBy;
return this;
}
public Response build() {
if (request == null) throw new IllegalStateException("Response has no request.");
if (code == -1) throw new IllegalStateException("Response has no code.");
return new Response(this);
}
}
}

0
framework/src/com/squareup/okhttp/ResponseSource.java Normal file → Executable file
View File

6
framework/src/com/squareup/okhttp/Route.java Normal file → Executable file
View File

@ -59,13 +59,13 @@ public class Route {
return inetSocketAddress;
}
/** Returns true if this route uses modern tls. */
/** Returns true if this route uses modern TLS. */
public boolean isModernTls() {
return modernTls;
}
/** Returns a copy of this route with flipped tls mode. */
public Route flipTlsMode() {
/** Returns a copy of this route with flipped TLS mode. */
Route flipTlsMode() {
return new Route(address, proxy, inetSocketAddress, !modernTls);
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
/**
* A blacklist of failed routes to avoid when creating a new connection to a
* target address. This is used so that OkHttp can learn from its mistakes: if
* there was a failure attempting to connect to a specific IP address, proxy
* server or TLS mode, that failure is remembered and alternate routes are
* preferred.
*/
public final class RouteDatabase {
private final Set<Route> failedRoutes = new LinkedHashSet<Route>();
/** Records a failure connecting to {@code failedRoute}. */
public synchronized void failed(Route failedRoute, IOException failure) {
failedRoutes.add(failedRoute);
if (!(failure instanceof SSLHandshakeException)) {
// If the problem was not related to SSL then it will also fail with
// a different TLS mode therefore we can be proactive about it.
failedRoutes.add(failedRoute.flipTlsMode());
}
}
/** Records success connecting to {@code failedRoute}. */
public synchronized void connected(Route route) {
failedRoutes.remove(route);
}
/** Returns true if {@code route} has failed recently and should be avoided. */
public synchronized boolean shouldPostpone(Route route) {
return failedRoutes.contains(route);
}
public synchronized int failedRoutesCount() {
return failedRoutes.size();
}
}

0
framework/src/com/squareup/okhttp/TunnelRequest.java Normal file → Executable file
View File

View File

0
framework/src/com/squareup/okhttp/internal/Base64.java Normal file → Executable file
View File

View File

0
framework/src/com/squareup/okhttp/internal/Dns.java Normal file → Executable file
View File

View File

@ -20,10 +20,10 @@ package com.squareup.okhttp.internal;
* Runnable implementation which always sets its thread name.
*/
public abstract class NamedRunnable implements Runnable {
private String name;
private final String name;
public NamedRunnable(String name) {
this.name = name;
public NamedRunnable(String format, Object... args) {
this.name = String.format(format, args);
}
@Override public final void run() {

117
framework/src/com/squareup/okhttp/internal/Platform.java Normal file → Executable file
View File

@ -16,7 +16,6 @@
*/
package com.squareup.okhttp.internal;
import com.squareup.okhttp.OkHttpClient;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
@ -25,7 +24,7 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.NetworkInterface;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
@ -57,6 +56,11 @@ public class Platform {
return PLATFORM;
}
/** Prefix used on custom headers. */
public String getPrefix() {
return "OkHttp";
}
public void logW(String warning) {
System.out.println(warning);
}
@ -99,6 +103,11 @@ public class Platform {
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
}
public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
socket.connect(address, connectTimeout);
}
/**
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
* value blocks. This throws an {@link UnsupportedOperationException} on
@ -125,33 +134,21 @@ public class Platform {
}
}
/**
* Returns the maximum transmission unit of the network interface used by
* {@code socket}, or a reasonable default if this platform doesn't expose the
* MTU to the application layer.
*
* <p>The returned value should only be used as an optimization; such as to
* size buffers efficiently.
*/
public int getMtu(Socket socket) throws IOException {
return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
}
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
Method getMtu;
try {
getMtu = NetworkInterface.class.getMethod("getMTU");
} catch (NoSuchMethodException e) {
return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
}
// Attempt to find Android 2.3+ APIs.
Class<?> openSslSocketClass;
Method setUseSessionTickets;
Method setHostname;
try {
openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
try {
openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
} catch (ClassNotFoundException ignored) {
// Older platform before being unbundled.
openSslSocketClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
}
setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
@ -159,10 +156,10 @@ public class Platform {
try {
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
return new Android41(openSslSocketClass, setUseSessionTickets, setHostname,
setNpnProtocols, getNpnSelectedProtocol);
} catch (NoSuchMethodException ignored) {
return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
}
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
@ -179,55 +176,43 @@ public class Platform {
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
serverProviderClass);
return new JdkWithJettyNpnPlatform(
putMethod, getMethod, clientProviderClass, serverProviderClass);
} catch (ClassNotFoundException ignored) {
// NPN isn't on the classpath.
} catch (NoSuchMethodException ignored) {
// The NPN version isn't what we expect.
}
return getMtu != null ? new Java5(getMtu) : new Platform();
return new Platform();
}
private static class Java5 extends Platform {
private final Method getMtu;
private Java5(Method getMtu) {
this.getMtu = getMtu;
}
@Override public int getMtu(Socket socket) throws IOException {
try {
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
socket.getLocalAddress());
return (Integer) getMtu.invoke(networkInterface);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
throw new RuntimeException(e.getCause());
}
}
}
/**
* Android version 2.3 and newer support TLS session tickets and server name
* indication (SNI).
*/
private static class Android23 extends Java5 {
/** 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(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
Method setHostname) {
super(getMtu);
private Android23(
Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname) {
this.openSslSocketClass = openSslSocketClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
}
@Override public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
try {
socket.connect(address, connectTimeout);
} catch (SecurityException se) {
// Before android 4.3, socket.connect could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
ioException.initCause(se);
throw ioException;
}
}
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
super.enableTlsExtensions(socket, uriHost);
if (openSslSocketClass.isInstance(socket)) {
@ -249,9 +234,9 @@ public class Platform {
private final Method setNpnProtocols;
private final Method getNpnSelectedProtocol;
private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
Method setNpnProtocols, Method getNpnSelectedProtocol) {
super(openSslSocketClass, setUseSessionTickets, setHostname);
this.setNpnProtocols = setNpnProtocols;
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
}
@ -283,19 +268,15 @@ public class Platform {
}
}
/**
* OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
* path.
*/
private static class JdkWithJettyNpnPlatform extends Java5 {
/** 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 getMtu, Method putMethod, Method getMethod,
Class<?> clientProviderClass, Class<?> serverProviderClass) {
super(getMtu);
public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
Class<?> serverProviderClass) {
this.putMethod = putMethod;
this.getMethod = getMethod;
this.clientProviderClass = clientProviderClass;
@ -328,7 +309,7 @@ public class Platform {
JettyNpnProvider provider =
(JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
if (!provider.unsupported && provider.selected == null) {
Logger logger = Logger.getLogger(OkHttpClient.class.getName());
Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
logger.log(Level.INFO,
"NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
return null;

View File

@ -146,8 +146,7 @@ public class StrictLineReader implements Closeable {
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@Override
public String toString() {
@Override public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());

67
framework/src/com/squareup/okhttp/internal/Util.java Normal file → Executable file
View File

@ -24,11 +24,19 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URL;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
/** Junk drawer of utility methods. */
@ -46,6 +54,9 @@ public final class Util {
public static final Charset UTF_8 = Charset.forName("UTF-8");
private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
private static final char[] DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private Util() {
}
@ -126,6 +137,21 @@ public final class Util {
}
}
/**
* Closes {@code serverSocket}, ignoring any checked exceptions. Does nothing if
* {@code serverSocket} is null.
*/
public static void closeQuietly(ServerSocket serverSocket) {
if (serverSocket != null) {
try {
serverSocket.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.
@ -258,6 +284,8 @@ public final class Util {
* buffer.
*/
public static long skipByReading(InputStream in, long byteCount) throws IOException {
if (byteCount == 0) return 0L;
// acquire the shared skip buffer.
byte[] buffer = skipBuffer.getAndSet(null);
if (buffer == null) {
@ -324,4 +352,43 @@ public final class Util {
}
return result.toString();
}
/** Returns a 32 character string containing a hash of {@code s}. */
public static String hash(String s) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5bytes = messageDigest.digest(s.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);
}
/** Returns an immutable copy of {@code list}. */
public static <T> List<T> immutableList(List<T> list) {
return Collections.unmodifiableList(new ArrayList<T>(list));
}
public static ThreadFactory daemonThreadFactory(final String name) {
return new ThreadFactory() {
@Override public Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, name);
result.setDaemon(true);
return result;
}
};
}
}

View File

@ -79,11 +79,11 @@ abstract class AbstractHttpInputStream extends InputStream {
* 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 {
protected final void endOfInput() throws IOException {
if (cacheRequest != null) {
cacheBody.close();
}
httpEngine.release(streamCancelled);
httpEngine.release(false);
}
/**

View File

@ -1,40 +0,0 @@
/*
* 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");
}
}
}

View File

@ -27,11 +27,11 @@ final class HeaderParser {
int pos = 0;
while (pos < value.length()) {
int tokenStart = pos;
pos = skipUntil(value, pos, "=,");
pos = skipUntil(value, pos, "=,;");
String directive = value.substring(tokenStart, pos).trim();
if (pos == value.length() || value.charAt(pos) == ',') {
pos++; // consume ',' (if necessary)
if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') {
pos++; // consume ',' or ';' (if necessary)
handler.handle(directive, null);
continue;
}
@ -52,7 +52,7 @@ final class HeaderParser {
// unquoted string
} else {
int parameterStart = pos;
pos = skipUntil(value, pos, ",");
pos = skipUntil(value, pos, ",;");
parameter = value.substring(parameterStart, pos).trim();
}

View File

@ -16,7 +16,8 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.Base64;
import com.squareup.okhttp.OkAuthenticator;
import com.squareup.okhttp.OkAuthenticator.Challenge;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetAddress;
@ -27,11 +28,57 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import static com.squareup.okhttp.OkAuthenticator.Credential;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
/** Handles HTTP authentication headers from origin and proxy servers. */
public final class HttpAuthenticator {
/** Uses the global authenticator to get the password. */
public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
@Override public Credential authenticate(
Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
for (Challenge challenge : challenges) {
if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
continue;
}
PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(url.getHost(),
getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
challenge.getRealm(), challenge.getScheme(), url, Authenticator.RequestorType.SERVER);
if (auth != null) {
return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
}
}
return null;
}
@Override public Credential authenticateProxy(
Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
for (Challenge challenge : challenges) {
if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
continue;
}
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
Authenticator.RequestorType.PROXY);
if (auth != null) {
return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
}
}
return null;
}
private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
? ((InetSocketAddress) proxy.address()).getAddress()
: InetAddress.getByName(url.getHost());
}
};
private HttpAuthenticator() {
}
@ -41,68 +88,33 @@ public final class HttpAuthenticator {
* @return true if credentials have been added to successorRequestHeaders
* and another request should be attempted.
*/
public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
throw new IllegalArgumentException();
public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
throws IOException {
String responseField;
String requestField;
if (responseCode == HTTP_UNAUTHORIZED) {
responseField = "WWW-Authenticate";
requestField = "Authorization";
} else if (responseCode == HTTP_PROXY_AUTH) {
responseField = "Proxy-Authenticate";
requestField = "Proxy-Authorization";
} else {
throw new IllegalArgumentException(); // TODO: ProtocolException?
}
// Keep asking for username/password until authorized.
String challengeHeader =
responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
if (credentials == null) {
return false; // Could not find credentials so end the request cycle.
}
// Add authorization credentials, bypassing the already-connected check.
String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
successorRequestHeaders.set(fieldName, credentials);
return true;
}
/**
* Returns the authorization credentials that may satisfy the challenge.
* Returns null if a challenge header was not provided or if credentials
* were not available.
*/
private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
Proxy proxy, URL url) throws IOException {
List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
if (challenges.isEmpty()) {
return null;
return false; // Could not find a challenge so end the request cycle.
}
for (Challenge challenge : challenges) {
// Use the global authenticator to get the password.
PasswordAuthentication auth;
if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
} else {
auth = Authenticator.requestPasswordAuthentication(url.getHost(),
getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
challenge.scheme, url, Authenticator.RequestorType.SERVER);
}
if (auth == null) {
continue;
}
// Use base64 to encode the username and password.
String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
String encoded = Base64.encode(bytes);
return challenge.scheme + " " + encoded;
Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
? authenticator.authenticateProxy(proxy, url, challenges)
: authenticator.authenticate(proxy, url, challenges);
if (credential == null) {
return false; // Could not satisfy the challenge so end the request cycle.
}
return null;
}
private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
// Add authorization credentials, bypassing the already-connected check.
successorRequestHeaders.set(requestField, credential.getHeaderValue());
return true;
}
/**
@ -134,7 +146,7 @@ public final class HttpAuthenticator {
// 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())) {
if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
break; // Unexpected challenge parameter; give up!
}
@ -151,25 +163,4 @@ public final class HttpAuthenticator {
}
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();
}
}
}

View File

@ -36,14 +36,13 @@ final class HttpDate {
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"));
rfc1123.setTimeZone(TimeZone.getTimeZone("GMT"));
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}. */
private static final String[] BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS = new String[] {
"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",
@ -54,19 +53,26 @@ final class HttpDate {
/* 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.
*/
private static final DateFormat[] BROWSER_COMPATIBLE_DATE_FORMATS =
new DateFormat[BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length];
/** 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) {
} catch (ParseException ignored) {
}
for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
try {
return new SimpleDateFormat(formatString, Locale.US).parse(value);
} catch (ParseException ignore) {
synchronized (BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS) {
for (int i = 0, count = BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length; i < count; i++) {
DateFormat format = BROWSER_COMPATIBLE_DATE_FORMATS[i];
if (format == null) {
format = new SimpleDateFormat(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS[i], Locale.US);
BROWSER_COMPATIBLE_DATE_FORMATS[i] = format;
}
try {
return format.parse(value);
} catch (ParseException ignored) {
}
}
}
return null;

View File

@ -19,6 +19,8 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns;
@ -31,6 +33,7 @@ import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
@ -85,7 +88,8 @@ public class HttpEngine {
};
public static final int HTTP_CONTINUE = 100;
protected final HttpURLConnectionImpl policy;
protected final Policy policy;
protected final OkHttpClient client;
protected final String method;
@ -106,6 +110,9 @@ public class HttpEngine {
/** The time when the request headers were written, or -1 if they haven't been written yet. */
long sentRequestMillis = -1;
/** Whether the connection has been established. */
boolean connected;
/**
* True if this client added an "Accept-Encoding: gzip" header field and is
* therefore responsible for also decompressing the transfer stream.
@ -137,14 +144,15 @@ public class HttpEngine {
/**
* @param requestHeaders the client's supplied request headers. This class
* creates a private copy that it can mutate.
* 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.
* 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,
public HttpEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
this.client = client;
this.policy = policy;
this.method = method;
this.connection = connection;
@ -175,8 +183,9 @@ public class HttpEngine {
prepareRawRequestHeaders();
initResponseSource();
if (policy.responseCache != null) {
policy.responseCache.trackResponse(responseSource);
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache != null) {
responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
@ -196,8 +205,7 @@ public class HttpEngine {
if (responseSource.requiresConnection()) {
sendSocketRequest();
} else if (connection != null) {
policy.connectionPool.recycle(connection);
policy.getFailedRoutes().remove(connection.getRoute());
client.getConnectionPool().recycle(connection);
connection = null;
}
}
@ -208,15 +216,14 @@ public class HttpEngine {
*/
private void initResponseSource() throws IOException {
responseSource = ResponseSource.NETWORK;
if (!policy.getUseCaches() || policy.responseCache == null) {
return;
}
if (!policy.getUseCaches()) return;
CacheResponse candidate =
policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
if (candidate == null) {
return;
}
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache == null) return;
CacheResponse candidate = responseCache.get(
uri, method, requestHeaders.getHeaders().toMultimap(false));
if (candidate == null) return;
Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
cachedResponseBody = candidate.getBody();
@ -274,22 +281,24 @@ public class HttpEngine {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
if (uri.getScheme().equalsIgnoreCase("https")) {
sslSocketFactory = policy.sslSocketFactory;
hostnameVerifier = policy.hostnameVerifier;
sslSocketFactory = client.getSslSocketFactory();
hostnameVerifier = client.getHostnameVerifier();
}
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
hostnameVerifier, policy.requestedProxy);
routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
Dns.DEFAULT, policy.getFailedRoutes());
hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports());
routeSelector = new RouteSelector(address, uri, client.getProxySelector(),
client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase());
}
connection = routeSelector.next();
connection = routeSelector.next(method);
if (!connection.isConnected()) {
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
policy.connectionPool.maybeShare(connection);
policy.getFailedRoutes().remove(connection.getRoute());
connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
client.getConnectionPool().maybeShare(connection);
client.getRoutesDatabase().connected(connection.getRoute());
} else if (!connection.isSpdy()) {
connection.updateReadTimeout(client.getReadTimeout());
}
connected(connection);
if (connection.getRoute().getProxy() != policy.requestedProxy) {
if (connection.getRoute().getProxy() != client.getProxy()) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
}
@ -300,6 +309,8 @@ public class HttpEngine {
* pool. Subclasses use this hook to get a reference to the TLS data.
*/
protected void connected(Connection connection) {
policy.setSelectedProxy(connection.getRoute().getProxy());
connected = true;
}
/**
@ -328,7 +339,7 @@ public class HttpEngine {
}
boolean hasRequestBody() {
return method.equals("POST") || method.equals("PUT");
return method.equals("POST") || method.equals("PUT") || method.equals("PATCH");
}
/** Returns the request body or null if this request doesn't have a body. */
@ -387,17 +398,20 @@ public class HttpEngine {
private void maybeCache() throws IOException {
// Are we caching at all?
if (!policy.getUseCaches() || policy.responseCache == null) {
return;
}
if (!policy.getUseCaches()) return;
OkResponseCache responseCache = client.getOkResponseCache();
if (responseCache == null) return;
HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
// Should we cache this response for this request?
if (!responseHeaders.isCacheable(requestHeaders)) {
responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
return;
}
// Offer this request to the cache.
cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
cacheRequest = responseCache.put(uri, connectionToCache);
}
/**
@ -409,7 +423,7 @@ public class HttpEngine {
public final void automaticallyReleaseConnectionToPool() {
automaticallyReleaseConnectionToPool = true;
if (connection != null && connectionReleased) {
policy.connectionPool.recycle(connection);
client.getConnectionPool().recycle(connection);
connection = null;
}
}
@ -419,7 +433,7 @@ public class HttpEngine {
* closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
* the connection will be used to follow a redirect.
*/
public final void release(boolean streamCancelled) {
public final void release(boolean streamCanceled) {
// If the response body comes from the cache, close it.
if (responseBodyIn == cachedResponseBody) {
Util.closeQuietly(responseBodyIn);
@ -428,12 +442,12 @@ public class HttpEngine {
if (!connectionReleased && connection != null) {
connectionReleased = true;
if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
responseTransferIn)) {
if (transport == null
|| !transport.makeReusable(streamCanceled, requestBodyOut, responseTransferIn)) {
Util.closeQuietly(connection);
connection = null;
} else if (automaticallyReleaseConnectionToPool) {
policy.connectionPool.recycle(connection);
client.getConnectionPool().recycle(connection);
connection = null;
}
}
@ -521,7 +535,7 @@ public class HttpEngine {
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
}
CookieHandler cookieHandler = policy.cookieHandler;
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
requestHeaders.addCookies(
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
@ -635,9 +649,17 @@ public class HttpEngine {
if (cachedResponseHeaders.validate(responseHeaders)) {
release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
setResponse(combinedHeaders, cachedResponseBody);
policy.responseCache.trackConditionalCacheHit();
policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
this.responseHeaders = combinedHeaders;
// Update the cache after applying the combined headers but before initializing the content
// stream, otherwise the Content-Encoding header (if present) will be stripped from the
// combined headers and not end up in the cache file if transparent gzip compression is
// turned on.
OkResponseCache responseCache = client.getOkResponseCache();
responseCache.trackConditionalCacheHit();
responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
initContentStream(cachedResponseBody);
return;
} else {
Util.closeQuietly(cachedResponseBody);
@ -656,7 +678,7 @@ public class HttpEngine {
}
public void receiveHeaders(RawHeaders headers) throws IOException {
CookieHandler cookieHandler = policy.cookieHandler;
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
cookieHandler.put(uri, headers.toMultimap(true));
}

View File

@ -1,608 +0,0 @@
/*
* 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();
}
}
}

View File

@ -78,18 +78,23 @@ public final class HttpTransport implements Transport {
}
// Stream a request body of a known length.
int fixedContentLength = httpEngine.policy.getFixedContentLength();
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
writeRequestHeaders();
return new FixedLengthOutputStream(requestOut, fixedContentLength);
}
long contentLength = httpEngine.requestHeaders.getContentLength();
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Use setFixedLengthStreamingMode() or "
+ "setChunkedStreamingMode() for requests larger than 2 GiB.");
}
// Buffer a request body of a known length.
int contentLength = httpEngine.requestHeaders.getContentLength();
if (contentLength != -1) {
writeRequestHeaders();
return new RetryableOutputStream(contentLength);
return new RetryableOutputStream((int) contentLength);
}
// Buffer a request body of an unknown length. Don't write request
@ -127,15 +132,18 @@ public final class HttpTransport implements Transport {
}
@Override public ResponseHeaders readResponseHeaders() throws IOException {
RawHeaders headers = RawHeaders.fromBytes(socketIn);
httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
httpEngine.receiveHeaders(headers);
return new ResponseHeaders(httpEngine.uri, headers);
RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
httpEngine.receiveHeaders(rawHeaders);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("http/1.1");
return headers;
}
public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
InputStream responseBodyIn) {
if (streamCancelled) {
if (streamCanceled) {
return false;
}
@ -169,6 +177,10 @@ public final class HttpTransport implements Transport {
* 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.
*
* <p>A stream may be discarded to encourage response caching (a response
* cannot be cached unless it is consumed completely) or to enable connection
* reuse.
*/
private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
Connection connection = httpEngine.connection;
@ -212,9 +224,9 @@ public final class HttpTransport implements Transport {
/** An HTTP body with a fixed length known in advance. */
private static final class FixedLengthOutputStream extends AbstractOutputStream {
private final OutputStream socketOut;
private int bytesRemaining;
private long bytesRemaining;
private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
private FixedLengthOutputStream(OutputStream socketOut, long bytesRemaining) {
this.socketOut = socketOut;
this.bytesRemaining = bytesRemaining;
}
@ -358,14 +370,14 @@ public final class HttpTransport implements Transport {
/** An HTTP body with a fixed length specified in advance. */
private static class FixedLengthInputStream extends AbstractHttpInputStream {
private int bytesRemaining;
private long bytesRemaining;
public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
int length) throws IOException {
long length) throws IOException {
super(is, httpEngine, cacheRequest);
bytesRemaining = length;
if (bytesRemaining == 0) {
endOfInput(false);
endOfInput();
}
}
@ -375,7 +387,7 @@ public final class HttpTransport implements Transport {
if (bytesRemaining == 0) {
return -1;
}
int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
int read = in.read(buffer, offset, (int) Math.min(count, bytesRemaining));
if (read == -1) {
unexpectedEndOfInput(); // the server didn't supply the promised content length
throw new ProtocolException("unexpected end of stream");
@ -383,14 +395,14 @@ public final class HttpTransport implements Transport {
bytesRemaining -= read;
cacheWrite(buffer, offset, read);
if (bytesRemaining == 0) {
endOfInput(false);
endOfInput();
}
return read;
}
@Override public int available() throws IOException {
checkNotClosed();
return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
return bytesRemaining == 0 ? 0 : (int) Math.min(in.available(), bytesRemaining);
}
@Override public void close() throws IOException {
@ -460,7 +472,7 @@ public final class HttpTransport implements Transport {
RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
httpEngine.receiveHeaders(rawResponseHeaders);
endOfInput(false);
endOfInput();
}
}

View File

@ -18,33 +18,28 @@
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.Route;
import com.squareup.okhttp.internal.AbstractOutputStream;
import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
import com.squareup.okhttp.internal.Platform;
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.SocketPermission;
import java.net.URL;
import java.security.Permission;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@ -62,10 +57,10 @@ import static com.squareup.okhttp.internal.Util.getEffectivePort;
* 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 {
public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
/** Numeric status code, 307: Temporary Redirect. */
static final int HTTP_TEMP_REDIRECT = 307;
public static final int HTTP_TEMP_REDIRECT = 307;
/**
* How many redirects should we follow? Chrome follows 21; Firefox, curl,
@ -73,51 +68,19 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/
private static final int MAX_REDIRECTS = 20;
/**
* The minimum number of request body bytes to transmit before we're willing
* to let a routine {@link IOException} bubble up to the user. This is used to
* size a buffer for data that will be replayed upon error.
*/
private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
private final boolean followProtocolRedirects;
/** The proxy requested by the client, or null for a proxy to be selected automatically. */
final Proxy requestedProxy;
final ProxySelector proxySelector;
final CookieHandler cookieHandler;
final OkResponseCache responseCache;
final ConnectionPool connectionPool;
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
SSLSocketFactory sslSocketFactory;
HostnameVerifier hostnameVerifier;
final Set<Route> failedRoutes;
final OkHttpClient client;
private final RawHeaders rawRequestHeaders = new RawHeaders();
/** Like the superclass field of the same name, but a long and available on all platforms. */
private long fixedContentLength = -1;
private int redirectionCount;
private FaultRecoveringOutputStream faultRecoveringRequestBody;
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
private Proxy selectedProxy;
public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
public HttpURLConnectionImpl(URL url, OkHttpClient client) {
super(url);
this.followProtocolRedirects = client.getFollowProtocolRedirects();
this.failedRoutes = failedRoutes;
this.requestedProxy = client.getProxy();
this.proxySelector = client.getProxySelector();
this.cookieHandler = client.getCookieHandler();
this.connectionPool = client.getConnectionPool();
this.sslSocketFactory = client.getSslSocketFactory();
this.hostnameVerifier = client.getHostnameVerifier();
this.responseCache = responseCache;
}
Set<Route> getFailedRoutes() {
return failedRoutes;
this.client = client;
}
@Override public final void connect() throws IOException {
@ -197,7 +160,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
try {
return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
} catch (IOException e) {
return null;
return Collections.emptyMap();
}
}
@ -241,29 +204,14 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
throw new ProtocolException("cannot write request body after response has been read");
}
if (faultRecoveringRequestBody == null) {
faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
@Override protected OutputStream replacementStream(IOException e) throws IOException {
if (httpEngine.getRequestBody() instanceof AbstractOutputStream
&& ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
return null; // Don't recover once the underlying stream has been closed.
}
if (handleFailure(e)) {
return httpEngine.getRequestBody();
}
return null; // This is a permanent failure.
}
};
}
return faultRecoveringRequestBody;
return out;
}
@Override public final Permission getPermission() throws IOException {
String hostName = getURL().getHost();
int hostPort = Util.getEffectivePort(getURL());
if (usingProxy()) {
InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address();
hostName = proxyAddress.getHostName();
hostPort = proxyAddress.getPort();
}
@ -277,6 +225,22 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return rawRequestHeaders.get(field);
}
@Override public void setConnectTimeout(int timeoutMillis) {
client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
@Override public int getConnectTimeout() {
return client.getConnectTimeout();
}
@Override public void setReadTimeout(int timeoutMillis) {
client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
@Override public int getReadTimeout() {
return client.getReadTimeout();
}
private void initHttpEngine() throws IOException {
if (httpEngineFailure != null) {
throw httpEngineFailure;
@ -290,8 +254,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
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
} else if (!method.equals("POST") && !method.equals("PUT") && !method.equals("PATCH")) {
// If the request method is neither POST nor PUT nor PATCH, then you're not writing
throw new ProtocolException(method + " does not support writing");
}
}
@ -302,17 +266,16 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
}
protected HttpURLConnection getHttpConnectionToCache() {
@Override public 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);
return new HttpEngine(client, this, method, requestHeaders, connection, requestBody);
} else if (url.getProtocol().equals("https")) {
return new HttpsURLConnectionImpl.HttpsEngine(
this, method, requestHeaders, connection, requestBody);
return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
} else {
throw new AssertionError();
}
@ -348,7 +311,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// 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();
int responseCode = httpEngine.getResponseCode();
if (responseCode == HTTP_MULT_CHOICE
|| responseCode == HTTP_MOVED_PERM
|| responseCode == HTTP_MOVED_TEMP
@ -358,8 +321,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
throw new HttpRetryException("Cannot retry streamed HTTP body",
httpEngine.getResponseCode());
throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
}
if (retry == Retry.DIFFERENT_CONNECTION) {
@ -370,6 +332,11 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
(RetryableOutputStream) requestBody);
if (requestBody == null) {
// Drop the Content-Length header when redirected from POST to GET.
httpEngine.getRequestHeaders().removeContentLength();
}
}
}
@ -384,6 +351,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
if (readResponse) {
httpEngine.readResponse();
}
return true;
} catch (IOException e) {
if (handleFailure(e)) {
@ -407,8 +375,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
OutputStream requestBody = httpEngine.getRequestBody();
boolean canRetryRequestBody = requestBody == null
|| requestBody instanceof RetryableOutputStream
|| (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
|| requestBody instanceof RetryableOutputStream;
if (routeSelector == null && httpEngine.connection == null // No connection.
|| routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
|| !isRecoverable(e)
@ -418,15 +385,9 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
httpEngine.release(true);
RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
? (RetryableOutputStream) requestBody
: null;
RetryableOutputStream retryableOutputStream = (RetryableOutputStream) requestBody;
httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
httpEngine.sendRequest();
faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
}
return true;
}
@ -451,13 +412,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
/**
* Returns the retry action to take for the current response headers. The
* headers, proxy and target URL or this connection may be adjusted to
* headers, proxy and target URL for this connection may be adjusted to
* prepare for a follow up request.
*/
private Retry processResponseHeaders() throws IOException {
Proxy selectedProxy = httpEngine.connection != null
? httpEngine.connection.getRoute().getProxy()
: requestedProxy;
: client.getProxy();
final int responseCode = getResponseCode();
switch (responseCode) {
case HTTP_PROXY_AUTH:
@ -466,8 +427,9 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
// fall-through
case HTTP_UNAUTHORIZED:
boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
selectedProxy, url);
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
case HTTP_MULT_CHOICE:
@ -496,7 +458,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return Retry.NONE; // Don't follow redirects to unsupported protocols.
}
boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
if (!sameProtocol && !followProtocolRedirects) {
if (!sameProtocol && !client.getFollowProtocolRedirects()) {
return Retry.NONE; // This client doesn't follow redirects across protocols.
}
boolean sameHost = previousUrl.getHost().equals(url.getHost());
@ -513,17 +475,29 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
final int getFixedContentLength() {
@Override public final long getFixedContentLength() {
return fixedContentLength;
}
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
final int getChunkLength() {
@Override public final int getChunkLength() {
return chunkLength;
}
@Override public final boolean usingProxy() {
return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
if (selectedProxy != null) {
return isValidNonDirectProxy(selectedProxy);
}
// This behavior is a bit odd (but is probably justified by the
// oddness of the APIs involved). Before a connection is established,
// this method will return true only if this connection was explicitly
// opened with a Proxy. We don't attempt to query the ProxySelector
// at all.
return isValidNonDirectProxy(client.getProxy());
}
private static boolean isValidNonDirectProxy(Proxy proxy) {
return proxy != null && proxy.type() != Proxy.Type.DIRECT;
}
@Override public String getResponseMessage() throws IOException {
@ -541,7 +515,21 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
if (field == null) {
throw new NullPointerException("field == null");
}
rawRequestHeaders.set(field, newValue);
if (newValue == null) {
// Silently ignore null header values for backwards compatibility with older
// android versions as well as with other URLConnection implementations.
//
// Some implementations send a malformed HTTP header when faced with
// such requests, we respect the spec and ignore the header.
Platform.get().logW("Ignoring header " + field + " because its value was null.");
return;
}
if ("X-Android-Transports".equals(field)) {
setTransports(newValue, false /* append */);
} else {
rawRequestHeaders.set(field, newValue);
}
}
@Override public final void addRequestProperty(String field, String value) {
@ -551,6 +539,52 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
if (field == null) {
throw new NullPointerException("field == null");
}
rawRequestHeaders.add(field, value);
if (value == null) {
// Silently ignore null header values for backwards compatibility with older
// android versions as well as with other URLConnection implementations.
//
// Some implementations send a malformed HTTP header when faced with
// such requests, we respect the spec and ignore the header.
Platform.get().logW("Ignoring header " + field + " because its value was null.");
return;
}
if ("X-Android-Transports".equals(field)) {
setTransports(value, true /* append */);
} else {
rawRequestHeaders.add(field, value);
}
}
/*
* Splits and validates a comma-separated string of transports.
* When append == false, we require that the transport list contains "http/1.1".
*/
private void setTransports(String transportsString, boolean append) {
List<String> transportsList = new ArrayList<String>();
if (append) {
transportsList.addAll(client.getTransports());
}
for (String transport : transportsString.split(",", -1)) {
transportsList.add(transport);
}
client.setTransports(transportsList);
}
@Override public void setFixedLengthStreamingMode(int contentLength) {
setFixedLengthStreamingMode((long) contentLength);
}
// @Override Don't override: this overload method doesn't exist prior to Java 1.7.
public void setFixedLengthStreamingMode(long contentLength) {
if (super.connected) throw new IllegalStateException("Already connected");
if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
this.fixedContentLength = contentLength;
super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
}
@Override public final void setSelectedProxy(Proxy proxy) {
this.selectedProxy = proxy;
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.net.CacheResponse;
import java.net.SecureCacheResponse;
import java.net.URL;
import javax.net.ssl.SSLSocket;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
public final class HttpsEngine extends HttpEngine {
/**
* Stash of HttpsEngine.connection.socket to implement requests like {@code
* HttpsURLConnection#getCipherSuite} even after the connection has been
* recycled.
*/
private SSLSocket sslSocket;
public HttpsEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBody) throws IOException {
super(client, policy, method, requestHeaders, connection, requestBody);
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
}
@Override protected void connected(Connection connection) {
this.sslSocket = (SSLSocket) connection.getSocket();
super.connected(connection);
}
@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 path.
return false;
}
public SSLSocket getSslSocket() {
return sslSocket;
}
@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());
}
}

View File

@ -16,14 +16,11 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
import android.annotation.SuppressLint;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Route;
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;
@ -33,24 +30,20 @@ import java.security.Principal;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
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, OkResponseCache responseCache,
Set<Route> failedRoutes) {
public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
super(url);
delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
delegate = new HttpUrlConnectionDelegate(url, client);
}
@Override public String getCipherSuite() {
@ -120,294 +113,247 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
}
private SSLSocket getSslSocket() {
if (delegate.httpEngine == null || delegate.httpEngine.sentRequestMillis == -1) {
if (delegate.httpEngine == null || !delegate.httpEngine.connected) {
throw new IllegalStateException("Connection has not yet been established");
}
return delegate.httpEngine instanceof HttpsEngine
? ((HttpsEngine) delegate.httpEngine).sslSocket
? ((HttpsEngine) delegate.httpEngine).getSslSocket()
: null; // Not HTTPS! Probably an https:// to http:// redirect.
}
@Override
public void disconnect() {
@Override public void disconnect() {
delegate.disconnect();
}
@Override
public InputStream getErrorStream() {
@Override public InputStream getErrorStream() {
return delegate.getErrorStream();
}
@Override
public String getRequestMethod() {
@Override public String getRequestMethod() {
return delegate.getRequestMethod();
}
@Override
public int getResponseCode() throws IOException {
@Override public int getResponseCode() throws IOException {
return delegate.getResponseCode();
}
@Override
public String getResponseMessage() throws IOException {
@Override public String getResponseMessage() throws IOException {
return delegate.getResponseMessage();
}
@Override
public void setRequestMethod(String method) throws ProtocolException {
@Override public void setRequestMethod(String method) throws ProtocolException {
delegate.setRequestMethod(method);
}
@Override
public boolean usingProxy() {
@Override public boolean usingProxy() {
return delegate.usingProxy();
}
@Override
public boolean getInstanceFollowRedirects() {
@Override public boolean getInstanceFollowRedirects() {
return delegate.getInstanceFollowRedirects();
}
@Override
public void setInstanceFollowRedirects(boolean followRedirects) {
@Override public void setInstanceFollowRedirects(boolean followRedirects) {
delegate.setInstanceFollowRedirects(followRedirects);
}
@Override
public void connect() throws IOException {
@Override public void connect() throws IOException {
connected = true;
delegate.connect();
}
@Override
public boolean getAllowUserInteraction() {
@Override public boolean getAllowUserInteraction() {
return delegate.getAllowUserInteraction();
}
@Override
public Object getContent() throws IOException {
@Override public Object getContent() throws IOException {
return delegate.getContent();
}
@SuppressWarnings("unchecked") // Spec does not generify
@Override
public Object getContent(Class[] types) throws IOException {
@Override public Object getContent(Class[] types) throws IOException {
return delegate.getContent(types);
}
@Override
public String getContentEncoding() {
@Override public String getContentEncoding() {
return delegate.getContentEncoding();
}
@Override
public int getContentLength() {
@Override public int getContentLength() {
return delegate.getContentLength();
}
@Override
public String getContentType() {
@Override public String getContentType() {
return delegate.getContentType();
}
@Override
public long getDate() {
@Override public long getDate() {
return delegate.getDate();
}
@Override
public boolean getDefaultUseCaches() {
@Override public boolean getDefaultUseCaches() {
return delegate.getDefaultUseCaches();
}
@Override
public boolean getDoInput() {
@Override public boolean getDoInput() {
return delegate.getDoInput();
}
@Override
public boolean getDoOutput() {
@Override public boolean getDoOutput() {
return delegate.getDoOutput();
}
@Override
public long getExpiration() {
@Override public long getExpiration() {
return delegate.getExpiration();
}
@Override
public String getHeaderField(int pos) {
@Override public String getHeaderField(int pos) {
return delegate.getHeaderField(pos);
}
@Override
public Map<String, List<String>> getHeaderFields() {
@Override public Map<String, List<String>> getHeaderFields() {
return delegate.getHeaderFields();
}
@Override
public Map<String, List<String>> getRequestProperties() {
@Override public Map<String, List<String>> getRequestProperties() {
return delegate.getRequestProperties();
}
@Override
public void addRequestProperty(String field, String newValue) {
@Override public void addRequestProperty(String field, String newValue) {
delegate.addRequestProperty(field, newValue);
}
@Override
public String getHeaderField(String key) {
@Override public String getHeaderField(String key) {
return delegate.getHeaderField(key);
}
@Override
public long getHeaderFieldDate(String field, long defaultValue) {
@Override public long getHeaderFieldDate(String field, long defaultValue) {
return delegate.getHeaderFieldDate(field, defaultValue);
}
@Override
public int getHeaderFieldInt(String field, int defaultValue) {
@Override public int getHeaderFieldInt(String field, int defaultValue) {
return delegate.getHeaderFieldInt(field, defaultValue);
}
@Override
public String getHeaderFieldKey(int position) {
@Override public String getHeaderFieldKey(int position) {
return delegate.getHeaderFieldKey(position);
}
@Override
public long getIfModifiedSince() {
@Override public long getIfModifiedSince() {
return delegate.getIfModifiedSince();
}
@Override
public InputStream getInputStream() throws IOException {
@Override public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
@Override
public long getLastModified() {
@Override public long getLastModified() {
return delegate.getLastModified();
}
@Override
public OutputStream getOutputStream() throws IOException {
@Override public OutputStream getOutputStream() throws IOException {
return delegate.getOutputStream();
}
@Override
public Permission getPermission() throws IOException {
@Override public Permission getPermission() throws IOException {
return delegate.getPermission();
}
@Override
public String getRequestProperty(String field) {
@Override public String getRequestProperty(String field) {
return delegate.getRequestProperty(field);
}
@Override
public URL getURL() {
@Override public URL getURL() {
return delegate.getURL();
}
@Override
public boolean getUseCaches() {
@Override public boolean getUseCaches() {
return delegate.getUseCaches();
}
@Override
public void setAllowUserInteraction(boolean newValue) {
@Override public void setAllowUserInteraction(boolean newValue) {
delegate.setAllowUserInteraction(newValue);
}
@Override
public void setDefaultUseCaches(boolean newValue) {
@Override public void setDefaultUseCaches(boolean newValue) {
delegate.setDefaultUseCaches(newValue);
}
@Override
public void setDoInput(boolean newValue) {
@Override public void setDoInput(boolean newValue) {
delegate.setDoInput(newValue);
}
@Override
public void setDoOutput(boolean newValue) {
@Override public void setDoOutput(boolean newValue) {
delegate.setDoOutput(newValue);
}
@Override
public void setIfModifiedSince(long newValue) {
@Override public void setIfModifiedSince(long newValue) {
delegate.setIfModifiedSince(newValue);
}
@Override
public void setRequestProperty(String field, String newValue) {
@Override public void setRequestProperty(String field, String newValue) {
delegate.setRequestProperty(field, newValue);
}
@Override
public void setUseCaches(boolean newValue) {
@Override public void setUseCaches(boolean newValue) {
delegate.setUseCaches(newValue);
}
@Override
public void setConnectTimeout(int timeoutMillis) {
@Override public void setConnectTimeout(int timeoutMillis) {
delegate.setConnectTimeout(timeoutMillis);
}
@Override
public int getConnectTimeout() {
@Override public int getConnectTimeout() {
return delegate.getConnectTimeout();
}
@Override
public void setReadTimeout(int timeoutMillis) {
@Override public void setReadTimeout(int timeoutMillis) {
delegate.setReadTimeout(timeoutMillis);
}
@Override
public int getReadTimeout() {
@Override public int getReadTimeout() {
return delegate.getReadTimeout();
}
@Override
public String toString() {
@Override public String toString() {
return delegate.toString();
}
@Override
public void setFixedLengthStreamingMode(int contentLength) {
@Override public void setFixedLengthStreamingMode(int contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
@Override
public void setChunkedStreamingMode(int chunkLength) {
@Override public void setChunkedStreamingMode(int chunkLength) {
delegate.setChunkedStreamingMode(chunkLength);
}
@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
delegate.hostnameVerifier = hostnameVerifier;
delegate.client.setHostnameVerifier(hostnameVerifier);
}
@Override public HostnameVerifier getHostnameVerifier() {
return delegate.hostnameVerifier;
return delegate.client.getHostnameVerifier();
}
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
delegate.sslSocketFactory = sslSocketFactory;
delegate.client.setSslSocketFactory(sslSocketFactory);
}
@Override public SSLSocketFactory getSSLSocketFactory() {
return delegate.sslSocketFactory;
return delegate.client.getSslSocketFactory();
}
@SuppressLint("NewApi")
@Override public void setFixedLengthStreamingMode(long contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
Set<Route> failedRoutes) {
super(url, client, responseCache, failedRoutes);
private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
super(url, client);
}
@Override protected HttpURLConnection getHttpConnectionToCache() {
@Override public HttpURLConnection getHttpConnectionToCache() {
return HttpsURLConnectionImpl.this;
}
@ -417,45 +363,4 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
: 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());
}
}
}

View File

@ -1,55 +0,0 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.ResponseSource;
import java.io.IOException;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
/**
* An extended response cache API. Unlike {@link java.net.ResponseCache}, this
* interface supports conditional caching and statistics.
*
* <p>Along with the rest of the {@code internal} package, this is not a public
* API. Applications wishing to supply their own caches must use the more
* limited {@link java.net.ResponseCache} interface.
*/
public interface OkResponseCache {
CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
throws IOException;
CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
/**
* Handles a conditional request hit by updating the stored cache response
* with the headers from {@code httpConnection}. The cached response body is
* not updated. If the stored response has changed since {@code
* conditionalCacheHit} was returned, this does nothing.
*/
void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
/** Track an conditional GET that was satisfied by this cache. */
void trackConditionalCacheHit();
/** Track an HTTP response being satisfied by {@code source}. */
void trackResponse(ResponseSource source);
}

View File

@ -15,6 +15,7 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import java.io.IOException;
import java.net.CacheRequest;
@ -41,6 +42,9 @@ public final class OkResponseCacheAdapter implements OkResponseCache {
return responseCache.put(uri, urlConnection);
}
@Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
}
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
throws IOException {
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.http;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
public interface Policy {
/** Returns true if HTTP response caches should be used. */
boolean getUseCaches();
/** Returns the HttpURLConnection instance to store in the cache. */
HttpURLConnection getHttpConnectionToCache();
/** Returns the current destination URL, possibly a redirect. */
URL getURL();
/** Returns the If-Modified-Since timestamp, or 0 if none is set. */
long getIfModifiedSince();
/** Returns true if a non-direct proxy is specified. */
boolean usingProxy();
/** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
int getChunkLength();
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
long getFixedContentLength();
/**
* Sets the current proxy that this connection is using.
* @see java.net.HttpURLConnection#usingProxy
*/
void setSelectedProxy(Proxy proxy);
}

View File

@ -32,6 +32,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* The HTTP status and unparsed header fields of a single HTTP message. Values
@ -122,23 +123,6 @@ public final class RawHeaders {
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"
@ -180,14 +164,17 @@ public final class RawHeaders {
/**
* Add an HTTP header line containing a field name, a literal colon, and a
* value.
* value. This works around empty header names and header names that start
* with a colon (created by old broken SPDY versions of the response cache).
*/
public void addLine(String line) {
int index = line.indexOf(":");
if (index == -1) {
addLenient("", line);
} else {
int index = line.indexOf(":", 1);
if (index != -1) {
addLenient(line.substring(0, index), line.substring(index + 1));
} else if (line.startsWith(":")) {
addLenient("", line.substring(1)); // Empty header name.
} else {
addLenient("", line); // No header name.
}
}
@ -248,6 +235,15 @@ public final class RawHeaders {
return namesAndValues.get(fieldNameIndex);
}
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < length(); i++) {
result.add(getFieldName(i));
}
return Collections.unmodifiableSet(result);
}
/** Returns the value at {@code index} or null if that is out of range. */
public String getValue(int index) {
int valueIndex = index * 2 + 1;
@ -267,6 +263,20 @@ public final class RawHeaders {
return null;
}
/** Returns an immutable list of the header values for {@code name}. */
public List<String> values(String name) {
List<String> result = null;
for (int i = 0; i < length(); i++) {
if (name.equalsIgnoreCase(getFieldName(i))) {
if (result == null) result = new ArrayList<String>(2);
result.add(getValue(i));
}
}
return result != null
? Collections.unmodifiableList(result)
: Collections.<String>emptyList();
}
/** @param fieldNames a case-insensitive set of HTTP header field names. */
public RawHeaders getAll(Set<String> fieldNames) {
RawHeaders result = new RawHeaders();
@ -401,10 +411,13 @@ public final class RawHeaders {
return result;
}
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
/** Returns headers for a name value block containing a SPDY response. */
public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) throws IOException {
if (nameValueBlock.size() % 2 != 0) {
throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
}
String status = null;
String version = null;
RawHeaders result = new RawHeaders();
for (int i = 0; i < nameValueBlock.size(); i += 2) {
String name = nameValueBlock.get(i);
@ -414,11 +427,21 @@ public final class RawHeaders {
if (end == -1) {
end = values.length();
}
result.namesAndValues.add(name);
result.namesAndValues.add(values.substring(start, end));
String value = values.substring(start, end);
if (":status".equals(name)) {
status = value;
} else if (":version".equals(name)) {
version = value;
} else {
result.namesAndValues.add(name);
result.namesAndValues.add(value);
}
start = end + 1;
}
}
if (status == null) throw new ProtocolException("Expected ':status' header not present");
if (version == null) throw new ProtocolException("Expected ':version' header not present");
result.setStatusLine(version + " " + status);
return result;
}
}

View File

@ -48,7 +48,7 @@ public final class RequestHeaders {
*/
private boolean hasAuthorization;
private int contentLength = -1;
private long contentLength = -1;
private String transferEncoding;
private String userAgent;
private String host;
@ -157,7 +157,7 @@ public final class RequestHeaders {
return hasAuthorization;
}
public int getContentLength() {
public long getContentLength() {
return contentLength;
}
@ -205,14 +205,26 @@ public final class RequestHeaders {
this.transferEncoding = "chunked";
}
public void setContentLength(int contentLength) {
public void setContentLength(long contentLength) {
if (this.contentLength != -1) {
headers.removeAll("Content-Length");
}
headers.add("Content-Length", Integer.toString(contentLength));
headers.add("Content-Length", Long.toString(contentLength));
this.contentLength = contentLength;
}
/**
* Remove the Content-Length headers. Call this when dropping the body on a
* request or response, such as when a redirect changes the method from POST
* to GET.
*/
public void removeContentLength() {
if (contentLength != -1) {
headers.removeAll("Content-Length");
contentLength = -1;
}
}
public void setUserAgent(String userAgent) {
if (this.userAgent != null) {
headers.removeAll("User-Agent");
@ -282,9 +294,24 @@ public final class RequestHeaders {
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());
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
headers.add(key, buildCookieHeader(entry.getValue()));
}
}
}
/**
* Send all cookies in one big header, as recommended by
* <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
*/
private String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cookies.size(); i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
}
}

View File

@ -17,6 +17,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.internal.Platform;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
@ -34,13 +35,16 @@ import static com.squareup.okhttp.internal.Util.equal;
public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** HTTP header name for the local time when the response was received. */
private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
/** HTTP synthetic header with the response source. */
static final String RESPONSE_SOURCE = "X-Android-Response-Source";
static final String RESPONSE_SOURCE = Platform.get().getPrefix() + "-Response-Source";
/** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */
static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport";
private final URI uri;
private final RawHeaders headers;
@ -110,8 +114,9 @@ public final class ResponseHeaders {
private String contentEncoding;
private String transferEncoding;
private int contentLength = -1;
private long contentLength = -1;
private String connection;
private String contentType;
public ResponseHeaders(URI uri, RawHeaders headers) {
this.uri = uri;
@ -168,9 +173,11 @@ public final class ResponseHeaders {
transferEncoding = value;
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
try {
contentLength = Integer.parseInt(value);
contentLength = Long.parseLong(value);
} catch (NumberFormatException ignored) {
}
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
contentType = value;
} else if ("Connection".equalsIgnoreCase(fieldName)) {
connection = value;
} else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
@ -259,10 +266,14 @@ public final class ResponseHeaders {
return contentEncoding;
}
public int getContentLength() {
public long getContentLength() {
return contentLength;
}
public String getContentType() {
return contentType;
}
public String getConnection() {
return connection;
}
@ -278,6 +289,10 @@ public final class ResponseHeaders {
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
}
public void setTransport(String transport) {
headers.set(SELECTED_TRANSPORT, transport);
}
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.

View File

@ -19,6 +19,7 @@ import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.RouteDatabase;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
@ -32,8 +33,6 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@ -55,7 +54,7 @@ public final class RouteSelector {
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
private final Set<Route> failedRoutes;
private final RouteDatabase routeDatabase;
/* The most recently attempted route. */
private Proxy lastProxy;
@ -78,13 +77,13 @@ public final class RouteSelector {
private final List<Route> postponedRoutes;
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
Dns dns, Set<Route> failedRoutes) {
Dns dns, RouteDatabase routeDatabase) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
this.failedRoutes = failedRoutes;
this.routeDatabase = routeDatabase;
this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy());
@ -103,11 +102,11 @@ public final class RouteSelector {
*
* @throws NoSuchElementException if there are no more routes to attempt.
*/
public Connection next() throws IOException {
public Connection next(String method) throws IOException {
// Always prefer pooled connections over new connections.
Connection pooled = pool.get(address);
if (pooled != null) {
return pooled;
for (Connection pooled; (pooled = pool.get(address)) != null; ) {
if (method.equals("GET") || pooled.isReadable()) return pooled;
pooled.close();
}
// Compute the next route to attempt.
@ -128,11 +127,11 @@ public final class RouteSelector {
boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
if (failedRoutes.contains(route)) {
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be
// tried last.
return next();
return next(method);
}
return new Connection(route);
@ -149,12 +148,7 @@ public final class RouteSelector {
proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
}
failedRoutes.add(failedRoute);
if (!(failure instanceof SSLHandshakeException)) {
// If the problem was not related to SSL then it will also fail with
// a different Tls mode therefore we can be proactive about it.
failedRoutes.add(failedRoute.flipTlsMode());
}
routeDatabase.failed(failedRoute, failure);
}
/** Resets {@link #nextProxy} to the first option. */

View File

@ -16,6 +16,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.spdy.ErrorCode;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import com.squareup.okhttp.internal.spdy.SpdyStream;
import java.io.IOException;
@ -36,6 +37,10 @@ public final class SpdyTransport implements Transport {
}
@Override public OutputStream createRequestBody() throws IOException {
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
}
// TODO: if we aren't streaming up to the server, we should buffer the whole request
writeRequestHeaders();
return stream.getOutputStream();
@ -55,7 +60,7 @@ public final class SpdyTransport implements Transport {
boolean hasResponseBody = true;
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
hasResponseBody);
stream.setReadTimeout(httpEngine.policy.getReadTimeout());
stream.setReadTimeout(httpEngine.client.getReadTimeout());
}
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
@ -69,24 +74,26 @@ public final class SpdyTransport implements Transport {
@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);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("spdy/3");
return headers;
}
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
}
@Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
@Override public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
InputStream responseBodyIn) {
if (streamCancelled) {
if (streamCanceled) {
if (stream != null) {
stream.closeLater(SpdyStream.RST_CANCEL);
stream.closeLater(ErrorCode.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
// 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;
}

View File

@ -59,6 +59,6 @@ interface Transport {
InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
/** Returns true if the underlying connection can be recycled. */
boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut,
boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
InputStream responseBodyIn);
}

View File

@ -25,9 +25,9 @@ import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
private boolean inputExhausted;
UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine)
UnknownLengthHttpInputStream(InputStream in, CacheRequest cacheRequest, HttpEngine httpEngine)
throws IOException {
super(is, httpEngine, cacheRequest);
super(in, httpEngine, cacheRequest);
}
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
@ -39,7 +39,7 @@ final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
int read = in.read(buffer, offset, count);
if (read == -1) {
inputExhausted = true;
endOfInput(false);
endOfInput();
return -1;
}
cacheWrite(buffer, offset, read);

View File

@ -0,0 +1,67 @@
package com.squareup.okhttp.internal.spdy;
public enum ErrorCode {
/** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */
NO_ERROR(0, -1, 0),
PROTOCOL_ERROR(1, 1, 1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
INVALID_STREAM(1, 2, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
UNSUPPORTED_VERSION(1, 4, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
STREAM_IN_USE(1, 8, -1),
/** A subtype of PROTOCOL_ERROR used by SPDY. */
STREAM_ALREADY_CLOSED(1, 9, -1),
INTERNAL_ERROR(2, 6, 2),
FLOW_CONTROL_ERROR(3, 7, -1),
STREAM_CLOSED(5, -1, -1),
FRAME_TOO_LARGE(6, 11, -1),
REFUSED_STREAM(7, 3, -1),
CANCEL(8, 5, -1),
COMPRESSION_ERROR(9, -1, -1),
INVALID_CREDENTIALS(-1, 10, -1);
public final int httpCode;
public final int spdyRstCode;
public final int spdyGoAwayCode;
private ErrorCode(int httpCode, int spdyRstCode, int spdyGoAwayCode) {
this.httpCode = httpCode;
this.spdyRstCode = spdyRstCode;
this.spdyGoAwayCode = spdyGoAwayCode;
}
public static ErrorCode fromSpdy3Rst(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.spdyRstCode == code) return errorCode;
}
return null;
}
public static ErrorCode fromHttp2(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.httpCode == code) return errorCode;
}
return null;
}
public static ErrorCode fromSpdyGoAway(int code) {
for (ErrorCode errorCode : ErrorCode.values()) {
if (errorCode.spdyGoAwayCode == code) return errorCode;
}
return null;
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/** Reads transport frames for SPDY/3 or HTTP/2.0. */
public interface FrameReader extends Closeable {
void readConnectionHeader() throws IOException;
boolean nextFrame(Handler handler) throws IOException;
public interface Handler {
void data(boolean inFinished, int streamId, InputStream in, int length) throws IOException;
/**
* Create or update incoming headers, creating the corresponding streams
* if necessary. Frames that trigger this are SPDY SYN_STREAM, HEADERS, and
* SYN_REPLY, and HTTP/2.0 HEADERS and PUSH_PROMISE.
*
* @param inFinished true if the sender will not send further frames.
* @param outFinished true if the receiver should not send further frames.
* @param streamId the stream owning these headers.
* @param associatedStreamId the stream that triggered the sender to create
* this stream.
* @param priority or -1 for no priority. For SPDY, priorities range from 0
* (highest) thru 7 (lowest). For HTTP/2.0, priorities range from 0
* (highest) thru 2**31-1 (lowest).
*/
void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
int priority, List<String> nameValueBlock, HeadersMode headersMode);
void rstStream(int streamId, ErrorCode errorCode);
void settings(boolean clearPrevious, Settings settings);
void noop();
void ping(boolean reply, int payload1, int payload2);
void goAway(int lastGoodStreamId, ErrorCode errorCode);
void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl);
void priority(int streamId, int priority);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.Closeable;
import java.io.IOException;
import java.util.List;
/** Writes transport frames for SPDY/3 or HTTP/2.0. */
public interface FrameWriter extends Closeable {
/** HTTP/2.0 only. */
void connectionHeader() throws IOException;
/** SPDY/3 only. */
void flush() throws IOException;
void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
int priority, int slot, List<String> nameValueBlock) throws IOException;
void synReply(boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException;
void headers(int streamId, List<String> nameValueBlock) throws IOException;
void rstStream(int streamId, ErrorCode errorCode) throws IOException;
void data(boolean outFinished, int streamId, byte[] data) throws IOException;
void data(boolean outFinished, int streamId, byte[] data, int offset, int byteCount)
throws IOException;
void settings(Settings settings) throws IOException;
void noop() throws IOException;
void ping(boolean reply, int payload1, int payload2) throws IOException;
void goAway(int lastGoodStreamId, ErrorCode errorCode) throws IOException;
void windowUpdate(int streamId, int deltaWindowSize) throws IOException;
}

View File

@ -0,0 +1,49 @@
/*
* 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.spdy;
enum HeadersMode {
SPDY_SYN_STREAM,
SPDY_REPLY,
SPDY_HEADERS,
HTTP_20_HEADERS;
/** Returns true if it is an error these headers to create a new stream. */
public boolean failIfStreamAbsent() {
return this == SPDY_REPLY || this == SPDY_HEADERS;
}
/** Returns true if it is an error these headers to update an existing stream. */
public boolean failIfStreamPresent() {
return this == SPDY_SYN_STREAM;
}
/**
* Returns true if it is an error these headers to be the initial headers of a
* response.
*/
public boolean failIfHeadersAbsent() {
return this == SPDY_HEADERS;
}
/**
* Returns true if it is an error these headers to be update existing headers
* of a response.
*/
public boolean failIfHeadersPresent() {
return this == SPDY_REPLY;
}
}

View File

@ -0,0 +1,371 @@
package com.squareup.okhttp.internal.spdy;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
/**
* Read and write HPACK v03.
* http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03
*/
final class Hpack {
static class HeaderEntry {
private final String name;
private final String value;
HeaderEntry(String name, String value) {
this.name = name;
this.value = value;
}
// TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
int length() {
return 32 + name.length() + value.length();
}
}
static final int PREFIX_5_BITS = 0x1f;
static final int PREFIX_6_BITS = 0x3f;
static final int PREFIX_7_BITS = 0x7f;
static final int PREFIX_8_BITS = 0xff;
static final List<HeaderEntry> INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList(
new HeaderEntry(":scheme", "http"),
new HeaderEntry(":scheme", "https"),
new HeaderEntry(":host", ""),
new HeaderEntry(":path", "/"),
new HeaderEntry(":method", "GET"),
new HeaderEntry("accept", ""),
new HeaderEntry("accept-charset", ""),
new HeaderEntry("accept-encoding", ""),
new HeaderEntry("accept-language", ""),
new HeaderEntry("cookie", ""),
new HeaderEntry("if-modified-since", ""),
new HeaderEntry("user-agent", ""),
new HeaderEntry("referer", ""),
new HeaderEntry("authorization", ""),
new HeaderEntry("allow", ""),
new HeaderEntry("cache-control", ""),
new HeaderEntry("connection", ""),
new HeaderEntry("content-length", ""),
new HeaderEntry("content-type", ""),
new HeaderEntry("date", ""),
new HeaderEntry("expect", ""),
new HeaderEntry("from", ""),
new HeaderEntry("if-match", ""),
new HeaderEntry("if-none-match", ""),
new HeaderEntry("if-range", ""),
new HeaderEntry("if-unmodified-since", ""),
new HeaderEntry("max-forwards", ""),
new HeaderEntry("proxy-authorization", ""),
new HeaderEntry("range", ""),
new HeaderEntry("via", "")
);
static final List<HeaderEntry> INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList(
new HeaderEntry(":status", "200"),
new HeaderEntry("age", ""),
new HeaderEntry("cache-control", ""),
new HeaderEntry("content-length", ""),
new HeaderEntry("content-type", ""),
new HeaderEntry("date", ""),
new HeaderEntry("etag", ""),
new HeaderEntry("expires", ""),
new HeaderEntry("last-modified", ""),
new HeaderEntry("server", ""),
new HeaderEntry("set-cookie", ""),
new HeaderEntry("vary", ""),
new HeaderEntry("via", ""),
new HeaderEntry("access-control-allow-origin", ""),
new HeaderEntry("accept-ranges", ""),
new HeaderEntry("allow", ""),
new HeaderEntry("connection", ""),
new HeaderEntry("content-disposition", ""),
new HeaderEntry("content-encoding", ""),
new HeaderEntry("content-language", ""),
new HeaderEntry("content-location", ""),
new HeaderEntry("content-range", ""),
new HeaderEntry("link", ""),
new HeaderEntry("location", ""),
new HeaderEntry("proxy-authenticate", ""),
new HeaderEntry("refresh", ""),
new HeaderEntry("retry-after", ""),
new HeaderEntry("strict-transport-security", ""),
new HeaderEntry("transfer-encoding", ""),
new HeaderEntry("www-authenticate", "")
);
// Update these when initial tables change to sum of each entry length.
static final int INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH = 1262;
static final int INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH = 1304;
private Hpack() {
}
static class Reader {
private final long maxBufferSize = 4096; // TODO: needs to come from settings.
private final DataInputStream in;
private final BitSet referenceSet = new BitSet();
private final List<HeaderEntry> headerTable;
private final List<String> emittedHeaders = new ArrayList<String>();
private long bufferSize = 0;
private long bytesLeft = 0;
Reader(DataInputStream in, boolean client) {
this.in = in;
if (client) { // we are reading from the server
this.headerTable = new ArrayList<HeaderEntry>(INITIAL_SERVER_TO_CLIENT_HEADER_TABLE);
this.bufferSize = INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH;
} else {
this.headerTable = new ArrayList<HeaderEntry>(INITIAL_CLIENT_TO_SERVER_HEADER_TABLE);
this.bufferSize = INITIAL_CLIENT_TO_SERVER_HEADER_TABLE_LENGTH;
}
}
/**
* Read {@code byteCount} bytes of headers from the source stream into the
* set of emitted headers.
*/
public void readHeaders(int byteCount) throws IOException {
bytesLeft += byteCount;
// TODO: limit to 'byteCount' bytes?
while (bytesLeft > 0) {
int b = readByte();
if ((b & 0x80) != 0) {
int index = readInt(b, PREFIX_7_BITS);
readIndexedHeader(index);
} else if (b == 0x60) {
readLiteralHeaderWithoutIndexingNewName();
} else if ((b & 0xe0) == 0x60) {
int index = readInt(b, PREFIX_5_BITS);
readLiteralHeaderWithoutIndexingIndexedName(index - 1);
} else if (b == 0x40) {
readLiteralHeaderWithIncrementalIndexingNewName();
} else if ((b & 0xe0) == 0x40) {
int index = readInt(b, PREFIX_5_BITS);
readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
} else if (b == 0) {
readLiteralHeaderWithSubstitutionIndexingNewName();
} else if ((b & 0xc0) == 0) {
int index = readInt(b, PREFIX_6_BITS);
readLiteralHeaderWithSubstitutionIndexingIndexedName(index - 1);
} else {
throw new AssertionError();
}
}
}
public void emitReferenceSet() {
for (int i = referenceSet.nextSetBit(0); i != -1; i = referenceSet.nextSetBit(i + 1)) {
emittedHeaders.add(getName(i));
emittedHeaders.add(getValue(i));
}
}
/**
* Returns all headers emitted since they were last cleared, then clears the
* emitted headers.
*/
public List<String> getAndReset() {
List<String> result = new ArrayList<String>(emittedHeaders);
emittedHeaders.clear();
return result;
}
private void readIndexedHeader(int index) {
if (referenceSet.get(index)) {
referenceSet.clear(index);
} else {
referenceSet.set(index);
}
}
private void readLiteralHeaderWithoutIndexingIndexedName(int index)
throws IOException {
String name = getName(index);
String value = readString();
emittedHeaders.add(name);
emittedHeaders.add(value);
}
private void readLiteralHeaderWithoutIndexingNewName()
throws IOException {
String name = readString();
String value = readString();
emittedHeaders.add(name);
emittedHeaders.add(value);
}
private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
throws IOException {
String name = getName(nameIndex);
String value = readString();
int index = headerTable.size(); // append to tail
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
String name = readString();
String value = readString();
int index = headerTable.size(); // append to tail
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex)
throws IOException {
int index = readInt(readByte(), PREFIX_8_BITS);
String name = getName(nameIndex);
String value = readString();
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private void readLiteralHeaderWithSubstitutionIndexingNewName() throws IOException {
String name = readString();
int index = readInt(readByte(), PREFIX_8_BITS);
String value = readString();
insertIntoHeaderTable(index, new HeaderEntry(name, value));
}
private String getName(int index) {
return headerTable.get(index).name;
}
private String getValue(int index) {
return headerTable.get(index).value;
}
private void insertIntoHeaderTable(int index, HeaderEntry entry) {
int delta = entry.length();
if (index != headerTable.size()) {
delta -= headerTable.get(index).length();
}
// if the new or replacement header is too big, drop all entries.
if (delta > maxBufferSize) {
headerTable.clear();
bufferSize = 0;
// emit the large header to the callback.
emittedHeaders.add(entry.name);
emittedHeaders.add(entry.value);
return;
}
// Prune headers to the required length.
while (bufferSize + delta > maxBufferSize) {
remove(0);
index--;
}
if (index < 0) { // we pruned it, so insert at beginning
index = 0;
headerTable.add(index, entry);
} else if (index == headerTable.size()) { // append to the end
headerTable.add(index, entry);
} else { // replace value at same position
headerTable.set(index, entry);
}
bufferSize += delta;
referenceSet.set(index);
}
private void remove(int index) {
bufferSize -= headerTable.remove(index).length();
}
private int readByte() throws IOException {
bytesLeft--;
return in.readByte() & 0xff;
}
int readInt(int firstByte, int prefixMask) throws IOException {
int prefix = firstByte & prefixMask;
if (prefix < prefixMask) {
return prefix; // This was a single byte value.
}
// This is a multibyte value. Read 7 bits at a time.
int result = prefixMask;
int shift = 0;
while (true) {
int b = readByte();
if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
result += (b & 0x7f) << shift;
shift += 7;
} else {
result += b << shift; // Last byte.
break;
}
}
return result;
}
/**
* Reads a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this method
* may be used to read strings that are known to be ASCII-only.
*/
public String readString() throws IOException {
int firstByte = readByte();
int length = readInt(firstByte, PREFIX_8_BITS);
byte[] encoded = new byte[length];
bytesLeft -= length;
in.readFully(encoded);
return new String(encoded, "UTF-8");
}
}
static class Writer {
private final OutputStream out;
Writer(OutputStream out) {
this.out = out;
}
public void writeHeaders(List<String> nameValueBlock) throws IOException {
// TODO: implement a compression strategy.
for (int i = 0, size = nameValueBlock.size(); i < size; i += 2) {
out.write(0x60); // Literal Header without Indexing - New Name.
writeString(nameValueBlock.get(i));
writeString(nameValueBlock.get(i + 1));
}
}
public void writeInt(int value, int prefixMask, int bits) throws IOException {
// Write the raw value for a single byte value.
if (value < prefixMask) {
out.write(bits | value);
return;
}
// Write the mask to start a multibyte value.
out.write(bits | prefixMask);
value -= prefixMask;
// Write 7 bits at a time 'til we're done.
while (value >= 0x80) {
int b = value & 0x7f;
out.write(b | 0x80);
value >>>= 7;
}
out.write(value);
}
/**
* Writes a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this
* method can be used to write strings that are known to be ASCII-only.
*/
public void writeString(String headerName) throws IOException {
byte[] bytes = headerName.getBytes("UTF-8");
writeInt(bytes.length, PREFIX_8_BITS, 0);
out.write(bytes);
}
}
}

View File

@ -0,0 +1,385 @@
/*
* 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.spdy;
import com.squareup.okhttp.internal.Util;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
/**
* Read and write http/2 v06 frames.
* http://tools.ietf.org/html/draft-ietf-httpbis-http2-06
*/
final class Http20Draft06 implements Variant {
private static final byte[] CONNECTION_HEADER;
static {
try {
CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
static final int TYPE_DATA = 0x0;
static final int TYPE_HEADERS = 0x1;
static final int TYPE_PRIORITY = 0x2;
static final int TYPE_RST_STREAM = 0x3;
static final int TYPE_SETTINGS = 0x4;
static final int TYPE_PUSH_PROMISE = 0x5;
static final int TYPE_PING = 0x6;
static final int TYPE_GOAWAY = 0x7;
static final int TYPE_WINDOW_UPDATE = 0x9;
static final int TYPE_CONTINUATION = 0xa;
static final int FLAG_END_STREAM = 0x1;
/** Used for headers, push-promise and continuation. */
static final int FLAG_END_HEADERS = 0x4;
static final int FLAG_PRIORITY = 0x8;
static final int FLAG_PONG = 0x1;
static final int FLAG_END_FLOW_CONTROL = 0x1;
@Override public FrameReader newReader(InputStream in, boolean client) {
return new Reader(in, client);
}
@Override public FrameWriter newWriter(OutputStream out, boolean client) {
return new Writer(out, client);
}
static final class Reader implements FrameReader {
private final DataInputStream in;
private final boolean client;
private final Hpack.Reader hpackReader;
Reader(InputStream in, boolean client) {
this.in = new DataInputStream(in);
this.client = client;
this.hpackReader = new Hpack.Reader(this.in, client);
}
@Override public void readConnectionHeader() throws IOException {
if (client) return; // Nothing to read; servers don't send connection headers!
byte[] connectionHeader = new byte[CONNECTION_HEADER.length];
in.readFully(connectionHeader);
if (!Arrays.equals(connectionHeader, CONNECTION_HEADER)) {
throw ioException("Expected a connection header but was "
+ Arrays.toString(connectionHeader));
}
}
@Override 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();
int length = (w1 & 0xffff0000) >> 16;
int type = (w1 & 0xff00) >> 8;
int flags = w1 & 0xff;
// boolean r = (w2 & 0x80000000) != 0; // Reserved.
int streamId = (w2 & 0x7fffffff);
switch (type) {
case TYPE_DATA:
readData(handler, flags, length, streamId);
return true;
case TYPE_HEADERS:
readHeaders(handler, flags, length, streamId);
return true;
case TYPE_PRIORITY:
readPriority(handler, flags, length, streamId);
return true;
case TYPE_RST_STREAM:
readRstStream(handler, flags, length, streamId);
return true;
case TYPE_SETTINGS:
readSettings(handler, flags, length, streamId);
return true;
case TYPE_PUSH_PROMISE:
readPushPromise(handler, flags, length, streamId);
return true;
case TYPE_PING:
readPing(handler, flags, length, streamId);
return true;
case TYPE_GOAWAY:
readGoAway(handler, flags, length, streamId);
return true;
case TYPE_WINDOW_UPDATE:
readWindowUpdate(handler, flags, length, streamId);
return true;
}
throw new UnsupportedOperationException("TODO");
}
private void readHeaders(Handler handler, int flags, int length, int streamId)
throws IOException {
if (streamId == 0) throw ioException("TYPE_HEADERS streamId == 0");
boolean inFinished = (flags & FLAG_END_STREAM) != 0;
while (true) {
hpackReader.readHeaders(length);
if ((flags & FLAG_END_HEADERS) != 0) {
hpackReader.emitReferenceSet();
List<String> namesAndValues = hpackReader.getAndReset();
int priority = -1; // TODO: priority
handler.headers(false, inFinished, streamId, -1, priority, namesAndValues,
HeadersMode.HTTP_20_HEADERS);
return;
}
// Read another continuation frame.
int w1 = in.readInt();
int w2 = in.readInt();
length = (w1 & 0xffff0000) >> 16;
int newType = (w1 & 0xff00) >> 8;
flags = w1 & 0xff;
// TODO: remove in draft 8: CONTINUATION no longer sets END_STREAM
inFinished = (flags & FLAG_END_STREAM) != 0;
// boolean u = (w2 & 0x80000000) != 0; // Unused.
int newStreamId = (w2 & 0x7fffffff);
if (newType != TYPE_CONTINUATION) {
throw ioException("TYPE_CONTINUATION didn't have FLAG_END_HEADERS");
}
if (newStreamId != streamId) throw ioException("TYPE_CONTINUATION streamId changed");
}
}
private void readData(Handler handler, int flags, int length, int streamId) throws IOException {
boolean inFinished = (flags & FLAG_END_STREAM) != 0;
handler.data(inFinished, streamId, in, length);
}
private void readPriority(Handler handler, int flags, int length, int streamId)
throws IOException {
if (length != 4) throw ioException("TYPE_PRIORITY length: %d != 4", length);
if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0");
int w1 = in.readInt();
// boolean r = (w1 & 0x80000000) != 0; // Reserved.
int priority = (w1 & 0x7fffffff);
handler.priority(streamId, priority);
}
private void readRstStream(Handler handler, int flags, int length, int streamId)
throws IOException {
if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length);
if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0");
int errorCodeInt = in.readInt();
ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
if (errorCode == null) {
throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
}
handler.rstStream(streamId, errorCode);
}
private void readSettings(Handler handler, int flags, int length, int streamId)
throws IOException {
if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length);
if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0");
Settings settings = new Settings();
for (int i = 0; i < length; i += 8) {
int w1 = in.readInt();
int value = in.readInt();
// int r = (w1 & 0xff000000) >>> 24; // Reserved.
int id = w1 & 0xffffff;
settings.set(id, 0, value);
}
handler.settings(false, settings);
}
private void readPushPromise(Handler handler, int flags, int length, int streamId) {
// TODO:
}
private void readPing(Handler handler, int flags, int length, int streamId) throws IOException {
if (length != 8) throw ioException("TYPE_PING length != 8: %s", length);
if (streamId != 0) throw ioException("TYPE_PING streamId != 0");
int payload1 = in.readInt();
int payload2 = in.readInt();
boolean reply = (flags & FLAG_PONG) != 0;
handler.ping(reply, payload1, payload2);
}
private void readGoAway(Handler handler, int flags, int length, int streamId)
throws IOException {
if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length);
int lastStreamId = in.readInt();
int errorCodeInt = in.readInt();
int opaqueDataLength = length - 8;
ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
if (errorCode == null) {
throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
}
if (Util.skipByReading(in, opaqueDataLength) != opaqueDataLength) {
throw new IOException("TYPE_GOAWAY opaque data was truncated");
}
handler.goAway(lastStreamId, errorCode);
}
private void readWindowUpdate(Handler handler, int flags, int length, int streamId)
throws IOException {
int w1 = in.readInt();
// boolean r = (w1 & 0x80000000) != 0; // Reserved.
int windowSizeIncrement = (w1 & 0x7fffffff);
boolean endFlowControl = (flags & FLAG_END_FLOW_CONTROL) != 0;
handler.windowUpdate(streamId, windowSizeIncrement, endFlowControl);
}
private static IOException ioException(String message, Object... args) throws IOException {
throw new IOException(String.format(message, args));
}
@Override public void close() throws IOException {
in.close();
}
}
static final class Writer implements FrameWriter {
private final DataOutputStream out;
private final boolean client;
private final ByteArrayOutputStream hpackBuffer;
private final Hpack.Writer hpackWriter;
Writer(OutputStream out, boolean client) {
this.out = new DataOutputStream(out);
this.client = client;
this.hpackBuffer = new ByteArrayOutputStream();
this.hpackWriter = new Hpack.Writer(hpackBuffer);
}
@Override public synchronized void flush() throws IOException {
out.flush();
}
@Override public synchronized void connectionHeader() throws IOException {
if (!client) return; // Nothing to write; servers don't send connection headers!
out.write(CONNECTION_HEADER);
}
@Override public synchronized void synStream(boolean outFinished, boolean inFinished,
int streamId, int associatedStreamId, int priority, int slot, List<String> nameValueBlock)
throws IOException {
if (inFinished) throw new UnsupportedOperationException();
headers(outFinished, streamId, priority, nameValueBlock);
}
@Override public synchronized void synReply(boolean outFinished, int streamId,
List<String> nameValueBlock) throws IOException {
headers(outFinished, streamId, -1, nameValueBlock);
}
@Override public synchronized void headers(int streamId, List<String> nameValueBlock)
throws IOException {
headers(false, streamId, -1, nameValueBlock);
}
private void headers(boolean outFinished, int streamId, int priority,
List<String> nameValueBlock) throws IOException {
hpackBuffer.reset();
hpackWriter.writeHeaders(nameValueBlock);
int type = TYPE_HEADERS;
// TODO: implement CONTINUATION
int length = hpackBuffer.size();
int flags = FLAG_END_HEADERS;
if (outFinished) flags |= FLAG_END_STREAM;
if (priority != -1) flags |= FLAG_PRIORITY;
out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
out.writeInt(streamId & 0x7fffffff);
if (priority != -1) out.writeInt(priority & 0x7fffffff);
hpackBuffer.writeTo(out);
}
@Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
throws IOException {
throw new UnsupportedOperationException("TODO");
}
@Override public void data(boolean outFinished, int streamId, byte[] data) throws IOException {
data(outFinished, streamId, data, 0, data.length);
}
@Override public synchronized void data(boolean outFinished, int streamId, byte[] data,
int offset, int byteCount) throws IOException {
int type = TYPE_DATA;
int flags = 0;
if (outFinished) flags |= FLAG_END_STREAM;
out.writeInt((byteCount & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
out.writeInt(streamId & 0x7fffffff);
out.write(data, offset, byteCount);
}
@Override public synchronized void settings(Settings settings) throws IOException {
int type = TYPE_SETTINGS;
int length = settings.size() * 8;
int flags = 0;
int streamId = 0;
out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
out.writeInt(streamId & 0x7fffffff);
for (int i = 0; i < Settings.COUNT; i++) {
if (!settings.isSet(i)) continue;
out.writeInt(i & 0xffffff);
out.writeInt(settings.get(i));
}
}
@Override public synchronized void noop() throws IOException {
throw new UnsupportedOperationException();
}
@Override public synchronized void ping(boolean reply, int payload1, int payload2)
throws IOException {
// TODO
}
@Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode)
throws IOException {
// TODO
}
@Override public synchronized void windowUpdate(int streamId, int deltaWindowSize)
throws IOException {
// TODO
}
@Override public void close() throws IOException {
out.close();
}
}
}

View File

@ -22,7 +22,7 @@ import java.io.IOException;
public interface IncomingStreamHandler {
IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
@Override public void receive(SpdyStream stream) throws IOException {
stream.close(SpdyStream.RST_REFUSED_STREAM);
stream.close(ErrorCode.REFUSED_STREAM);
}
};

View File

@ -0,0 +1,123 @@
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.util.ArrayList;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* Reads a SPDY/3 Name/Value header block. This class is made complicated by the
* requirement that we're strict with which bytes we put in the compressed bytes
* buffer. We need to put all compressed bytes into that buffer -- but no other
* bytes.
*/
class NameValueBlockReader implements Closeable {
private final DataInputStream nameValueBlockIn;
private final FillableInflaterInputStream fillableInflaterInputStream;
private int compressedLimit;
NameValueBlockReader(final InputStream in) {
// Limit the inflater input stream to only those bytes in the Name/Value block. We cut the
// inflater off at its source because we can't predict the ratio of compressed bytes to
// uncompressed bytes.
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(Spdy3.DICTIONARY);
result = super.inflate(buffer, offset, count);
}
return result;
}
};
fillableInflaterInputStream = new FillableInflaterInputStream(throttleStream, inflater);
nameValueBlockIn = new DataInputStream(fillableInflaterInputStream);
}
/** Extend the inflater stream so we can eagerly fill the compressed bytes buffer if necessary. */
static class FillableInflaterInputStream extends InflaterInputStream {
public FillableInflaterInputStream(InputStream in, Inflater inf) {
super(in, inf);
}
@Override public void fill() throws IOException {
super.fill(); // This method is protected in the superclass.
}
}
public List<String> readNameValueBlock(int length) throws IOException {
this.compressedLimit += length;
try {
int numberOfPairs = nameValueBlockIn.readInt();
if (numberOfPairs < 0) {
throw new IOException("numberOfPairs < 0: " + numberOfPairs);
}
if (numberOfPairs > 1024) {
throw new IOException("numberOfPairs > 1024: " + numberOfPairs);
}
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 new IOException("name.length == 0");
entries.add(name);
entries.add(values);
}
doneReading();
return entries;
} catch (DataFormatException e) {
throw new IOException(e.getMessage());
}
}
private void doneReading() throws IOException {
if (compressedLimit == 0) return;
// Read any outstanding unread bytes. One side-effect of deflate compression is that sometimes
// there are bytes remaining in the stream after we've consumed all of the content.
fillableInflaterInputStream.fill();
if (compressedLimit != 0) {
throw new IOException("compressedLimit > 0: " + compressedLimit);
}
}
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");
}
@Override public void close() throws IOException {
nameValueBlockIn.close();
}
}

View File

View File

@ -31,23 +31,29 @@ final class Settings {
static final int PERSISTED = 0x2;
/** Sender's estimate of max incoming kbps. */
static final int UPLOAD_BANDWIDTH = 0x1;
static final int UPLOAD_BANDWIDTH = 1;
/** Sender's estimate of max outgoing kbps. */
static final int DOWNLOAD_BANDWIDTH = 0x2;
static final int DOWNLOAD_BANDWIDTH = 2;
/** Sender's estimate of milliseconds between sending a request and receiving a response. */
static final int ROUND_TRIP_TIME = 0x3;
static final int ROUND_TRIP_TIME = 3;
/** Sender's maximum number of concurrent streams. */
static final int MAX_CONCURRENT_STREAMS = 0x4;
static final int MAX_CONCURRENT_STREAMS = 4;
/** Current CWND in Packets. */
static final int CURRENT_CWND = 0x5;
static final int CURRENT_CWND = 5;
/** Retransmission rate. Percentage */
static final int DOWNLOAD_RETRANS_RATE = 0x6;
static final int DOWNLOAD_RETRANS_RATE = 6;
/** Window size in bytes. */
static final int INITIAL_WINDOW_SIZE = 0x7;
static final int INITIAL_WINDOW_SIZE = 7;
/** Window size in bytes. */
static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8;
static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8;
/** Flow control options. */
static final int FLOW_CONTROL_OPTIONS = 9;
/** Total number of settings. */
static final int COUNT = 0x9;
static final int COUNT = 10;
/** If set, flow control is disabled for streams directed to the sender of these settings. */
static final int FLOW_CONTROL_OPTIONS_DISABLED = 0x1;
/** Bitfield of which flags that values. */
private int set;
@ -146,6 +152,13 @@ final class Settings {
return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
}
// TODO: honor this setting.
boolean isFlowControlDisabled() {
int bit = 1 << FLOW_CONTROL_OPTIONS;
int value = (bit & set) != 0 ? values[FLOW_CONTROL_OPTIONS] : 0;
return (value & FLOW_CONTROL_OPTIONS_DISABLED) != 0;
}
/**
* Returns true if this user agent should use this setting in future SPDY
* connections to the same host.

View File

@ -0,0 +1,463 @@
/*
* 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.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.List;
import java.util.zip.Deflater;
final class Spdy3 implements Variant {
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 FLAG_FIN = 0x1;
static final int FLAG_UNIDIRECTIONAL = 0x2;
static final int VERSION = 3;
static final byte[] DICTIONARY;
static {
try {
DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+ ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+ "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
@Override public FrameReader newReader(InputStream in, boolean client) {
return new Reader(in, client);
}
@Override public FrameWriter newWriter(OutputStream out, boolean client) {
return new Writer(out, client);
}
/** Read spdy/3 frames. */
static final class Reader implements FrameReader {
private final DataInputStream in;
private final boolean client;
private final NameValueBlockReader nameValueBlockReader;
Reader(InputStream in, boolean client) {
this.in = new DataInputStream(in);
this.nameValueBlockReader = new NameValueBlockReader(in);
this.client = client;
}
@Override public void readConnectionHeader() {
}
/**
* Send the next frame to {@code handler}. Returns true unless there are no
* more frames on the stream.
*/
@Override 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 TYPE_SYN_STREAM:
readSynStream(handler, flags, length);
return true;
case TYPE_SYN_REPLY:
readSynReply(handler, flags, length);
return true;
case TYPE_RST_STREAM:
readRstStream(handler, flags, length);
return true;
case TYPE_SETTINGS:
readSettings(handler, flags, length);
return true;
case TYPE_NOOP:
if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
handler.noop();
return true;
case TYPE_PING:
readPing(handler, flags, length);
return true;
case TYPE_GOAWAY:
readGoAway(handler, flags, length);
return true;
case TYPE_HEADERS:
readHeaders(handler, flags, length);
return true;
case TYPE_WINDOW_UPDATE:
readWindowUpdate(handler, flags, length);
return true;
case TYPE_CREDENTIAL:
Util.skipByReading(in, length);
throw new UnsupportedOperationException("TODO"); // TODO: implement
default:
throw new IOException("Unexpected frame");
}
} else {
int streamId = w1 & 0x7fffffff;
boolean inFinished = (flags & FLAG_FIN) != 0;
handler.data(inFinished, 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 = nameValueBlockReader.readNameValueBlock(length - 10);
boolean inFinished = (flags & FLAG_FIN) != 0;
boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0;
handler.headers(outFinished, inFinished, streamId, associatedStreamId, priority,
nameValueBlock, HeadersMode.SPDY_SYN_STREAM);
}
private void readSynReply(Handler handler, int flags, int length) throws IOException {
int w1 = in.readInt();
int streamId = w1 & 0x7fffffff;
List<String> nameValueBlock = nameValueBlockReader.readNameValueBlock(length - 4);
boolean inFinished = (flags & FLAG_FIN) != 0;
handler.headers(false, inFinished, streamId, -1, -1, nameValueBlock, HeadersMode.SPDY_REPLY);
}
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 errorCodeInt = in.readInt();
ErrorCode errorCode = ErrorCode.fromSpdy3Rst(errorCodeInt);
if (errorCode == null) {
throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
}
handler.rstStream(streamId, errorCode);
}
private void readHeaders(Handler handler, int flags, int length) throws IOException {
int w1 = in.readInt();
int streamId = w1 & 0x7fffffff;
List<String> nameValueBlock = nameValueBlockReader.readNameValueBlock(length - 4);
handler.headers(false, false, streamId, -1, -1, nameValueBlock, HeadersMode.SPDY_HEADERS);
}
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(streamId, deltaWindowSize, false);
}
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();
boolean reply = client == ((id % 2) == 1);
handler.ping(reply, id, 0);
}
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 errorCodeInt = in.readInt();
ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt);
if (errorCode == null) {
throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
}
handler.goAway(lastGoodStreamId, errorCode);
}
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);
}
boolean clearPrevious = (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0;
handler.settings(clearPrevious, 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, nameValueBlockReader);
}
}
/** Write spdy/3 frames. */
static final class Writer implements FrameWriter {
private final DataOutputStream out;
private final ByteArrayOutputStream nameValueBlockBuffer;
private final DataOutputStream nameValueBlockOut;
private final boolean client;
Writer(OutputStream out, boolean client) {
this.out = new DataOutputStream(out);
this.client = client;
Deflater deflater = new Deflater();
deflater.setDictionary(DICTIONARY);
nameValueBlockBuffer = new ByteArrayOutputStream();
nameValueBlockOut = new DataOutputStream(
Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
}
@Override public synchronized void connectionHeader() {
// Do nothing: no connection header for SPDY/3.
}
@Override public synchronized void flush() throws IOException {
out.flush();
}
@Override public synchronized void synStream(boolean outFinished, boolean inFinished,
int streamId, int associatedStreamId, int priority, int slot, List<String> nameValueBlock)
throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int length = 10 + nameValueBlockBuffer.size();
int type = TYPE_SYN_STREAM;
int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0);
int unused = 0;
out.writeInt(0x80000000 | (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();
}
@Override public synchronized void synReply(
boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int type = TYPE_SYN_REPLY;
int flags = (outFinished ? FLAG_FIN : 0);
int length = nameValueBlockBuffer.size() + 4;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
nameValueBlockBuffer.writeTo(out);
out.flush();
}
@Override public synchronized void headers(int streamId, List<String> nameValueBlock)
throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int flags = 0;
int type = TYPE_HEADERS;
int length = nameValueBlockBuffer.size() + 4;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
nameValueBlockBuffer.writeTo(out);
out.flush();
}
@Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
throws IOException {
if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
int flags = 0;
int type = TYPE_RST_STREAM;
int length = 8;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
out.writeInt(errorCode.spdyRstCode);
out.flush();
}
@Override public synchronized void data(boolean outFinished, int streamId, byte[] data)
throws IOException {
data(outFinished, streamId, data, 0, data.length);
}
@Override public synchronized void data(boolean outFinished, int streamId, byte[] data,
int offset, int byteCount) throws IOException {
int flags = (outFinished ? FLAG_FIN : 0);
out.writeInt(streamId & 0x7fffffff);
out.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff);
out.write(data, offset, byteCount);
}
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();
}
@Override public synchronized void settings(Settings settings) throws IOException {
int type = TYPE_SETTINGS;
int flags = 0;
int size = settings.size();
int length = 4 + size * 8;
out.writeInt(0x80000000 | (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();
}
@Override public synchronized void noop() throws IOException {
int type = TYPE_NOOP;
int length = 0;
int flags = 0;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.flush();
}
@Override public synchronized void ping(boolean reply, int payload1, int payload2)
throws IOException {
boolean payloadIsReply = client != ((payload1 % 2) == 1);
if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply");
int type = TYPE_PING;
int flags = 0;
int length = 4;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(payload1);
out.flush();
}
@Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode)
throws IOException {
if (errorCode.spdyGoAwayCode == -1) throw new IllegalArgumentException();
int type = TYPE_GOAWAY;
int flags = 0;
int length = 8;
out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(lastGoodStreamId);
out.writeInt(errorCode.spdyGoAwayCode);
out.flush();
}
@Override public synchronized void windowUpdate(int streamId, int deltaWindowSize)
throws IOException {
int type = TYPE_WINDOW_UPDATE;
int flags = 0;
int length = 8;
out.writeInt(0x80000000 | (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);
}
}
}

View File

@ -32,8 +32,6 @@ 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.
@ -48,38 +46,21 @@ 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 writes are guarded by frameWriter.
//
// 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
// frameWriter (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;
private static final ExecutorService executor = new ThreadPoolExecutor(0,
Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
Util.daemonThreadFactory("OkHttp SpdyConnection"));
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());
/** The protocol variant, like SPDY/3 or HTTP-draft-06/2.0. */
final Variant variant;
/** True if this peer initiated the connection. */
final boolean client;
@ -89,8 +70,8 @@ public final class SpdyConnection implements Closeable {
* run on the callback executor.
*/
private final IncomingStreamHandler handler;
private final SpdyReader spdyReader;
private final SpdyWriter spdyWriter;
private final FrameReader frameReader;
private final FrameWriter frameWriter;
private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
private final String hostName;
@ -103,14 +84,15 @@ public final class SpdyConnection implements Closeable {
private Map<Integer, Ping> pings;
private int nextPingId;
/** Lazily-created settings for this connection. */
/** Lazily-created settings for the peer. */
Settings settings;
private SpdyConnection(Builder builder) {
variant = builder.variant;
client = builder.client;
handler = builder.handler;
spdyReader = new SpdyReader(builder.in);
spdyWriter = new SpdyWriter(builder.out);
frameReader = variant.newReader(builder.in, client);
frameWriter = variant.newWriter(builder.out, client);
nextStreamId = builder.client ? 1 : 2;
nextPingId = builder.client ? 1 : 2;
@ -140,15 +122,18 @@ public final class SpdyConnection implements Closeable {
}
private synchronized void setIdle(boolean value) {
idleStartTimeNs = value ? System.nanoTime() : 0L;
idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE;
}
/** Returns true if this connection is idle. */
public synchronized boolean isIdle() {
return idleStartTimeNs != 0L;
return idleStartTimeNs != Long.MAX_VALUE;
}
/** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
/**
* Returns the time in ns when this connection became idle or Long.MAX_VALUE
* if connection is not idle.
*/
public synchronized long getIdleStartTimeNs() {
return idleStartTimeNs;
}
@ -157,80 +142,80 @@ public final class SpdyConnection implements Closeable {
* 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}.
* 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}.
* 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);
boolean outFinished = !out;
boolean inFinished = !in;
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 (frameWriter) {
synchronized (this) {
if (shutdown) {
throw new IOException("shutdown");
}
streamId = nextStreamId;
nextStreamId += 2;
stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, settings);
stream = new SpdyStream(
streamId, this, outFinished, inFinished, priority, requestHeaders, settings);
if (stream.isOpen()) {
streams.put(streamId, stream);
setIdle(false);
}
}
spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, requestHeaders);
frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, priority, slot,
requestHeaders);
}
return stream;
}
void writeSynReply(int streamId, int flags, List<String> alternating) throws IOException {
spdyWriter.synReply(flags, streamId, alternating);
void writeSynReply(int streamId, boolean outFinished, List<String> alternating)
throws IOException {
frameWriter.synReply(outFinished, 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);
}
public void writeData(int streamId, boolean outFinished, byte[] buffer, int offset, int byteCount)
throws IOException {
frameWriter.data(outFinished, streamId, buffer, offset, byteCount);
}
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 writeSynResetLater(final int streamId, final ErrorCode errorCode) {
executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
writeSynReset(streamId, errorCode);
} catch (IOException ignored) {
}
}
});
}
void writeSynReset(int streamId, int statusCode) throws IOException {
spdyWriter.rstStream(streamId, statusCode);
void writeSynReset(int streamId, ErrorCode statusCode) throws IOException {
frameWriter.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) {
}
}
});
executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
writeWindowUpdate(streamId, deltaWindowSize);
} catch (IOException ignored) {
}
}
});
}
void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
spdyWriter.windowUpdate(streamId, deltaWindowSize);
frameWriter.windowUpdate(streamId, deltaWindowSize);
}
/**
@ -249,26 +234,28 @@ public final class SpdyConnection implements Closeable {
if (pings == null) pings = new HashMap<Integer, Ping>();
pings.put(pingId, ping);
}
writePing(pingId, ping);
writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, 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)) {
private void writePingLater(
final boolean reply, final int payload1, final int payload2, final Ping ping) {
executor.submit(new NamedRunnable("OkHttp SPDY Writer %s ping %08x%08x",
hostName, payload1, payload2) {
@Override public void execute() {
try {
writePing(streamId, ping);
writePing(reply, payload1, payload2, ping);
} catch (IOException ignored) {
}
}
});
}
private void writePing(int id, Ping ping) throws IOException {
synchronized (spdyWriter) {
private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException {
synchronized (frameWriter) {
// Observe the sent time immediately before performing I/O.
if (ping != null) ping.send();
spdyWriter.ping(0, id);
frameWriter.ping(reply, payload1, payload2);
}
}
@ -278,13 +265,11 @@ public final class SpdyConnection implements Closeable {
/** Sends a noop frame to the peer. */
public void noop() throws IOException {
spdyWriter.noop();
frameWriter.noop();
}
public void flush() throws IOException {
synchronized (spdyWriter) {
spdyWriter.out.flush();
}
frameWriter.flush();
}
/**
@ -292,12 +277,9 @@ public final class SpdyConnection implements Closeable {
* 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) {
public void shutdown(ErrorCode statusCode) throws IOException {
synchronized (frameWriter) {
int lastGoodStreamId;
synchronized (this) {
if (shutdown) {
@ -306,7 +288,7 @@ public final class SpdyConnection implements Closeable {
shutdown = true;
lastGoodStreamId = this.lastGoodStreamId;
}
spdyWriter.goAway(0, lastGoodStreamId, statusCode);
frameWriter.goAway(lastGoodStreamId, statusCode);
}
}
@ -316,14 +298,14 @@ public final class SpdyConnection implements Closeable {
* internal executor services.
*/
@Override public void close() throws IOException {
close(GOAWAY_OK, SpdyStream.RST_CANCEL);
close(ErrorCode.NO_ERROR, ErrorCode.CANCEL);
}
private void close(int shutdownStatusCode, int rstStatusCode) throws IOException {
private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException {
assert (!Thread.holdsLock(this));
IOException thrown = null;
try {
shutdown(shutdownStatusCode);
shutdown(connectionCode);
} catch (IOException e) {
thrown = e;
}
@ -345,7 +327,7 @@ public final class SpdyConnection implements Closeable {
if (streamsToClose != null) {
for (SpdyStream stream : streamsToClose) {
try {
stream.close(rstStatusCode);
stream.close(streamCode);
} catch (IOException e) {
if (thrown != null) thrown = e;
}
@ -359,12 +341,12 @@ public final class SpdyConnection implements Closeable {
}
try {
spdyReader.close();
frameReader.close();
} catch (IOException e) {
thrown = e;
}
try {
spdyWriter.close();
frameWriter.close();
} catch (IOException e) {
if (thrown == null) thrown = e;
}
@ -372,12 +354,30 @@ public final class SpdyConnection implements Closeable {
if (thrown != null) throw thrown;
}
/**
* Sends a connection header if the current variant requires it. This should
* be called after {@link Builder#build} for all new connections.
*/
public void sendConnectionHeader() throws IOException {
frameWriter.connectionHeader();
frameWriter.settings(new Settings());
}
/**
* Reads a connection header if the current variant requires it. This should
* be called after {@link Builder#build} for all new connections.
*/
public void readConnectionHeader() throws IOException {
frameReader.readConnectionHeader();
}
public static class Builder {
private String hostName;
private InputStream in;
private OutputStream out;
private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
public boolean client;
private Variant variant = Variant.SPDY3;
private boolean client;
public Builder(boolean client, Socket socket) throws IOException {
this("", client, socket.getInputStream(), socket.getOutputStream());
@ -411,110 +411,119 @@ public final class SpdyConnection implements Closeable {
return this;
}
public Builder spdy3() {
this.variant = Variant.SPDY3;
return this;
}
public Builder http20Draft06() {
this.variant = Variant.HTTP_20_DRAFT_06;
return this;
}
public SpdyConnection build() {
return new SpdyConnection(this);
}
}
private class Reader implements Runnable, SpdyReader.Handler {
private class Reader implements Runnable, FrameReader.Handler {
@Override public void run() {
int shutdownStatusCode = GOAWAY_INTERNAL_ERROR;
int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR;
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
while (spdyReader.nextFrame(this)) {
while (frameReader.nextFrame(this)) {
}
shutdownStatusCode = GOAWAY_OK;
rstStatusCode = SpdyStream.RST_CANCEL;
connectionErrorCode = ErrorCode.NO_ERROR;
streamErrorCode = ErrorCode.CANCEL;
} catch (IOException e) {
shutdownStatusCode = GOAWAY_PROTOCOL_ERROR;
rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR;
connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
streamErrorCode = ErrorCode.PROTOCOL_ERROR;
} finally {
try {
close(shutdownStatusCode, rstStatusCode);
close(connectionErrorCode, streamErrorCode);
} catch (IOException ignored) {
}
}
}
@Override public void data(int flags, int streamId, InputStream in, int length)
@Override public void data(boolean inFinished, int streamId, InputStream in, int length)
throws IOException {
SpdyStream dataStream = getStream(streamId);
if (dataStream == null) {
writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
Util.skipByReading(in, length);
return;
}
dataStream.receiveData(in, length);
if ((flags & SpdyConnection.FLAG_FIN) != 0) {
if (inFinished) {
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;
@Override public void headers(boolean outFinished, boolean inFinished, int streamId,
int associatedStreamId, int priority, List<String> nameValueBlock,
HeadersMode headersMode) {
SpdyStream stream;
synchronized (SpdyConnection.this) {
synStream =
new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock,
settings);
if (shutdown) {
// If we're shutdown, don't bother with this stream.
if (shutdown) return;
stream = getStream(streamId);
if (stream == null) {
// The headers claim to be for an existing stream, but we don't have one.
if (headersMode.failIfStreamAbsent()) {
writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
return;
}
// If the stream ID is less than the last created ID, assume it's already closed.
if (streamId <= lastGoodStreamId) return;
// If the stream ID is in the client's namespace, assume it's already closed.
if (streamId % 2 == nextStreamId % 2) return;
// Create a stream.
final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished,
inFinished, priority, nameValueBlock, settings);
lastGoodStreamId = streamId;
streams.put(streamId, newStream);
executor.submit(new NamedRunnable("OkHttp Callback %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
handler.receive(newStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
return;
}
lastGoodStreamId = streamId;
previous = streams.put(streamId, synStream);
}
if (previous != null) {
previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
// The headers claim to be for a new stream, but we already have one.
if (headersMode.failIfStreamPresent()) {
stream.closeLater(ErrorCode.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);
}
}
});
// Update an existing stream.
stream.receiveHeaders(nameValueBlock, headersMode);
if (inFinished) stream.receiveFin();
}
@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) {
@Override public void rstStream(int streamId, ErrorCode errorCode) {
SpdyStream rstStream = removeStream(streamId);
if (rstStream != null) {
rstStream.receiveRstStream(statusCode);
rstStream.receiveRstStream(errorCode);
}
}
@Override public void settings(int flags, Settings newSettings) {
@Override public void settings(boolean clearPrevious, Settings newSettings) {
SpdyStream[] streamsToNotify = null;
synchronized (SpdyConnection.this) {
if (settings == null || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
if (settings == null || clearPrevious) {
settings = newSettings;
} else {
settings.merge(newSettings);
@ -528,8 +537,9 @@ public final class SpdyConnection implements Closeable {
// 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.
// TODO: this can block the reader thread until a write completes. That's bad!
synchronized (stream) {
synchronized (this) {
synchronized (SpdyConnection.this) {
stream.receiveSettings(settings);
}
}
@ -540,19 +550,19 @@ public final class SpdyConnection implements Closeable {
@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);
@Override public void ping(boolean reply, int payload1, int payload2) {
if (reply) {
Ping ping = removePing(payload1);
if (ping != null) {
ping.receive();
}
} else {
// Send a reply to a client ping if this is a server and vice versa.
writePingLater(true, payload1, payload2, null);
}
}
@Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
@Override public void goAway(int lastGoodStreamId, ErrorCode errorCode) {
synchronized (SpdyConnection.this) {
shutdown = true;
@ -562,18 +572,28 @@ public final class SpdyConnection implements Closeable {
Map.Entry<Integer, SpdyStream> entry = i.next();
int streamId = entry.getKey();
if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
entry.getValue().receiveRstStream(SpdyStream.RST_REFUSED_STREAM);
entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM);
i.remove();
}
}
}
}
@Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
@Override public void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl) {
if (streamId == 0) {
// TODO: honor whole-stream flow control
return;
}
// TODO: honor endFlowControl
SpdyStream stream = getStream(streamId);
if (stream != null) {
stream.receiveWindowUpdate(deltaWindowSize);
}
}
@Override public void priority(int streamId, int priority) {
// TODO: honor priority.
}
}
}

View File

@ -1,326 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.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.io.UnsupportedEncodingException;
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;
static {
try {
DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+ ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+ "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError();
}
}
private final DataInputStream in;
private final DataInputStream nameValueBlockIn;
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.getMessage());
}
}
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);
}
}

View File

@ -26,8 +26,6 @@ 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 {
@ -35,35 +33,6 @@ 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
@ -75,7 +44,6 @@ public final class SpdyStream {
private final int id;
private final SpdyConnection connection;
private final int priority;
private final int slot;
private long readTimeoutMillis = 0;
private int writeWindowSize;
@ -93,28 +61,19 @@ public final class SpdyStream {
* 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;
private ErrorCode errorCode = null;
SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
List<String> requestHeaders, Settings settings) {
SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished,
int priority, 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.in.finished = inFinished;
this.out.finished = outFinished;
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);
}
@ -129,7 +88,7 @@ public final class SpdyStream {
* reports itself as not open. This is because input data is buffered.
*/
public synchronized boolean isOpen() {
if (rstStatusCode != -1) {
if (errorCode != null) {
return false;
}
if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) {
@ -157,14 +116,28 @@ public final class SpdyStream {
* have not been received yet.
*/
public synchronized List<String> getResponseHeaders() throws IOException {
long remaining = 0;
long start = 0;
if (readTimeoutMillis != 0) {
start = (System.nanoTime() / 1000000);
remaining = readTimeoutMillis;
}
try {
while (responseHeaders == null && rstStatusCode == -1) {
wait();
while (responseHeaders == null && errorCode == null) {
if (readTimeoutMillis == 0) { // No timeout configured.
wait();
} else if (remaining > 0) {
wait(remaining);
remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
} else {
throw new SocketTimeoutException("Read response header timeout. readTimeoutMillis: "
+ readTimeoutMillis);
}
}
if (responseHeaders != null) {
return responseHeaders;
}
throw new IOException("stream was reset: " + rstStatusString());
throw new IOException("stream was reset: " + errorCode);
} catch (InterruptedException e) {
InterruptedIOException rethrow = new InterruptedIOException();
rethrow.initCause(e);
@ -173,15 +146,11 @@ public final class SpdyStream {
}
/**
* 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}.
* Returns the reason why this stream was closed, or null if it closed
* normally or has not yet been closed.
*/
public synchronized int getRstStatusCode() {
return rstStatusCode;
public synchronized ErrorCode getErrorCode() {
return errorCode;
}
/**
@ -192,7 +161,7 @@ public final class SpdyStream {
*/
public void reply(List<String> responseHeaders, boolean out) throws IOException {
assert (!Thread.holdsLock(SpdyStream.this));
int flags = 0;
boolean outFinished = false;
synchronized (this) {
if (responseHeaders == null) {
throw new NullPointerException("responseHeaders == null");
@ -206,10 +175,10 @@ public final class SpdyStream {
this.responseHeaders = responseHeaders;
if (!out) {
this.out.finished = true;
flags |= SpdyConnection.FLAG_FIN;
outFinished = true;
}
}
connection.writeSynReply(id, flags, responseHeaders);
connection.writeSynReply(id, outFinished, responseHeaders);
}
/**
@ -248,7 +217,7 @@ public final class SpdyStream {
* Abnormally terminate this stream. This blocks until the {@code RST_STREAM}
* frame has been transmitted.
*/
public void close(int rstStatusCode) throws IOException {
public void close(ErrorCode rstStatusCode) throws IOException {
if (!closeInternal(rstStatusCode)) {
return; // Already closed.
}
@ -259,68 +228,61 @@ public final class SpdyStream {
* Abnormally terminate this stream. This enqueues a {@code RST_STREAM}
* frame and returns immediately.
*/
public void closeLater(int rstStatusCode) {
if (!closeInternal(rstStatusCode)) {
public void closeLater(ErrorCode errorCode) {
if (!closeInternal(errorCode)) {
return; // Already closed.
}
connection.writeSynResetLater(id, rstStatusCode);
connection.writeSynResetLater(id, errorCode);
}
/** Returns true if this stream was closed. */
private boolean closeInternal(int rstStatusCode) {
private boolean closeInternal(ErrorCode errorCode) {
assert (!Thread.holdsLock(this));
synchronized (this) {
if (this.rstStatusCode != -1) {
if (this.errorCode != null) {
return false;
}
if (in.finished && out.finished) {
return false;
}
this.rstStatusCode = rstStatusCode;
this.errorCode = errorCode;
notifyAll();
}
connection.removeStream(id);
return true;
}
void receiveReply(List<String> strings) throws IOException {
void receiveHeaders(List<String> headers, HeadersMode headersMode) {
assert (!Thread.holdsLock(SpdyStream.this));
boolean streamInUseError = false;
ErrorCode errorCode = null;
boolean open = true;
synchronized (this) {
if (isLocallyInitiated() && responseHeaders == null) {
responseHeaders = strings;
open = isOpen();
notifyAll();
if (responseHeaders == null) {
if (headersMode.failIfHeadersAbsent()) {
errorCode = ErrorCode.PROTOCOL_ERROR;
} else {
responseHeaders = headers;
open = isOpen();
notifyAll();
}
} else {
streamInUseError = true;
if (headersMode.failIfHeadersPresent()) {
errorCode = ErrorCode.STREAM_IN_USE;
} else {
List<String> newHeaders = new ArrayList<String>();
newHeaders.addAll(responseHeaders);
newHeaders.addAll(headers);
this.responseHeaders = newHeaders;
}
}
}
if (streamInUseError) {
closeLater(SpdyStream.RST_STREAM_IN_USE);
if (errorCode != null) {
closeLater(errorCode);
} 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);
@ -339,18 +301,20 @@ public final class SpdyStream {
}
}
synchronized void receiveRstStream(int statusCode) {
if (rstStatusCode == -1) {
rstStatusCode = statusCode;
synchronized void receiveRstStream(ErrorCode errorCode) {
if (this.errorCode == null) {
this.errorCode = errorCode;
notifyAll();
}
}
private void setSettings(Settings settings) {
// TODO: For HTTP/2.0, also adjust the stream flow control window size
// by the difference between the new value and the old value.
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;
this.writeWindowSize = settings != null
? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE)
: Settings.DEFAULT_INITIAL_WINDOW_SIZE;
}
void receiveSettings(Settings settings) {
@ -364,19 +328,10 @@ public final class SpdyStream {
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,
@ -496,7 +451,7 @@ public final class SpdyStream {
remaining = readTimeoutMillis;
}
try {
while (pos == -1 && !finished && !closed && rstStatusCode == -1) {
while (pos == -1 && !finished && !closed && errorCode == null) {
if (readTimeoutMillis == 0) {
SpdyStream.this.wait();
} else if (remaining > 0) {
@ -534,7 +489,7 @@ public final class SpdyStream {
// 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);
closeLater(ErrorCode.FLOW_CONTROL_ERROR);
return;
}
@ -583,8 +538,8 @@ public final class SpdyStream {
if (closed) {
throw new IOException("stream closed");
}
if (rstStatusCode != -1) {
throw new IOException("stream was reset: " + rstStatusString());
if (errorCode != null) {
throw new IOException("stream was reset: " + errorCode);
}
}
}
@ -602,7 +557,7 @@ public final class SpdyStream {
// 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);
SpdyStream.this.close(ErrorCode.CANCEL);
} else if (!open) {
connection.removeStream(id);
}
@ -614,7 +569,7 @@ public final class SpdyStream {
*/
private final class SpdyDataOutputStream extends OutputStream {
private final byte[] buffer = new byte[8192];
private int pos = DATA_FRAME_HEADER_LENGTH;
private int pos = 0;
/** True if the caller has closed this stream. */
private boolean closed;
@ -656,7 +611,7 @@ public final class SpdyStream {
@Override public void flush() throws IOException {
assert (!Thread.holdsLock(SpdyStream.this));
checkNotClosed();
if (pos > DATA_FRAME_HEADER_LENGTH) {
if (pos > 0) {
writeFrame(false);
connection.flush();
}
@ -670,27 +625,23 @@ public final class SpdyStream {
}
closed = true;
}
writeFrame(true);
if (!out.finished) {
writeFrame(true);
}
connection.flush();
cancelStreamIfNecessary();
}
private void writeFrame(boolean last) throws IOException {
private void writeFrame(boolean outFinished) throws IOException {
assert (!Thread.holdsLock(SpdyStream.this));
int length = pos - DATA_FRAME_HEADER_LENGTH;
int length = pos;
synchronized (SpdyStream.this) {
waitUntilWritable(length, last);
waitUntilWritable(length, outFinished);
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;
connection.writeData(id, outFinished, buffer, 0, pos);
pos = 0;
}
/**
@ -709,8 +660,8 @@ public final class SpdyStream {
throw new IOException("stream closed");
} else if (finished) {
throw new IOException("stream finished");
} else if (rstStatusCode != -1) {
throw new IOException("stream was reset: " + rstStatusString());
} else if (errorCode != null) {
throw new IOException("stream was reset: " + errorCode);
}
}
} catch (InterruptedException e) {
@ -724,8 +675,8 @@ public final class SpdyStream {
throw new IOException("stream closed");
} else if (finished) {
throw new IOException("stream finished");
} else if (rstStatusCode != -1) {
throw new IOException("stream was reset: " + rstStatusString());
} else if (errorCode != null) {
throw new IOException("stream was reset: " + errorCode);
}
}
}

View File

@ -1,176 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.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);
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.spdy;
import java.io.InputStream;
import java.io.OutputStream;
/** A version and dialect of the framed socket protocol. */
interface Variant {
Variant SPDY3 = new Spdy3();
Variant HTTP_20_DRAFT_06 = new Http20Draft06();
/**
* @param client true if this is the HTTP client's reader, reading frames from
* a peer SPDY or HTTP/2 server.
*/
FrameReader newReader(InputStream in, boolean client);
/**
* @param client true if this is the HTTP client's writer, writing frames to a
* peer SPDY or HTTP/2 server.
*/
FrameWriter newWriter(OutputStream out, boolean client);
}

View File

@ -0,0 +1,407 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.tls;
import javax.security.auth.x500.X500Principal;
/**
* A distinguished name (DN) parser. This parser only supports extracting a
* string value from a DN. It doesn't support values in the hex-string style.
*/
final class DistinguishedNameParser {
private final String dn;
private final int length;
private int pos;
private int beg;
private int end;
/** Temporary variable to store positions of the currently parsed item. */
private int cur;
/** Distinguished name characters. */
private char[] chars;
public DistinguishedNameParser(X500Principal principal) {
// RFC2253 is used to ensure we get attributes in the reverse
// order of the underlying ASN.1 encoding, so that the most
// significant values of repeated attributes occur first.
this.dn = principal.getName(X500Principal.RFC2253);
this.length = this.dn.length();
}
// gets next attribute type: (ALPHA 1*keychar) / oid
private String nextAT() {
// skip preceding space chars, they can present after
// comma or semicolon (compatibility with RFC 1779)
for (; pos < length && chars[pos] == ' '; pos++) {
}
if (pos == length) {
return null; // reached the end of DN
}
// mark the beginning of attribute type
beg = pos;
// attribute type chars
pos++;
for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
// we don't follow exact BNF syntax here:
// accept any char except space and '='
}
if (pos >= length) {
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
// mark the end of attribute type
end = pos;
// skip trailing space chars between attribute type and '='
// (compatibility with RFC 1779)
if (chars[pos] == ' ') {
for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
}
if (chars[pos] != '=' || pos == length) {
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
}
pos++; //skip '=' char
// skip space chars between '=' and attribute value
// (compatibility with RFC 1779)
for (; pos < length && chars[pos] == ' '; pos++) {
}
// in case of oid attribute type skip its prefix: "oid." or "OID."
// (compatibility with RFC 1779)
if ((end - beg > 4) && (chars[beg + 3] == '.')
&& (chars[beg] == 'O' || chars[beg] == 'o')
&& (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
&& (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
beg += 4;
}
return new String(chars, beg, end - beg);
}
// gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
private String quotedAV() {
pos++;
beg = pos;
end = beg;
while (true) {
if (pos == length) {
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
if (chars[pos] == '"') {
// enclosing quotation was found
pos++;
break;
} else if (chars[pos] == '\\') {
chars[end] = getEscaped();
} else {
// shift char: required for string with escaped chars
chars[end] = chars[pos];
}
pos++;
end++;
}
// skip trailing space chars before comma or semicolon.
// (compatibility with RFC 1779)
for (; pos < length && chars[pos] == ' '; pos++) {
}
return new String(chars, beg, end - beg);
}
// gets hex string attribute value: "#" hexstring
private String hexAV() {
if (pos + 4 >= length) {
// encoded byte array must be not less then 4 c
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
beg = pos; // store '#' position
pos++;
while (true) {
// check for end of attribute value
// looks for space and component separators
if (pos == length || chars[pos] == '+' || chars[pos] == ','
|| chars[pos] == ';') {
end = pos;
break;
}
if (chars[pos] == ' ') {
end = pos;
pos++;
// skip trailing space chars before comma or semicolon.
// (compatibility with RFC 1779)
for (; pos < length && chars[pos] == ' '; pos++) {
}
break;
} else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
chars[pos] += 32; //to low case
}
pos++;
}
// verify length of hex string
// encoded byte array must be not less then 4 and must be even number
int hexLen = end - beg; // skip first '#' char
if (hexLen < 5 || (hexLen & 1) == 0) {
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
// get byte encoding from string representation
byte[] encoded = new byte[hexLen / 2];
for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
encoded[i] = (byte) getByte(p);
}
return new String(chars, beg, hexLen);
}
// gets string attribute value: *( stringchar / pair )
private String escapedAV() {
beg = pos;
end = pos;
while (true) {
if (pos >= length) {
// the end of DN has been found
return new String(chars, beg, end - beg);
}
switch (chars[pos]) {
case '+':
case ',':
case ';':
// separator char has been found
return new String(chars, beg, end - beg);
case '\\':
// escaped char
chars[end++] = getEscaped();
pos++;
break;
case ' ':
// need to figure out whether space defines
// the end of attribute value or not
cur = end;
pos++;
chars[end++] = ' ';
for (; pos < length && chars[pos] == ' '; pos++) {
chars[end++] = ' ';
}
if (pos == length || chars[pos] == ',' || chars[pos] == '+'
|| chars[pos] == ';') {
// separator char or the end of DN has been found
return new String(chars, beg, cur - beg);
}
break;
default:
chars[end++] = chars[pos];
pos++;
}
}
}
// returns escaped char
private char getEscaped() {
pos++;
if (pos == length) {
throw new IllegalStateException("Unexpected end of DN: " + dn);
}
switch (chars[pos]) {
case '"':
case '\\':
case ',':
case '=':
case '+':
case '<':
case '>':
case '#':
case ';':
case ' ':
case '*':
case '%':
case '_':
//FIXME: escaping is allowed only for leading or trailing space char
return chars[pos];
default:
// RFC doesn't explicitly say that escaped hex pair is
// interpreted as UTF-8 char. It only contains an example of such DN.
return getUTF8();
}
}
// decodes UTF-8 char
// see http://www.unicode.org for UTF-8 bit distribution table
private char getUTF8() {
int res = getByte(pos);
pos++; //FIXME tmp
if (res < 128) { // one byte: 0-7F
return (char) res;
} else if (res >= 192 && res <= 247) {
int count;
if (res <= 223) { // two bytes: C0-DF
count = 1;
res = res & 0x1F;
} else if (res <= 239) { // three bytes: E0-EF
count = 2;
res = res & 0x0F;
} else { // four bytes: F0-F7
count = 3;
res = res & 0x07;
}
int b;
for (int i = 0; i < count; i++) {
pos++;
if (pos == length || chars[pos] != '\\') {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
pos++;
b = getByte(pos);
pos++; //FIXME tmp
if ((b & 0xC0) != 0x80) {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
res = (res << 6) + (b & 0x3F);
}
return (char) res;
} else {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
}
// Returns byte representation of a char pair
// The char pair is composed of DN char in
// specified 'position' and the next char
// According to BNF syntax:
// hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
// / "a" / "b" / "c" / "d" / "e" / "f"
private int getByte(int position) {
if (position + 1 >= length) {
throw new IllegalStateException("Malformed DN: " + dn);
}
int b1, b2;
b1 = chars[position];
if (b1 >= '0' && b1 <= '9') {
b1 = b1 - '0';
} else if (b1 >= 'a' && b1 <= 'f') {
b1 = b1 - 87; // 87 = 'a' - 10
} else if (b1 >= 'A' && b1 <= 'F') {
b1 = b1 - 55; // 55 = 'A' - 10
} else {
throw new IllegalStateException("Malformed DN: " + dn);
}
b2 = chars[position + 1];
if (b2 >= '0' && b2 <= '9') {
b2 = b2 - '0';
} else if (b2 >= 'a' && b2 <= 'f') {
b2 = b2 - 87; // 87 = 'a' - 10
} else if (b2 >= 'A' && b2 <= 'F') {
b2 = b2 - 55; // 55 = 'A' - 10
} else {
throw new IllegalStateException("Malformed DN: " + dn);
}
return (b1 << 4) + b2;
}
/**
* Parses the DN and returns the most significant attribute value
* for an attribute type, or null if none found.
*
* @param attributeType attribute type to look for (e.g. "ca")
*/
public String findMostSpecific(String attributeType) {
// Initialize internal state.
pos = 0;
beg = 0;
end = 0;
cur = 0;
chars = dn.toCharArray();
String attType = nextAT();
if (attType == null) {
return null;
}
while (true) {
String attValue = "";
if (pos == length) {
return null;
}
switch (chars[pos]) {
case '"':
attValue = quotedAV();
break;
case '#':
attValue = hexAV();
break;
case '+':
case ',':
case ';': // compatibility with RFC 1779: semicolon can separate RDNs
//empty attribute value
break;
default:
attValue = escapedAV();
}
// Values are ordered from most specific to least specific
// due to the RFC2253 formatting. So take the first match
// we see.
if (attributeType.equalsIgnoreCase(attType)) {
return attValue;
}
if (pos >= length) {
return null;
}
if (chars[pos] == ',' || chars[pos] == ';') {
} else if (chars[pos] != '+') {
throw new IllegalStateException("Malformed DN: " + dn);
}
pos++;
attType = nextAT();
if (attType == null) {
throw new IllegalStateException("Malformed DN: " + dn);
}
}
}
}

View File

@ -0,0 +1,194 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.tls;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
/**
* A HostnameVerifier consistent with <a
* href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
*/
public final class OkHostnameVerifier implements HostnameVerifier {
public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This
* is an approximation of Android's private InetAddress#isNumeric API.
*
* <p>This matches IPv6 addresses as a hex string containing at least one
* colon, and possibly including dots after the first colon. It matches IPv4
* addresses as strings containing only decimal digits and dots. This pattern
* matches strings like "a:.23" and "54" that are neither IP addresses nor
* hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
"([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
private static final int ALT_DNS_NAME = 2;
private static final int ALT_IPA_NAME = 7;
private OkHostnameVerifier() {
}
public boolean verify(String host, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
return verify(host, (X509Certificate) certificates[0]);
} catch (SSLException e) {
return false;
}
}
public boolean verify(String host, X509Certificate certificate) {
return verifyAsIpAddress(host)
? verifyIpAddress(host, certificate)
: verifyHostName(host, certificate);
}
static boolean verifyAsIpAddress(String host) {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
}
/**
* Returns true if {@code certificate} matches {@code ipAddress}.
*/
private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
if (ipAddress.equalsIgnoreCase(altName)) {
return true;
}
}
return false;
}
/**
* Returns true if {@code certificate} matches {@code hostName}.
*/
private boolean verifyHostName(String hostName, X509Certificate certificate) {
hostName = hostName.toLowerCase(Locale.US);
boolean hasDns = false;
for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
hasDns = true;
if (verifyHostName(hostName, altName)) {
return true;
}
}
if (!hasDns) {
X500Principal principal = certificate.getSubjectX500Principal();
// RFC 2818 advises using the most specific name for matching.
String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
if (cn != null) {
return verifyHostName(hostName, cn);
}
}
return false;
}
private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
List<String> result = new ArrayList<String>();
try {
Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
if (subjectAltNames == null) {
return Collections.emptyList();
}
for (Object subjectAltName : subjectAltNames) {
List<?> entry = (List<?>) subjectAltName;
if (entry == null || entry.size() < 2) {
continue;
}
Integer altNameType = (Integer) entry.get(0);
if (altNameType == null) {
continue;
}
if (altNameType == type) {
String altName = (String) entry.get(1);
if (altName != null) {
result.add(altName);
}
}
}
return result;
} catch (CertificateParsingException e) {
return Collections.emptyList();
}
}
/**
* Returns true if {@code hostName} matches the name or pattern {@code cn}.
*
* @param hostName lowercase host name.
* @param cn certificate host name. May include wildcards like
* {@code *.android.com}.
*/
public boolean verifyHostName(String hostName, String cn) {
// Check length == 0 instead of .isEmpty() to support Java 5.
if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) {
return false;
}
cn = cn.toLowerCase(Locale.US);
if (!cn.contains("*")) {
return hostName.equals(cn);
}
if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
return true; // "*.foo.com" matches "foo.com"
}
int asterisk = cn.indexOf('*');
int dot = cn.indexOf('.');
if (asterisk > dot) {
return false; // malformed; wildcard must be in the first part of the cn
}
if (!hostName.regionMatches(0, cn, 0, asterisk)) {
return false; // prefix before '*' doesn't match
}
int suffixLength = cn.length() - (asterisk + 1);
int suffixStart = hostName.length() - suffixLength;
if (hostName.indexOf('.', asterisk) < suffixStart) {
// TODO: remove workaround for *.clients.google.com http://b/5426333
if (!hostName.endsWith(".clients.google.com")) {
return false; // wildcard '*' can't match a '.'
}
}
if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
return false; // suffix after '*' doesn't match
}
return true;
}
}