diff --git a/framework/src/com/squareup/okhttp/Address.java b/framework/src/com/squareup/okhttp/Address.java old mode 100644 new mode 100755 index cd41ac99..b34bd912 --- a/framework/src/com/squareup/okhttp/Address.java +++ b/framework/src/com/squareup/okhttp/Address.java @@ -15,8 +15,10 @@ */ package com.squareup.okhttp; +import com.squareup.okhttp.internal.Util; import java.net.Proxy; import java.net.UnknownHostException; +import java.util.List; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; @@ -38,16 +40,23 @@ public final class Address { final int uriPort; final SSLSocketFactory sslSocketFactory; final HostnameVerifier hostnameVerifier; + final OkAuthenticator authenticator; + final List transports; public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory, - HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException { + HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy, + List 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 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; } } diff --git a/framework/src/com/squareup/okhttp/Connection.java b/framework/src/com/squareup/okhttp/Connection.java old mode 100644 new mode 100755 index 6a6c84dc..6bb9cb3a --- a/framework/src/com/squareup/okhttp/Connection.java +++ b/framework/src/com/squareup/okhttp/Connection.java @@ -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); + } } diff --git a/framework/src/com/squareup/okhttp/ConnectionPool.java b/framework/src/com/squareup/okhttp/ConnectionPool.java old mode 100644 new mode 100755 index 933bd737..42b70b98 --- a/framework/src/com/squareup/okhttp/ConnectionPool.java +++ b/framework/src/com/squareup/okhttp/ConnectionPool.java @@ -80,8 +80,9 @@ public class ConnectionPool { private final LinkedList connections = new LinkedList(); /** We use a single background thread to cleanup expired connections. */ - private final ExecutorService executorService = - new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(), + Util.daemonThreadFactory("OkHttp ConnectionPool")); private final Callable connectionsCleanupCallable = new Callable() { @Override public Void call() throws Exception { List expiredConnections = new ArrayList(MAX_CONNECTIONS_TO_CLEANUP); @@ -215,8 +216,6 @@ public class ConnectionPool { *

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); } /** diff --git a/framework/src/com/squareup/okhttp/Dispatcher.java b/framework/src/com/squareup/okhttp/Dispatcher.java new file mode 100755 index 00000000..1982a8ac --- /dev/null +++ b/framework/src/com/squareup/okhttp/Dispatcher.java @@ -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()); + private final Map> enqueuedJobs = new LinkedHashMap>(); + + public synchronized void enqueue( + OkHttpClient client, Request request, Response.Receiver responseReceiver) { + Job job = new Job(this, client, request, responseReceiver); + List jobsForTag = enqueuedJobs.get(request.tag()); + if (jobsForTag == null) { + jobsForTag = new ArrayList(2); + enqueuedJobs.put(request.tag(), jobsForTag); + } + jobsForTag.add(job); + executorService.execute(job); + } + + public synchronized void cancel(Object tag) { + List jobs = enqueuedJobs.remove(tag); + if (jobs == null) return; + for (Job job : jobs) { + executorService.remove(job); + } + } + + synchronized void finished(Job job) { + List 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; + } + } +} diff --git a/framework/src/com/squareup/okhttp/Failure.java b/framework/src/com/squareup/okhttp/Failure.java new file mode 100755 index 00000000..a3547003 --- /dev/null +++ b/framework/src/com/squareup/okhttp/Failure.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

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

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. + * + *

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. + * + *

The following transports are currently supported: + *

+ * + *

This is an evolving set. 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. + * + *

If multiple protocols are specified, NPN will + * be used to negotiate a transport. Future releases may use another mechanism + * (such as ALPN) + * 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 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 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}. + * + *

This code configures OkHttp to handle all HTTP and HTTPS connections + * created with {@link URL#openConnection()}:

   {@code
+   *
+   *   OkHttpClient okHttpClient = new OkHttpClient();
+   *   URL.setURLStreamHandlerFactory(okHttpClient);
+   * }
+ */ + 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(); + } + }; + } } diff --git a/framework/src/com/squareup/okhttp/OkResponseCache.java b/framework/src/com/squareup/okhttp/OkResponseCache.java old mode 100644 new mode 100755 index b7e3801d..ffe6f54b --- a/framework/src/com/squareup/okhttp/OkResponseCache.java +++ b/framework/src/com/squareup/okhttp/OkResponseCache.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * This class is in beta. APIs are subject to change! */ public interface OkResponseCache { + CacheResponse get(URI uri, String requestMethod, Map> 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); } diff --git a/framework/src/com/squareup/okhttp/Request.java b/framework/src/com/squareup/okhttp/Request.java new file mode 100755 index 00000000..f95303eb --- /dev/null +++ b/framework/src/com/squareup/okhttp/Request.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * 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 headers(String name) { + return headers.values(name); + } + + public Set 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); + } + } +} diff --git a/framework/src/com/squareup/okhttp/Response.java b/framework/src/com/squareup/okhttp/Response.java new file mode 100755 index 00000000..1e67968c --- /dev/null +++ b/framework/src/com/squareup/okhttp/Response.java @@ -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. + * + *

Warning: Experimental OkHttp 2.0 API

+ * 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 + * not the same request instance provided to the HTTP client: + *
    + *
  • 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. + *
  • 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. + *
+ */ + 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 headers(String name) { + return headers.values(name); + } + + public Set 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. + * + *

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. + */ + //

Body.ready() vs. InputStream.available()

+ // 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. + * + *

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. + * + *

Non-blocking responses

+ * + *

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. + * + *

Return true to indicate that the receiver has finished handling the + * response body. If the response body has unread data, it will be + * discarded. + * + *

When the response body has been fully consumed the returned value is + * undefined. + * + *

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); + } + } +} diff --git a/framework/src/com/squareup/okhttp/ResponseSource.java b/framework/src/com/squareup/okhttp/ResponseSource.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/Route.java b/framework/src/com/squareup/okhttp/Route.java old mode 100644 new mode 100755 index 6968c604..4b8786d2 --- a/framework/src/com/squareup/okhttp/Route.java +++ b/framework/src/com/squareup/okhttp/Route.java @@ -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); } diff --git a/framework/src/com/squareup/okhttp/RouteDatabase.java b/framework/src/com/squareup/okhttp/RouteDatabase.java new file mode 100755 index 00000000..9cbeaa73 --- /dev/null +++ b/framework/src/com/squareup/okhttp/RouteDatabase.java @@ -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 failedRoutes = new LinkedHashSet(); + + /** 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(); + } +} diff --git a/framework/src/com/squareup/okhttp/TunnelRequest.java b/framework/src/com/squareup/okhttp/TunnelRequest.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/Base64.java b/framework/src/com/squareup/okhttp/internal/Base64.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/Dns.java b/framework/src/com/squareup/okhttp/internal/Dns.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/NamedRunnable.java b/framework/src/com/squareup/okhttp/internal/NamedRunnable.java old mode 100644 new mode 100755 index ce430b27..992b2ae4 --- a/framework/src/com/squareup/okhttp/internal/NamedRunnable.java +++ b/framework/src/com/squareup/okhttp/internal/NamedRunnable.java @@ -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() { diff --git a/framework/src/com/squareup/okhttp/internal/Platform.java b/framework/src/com/squareup/okhttp/internal/Platform.java old mode 100644 new mode 100755 index 6b4ac343..d5884b18 --- a/framework/src/com/squareup/okhttp/internal/Platform.java +++ b/framework/src/com/squareup/okhttp/internal/Platform.java @@ -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. - * - *

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; diff --git a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java old mode 100644 new mode 100755 index 3ddc693c..74af6fd5 --- a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java +++ b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java @@ -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()); diff --git a/framework/src/com/squareup/okhttp/internal/Util.java b/framework/src/com/squareup/okhttp/internal/Util.java old mode 100644 new mode 100755 index 290e5ea9..9c5b008a --- a/framework/src/com/squareup/okhttp/internal/Util.java +++ b/framework/src/com/squareup/okhttp/internal/Util.java @@ -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 skipBuffer = new AtomicReference(); + 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 List immutableList(List list) { + return Collections.unmodifiableList(new ArrayList(list)); + } + + public static ThreadFactory daemonThreadFactory(final String name) { + return new ThreadFactory() { + @Override public Thread newThread(Runnable runnable) { + Thread result = new Thread(runnable, name); + result.setDaemon(true); + return result; + } + }; + } } diff --git a/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java b/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java old mode 100644 new mode 100755 index 187f3b6e..a5d39b30 --- a/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java +++ b/framework/src/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java @@ -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); } /** diff --git a/framework/src/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java deleted file mode 100644 index 90675b06..00000000 --- a/framework/src/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java +++ /dev/null @@ -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. - * - *

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"); - } - } -} diff --git a/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java b/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java old mode 100644 new mode 100755 index 12e64097..d5f0f4f9 --- a/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java +++ b/framework/src/com/squareup/okhttp/internal/http/HeaderParser.java @@ -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(); } diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java old mode 100644 new mode 100755 index 4ccd12aa..1ad36898 --- a/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpAuthenticator.java @@ -16,7 +16,8 @@ */ package com.squareup.okhttp.internal.http; -import com.squareup.okhttp.internal.Base64; +import com.squareup.okhttp.OkAuthenticator; +import com.squareup.okhttp.OkAuthenticator.Challenge; import java.io.IOException; import java.net.Authenticator; import java.net.InetAddress; @@ -27,11 +28,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 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 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 challenges = parseChallenges(responseHeaders, challengeHeader); + List 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(); - } - } } diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpDate.java b/framework/src/com/squareup/okhttp/internal/http/HttpDate.java old mode 100644 new mode 100755 index acb5fda9..b4d2c7c0 --- a/framework/src/com/squareup/okhttp/internal/http/HttpDate.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpDate.java @@ -36,14 +36,13 @@ final class HttpDate { new ThreadLocal() { @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; diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java old mode 100644 new mode 100755 index 7a06dca5..4a2dad40 --- a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java @@ -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> 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)); } diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java deleted file mode 100644 index 87351669..00000000 --- a/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java +++ /dev/null @@ -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> 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: - *

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

A typical HTTPS file looks like this: - *

{@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
-     * }
- * 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. - * - *

Next is the response status line, followed by the number of HTTP - * response header lines, followed by those lines. - * - *

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> 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> 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> getHeaders() { - return entry.responseHeaders.toMultimap(true); - } - - @Override public InputStream getBody() { - return in; - } - - @Override public String getCipherSuite() { - return entry.cipherSuite; - } - - @Override public List 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 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(); - } - } -} diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java old mode 100644 new mode 100755 index f6d77b25..c9678308 --- a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java @@ -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. + * + *

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(); } } diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java old mode 100644 new mode 100755 index eabe649d..fb4a7048 --- a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -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 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 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 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 transportsList = new ArrayList(); + 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; } } diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsEngine.java b/framework/src/com/squareup/okhttp/internal/http/HttpsEngine.java new file mode 100755 index 00000000..2bc1d68e --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/http/HttpsEngine.java @@ -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()); + } +} diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java old mode 100644 new mode 100755 index 235f8629..e64fb98c --- a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java +++ b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java @@ -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 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> getHeaderFields() { + @Override public Map> getHeaderFields() { return delegate.getHeaderFields(); } - @Override - public Map> getRequestProperties() { + @Override public Map> 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 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()); - } - } } diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java deleted file mode 100644 index 5829f024..00000000 --- a/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java +++ /dev/null @@ -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. - * - *

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> requestHeaders) - throws IOException; - - CacheRequest put(URI uri, URLConnection urlConnection) throws IOException; - - /** - * Handles a conditional request hit by updating the stored cache response - * with the headers from {@code httpConnection}. The cached response body is - * not updated. If the stored response has changed since {@code - * conditionalCacheHit} was returned, this does nothing. - */ - void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException; - - /** Track an conditional GET that was satisfied by this cache. */ - void trackConditionalCacheHit(); - - /** Track an HTTP response being satisfied by {@code source}. */ - void trackResponse(ResponseSource source); -} diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java old mode 100644 new mode 100755 index 2ac915a8..5335c2bc --- a/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java +++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java @@ -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 { } diff --git a/framework/src/com/squareup/okhttp/internal/http/Policy.java b/framework/src/com/squareup/okhttp/internal/http/Policy.java new file mode 100755 index 00000000..0a29d4b1 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/http/Policy.java @@ -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); +} diff --git a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java old mode 100644 new mode 100755 index eba887ec..8b453207 --- a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java @@ -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 names() { + TreeSet result = new TreeSet(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 values(String name) { + List result = null; + for (int i = 0; i < length(); i++) { + if (name.equalsIgnoreCase(getFieldName(i))) { + if (result == null) result = new ArrayList(2); + result.add(getValue(i)); + } + } + return result != null + ? Collections.unmodifiableList(result) + : Collections.emptyList(); + } + /** @param fieldNames a case-insensitive set of HTTP header field names. */ public RawHeaders getAll(Set fieldNames) { RawHeaders result = new RawHeaders(); @@ -401,10 +411,13 @@ public final class RawHeaders { return result; } - public static RawHeaders fromNameValueBlock(List nameValueBlock) { + /** Returns headers for a name value block containing a SPDY response. */ + public static RawHeaders fromNameValueBlock(List 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; } } diff --git a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java old mode 100644 new mode 100755 index 5ec4fcca..71c3cd0f --- a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java @@ -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> allCookieHeaders) { for (Map.Entry> 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 + * RFC 6265. + */ + private String buildCookieHeader(List 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(); + } } diff --git a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java old mode 100644 new mode 100755 index 2ab564dc..69e86568 --- a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java +++ b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java @@ -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. diff --git a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java old mode 100644 new mode 100755 index ce0a71d8..1055e4f0 --- a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java +++ b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java @@ -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 failedRoutes; + private final RouteDatabase routeDatabase; /* The most recently attempted route. */ private Proxy lastProxy; @@ -78,13 +77,13 @@ public final class RouteSelector { private final List postponedRoutes; public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool, - Dns dns, Set 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(); 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. */ diff --git a/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java b/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java old mode 100644 new mode 100755 index 18ab5668..471539a4 --- a/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java +++ b/framework/src/com/squareup/okhttp/internal/http/SpdyTransport.java @@ -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 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; } diff --git a/framework/src/com/squareup/okhttp/internal/http/Transport.java b/framework/src/com/squareup/okhttp/internal/http/Transport.java old mode 100644 new mode 100755 index 518827e8..d408bfec --- a/framework/src/com/squareup/okhttp/internal/http/Transport.java +++ b/framework/src/com/squareup/okhttp/internal/http/Transport.java @@ -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); } diff --git a/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java b/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java old mode 100644 new mode 100755 index 729e0b92..ca6bb59f --- a/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java +++ b/framework/src/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java @@ -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); diff --git a/framework/src/com/squareup/okhttp/internal/spdy/ErrorCode.java b/framework/src/com/squareup/okhttp/internal/spdy/ErrorCode.java new file mode 100755 index 00000000..d3a32e11 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/ErrorCode.java @@ -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; + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/FrameReader.java b/framework/src/com/squareup/okhttp/internal/spdy/FrameReader.java new file mode 100755 index 00000000..1371262e --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/FrameReader.java @@ -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 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); + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/FrameWriter.java b/framework/src/com/squareup/okhttp/internal/spdy/FrameWriter.java new file mode 100755 index 00000000..354f43d1 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/FrameWriter.java @@ -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 nameValueBlock) throws IOException; + void synReply(boolean outFinished, int streamId, List nameValueBlock) throws IOException; + void headers(int streamId, List 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; +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/HeadersMode.java b/framework/src/com/squareup/okhttp/internal/spdy/HeadersMode.java new file mode 100755 index 00000000..e16e176a --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/HeadersMode.java @@ -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; + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Hpack.java b/framework/src/com/squareup/okhttp/internal/spdy/Hpack.java new file mode 100755 index 00000000..c3ca8f11 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/Hpack.java @@ -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 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 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 headerTable; + private final List emittedHeaders = new ArrayList(); + 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(INITIAL_SERVER_TO_CLIENT_HEADER_TABLE); + this.bufferSize = INITIAL_SERVER_TO_CLIENT_HEADER_TABLE_LENGTH; + } else { + this.headerTable = new ArrayList(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 getAndReset() { + List result = new ArrayList(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 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); + } + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Http20Draft06.java b/framework/src/com/squareup/okhttp/internal/spdy/Http20Draft06.java new file mode 100755 index 00000000..3d53f484 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/Http20Draft06.java @@ -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 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 nameValueBlock) + throws IOException { + if (inFinished) throw new UnsupportedOperationException(); + headers(outFinished, streamId, priority, nameValueBlock); + } + + @Override public synchronized void synReply(boolean outFinished, int streamId, + List nameValueBlock) throws IOException { + headers(outFinished, streamId, -1, nameValueBlock); + } + + @Override public synchronized void headers(int streamId, List nameValueBlock) + throws IOException { + headers(false, streamId, -1, nameValueBlock); + } + + private void headers(boolean outFinished, int streamId, int priority, + List 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(); + } + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java old mode 100644 new mode 100755 index 875fff0f..44d4ea2b --- a/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java @@ -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); } }; diff --git a/framework/src/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java b/framework/src/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java new file mode 100755 index 00000000..b95d0138 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java @@ -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 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 entries = new ArrayList(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(); + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Ping.java b/framework/src/com/squareup/okhttp/internal/spdy/Ping.java old mode 100644 new mode 100755 diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Settings.java b/framework/src/com/squareup/okhttp/internal/spdy/Settings.java old mode 100644 new mode 100755 index 774d7912..05380e27 --- a/framework/src/com/squareup/okhttp/internal/spdy/Settings.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/Settings.java @@ -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. diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Spdy3.java b/framework/src/com/squareup/okhttp/internal/spdy/Spdy3.java new file mode 100755 index 00000000..5d9a49b3 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/Spdy3.java @@ -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 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 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 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 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 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 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 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); + } + } +} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java old mode 100644 new mode 100755 index fccd14f8..41724f07 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -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(), + 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(), 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 streams = new HashMap(); private final String hostName; @@ -103,14 +84,15 @@ public final class SpdyConnection implements Closeable { private Map 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 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 alternating) throws IOException { - spdyWriter.synReply(flags, streamId, alternating); + void writeSynReply(int streamId, boolean outFinished, List 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(); 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 nameValueBlock) { - final SpdyStream synStream; - final SpdyStream previous; + @Override public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, int priority, List 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 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 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 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. + } } } diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java deleted file mode 100644 index 7d3f2bd5..00000000 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java +++ /dev/null @@ -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 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 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 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 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 entries = new ArrayList(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 nameValueBlock); - - void synReply(int flags, int streamId, List nameValueBlock) throws IOException; - void headers(int flags, int streamId, List 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); - } -} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java old mode 100644 new mode 100755 index 744a04ea..a3ab3a44 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java +++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyStream.java @@ -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 requestHeaders, Settings settings) { + SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished, + int priority, List 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 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 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 strings) throws IOException { + void receiveHeaders(List 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 newHeaders = new ArrayList(); + 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 headers) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean protocolError = false; - synchronized (this) { - if (responseHeaders != null) { - List newHeaders = new ArrayList(); - 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); } } } diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java deleted file mode 100644 index b3d1d1f9..00000000 --- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyWriter.java +++ /dev/null @@ -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 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 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 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 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); - } -} diff --git a/framework/src/com/squareup/okhttp/internal/spdy/Variant.java b/framework/src/com/squareup/okhttp/internal/spdy/Variant.java new file mode 100755 index 00000000..8f48bcd2 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/spdy/Variant.java @@ -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); +} diff --git a/framework/src/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java b/framework/src/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java new file mode 100755 index 00000000..e0aef149 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/tls/DistinguishedNameParser.java @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.squareup.okhttp.internal.tls; + +import javax.security.auth.x500.X500Principal; + +/** + * A distinguished name (DN) parser. This parser only supports extracting a + * string value from a DN. It doesn't support values in the hex-string style. + */ +final class DistinguishedNameParser { + private final String dn; + private final int length; + private int pos; + private int beg; + private int end; + + /** Temporary variable to store positions of the currently parsed item. */ + private int cur; + + /** Distinguished name characters. */ + private char[] chars; + + public DistinguishedNameParser(X500Principal principal) { + // RFC2253 is used to ensure we get attributes in the reverse + // order of the underlying ASN.1 encoding, so that the most + // significant values of repeated attributes occur first. + this.dn = principal.getName(X500Principal.RFC2253); + this.length = this.dn.length(); + } + + // gets next attribute type: (ALPHA 1*keychar) / oid + private String nextAT() { + // skip preceding space chars, they can present after + // comma or semicolon (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + if (pos == length) { + return null; // reached the end of DN + } + + // mark the beginning of attribute type + beg = pos; + + // attribute type chars + pos++; + for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { + // we don't follow exact BNF syntax here: + // accept any char except space and '=' + } + if (pos >= length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + // mark the end of attribute type + end = pos; + + // skip trailing space chars between attribute type and '=' + // (compatibility with RFC 1779) + if (chars[pos] == ' ') { + for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { + } + + if (chars[pos] != '=' || pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + } + + pos++; //skip '=' char + + // skip space chars between '=' and attribute value + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + + // in case of oid attribute type skip its prefix: "oid." or "OID." + // (compatibility with RFC 1779) + if ((end - beg > 4) && (chars[beg + 3] == '.') + && (chars[beg] == 'O' || chars[beg] == 'o') + && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') + && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { + beg += 4; + } + + return new String(chars, beg, end - beg); + } + + // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION + private String quotedAV() { + pos++; + beg = pos; + end = beg; + while (true) { + + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + if (chars[pos] == '"') { + // enclosing quotation was found + pos++; + break; + } else if (chars[pos] == '\\') { + chars[end] = getEscaped(); + } else { + // shift char: required for string with escaped chars + chars[end] = chars[pos]; + } + pos++; + end++; + } + + // skip trailing space chars before comma or semicolon. + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + + return new String(chars, beg, end - beg); + } + + // gets hex string attribute value: "#" hexstring + private String hexAV() { + if (pos + 4 >= length) { + // encoded byte array must be not less then 4 c + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + beg = pos; // store '#' position + pos++; + while (true) { + + // check for end of attribute value + // looks for space and component separators + if (pos == length || chars[pos] == '+' || chars[pos] == ',' + || chars[pos] == ';') { + end = pos; + break; + } + + if (chars[pos] == ' ') { + end = pos; + pos++; + // skip trailing space chars before comma or semicolon. + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + break; + } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { + chars[pos] += 32; //to low case + } + + pos++; + } + + // verify length of hex string + // encoded byte array must be not less then 4 and must be even number + int hexLen = end - beg; // skip first '#' char + if (hexLen < 5 || (hexLen & 1) == 0) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + // get byte encoding from string representation + byte[] encoded = new byte[hexLen / 2]; + for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { + encoded[i] = (byte) getByte(p); + } + + return new String(chars, beg, hexLen); + } + + // gets string attribute value: *( stringchar / pair ) + private String escapedAV() { + beg = pos; + end = pos; + while (true) { + if (pos >= length) { + // the end of DN has been found + return new String(chars, beg, end - beg); + } + + switch (chars[pos]) { + case '+': + case ',': + case ';': + // separator char has been found + return new String(chars, beg, end - beg); + case '\\': + // escaped char + chars[end++] = getEscaped(); + pos++; + break; + case ' ': + // need to figure out whether space defines + // the end of attribute value or not + cur = end; + + pos++; + chars[end++] = ' '; + + for (; pos < length && chars[pos] == ' '; pos++) { + chars[end++] = ' '; + } + if (pos == length || chars[pos] == ',' || chars[pos] == '+' + || chars[pos] == ';') { + // separator char or the end of DN has been found + return new String(chars, beg, cur - beg); + } + break; + default: + chars[end++] = chars[pos]; + pos++; + } + } + } + + // returns escaped char + private char getEscaped() { + pos++; + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + switch (chars[pos]) { + case '"': + case '\\': + case ',': + case '=': + case '+': + case '<': + case '>': + case '#': + case ';': + case ' ': + case '*': + case '%': + case '_': + //FIXME: escaping is allowed only for leading or trailing space char + return chars[pos]; + default: + // RFC doesn't explicitly say that escaped hex pair is + // interpreted as UTF-8 char. It only contains an example of such DN. + return getUTF8(); + } + } + + // decodes UTF-8 char + // see http://www.unicode.org for UTF-8 bit distribution table + private char getUTF8() { + int res = getByte(pos); + pos++; //FIXME tmp + + if (res < 128) { // one byte: 0-7F + return (char) res; + } else if (res >= 192 && res <= 247) { + + int count; + if (res <= 223) { // two bytes: C0-DF + count = 1; + res = res & 0x1F; + } else if (res <= 239) { // three bytes: E0-EF + count = 2; + res = res & 0x0F; + } else { // four bytes: F0-F7 + count = 3; + res = res & 0x07; + } + + int b; + for (int i = 0; i < count; i++) { + pos++; + if (pos == length || chars[pos] != '\\') { + return 0x3F; //FIXME failed to decode UTF-8 char - return '?' + } + pos++; + + b = getByte(pos); + pos++; //FIXME tmp + if ((b & 0xC0) != 0x80) { + return 0x3F; //FIXME failed to decode UTF-8 char - return '?' + } + + res = (res << 6) + (b & 0x3F); + } + return (char) res; + } else { + return 0x3F; //FIXME failed to decode UTF-8 char - return '?' + } + } + + // Returns byte representation of a char pair + // The char pair is composed of DN char in + // specified 'position' and the next char + // According to BNF syntax: + // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + // / "a" / "b" / "c" / "d" / "e" / "f" + private int getByte(int position) { + if (position + 1 >= length) { + throw new IllegalStateException("Malformed DN: " + dn); + } + + int b1, b2; + + b1 = chars[position]; + if (b1 >= '0' && b1 <= '9') { + b1 = b1 - '0'; + } else if (b1 >= 'a' && b1 <= 'f') { + b1 = b1 - 87; // 87 = 'a' - 10 + } else if (b1 >= 'A' && b1 <= 'F') { + b1 = b1 - 55; // 55 = 'A' - 10 + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + + b2 = chars[position + 1]; + if (b2 >= '0' && b2 <= '9') { + b2 = b2 - '0'; + } else if (b2 >= 'a' && b2 <= 'f') { + b2 = b2 - 87; // 87 = 'a' - 10 + } else if (b2 >= 'A' && b2 <= 'F') { + b2 = b2 - 55; // 55 = 'A' - 10 + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + + return (b1 << 4) + b2; + } + + /** + * Parses the DN and returns the most significant attribute value + * for an attribute type, or null if none found. + * + * @param attributeType attribute type to look for (e.g. "ca") + */ + public String findMostSpecific(String attributeType) { + // Initialize internal state. + pos = 0; + beg = 0; + end = 0; + cur = 0; + chars = dn.toCharArray(); + + String attType = nextAT(); + if (attType == null) { + return null; + } + while (true) { + String attValue = ""; + + if (pos == length) { + return null; + } + + switch (chars[pos]) { + case '"': + attValue = quotedAV(); + break; + case '#': + attValue = hexAV(); + break; + case '+': + case ',': + case ';': // compatibility with RFC 1779: semicolon can separate RDNs + //empty attribute value + break; + default: + attValue = escapedAV(); + } + + // Values are ordered from most specific to least specific + // due to the RFC2253 formatting. So take the first match + // we see. + if (attributeType.equalsIgnoreCase(attType)) { + return attValue; + } + + if (pos >= length) { + return null; + } + + if (chars[pos] == ',' || chars[pos] == ';') { + } else if (chars[pos] != '+') { + throw new IllegalStateException("Malformed DN: " + dn); + } + + pos++; + attType = nextAT(); + if (attType == null) { + throw new IllegalStateException("Malformed DN: " + dn); + } + } + } +} diff --git a/framework/src/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/framework/src/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java new file mode 100755 index 00000000..a08773f6 --- /dev/null +++ b/framework/src/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java @@ -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 RFC 2818. + */ +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. + * + *

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 getSubjectAltNames(X509Certificate certificate, int type) { + List result = new ArrayList(); + 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; + } +}